From ea79fa079f6a71daac16a8d8f426c05564d4201b Mon Sep 17 00:00:00 2001 From: aprilnovak Date: Wed, 18 Aug 2021 15:49:58 -0500 Subject: [PATCH 01/59] Allow constant scaling factor to be applied to libMesh unstructured meshes. Refs #1872 --- include/openmc/mesh.h | 7 ++++++- src/mesh.cpp | 25 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index d3ca900cf..7a55b5f20 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -492,7 +492,7 @@ class LibMesh : public UnstructuredMesh { public: // Constructors LibMesh(pugi::xml_node node); - LibMesh(const std::string& filename); + LibMesh(const std::string& filename, const double length_multiplier = 1.0); // Overridden Methods void bins_crossed(Position r0, Position r1, const Direction& u, @@ -533,6 +533,9 @@ private: //! Translate an element pointer to a bin index int get_bin_from_element(const libMesh::Elem* elem) const; + //! Set the length multiplier to apply to each point in the mesh + void set_length_multiplier(const double length_multiplier); + // Data members unique_ptr m_; //!< pointer to the libMesh mesh instance vector> @@ -547,6 +550,8 @@ private: libMesh::BoundingBox bbox_; //!< bounding box of the mesh libMesh::dof_id_type first_element_id_; //!< id of the first element in the mesh + double length_multiplier_ {1.0}; //!< Constant multiplication factor to apply to mesh coordinates + bool specified_length_multiplier_ {false}; }; #endif diff --git a/src/mesh.cpp b/src/mesh.cpp index 4798bfcf5..7770dfb86 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -35,6 +35,7 @@ #ifdef LIBMESH #include "libmesh/mesh_tools.h" #include "libmesh/numeric_vector.h" +#include "libmesh/mesh_modification.h" #endif namespace openmc { @@ -2141,15 +2142,32 @@ void MOABMesh::write(const std::string& base_filename) const LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(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; + } + initialize(); } -LibMesh::LibMesh(const std::string& filename) +LibMesh::LibMesh(const std::string& filename, const double length_multiplier) { filename_ = filename; + + if (length_multiplier != 1.0) { + set_length_multiplier(length_multiplier); + } + initialize(); } +void LibMesh::set_length_multiplier(const double length_multiplier) +{ + length_multiplier_ = length_multiplier; + specified_length_multiplier_ = true; +} + void LibMesh::initialize() { if (!settings::libmesh_comm) { @@ -2162,6 +2180,11 @@ void LibMesh::initialize() m_ = make_unique(*settings::libmesh_comm, n_dimension_); m_->read(filename_); + + if (specified_length_multiplier_) { + libMesh::MeshTools::Modification::scale(*m_, length_multiplier_); + } + m_->prepare_for_use(); // ensure that the loaded mesh is 3 dimensional From 9165a836fe8aa49031ab00fcabb5acdcebe07602 Mon Sep 17 00:00:00 2001 From: April Novak Date: Thu, 19 Aug 2021 16:08:57 -0500 Subject: [PATCH 02/59] Apply suggestions from code review Co-authored-by: Patrick Shriwise --- include/openmc/mesh.h | 2 +- src/mesh.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 7a55b5f20..d14987d6d 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -492,7 +492,7 @@ class LibMesh : public UnstructuredMesh { public: // Constructors LibMesh(pugi::xml_node node); - LibMesh(const std::string& filename, const double length_multiplier = 1.0); + LibMesh(const std::string& filename, double length_multiplier = 1.0); // Overridden Methods void bins_crossed(Position r0, Position r1, const Direction& u, diff --git a/src/mesh.cpp b/src/mesh.cpp index 7770dfb86..20a49b1d9 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2151,7 +2151,7 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) initialize(); } -LibMesh::LibMesh(const std::string& filename, const double length_multiplier) +LibMesh::LibMesh(const std::string& filename, double length_multiplier) { filename_ = filename; @@ -2162,7 +2162,7 @@ LibMesh::LibMesh(const std::string& filename, const double length_multiplier) initialize(); } -void LibMesh::set_length_multiplier(const double length_multiplier) +void LibMesh::set_length_multiplier(double length_multiplier) { length_multiplier_ = length_multiplier; specified_length_multiplier_ = true; From f6bebf50d7199418cae280f78a031d9ec47b3877 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 25 Aug 2021 12:30:39 -0500 Subject: [PATCH 03/59] Add missing 'override' specifications in cell.h --- include/openmc/cell.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 5f6d33c97..6f9d87941 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -244,14 +244,14 @@ public: explicit CSGCell(pugi::xml_node cell_node); - bool contains(Position r, Direction u, int32_t on_surface) const; + 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; + Position r, Direction u, int32_t on_surface, Particle* p) const override; void to_hdf5_inner(hid_t group_id) const override; - BoundingBox bounding_box() const; + BoundingBox bounding_box() const override; protected: bool contains_simple(Position r, Direction u, int32_t on_surface) const; From 2d53bda17e55e3013ae44978118b88274d6f8d9e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 25 Aug 2021 12:31:47 -0500 Subject: [PATCH 04/59] Update fmt submodule --- vendor/fmt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/fmt b/vendor/fmt index 65ac626c5..d141cdbeb 160000 --- a/vendor/fmt +++ b/vendor/fmt @@ -1 +1 @@ -Subproject commit 65ac626c5856f5aad1f1542e79407a6714357043 +Subproject commit d141cdbeb0fb422a3fb7173b285fd38e0d1772dc From 7969bb14880df4f44a963ad7c42a7235b93e3128 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 25 Aug 2021 13:10:28 -0500 Subject: [PATCH 05/59] Use include instead of --- CMakeLists.txt | 2 -- include/openmc/cell.h | 2 +- include/openmc/material.h | 2 +- include/openmc/nuclide.h | 2 +- include/openmc/photon.h | 2 +- include/openmc/reaction.h | 2 +- include/openmc/tallies/filter.h | 2 +- include/openmc/tallies/filter_azimuthal.h | 2 +- include/openmc/tallies/filter_cell.h | 2 +- include/openmc/tallies/filter_cell_instance.h | 2 +- include/openmc/tallies/filter_collision.h | 2 +- include/openmc/tallies/filter_delayedgroup.h | 2 +- include/openmc/tallies/filter_energy.h | 2 +- include/openmc/tallies/filter_material.h | 2 +- include/openmc/tallies/filter_mu.h | 2 +- include/openmc/tallies/filter_polar.h | 2 +- include/openmc/tallies/filter_sph_harm.h | 2 +- include/openmc/tallies/filter_surface.h | 2 +- include/openmc/tallies/filter_universe.h | 2 +- include/openmc/tallies/tally.h | 2 +- include/openmc/volume_calc.h | 2 +- src/cell.cpp | 2 +- src/mesh.cpp | 2 +- src/surface.cpp | 2 +- src/tallies/filter_mesh.cpp | 2 +- src/tallies/filter_sph_harm.cpp | 2 +- src/tallies/filter_zernike.cpp | 2 +- 27 files changed, 26 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7326e7f35..75c66a233 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,8 +214,6 @@ add_subdirectory(vendor/xtensor) # GSL header-only library #=============================================================================== -set(GSL_LITE_OPT_INSTALL_COMPAT_HEADER ON CACHE BOOL - "Install MS-GSL compatibility header ") add_subdirectory(vendor/gsl-lite) # Make sure contract violations throw exceptions diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 6f9d87941..d56fd5f81 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -10,7 +10,7 @@ #include "hdf5.h" #include "pugixml.hpp" -#include +#include #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr diff --git a/include/openmc/material.h b/include/openmc/material.h index 5fc52eba8..709d20573 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -6,7 +6,7 @@ #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include +#include #include #include "openmc/bremsstrahlung.h" diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index f961e9808..b05c76bbb 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -7,7 +7,7 @@ #include #include // for pair -#include +#include #include #include "openmc/array.h" diff --git a/include/openmc/photon.h b/include/openmc/photon.h index bf64fa50c..09783fdb4 100644 --- a/include/openmc/photon.h +++ b/include/openmc/photon.h @@ -7,7 +7,7 @@ #include "openmc/vector.h" #include "xtensor/xtensor.hpp" -#include +#include #include #include diff --git a/include/openmc/reaction.h b/include/openmc/reaction.h index 6b276db20..46705acf5 100644 --- a/include/openmc/reaction.h +++ b/include/openmc/reaction.h @@ -7,7 +7,7 @@ #include #include "hdf5.h" -#include +#include #include "openmc/reaction_product.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index b0145f0e1..2eecfe0e4 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -6,7 +6,7 @@ #include #include "pugixml.hpp" -#include +#include #include "openmc/constants.h" #include "openmc/hdf5_interface.h" diff --git a/include/openmc/tallies/filter_azimuthal.h b/include/openmc/tallies/filter_azimuthal.h index 4cda9163f..2272b500a 100644 --- a/include/openmc/tallies/filter_azimuthal.h +++ b/include/openmc/tallies/filter_azimuthal.h @@ -4,7 +4,7 @@ #include "openmc/vector.h" #include -#include +#include #include "openmc/tallies/filter.h" diff --git a/include/openmc/tallies/filter_cell.h b/include/openmc/tallies/filter_cell.h index 6e2463f24..46d89811d 100644 --- a/include/openmc/tallies/filter_cell.h +++ b/include/openmc/tallies/filter_cell.h @@ -4,7 +4,7 @@ #include #include -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_cell_instance.h b/include/openmc/tallies/filter_cell_instance.h index d74850df7..4de3fcd29 100644 --- a/include/openmc/tallies/filter_cell_instance.h +++ b/include/openmc/tallies/filter_cell_instance.h @@ -4,7 +4,7 @@ #include #include -#include +#include #include "openmc/cell.h" #include "openmc/tallies/filter.h" diff --git a/include/openmc/tallies/filter_collision.h b/include/openmc/tallies/filter_collision.h index 46c64c553..3724b06cf 100644 --- a/include/openmc/tallies/filter_collision.h +++ b/include/openmc/tallies/filter_collision.h @@ -1,7 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_COLLISIONS_H #define OPENMC_TALLIES_FILTER_COLLISIONS_H -#include +#include #include #include "openmc/tallies/filter.h" diff --git a/include/openmc/tallies/filter_delayedgroup.h b/include/openmc/tallies/filter_delayedgroup.h index 868807f05..72ffa1db5 100644 --- a/include/openmc/tallies/filter_delayedgroup.h +++ b/include/openmc/tallies/filter_delayedgroup.h @@ -1,7 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_DELAYEDGROUP_H #define OPENMC_TALLIES_FILTER_DELAYEDGROUP_H -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_energy.h b/include/openmc/tallies/filter_energy.h index c820f28fb..000aaa28b 100644 --- a/include/openmc/tallies/filter_energy.h +++ b/include/openmc/tallies/filter_energy.h @@ -1,7 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_ENERGY_H #define OPENMC_TALLIES_FILTER_ENERGY_H -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_material.h b/include/openmc/tallies/filter_material.h index 8a67c1a75..f58fc9938 100644 --- a/include/openmc/tallies/filter_material.h +++ b/include/openmc/tallies/filter_material.h @@ -4,7 +4,7 @@ #include #include -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_mu.h b/include/openmc/tallies/filter_mu.h index de934d156..5299f6dd4 100644 --- a/include/openmc/tallies/filter_mu.h +++ b/include/openmc/tallies/filter_mu.h @@ -1,7 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_MU_H #define OPENMC_TALLIES_FILTER_MU_H -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_polar.h b/include/openmc/tallies/filter_polar.h index 23c00e619..e06aca1e0 100644 --- a/include/openmc/tallies/filter_polar.h +++ b/include/openmc/tallies/filter_polar.h @@ -3,7 +3,7 @@ #include -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_sph_harm.h b/include/openmc/tallies/filter_sph_harm.h index 498590d3c..5f5bf84f2 100644 --- a/include/openmc/tallies/filter_sph_harm.h +++ b/include/openmc/tallies/filter_sph_harm.h @@ -3,7 +3,7 @@ #include -#include +#include #include "openmc/tallies/filter.h" diff --git a/include/openmc/tallies/filter_surface.h b/include/openmc/tallies/filter_surface.h index 368fd09d0..358963fde 100644 --- a/include/openmc/tallies/filter_surface.h +++ b/include/openmc/tallies/filter_surface.h @@ -4,7 +4,7 @@ #include #include -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_universe.h b/include/openmc/tallies/filter_universe.h index afe583b2d..fde0b6397 100644 --- a/include/openmc/tallies/filter_universe.h +++ b/include/openmc/tallies/filter_universe.h @@ -4,7 +4,7 @@ #include #include -#include +#include #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 39e0b0af2..13b8317ee 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -10,7 +10,7 @@ #include "pugixml.hpp" #include "xtensor/xfixed.hpp" #include "xtensor/xtensor.hpp" -#include +#include #include #include diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index 33cc7343a..db96f250f 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -9,7 +9,7 @@ #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include +#include #include namespace openmc { diff --git a/src/cell.cpp b/src/cell.cpp index 109b66dde..fe138a089 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -10,7 +10,7 @@ #include #include -#include +#include #include "openmc/capi.h" #include "openmc/constants.h" diff --git a/src/mesh.cpp b/src/mesh.cpp index 4798bfcf5..232b34ad0 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2,7 +2,7 @@ #include // for copy, equal, min, min_element #include // for ceil #include // for size_t -#include +#include #include #ifdef OPENMC_MPI diff --git a/src/surface.cpp b/src/surface.cpp index c19de1e00..73a5aaaf0 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include "openmc/array.h" #include "openmc/container_util.h" diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index a5f52e95a..3f895b4f8 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -1,7 +1,7 @@ #include "openmc/tallies/filter_mesh.h" #include -#include +#include #include "openmc/capi.h" #include "openmc/constants.h" diff --git a/src/tallies/filter_sph_harm.cpp b/src/tallies/filter_sph_harm.cpp index 29976f3a1..359df379b 100644 --- a/src/tallies/filter_sph_harm.cpp +++ b/src/tallies/filter_sph_harm.cpp @@ -3,7 +3,7 @@ #include // For pair #include -#include +#include #include "openmc/capi.h" #include "openmc/error.h" diff --git a/src/tallies/filter_zernike.cpp b/src/tallies/filter_zernike.cpp index 65004983c..eb4c8bdfd 100644 --- a/src/tallies/filter_zernike.cpp +++ b/src/tallies/filter_zernike.cpp @@ -5,7 +5,7 @@ #include // For pair #include -#include +#include #include "openmc/capi.h" #include "openmc/error.h" From 7aba7bbb68e99c93e0f011225a42299685b28f02 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 25 Aug 2021 15:08:06 -0500 Subject: [PATCH 06/59] When writing depletion chain, don't use 'Nothing' for missing product --- openmc/deplete/nuclide.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index edba9748c..992248246 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -294,7 +294,8 @@ class Nuclide: for mode_type, daughter, br in self.decay_modes: mode_elem = ET.SubElement(elem, 'decay') mode_elem.set('type', mode_type) - mode_elem.set('target', daughter or "Nothing") + if daughter: + mode_elem.set('target', daughter) mode_elem.set('branching_ratio', str(br)) elem.set('reactions', str(len(self.reactions))) From c08d9bc142d292ccd78cd4596c9cf504fb067cf9 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 27 Aug 2021 11:08:42 -0500 Subject: [PATCH 07/59] Fix get_decay_modes ('unknown' wasn't recognized) --- openmc/data/decay.py | 7 +++++-- tests/unit_tests/test_data_decay.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 3e7d9599c..a4e5405a2 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -57,8 +57,11 @@ def get_decay_modes(value): List of successive decays, e.g. ('beta-', 'neutron') """ - return [_DECAY_MODES[int(x)][0] for x in - str(value).strip('0').replace('.', '')] + if int(value) == 10: + return ['unknown'] + else: + return [_DECAY_MODES[int(x)][0] for x in + str(value).strip('0').replace('.', '')] class FissionProductYields(EqualityMixin): diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index cb4560d51..06b8e6bed 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -31,6 +31,18 @@ def u235_yields(): return openmc.data.FissionProductYields.from_endf(filename) +def test_get_decay_modes(): + assert openmc.data.get_decay_modes(1.0) == ['beta-'] + assert openmc.data.get_decay_modes(6.0) == ['sf'] + assert openmc.data.get_decay_modes(10.0) == ['unknown'] + + assert openmc.data.get_decay_modes(1.5) == ['beta-', 'n'] + assert openmc.data.get_decay_modes(1.4) == ['beta-', 'alpha'] + assert openmc.data.get_decay_modes(1.55) == ['beta-', 'n', 'n'] + assert openmc.data.get_decay_modes(1.555) == ['beta-', 'n', 'n', 'n'] + assert openmc.data.get_decay_modes(2.4) == ['ec/beta+', 'alpha'] + + def test_nb90_halflife(nb90): ufloat_close(nb90.half_life, ufloat(52560.0, 180.0)) ufloat_close(nb90.decay_constant, log(2.)/nb90.half_life) From 6fd8971eb71231ec5ca550d132e0aab3c5ab8ce3 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 30 Aug 2021 22:41:44 -0500 Subject: [PATCH 08/59] Update recognized thermal scattering materials for ENDF/B-VIII.1 --- openmc/data/njoy.py | 165 ++++++++++++++++++++--------------------- openmc/data/thermal.py | 94 ++++++++++++----------- 2 files changed, 133 insertions(+), 126 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 0ef77695b..948bd8bb5 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -5,95 +5,74 @@ import shutil from subprocess import Popen, PIPE, STDOUT, CalledProcessError import tempfile from pathlib import Path +import warnings from . import endf +import openmc.data -# For a given MAT number, give a name for the ACE table and a list of ZAID -# identifiers. This is based on Appendix C in the ENDF manual. +# For a given material, give a name for the ACE table and a list of ZAID +# identifiers. ThermalTuple = namedtuple('ThermalTuple', ['name', 'zaids', 'nmix']) _THERMAL_DATA = { - 1: ThermalTuple('hh2o', [1001], 1), - 2: ThermalTuple('parah', [1001], 1), - 3: ThermalTuple('orthoh', [1001], 1), - 5: ThermalTuple('hyh2', [1001], 1), - 7: ThermalTuple('hzrh', [1001], 1), - 8: ThermalTuple('hcah2', [1001], 1), - 10: ThermalTuple('hice', [1001], 1), - 11: ThermalTuple('dd2o', [1002], 1), - 12: ThermalTuple('parad', [1002], 1), - 13: ThermalTuple('orthod', [1002], 1), - 14: ThermalTuple('dice', [1002], 1), - 26: ThermalTuple('be', [4009], 1), - 27: ThermalTuple('bebeo', [4009], 1), - 28: ThermalTuple('bebe2c', [4009], 1), - 30: ThermalTuple('graph', [6000, 6012, 6013], 1), - 31: ThermalTuple('grph10', [6000, 6012, 6013], 1), - 32: ThermalTuple('grph30', [6000, 6012, 6013], 1), - 33: ThermalTuple('lch4', [1001], 1), - 34: ThermalTuple('sch4', [1001], 1), - 35: ThermalTuple('sch4p2', [1001], 1), - 37: ThermalTuple('hch2', [1001], 1), - 38: ThermalTuple('mesi00', [1001], 1), - 39: ThermalTuple('lucite', [1001], 1), - 40: ThermalTuple('benz', [1001, 6000, 6012], 2), - 42: ThermalTuple('tol00', [1001], 1), - 43: ThermalTuple('sisic', [14028, 14029, 14030], 1), - 44: ThermalTuple('csic', [6000, 6012, 6013], 1), - 45: ThermalTuple('ouo2', [8016, 8017, 8018], 1), - 46: ThermalTuple('obeo', [8016, 8017, 8018], 1), - 47: ThermalTuple('sio2-a', [8016, 8017, 8018, 14028, 14029, 14030], 3), - 48: ThermalTuple('osap00', [92238], 1), - 49: ThermalTuple('sio2-b', [8016, 8017, 8018, 14028, 14029, 14030], 3), - 50: ThermalTuple('oice', [8016, 8017, 8018], 1), - 51: ThermalTuple('od2o', [8016, 8017, 8018], 1), - 52: ThermalTuple('mg24', [12024], 1), - 53: ThermalTuple('al27', [13027], 1), - 55: ThermalTuple('yyh2', [39089], 1), - 56: ThermalTuple('fe56', [26056], 1), - 58: ThermalTuple('zrzrh', [40000, 40090, 40091, 40092, 40094, 40096], 1), - 59: ThermalTuple('si00', [14028], 1), - 60: ThermalTuple('asap00', [13027], 1), - 71: ThermalTuple('n-un', [7014, 7015], 1), - 72: ThermalTuple('u-un', [92238], 1), - 75: ThermalTuple('uuo2', [8016, 8017, 8018], 1), + 'c_Al27': ThermalTuple('al27', [13027], 1), + 'c_Al_in_Al2O3': ThermalTuple('asap00', [13027], 1), + 'c_Be': ThermalTuple('be', [4009], 1), + 'c_Be_in_BeO': ThermalTuple('bebeo', [4009], 1), + 'c_Be_in_Be2C': ThermalTuple('bebe2c', [4009], 1), + 'c_Be_in_FLiBe': ThermalTuple('beflib', [4009], 1), + 'c_C6H6': ThermalTuple('benz', [1001, 6000, 6012], 2), + 'c_C_in_SiC': ThermalTuple('csic', [6000, 6012, 6013], 1), + 'c_Ca_in_CaH2': ThermalTuple('cacah2', [20040, 20042, 20043, 20044, 20046, 20048], 1), + 'c_D_in_D2O': ThermalTuple('dd2o', [1002], 1), + 'c_D_in_D2O_solid': ThermalTuple('dice', [1002], 1), + 'c_F_in_FLiBe': ThermalTuple('fflibe', [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_30p': ThermalTuple('grph30', [6000, 6012, 6013], 1), + 'c_H_in_C5O2H8': ThermalTuple('lucite', [1001], 1), + 'c_H_in_CaH2': ThermalTuple('hcah2', [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), + 'c_H_in_CH4_solid_phase_II': ThermalTuple('sch4p2', [1001], 1), + '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_Mesitylene': ThermalTuple('mesi00', [1001], 1), + 'c_H_in_ParaffinicOil': ThermalTuple('hparaf', [1001], 1), + 'c_H_in_Toluene': ThermalTuple('tol00', [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_Li_in_FLiBe': ThermalTuple('liflib', [3006, 3007], 1), + 'c_Mg24': ThermalTuple('mg24', [12024], 1), + 'c_N_in_UN': ThermalTuple('n-un', [7014, 7015], 1), + 'c_O_in_Al2O3': ThermalTuple('osap00', [92238], 1), + 'c_O_in_BeO': ThermalTuple('obeo', [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_UO2': ThermalTuple('ouo2', [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_Si28': ThermalTuple('si00', [14028], 1), + 'c_Si_in_SiC': ThermalTuple('sisic', [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_in_UN': ThermalTuple('u-un', [92238], 1), + 'c_U_in_UO2': ThermalTuple('uuo2', [8016, 8017, 8018], 1), + 'c_Y_in_YH2': ThermalTuple('yyh2', [39089], 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), } -def _get_thermal_data(ev, mat): - """Return appropriate ThermalTuple, accounting for bugs.""" - - # JEFF assigns MAT=59 to Ca in CaH2 (which is supposed to be silicon). - if ev.info['library'][0] == 'JEFF': - if ev.material == 59: - if 'CaH2' in ''.join(ev.info['description']): - zaids = [20040, 20042, 20043, 20044, 20046, 20048] - return ThermalTuple('cacah2', zaids, 1) - - # Before ENDF/B-VIII.0, crystalline graphite was MAT=31 - if ev.info['library'] != ('ENDF/B', 8, 0): - if ev.material == 31: - return _THERMAL_DATA[30] - - # ENDF/B incorrectly assigns MAT numbers for UO2 - # - # Material | ENDF Manual | VII.0 | VII.1 | VIII.0 - # ---------|-------------|-------|-------|------- - # O in UO2 | 45 | 75 | 75 | 75 - # U in UO2 | 75 | 76 | 48 | 48 - if ev.info['library'][0] == 'ENDF/B': - if ev.material == 75: - return _THERMAL_DATA[45] - version = ev.info['library'][1:] - if version in ((7, 1), (8, 0)) and ev.material == 48: - return _THERMAL_DATA[75] - if version == (7, 0) and ev.material == 76: - return _THERMAL_DATA[75] - - # If not a problematic material, use the dictionary as is - return _THERMAL_DATA[mat] - - _TEMPLATE_RECONR = """ reconr / %%%%%%%%%%%%%%%%%%% Reconstruct XS for neutrons %%%%%%%%%%%%%%%%%%%%%%% {nendf} {npendf} @@ -445,7 +424,8 @@ 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, - iwt=2, evaluation=None, evaluation_thermal=None, **kwargs): + iwt=2, evaluation=None, evaluation_thermal=None, + table_name=None, zaids=None, nmix=None, **kwargs): """Generate thermal scattering ACE file from ENDF files Parameters @@ -476,6 +456,12 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, evaluation_thermal : openmc.data.endf.Evaluation, optional If the ENDF thermal scattering sublibrary file contains multiple material evaluations, this argument indicates which evaluation to use. + table_name : str, optional + Name to assign to ACE table + zaids : list of int, optional + ZAIDs that the thermal scattering data applies to + nmix : int, optional + Number of atom types in mixed moderator **kwargs Keyword arguments passed to :func:`openmc.data.njoy.run` @@ -499,10 +485,21 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, ev_thermal = (evaluation_thermal if evaluation_thermal is not None else endf.Evaluation(filename_thermal)) mat_thermal = ev_thermal.material - zsymam_thermal = ev_thermal.target['zsymam'] + zsymam_thermal = ev_thermal.target['zsymam'].strip() + + # Determine name, isotopes, and number of atom types + if table_name and zaids and nmix: + data = ThermalTuple(table_name, zaids, nmix) + else: + with warnings.catch_warnings(record=True) as w: + proper_name = openmc.data.get_thermal_name(zsymam_thermal) + if w: + raise RuntimeError( + f"Thermal scattering material {zsymam_thermal} not " + "recognized. Please contact OpenMC developers at " + "https://openmc.discourse.group.") + data = _THERMAL_DATA[proper_name] - # Determine name, isotopes based on MAT number - data = _get_thermal_data(ev_thermal, mat_thermal) zaids = ' '.join(str(zaid) for zaid in data.zaids[:3]) # Determine name of library diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index d0adbc4fe..b807a9f59 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -28,52 +28,62 @@ from .thermal_angle_energy import (CoherentElasticAE, IncoherentElasticAE, _THERMAL_NAMES = { - 'c_Al27': ('al', 'al27', 'al-27'), - 'c_Al_in_Sapphire': ('asap00', 'asap'), - 'c_Be': ('be', 'be-metal', 'be-met', 'be00'), + '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_BeO': ('beo',), - 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00'), + 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)'), 'c_Be_in_Be2C': ('bebe2c',), - 'c_C6H6': ('benz', 'c6h6'), - 'c_C_in_SiC': ('csic', 'c-sic'), - 'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2'), - 'c_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00'), + '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_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00', 'd(d2o)'), 'c_D_in_D2O_ice': ('dice',), - 'c_Fe56': ('fe', 'fe56', 'fe-56'), - 'c_Graphite': ('graph', 'grph', 'gr', 'gr00'), - 'c_Graphite_10p': ('grph10',), - 'c_Graphite_30p': ('grph30',), - 'c_H_in_CaH2': ('hcah2', 'hca00'), - 'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00'), - 'c_H_in_CH4_liquid': ('lch4', 'lmeth'), - 'c_H_in_CH4_solid': ('sch4', 'smeth'), + 'c_F_in_FLiBe': ('fflibe', 'f(flibe)'), + 'c_Fe56': ('fe', 'fe56', 'fe-56', '26-fe- 56'), + 'c_Graphite': ('graph', 'grph', 'gr', 'gr00', 'graphite'), + 'c_Graphite_10p': ('grph10', '10p graphit'), + 'c_Graphite_30p': ('grph30', '30p graphit'), + 'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci', 'h(lucite)'), + 'c_H_in_CaH2': ('hcah2', 'hca00', 'h(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'), - 'c_H_in_H2O_solid': ('hice', 'h-ice', 'ice00'), - 'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci'), - 'c_H_in_Mesitylene': ('mesi00', 'mesi'), - 'c_H_in_Toluene': ('tol00', 'tol'), - 'c_H_in_YH2': ('hyh2', 'h-yh2'), - 'c_H_in_ZrH': ('hzrh', 'h-zrh', 'h-zr', 'h/zr', 'hzr', 'hzr00'), - 'c_Mg24': ('mg', 'mg24', 'mg00'), - 'c_O_in_Sapphire': ('osap00', 'osap'), - 'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00'), - 'c_O_in_D2O': ('od2o', 'o-d2o', 'ohw00'), - 'c_O_in_H2O_ice': ('oice', 'o-ice'), - 'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200'), - 'c_N_in_UN': ('n-un',), - 'c_ortho_D': ('orthod', 'orthoD', 'dortho', 'od200', 'ortod'), - 'c_ortho_H': ('orthoh', 'orthoH', 'hortho', 'oh200', 'ortoh'), - 'c_Si28': ('si00', 'sili'), - 'c_Si_in_SiC': ('sisic', 'si-sic'), - 'c_SiO2_alpha': ('sio2', 'sio2a'), - 'c_SiO2_beta': ('sio2b',), - 'c_para_D': ('parad', 'paraD', 'dpara', 'pd200'), - 'c_para_H': ('parah', 'paraH', 'hpara', 'ph200'), - 'c_U_in_UN': ('u-un',), - 'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200'), - 'c_Y_in_YH2': ('yyh2', 'y-yh2'), - 'c_Zr_in_ZrH': ('zrzrh', 'zr-zrh', 'zr-h', 'zr/h') + '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_Mesitylene': ('mesi00', 'mesi', 'mesi-phii'), + 'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin'), + '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_Li_in_FLiBe': ('liflib', 'li(flibe)'), + 'c_Mg24': ('mg', 'mg24', 'mg00', '24-mg'), + 'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l'), + '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_D2O': ('od2o', 'o-d2o', 'ohw00', 'o(d2o)'), + 'c_O_in_H2O_solid': ('oice', 'o-ice', 'o(ice-ih)'), + 'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200', 'o(uo2)'), + '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_Si28': ('si00', 'sili', 'si'), + 'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)'), + 'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha'), + 'c_SiO2_beta': ('sio2b', 'sio2beta'), + 'c_U_in_UN': ('u-un', 'u(un)', 'u(un) l'), + 'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200', 'u(uo2)'), + 'c_Y_in_YH2': ('yyh2', 'y-yh2', 'y(yh2)'), + '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 fa76ae54d155593def8902c6b08712ea4226f773 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 3 Sep 2021 15:25:32 -0500 Subject: [PATCH 09/59] Use nza input on ACER card 8 (for thermal scattering) --- openmc/data/njoy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 948bd8bb5..305edaf4f 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -149,7 +149,7 @@ acer / %%%%%%%%%%%%%%%%%%%%%%%% Write out in ACE format %%%%%%%%%%%%%%%%%%%%%%%% {nendf} {nthermal_acer_in} 0 {nace} {ndir} 2 0 1 .{ext}/ '{library}: {zsymam_thermal} processed by NJOY'/ -{mat} {temperature} '{data.name}' / +{mat} {temperature} '{data.name}' {nza} / {zaids} / 222 64 {mt_elastic} {elastic_type} {data.nmix} {energy_max} {iwt}/ """ @@ -500,7 +500,8 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, "https://openmc.discourse.group.") data = _THERMAL_DATA[proper_name] - zaids = ' '.join(str(zaid) for zaid in data.zaids[:3]) + zaids = ' '.join(str(zaid) for zaid in data.zaids) + nza = len(data.zaids) # Determine name of library library = '{}-{}.{}'.format(*ev_thermal.info['library']) From 74cb98926f553a97cbbde0f05dd3a61687a57018 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 8 Sep 2021 16:12:43 -0500 Subject: [PATCH 10/59] Adding error message for geometries using DAGMC with an executable not configured w/ DAGMC. --- src/dagmc.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dagmc.cpp b/src/dagmc.cpp index a6ff0e3b8..9b894d3bb 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -713,7 +713,13 @@ int32_t next_cell( namespace openmc { -void read_dagmc_universes(pugi::xml_node node) {}; +void read_dagmc_universes(pugi::xml_node node) +{ + if (check_for_node(node, "dagmc_universe")) { + fatal_error("DAGMC Universes are present but OpenMC was not configured" + "with DAGMC"); + } +}; void check_dagmc_root_univ() {}; } // namespace openmc From 71e33abb7285312ba0ad1a161536a8999a6c95c3 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 8 Sep 2021 17:02:43 -0500 Subject: [PATCH 11/59] Adding a note on how to create a geometry.xml file with only a DAGMC model. --- docs/source/io_formats/geometry.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index 1d04bb8fb..b04edca2b 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -406,3 +406,14 @@ Each ```` element can have the following attributes or sub-eleme A required string indicating the file to be loaded representing the DAGMC universe. *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 + + + + + From a6140709ccbab764bb82d32322211843c71e6ba5 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Sat, 11 Sep 2021 08:02:19 -0500 Subject: [PATCH 12/59] Allowing a scaling factor for MOAB meshes too. --- include/openmc/mesh.h | 16 +++++++----- src/mesh.cpp | 59 +++++++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index d14987d6d..d34ad2087 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -331,6 +331,15 @@ public: true}; //!< Write tallies onto the unstructured mesh at the end of a run std::string filename_; //!< Path to unstructured mesh file +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}; + private: //! Setup method for the mesh. Builds data structures, //! sets up element mapping, creates bounding boxes, etc. @@ -344,7 +353,7 @@ public: // Constructors MOABMesh() = default; MOABMesh(pugi::xml_node); - MOABMesh(const std::string& filename); + MOABMesh(const std::string& filename, double length_multiplier = 1.0); MOABMesh(std::shared_ptr external_mbi); // Overridden Methods @@ -533,9 +542,6 @@ private: //! Translate an element pointer to a bin index int get_bin_from_element(const libMesh::Elem* elem) const; - //! Set the length multiplier to apply to each point in the mesh - void set_length_multiplier(const double length_multiplier); - // Data members unique_ptr m_; //!< pointer to the libMesh mesh instance vector> @@ -550,8 +556,6 @@ private: libMesh::BoundingBox bbox_; //!< bounding box of the mesh libMesh::dof_id_type first_element_id_; //!< id of the first element in the mesh - double length_multiplier_ {1.0}; //!< Constant multiplication factor to apply to mesh coordinates - bool specified_length_multiplier_ {false}; }; #endif diff --git a/src/mesh.cpp b/src/mesh.cpp index 20a49b1d9..642e0e9d9 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -172,6 +172,12 @@ 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 if (check_for_node(node, "filename")) { filename_ = get_node_value(node, "filename"); @@ -222,6 +228,12 @@ void UnstructuredMesh::to_hdf5(hid_t group) const close_group(mesh_group); } +void UnstructuredMesh::set_length_multiplier(double length_multiplier) +{ + length_multiplier_ = length_multiplier; + specified_length_multiplier_ = true; +} + void StructuredMesh::get_indices(Position r, int* ijk, bool* in_mesh) const { *in_mesh = true; @@ -1586,9 +1598,14 @@ MOABMesh::MOABMesh(pugi::xml_node node) : UnstructuredMesh(node) initialize(); } -MOABMesh::MOABMesh(const std::string& filename) +MOABMesh::MOABMesh(const std::string& filename, double length_multiplier) { filename_ = filename; + + if (length_multiplier != 1.0) { + set_length_multiplier(length_multiplier); + } + initialize(); } @@ -1635,6 +1652,34 @@ void MOABMesh::initialize() fatal_error("Failed to add tetrahedra to an entity set."); } + if (specified_length_multiplier_) { + // get the connectivity of all tets + moab::Range adj; + rval = mbi_->get_adjacencies(ehs_, 0, true, adj, moab::Interface::UNION); + if (rval != moab::MB_SUCCESS) { + fatal_error("Failed to get adjacent vertices of tetrahedra."); + } + // scale all vertex coords by multiplier (done individually so not all + // coordinates are in memory twice at once) + for (auto vert : adj) { + // retrieve coords + std::array coord; + rval = mbi_->get_coords(&vert, 1, coord.data()); + if (rval != moab::MB_SUCCESS) { + fatal_error("Could not get coordinates of vertex."); + } + // scale coords + for (auto& c : coord) { + c *= length_multiplier_; + } + // set new coords + rval = mbi_->set_coords(&vert, 1, coord.data()); + if (rval != moab::MB_SUCCESS) { + fatal_error("Failed to set new vertex coordinates"); + } + } + } + // build acceleration data structures compute_barycentric_data(ehs_); build_kdtree(ehs_); @@ -2142,12 +2187,6 @@ void MOABMesh::write(const std::string& base_filename) const LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(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; - } - initialize(); } @@ -2162,12 +2201,6 @@ LibMesh::LibMesh(const std::string& filename, double length_multiplier) initialize(); } -void LibMesh::set_length_multiplier(double length_multiplier) -{ - length_multiplier_ = length_multiplier; - specified_length_multiplier_ = true; -} - void LibMesh::initialize() { if (!settings::libmesh_comm) { From 99cb54bf8e2abe6fff8d878b2e61345186155228 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 13 Sep 2021 15:42:15 -0500 Subject: [PATCH 13/59] Comment explaining special case in get_decay_modes --- openmc/data/decay.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index a4e5405a2..a0402b47d 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -58,6 +58,8 @@ def get_decay_modes(value): """ if int(value) == 10: + # The logic below would treat 10.0 as [1, 0] rather than [10] as it + # should, so we handle this case separately return ['unknown'] else: return [_DECAY_MODES[int(x)][0] for x in From 95c1143cfca90714d3c5b27e57bd6d359322e328 Mon Sep 17 00:00:00 2001 From: aprilnovak Date: Wed, 15 Sep 2021 10:20:57 -0500 Subject: [PATCH 14/59] Add length_multiplier to python API. Refs #1872 --- docs/source/io_formats/tallies.rst | 4 ++++ openmc/mesh.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index ad7227369..7f95f2985 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -318,6 +318,10 @@ attributes/sub-elements: :dimension: The number of mesh cells in each direction. (For regular mesh only.) + :length_multiplier: + A multiplicative factor to apply to the mesh coordinates in all directions. + (For unstructured mesh only.) + :lower_left: The lower-left corner of the structured mesh. If only two coordinates are given, it is assumed that the mesh is an x-y mesh. (For regular mesh only.) diff --git a/openmc/mesh.py b/openmc/mesh.py index 0e35aa6fe..f542ac5fe 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -611,6 +611,8 @@ class UnstructuredMesh(MeshBase): ---------- filename : str Location of the unstructured mesh file + length_multiplier: float + Constant multiplier to apply to mesh coordinates mesh_id : int Unique identifier for the mesh name : str @@ -626,11 +628,16 @@ class UnstructuredMesh(MeshBase): Name of the mesh filename : str Name of the file containing the unstructured mesh + length_multiplier: float + Multiplicative factor to apply to mesh coordinates library : str Mesh library used for the unstructured mesh tally output : bool Indicates whether or not automatic tally output should be generated for this mesh + specified_length_multiplier: bool + Indicates whether a non-unity length multiplier has been + applied to this mesh volumes : Iterable of float Volumes of the unstructured mesh elements total_volume : float @@ -639,13 +646,15 @@ class UnstructuredMesh(MeshBase): An iterable of element centroid coordinates, e.g. [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0), ...] """ - def __init__(self, filename, library, mesh_id=None, name=''): + def __init__(self, filename, library, mesh_id=None, name='', length_multiplier=1.0): super().__init__(mesh_id, name) self.filename = filename self._volumes = None self._centroids = None self.library = library self._output = True + self._specified_length_multiplier = False + self.length_multiplier = length_multiplier @property def filename(self): @@ -713,6 +722,18 @@ class UnstructuredMesh(MeshBase): Iterable, Real) self._centroids = centroids + @property + def length_multiplier(self): + return self._length_multiplier + + @length_multiplier.setter + def length_multiplier(self, length_multiplier): + cv.check_type("Unstructured mesh length multiplier", length_multiplier, Real) + self._length_multiplier = length_multiplier + + if (self._length_multiplier != 1.0): + self._specified_length_multiplier = True + def __repr__(self): string = super().__repr__() string += '{: <16}=\t{}\n'.format('\tFilename', self.filename) @@ -836,6 +857,9 @@ class UnstructuredMesh(MeshBase): subelement = ET.SubElement(element, "filename") subelement.text = self.filename + if (self._specified_length_multiplier): + element.set("length_multiplier", str(self.length_multiplier)) + return element @classmethod From 2c9ae96c1d94613d697fb02cad7c1967d32d1503 Mon Sep 17 00:00:00 2001 From: aprilnovak Date: Wed, 15 Sep 2021 10:27:10 -0500 Subject: [PATCH 15/59] Add length multiplier to vtk writing. Refs #1872 --- openmc/mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index f542ac5fe..bf7954fd4 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -784,7 +784,7 @@ class UnstructuredMesh(MeshBase): for centroid in self.centroids: # create a point for each centroid - point_id = points.InsertNextPoint(centroid) + point_id = points.InsertNextPoint(centroid * self.length_multiplier) # create a cell of type "Vertex" for each point cell_id = vertices.InsertNextCell(cell_dim, (point_id,)) From 58dd23b06af4ba3cca15c04f3846d2ac1ff0e040 Mon Sep 17 00:00:00 2001 From: aprilnovak Date: Wed, 15 Sep 2021 10:31:15 -0500 Subject: [PATCH 16/59] Add length multiplier to from_xml. Refs #1872 --- openmc/mesh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index bf7954fd4..c72152690 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -879,5 +879,6 @@ class UnstructuredMesh(MeshBase): mesh_id = int(get_text(elem, 'id')) filename = get_text(elem, 'filename') library = get_text(elem, 'library') + length_multiplier = float(get_text(elem, 'length_multiplier', 1.0)) - return cls(filename, library, mesh_id) + return cls(filename, library, mesh_id, '', length_multiplier) From 796de48e5dd74e37c261a133a62521cc880ecfff Mon Sep 17 00:00:00 2001 From: aprilnovak Date: Mon, 20 Sep 2021 10:58:48 -0500 Subject: [PATCH 17/59] Get length multiplier from HDF5. Refs #1872 --- openmc/mesh.py | 1 + src/mesh.cpp | 20 +++++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index c72152690..6dde9277c 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -215,6 +215,7 @@ class RegularMesh(MeshBase): mesh.lower_left = group['lower_left'][()] mesh.upper_right = group['upper_right'][()] mesh.width = group['width'][()] + mesh.length_multiplier = group['length_multiplier'][()] if 'length_multiplier' in group else 1.0 return mesh diff --git a/src/mesh.cpp b/src/mesh.cpp index 642e0e9d9..b538bca47 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -225,13 +225,19 @@ void UnstructuredMesh::to_hdf5(hid_t group) const write_dataset(mesh_group, "volumes", tet_vols); write_dataset(mesh_group, "centroids", centroids); + + if (specified_length_multiplier_) + write_dataset(mesh_group, "length_multiplier", length_multiplier_); + close_group(mesh_group); } void UnstructuredMesh::set_length_multiplier(double length_multiplier) { length_multiplier_ = length_multiplier; - specified_length_multiplier_ = true; + + if (length_multiplier_ != 1.0) + specified_length_multiplier_ = true; } void StructuredMesh::get_indices(Position r, int* ijk, bool* in_mesh) const @@ -1601,11 +1607,7 @@ MOABMesh::MOABMesh(pugi::xml_node node) : UnstructuredMesh(node) MOABMesh::MOABMesh(const std::string& filename, double length_multiplier) { filename_ = filename; - - if (length_multiplier != 1.0) { - set_length_multiplier(length_multiplier); - } - + set_length_multiplier(length_multiplier); initialize(); } @@ -2193,11 +2195,7 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) LibMesh::LibMesh(const std::string& filename, double length_multiplier) { filename_ = filename; - - if (length_multiplier != 1.0) { - set_length_multiplier(length_multiplier); - } - + set_length_multiplier(length_multiplier); initialize(); } From 1c219600a1459374bcd9d2fa6203c36d61075214 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Tue, 21 Sep 2021 18:34:13 +0000 Subject: [PATCH 18/59] Implementation of openmc.stats.Mixture distribution Added to_xml_element python method create nodes with probability and distribution definition in order to have the data bundled together. A different approach might be to save just two lists of "values". Implemented c++ Mixture class Parsing xml-data Sampling the distribution from the cummulative probability table --- include/openmc/distribution.h | 34 ++++++++++++++++++++++++++----- openmc/stats/univariate.py | 23 ++++++++++++++++++++- src/distribution.cpp | 38 +++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 44fd985d2..dcd31f429 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -24,6 +24,14 @@ public: virtual double sample(uint64_t* seed) const = 0; }; +using UPtrDist = unique_ptr; + +//! Return univariate probability distribution specified in XML file +//! \param[in] node XML node representing distribution +//! \return Unique pointer to distribution +UPtrDist distribution_from_xml(pugi::xml_node node); + + //============================================================================== //! A discrete distribution (probability mass function) //============================================================================== @@ -222,12 +230,28 @@ private: vector x_; //! Possible outcomes }; -using UPtrDist = unique_ptr; +//============================================================================== +//! Mixture distribution +//============================================================================== + +class Mixture : public Distribution { +public: + explicit Mixture(pugi::xml_node node); + + //! Sample a value from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample(uint64_t* seed) const; + + // d and p property + const vector& d() const { return d_; } + const vector& p() const { return p_; } +private: + vector d_; //!< Pointer to sub-distributions + vector p_; //!< tabulated probability density + vector c_; //!< cumulative distribution +}; -//! Return univariate probability distribution specified in XML file -//! \param[in] node XML node representing distribution -//! \return Unique pointer to distribution -UPtrDist distribution_from_xml(pugi::xml_node node); } // namespace openmc diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 4bbc50927..c7b6dadbe 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -812,7 +812,28 @@ class Mixture(Univariate): self._distribution = distribution def to_xml_element(self, element_name): - raise NotImplementedError + """Return XML representation of the mixture distribution + + Parameters + ---------- + element_name : str + XML element name + + Returns + ------- + element : xml.etree.ElementTree.Element + XML element containing mixture distribution data + + """ + element = ET.Element(element_name) + element.set("type", "mixture") + + for p,d in zip(self.probability, self.distribution): + data = ET.SubElement(element, "pair") + data.set("probability", str(p)) + data.append(d.to_xml_element("dist")) + + return element @classmethod def from_xml_element(cls, elem): diff --git a/src/distribution.cpp b/src/distribution.cpp index 4713110ab..f473513ba 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -287,6 +287,42 @@ double Equiprobable::sample(uint64_t* seed) const return xl + ((n - 1) * r - i) * (xr - xl); } +//============================================================================== +// Mixture implementation +//============================================================================== + +Mixture::Mixture(pugi::xml_node node) +{ + // Read and initialize tabular distribution + //auto params = get_node_array(node, "parameters"); + + double cumsum = 0.0; + for (pugi::xml_node pair = node.child("pair"); pair; pair = pair.next_sibling("pair")) { + if (!pair.attribute("probability")) openmc::fatal_error("Mixture pair element does not have probability."); + if (!pair.child("dist")) openmc::fatal_error("Mixture pair element does not have a distribution."); + double p = std::stod(pair.attribute("probability").value()); + p_.push_back(p); + cumsum += p; + c_.push_back(cumsum); + d_.push_back(distribution_from_xml(pair.child("dist"))); + } + std::transform(c_.begin(), c_.end(), c_.begin(), [cumsum](double c){ return c/cumsum;}); +} + +double Mixture::sample(uint64_t* seed) const { + // + // Sample value of CDF + double c = prn(seed); + + // Find first CDF bin which is above the sampled value and sample corresponding sub-distribution + for (size_t i = 0; i < c_.size(); i++) { + if (c<=c_[i]) return d_[i]->sample(seed); + } + + // This should not happen. Catch it + openmc::fatal_error("Bad Sampling in Mixture Distribution."); +} + //============================================================================== // Helper function //============================================================================== @@ -315,6 +351,8 @@ UPtrDist distribution_from_xml(pugi::xml_node node) dist = UPtrDist {new Discrete(node)}; } else if (type == "tabular") { dist = UPtrDist {new Tabular(node)}; + } else if (type == "mixture") { + dist = UPtrDist{new Mixture(node)}; } else { openmc::fatal_error("Invalid distribution type: " + type); } From e7fa663b3d7bab2dedc0b2327ac0ffa55588554e Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Thu, 23 Sep 2021 09:34:11 +0000 Subject: [PATCH 19/59] Update mixture distribution update storeage of c++ class update sampling -> uses std::lower_bound for O(log(N)) sampling of the distribution implemented from_xml_element updated settings.rst include a mixture in the regression tests --- docs/source/io_formats/settings.rst | 18 +++++--- include/openmc/distribution.h | 16 ++++--- openmc/stats/univariate.py | 22 +++++++++- src/distribution.cpp | 43 ++++++++++++------- tests/regression_tests/source/inputs_true.dat | 23 ++++++++++ .../regression_tests/source/results_true.dat | 2 +- tests/regression_tests/source/test.py | 4 +- 7 files changed, 98 insertions(+), 30 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 6ea8cfc56..b76b23e63 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -619,16 +619,17 @@ variable and whose sub-elements/attributes are as follows: :type: The type of the distribution. Valid options are "uniform", "discrete", - "tabular", "maxwell", and "watt". 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 + "tabular", "maxwell", "watt", and "mixture". The "uniform" option producess + 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). + fission spectrum (only used for energies). The "mixture" option produces samples + from univariate sub-distributions with given probabilities. *Default*: None @@ -649,6 +650,11 @@ variable and whose sub-elements/attributes are as follows: number :math:`a` that parameterizes the distribution :math:`p(x) dx = c x e^{-x/a} dx`. + For a "mixture" distribution, ``parameters`` provide the :math:'(p,d)' pairs + connecting the probabilites :math:'p' with the different sub-distributions + :math:'d'. All probabilities :math:'p' are given first followed by the corresponding + distributions :math:'d'. + .. note:: The above format should be used even when using the multi-group :ref:`energy_mode`. :interpolation: diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index dcd31f429..30640d07b 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -243,13 +243,17 @@ public: //! \return Sampled value double sample(uint64_t* seed) const; - // d and p property - const vector& d() const { return d_; } - const vector& p() const { return p_; } private: - vector d_; //!< Pointer to sub-distributions - vector p_; //!< tabulated probability density - vector c_; //!< cumulative distribution + struct Pair { + double cummulative_probability_ {0.0}; + UPtrDist distribution_; + // bool operator<(const Pair& o) const { + // return cummulative_probability_ < o.cummulative_probability_; + //} + bool operator<(double p) const { return cummulative_probability_ < p; } + }; + + vector distribution_; //!< sub-distributions + cummulative probabilities }; diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index c7b6dadbe..b5997b1cd 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -837,4 +837,24 @@ class Mixture(Univariate): @classmethod def from_xml_element(cls, elem): - raise NotImplementedError + """Generate mixture distribution from an XML element + + Parameters + ---------- + elem : xml.etree.ElementTree.Element + XML element + + Returns + ------- + openmc.stats.Mixture + Mixture distribution generated from XML element + + """ + P = [] + D = [] + for pair in elem: + if pair.tag == "pair": + P.append( float(get_text(pair, 'probability')) ) + D.append( Univariate.from_xml_element(pair.find("dist")) ) + + return cls(P,D) diff --git a/src/distribution.cpp b/src/distribution.cpp index f473513ba..4b52a35e0 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -297,30 +297,43 @@ Mixture::Mixture(pugi::xml_node node) //auto params = get_node_array(node, "parameters"); double cumsum = 0.0; - for (pugi::xml_node pair = node.child("pair"); pair; pair = pair.next_sibling("pair")) { + for (pugi::xml_node pair = node.child("pair"); pair; + pair = pair.next_sibling("pair")) { + // Check that required data exists if (!pair.attribute("probability")) openmc::fatal_error("Mixture pair element does not have probability."); if (!pair.child("dist")) openmc::fatal_error("Mixture pair element does not have a distribution."); - double p = std::stod(pair.attribute("probability").value()); - p_.push_back(p); - cumsum += p; - c_.push_back(cumsum); - d_.push_back(distribution_from_xml(pair.child("dist"))); + + // cummulative sum of probybilities + cumsum += std::stod(pair.attribute("probability").value()); + + // Save cummulative probybility and distrubution + distribution_.push_back( + {cumsum, distribution_from_xml(pair.child("dist"))}); + } + + // Normalize cummulative probabilities to 1 + for (auto& pair : distribution_) { + pair.cummulative_probability_ /= cumsum; } - std::transform(c_.begin(), c_.end(), c_.begin(), [cumsum](double c){ return c/cumsum;}); } -double Mixture::sample(uint64_t* seed) const { - // +double Mixture::sample(uint64_t* seed) const +{ // Sample value of CDF - double c = prn(seed); + const double p = prn(seed); - // Find first CDF bin which is above the sampled value and sample corresponding sub-distribution - for (size_t i = 0; i < c_.size(); i++) { - if (c<=c_[i]) return d_[i]->sample(seed); - } + // find matching distribution + const auto it = + std::lower_bound(distribution_.cbegin(), distribution_.cend(), p); // This should not happen. Catch it - openmc::fatal_error("Bad Sampling in Mixture Distribution."); + // TODO: Remove this check, when the code is trusted to always operate + if (it == distribution_.cend()) { + fatal_error("Bad Sampling in Mixture Distribution."); + } + + // Sample the choosen distribution + return it->distribution_->sample(seed); } //============================================================================== diff --git a/tests/regression_tests/source/inputs_true.dat b/tests/regression_tests/source/inputs_true.dat index de81a4179..eb60821c6 100644 --- a/tests/regression_tests/source/inputs_true.dat +++ b/tests/regression_tests/source/inputs_true.dat @@ -76,4 +76,27 @@ 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 + + + + diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 66b610b3a..1815324f6 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.033600E-01 1.676108E-03 +2.980096E-01 9.632798E-04 diff --git a/tests/regression_tests/source/test.py b/tests/regression_tests/source/test.py index 91316acc6..229ac8383 100644 --- a/tests/regression_tests/source/test.py +++ b/tests/regression_tests/source/test.py @@ -55,18 +55,20 @@ class SourceTestHarness(PyAPITestHarness): energy1 = openmc.stats.Maxwell(1.2895e6) energy2 = openmc.stats.Watt(0.988e6, 2.249e-6) energy3 = openmc.stats.Tabular(E, p, interpolation='histogram') + energy4 = openmc.stats.Mixture([1,2,3], [energy1, energy2, energy3]) source1 = openmc.Source(spatial1, angle1, energy1, strength=0.5) source2 = openmc.Source(spatial2, angle2, energy2, strength=0.3) source3 = openmc.Source(spatial3, angle3, energy3, strength=0.1) source4 = openmc.Source(spatial4, angle3, energy3, strength=0.1) source5 = openmc.Source(spatial5, angle3, energy3, strength=0.1) + source6 = openmc.Source(spatial5, angle3, energy4, strength=0.1) settings = openmc.Settings() settings.batches = 10 settings.inactive = 5 settings.particles = 1000 - settings.source = [source1, source2, source3, source4, source5] + settings.source = [source1, source2, source3, source4, source5, source6] settings.export_to_xml() From 60b5985746cce01c2542a54a0d2c936c9959981d Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Thu, 23 Sep 2021 09:54:39 +0000 Subject: [PATCH 20/59] update Mixture distribution use std::pair instead of custom type lambda for comparison in std::lower_bound instead of operator< --- include/openmc/distribution.h | 13 ++++--------- src/distribution.cpp | 10 +++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 30640d07b..8cdd78f78 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -244,16 +244,11 @@ public: double sample(uint64_t* seed) const; private: - struct Pair { - double cummulative_probability_ {0.0}; - UPtrDist distribution_; - // bool operator<(const Pair& o) const { - // return cummulative_probability_ < o.cummulative_probability_; - //} - bool operator<(double p) const { return cummulative_probability_ < p; } - }; + // Storrage for probability + distribution + using DistPair = std::pair; - vector distribution_; //!< sub-distributions + cummulative probabilities + vector + distribution_; //!< sub-distributions + cummulative probabilities }; diff --git a/src/distribution.cpp b/src/distribution.cpp index 4b52a35e0..e4775a5b7 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -308,12 +308,12 @@ Mixture::Mixture(pugi::xml_node node) // Save cummulative probybility and distrubution distribution_.push_back( - {cumsum, distribution_from_xml(pair.child("dist"))}); + std::make_pair(cumsum, distribution_from_xml(pair.child("dist")))); } // Normalize cummulative probabilities to 1 for (auto& pair : distribution_) { - pair.cummulative_probability_ /= cumsum; + pair.first /= cumsum; } } @@ -323,8 +323,8 @@ double Mixture::sample(uint64_t* seed) const const double p = prn(seed); // find matching distribution - const auto it = - std::lower_bound(distribution_.cbegin(), distribution_.cend(), p); + 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 // TODO: Remove this check, when the code is trusted to always operate @@ -333,7 +333,7 @@ double Mixture::sample(uint64_t* seed) const } // Sample the choosen distribution - return it->distribution_->sample(seed); + return it->second->sample(seed); } //============================================================================== From 3f24c3d4e19c3d8a132cbdbad8c0c79614299137 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 23 Sep 2021 16:22:42 -0500 Subject: [PATCH 21/59] Added a LIB_INIT module-level flag to openmc.lib. Began process of setting up model to be able to use the C-API instead of only working with XML files --- openmc/deplete/integrators.py | 39 +++++ openmc/deplete/operator.py | 3 +- openmc/lib/__init__.py | 2 + openmc/lib/core.py | 3 + openmc/lib/material.py | 1 - openmc/model/model.py | 280 +++++++++++++++++++++++++++++++--- 6 files changed, 303 insertions(+), 25 deletions(-) diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index fbf43a808..3055561be 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -581,3 +581,42 @@ class SILEQIIntegrator(SIIntegrator): proc_time += time1 + time2 return proc_time, [eos_conc, inter_conc], [res_bar] + + +def integrator_factory(method): + """This method is a factor for the integrator sub-classes + + Params + ------ + method : str + The type of integrator method to use. Valid values are: 'cecm', + 'predictor', 'cf4', 'epc_rk4', 'si_celi', 'si_leqi', 'celi', and 'leqi' + + Returns + ------- + integrator : Integrator + The type of integrator + + """ + + if method == 'cecm': + integrator = CECMIntegrator + elif method == 'predictor': + integrator = PredictorIntegrator + elif method == 'cf4': + integrator = CF4Integrator + elif method == 'epc_rk4': + integrator = EPCRK4Integrator + elif method == 'si_celi': + integrator = SICELIIntegrator + elif method == 'si_leqi': + integrator = SILEQIIntegrator + elif method == 'celi': + integrator = CELIIntegrator + elif method == 'leqi': + integrator = LEQIIntegrator + else: + msg = "Invalid integrator method: {}!".format(method) + raise ValueError(msg) + + return integrator diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 0e5cc9efa..9c67d3bec 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -536,7 +536,8 @@ class Operator(TransportOperator): # Initialize OpenMC library comm.barrier() - openmc.lib.init(intracomm=comm) + if not openmc.lib.LIB_INIT: + openmc.lib.init(intracomm=comm) # Generate tallies in memory materials = [openmc.lib.materials[int(i)] diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 82dc92ba4..7e766a93b 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -59,3 +59,5 @@ from .tally import * from .settings import settings from .math import * from .plot import * + +LIB_INIT = False diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 0da24aab8..e9601c332 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -109,6 +109,7 @@ def global_bounding_box(): return llc, urc + def calculate_volumes(): """Run stochastic volume calculation""" _dll.openmc_calculate_volumes() @@ -147,6 +148,7 @@ def export_properties(filename=None): def finalize(): """Finalize simulation and free memory""" _dll.openmc_finalize() + openmc.lib.LIB_INIT = False def find_cell(xyz): @@ -253,6 +255,7 @@ def init(args=None, intracomm=None): intracomm = c_void_p(address) _dll.openmc_init(argc, argv, intracomm) + openmc.lib.LIB_INIT = True def is_statepoint_batch(): diff --git a/openmc/lib/material.py b/openmc/lib/material.py index 6770721ab..fde197d3d 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -172,7 +172,6 @@ class Material(_FortranObjectWithID): @property def nuclides(self): return self._get_densities()[0] - return nuclides @property def densities(self): diff --git a/openmc/model/model.py b/openmc/model/model.py index d6c91e73e..7f58e9589 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,11 +1,17 @@ from collections.abc import Iterable from pathlib import Path import time +import warnings import h5py +import numpy as np import openmc -from openmc.checkvalue import check_type, check_value +from openmc.checkvalue import check_type, check_value, check_iterable_type, \ + check_length +import openmc.deplete as dep +from openmc.data.library import DataLibrary +from openmc.exceptions import DataError, InvalidIDError, SetupError class Model: @@ -31,6 +37,14 @@ class Model: Tallies information plots : openmc.Plots, optional Plot information + chain_file : str or Path, optional + Path to the depletion chain XML file. Defaults to the chain + found under the ``depletion_chain`` in the + :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. If a + str is provided it will be converted to a Path object. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. + If not given, values will be pulled from the ``chain_file``. Attributes ---------- @@ -44,11 +58,19 @@ class Model: Tallies information plots : openmc.Plots Plot information + chain_file : str or Path + Path to the depletion chain XML file. Defaults to the chain + found under the ``depletion_chain`` in the + :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. If a + str is provided it will be converted to a Path object. + fission_q : dict + Dictionary of nuclides and their fission Q values [eV]. + If not given, values will be pulled from the ``chain_file``. """ def __init__(self, geometry=None, materials=None, settings=None, - tallies=None, plots=None): + tallies=None, plots=None, chain_file=None, fission_q=None): self.geometry = openmc.Geometry() self.materials = openmc.Materials() self.settings = openmc.Settings() @@ -66,6 +88,19 @@ class Model: if plots is not None: self.plots = plots + self.chain_file = chain_file + self.fission_q = fission_q + self.depletion_operator = None + + if self.materials is None: + mats = self.geometry.get_all_materials().values() + else: + mats = self.materials + self._materials_by_id = {mat.id: mat for mat in mats} + cells = self.geometry.get_all_cells() + self._cells_by_id = {cell.id: cell for cell in cells.values()} + self._cells_by_name = {cell.name: cell for cell in cells.values()} + @property def geometry(self): return self._geometry @@ -86,6 +121,22 @@ class Model: def plots(self): return self._plots + @property + def chain_file(self): + return self._chain_file + + @property + def fission_q(self): + return self._fission_q + + @property + def depletion_operator(self): + return self._depletion_operator + + @property + def C_init(self): + return openmc.lib.LIB_INIT + @geometry.setter def geometry(self, geometry): check_type('geometry', geometry, openmc.Geometry) @@ -126,6 +177,25 @@ class Model: for plot in plots: self._plots.append(plot) + @chain_file.setter + def chain_file(self, chain_file): + check_type('chain_file', chain_file, (type(None), str, Path)) + if isinstance(chain_file, str): + self._chain_file = Path(chain_file) + else: + self._chain_file = chain_file + + @fission_q.setter + def fission_q(self, fission_q): + check_type('fission_q', fission_q, (type(None), dict)) + self._fission_q = fission_q + + @depletion_operator.setter + def depletion_operator(self, depletion_operator): + check_type('depletion_operator', depletion_operator, + (type(None), dep.Operator)) + self._depletion_operator = depletion_operator + @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): @@ -151,8 +221,39 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) + def init_C_api(self, use_depletion_operator=False): + """Initializes the model in memory via the C-API + + Parameters + ---------- + directory : str + Directory to write XML files to. If it doesn't exist already, it + will be created. + use_depletion_operator : bool, optional + If True, the model will be loaded using the depletion operator + including all isotopes necessary from fission. This parameter will + use the :attr:`Model.chain_file` and :attr:`Model.fission_q` + attributes. + """ + + if use_depletion_operator: + # Create OpenMC transport operator + self.depletion_operator = \ + dep.Operator(self.geometry, self.settings, + str(self.chain_file), fission_q=self.fission_q) + else: + openmc.lib.hard_reset() + if dep.comm.rank == 0: + self.export_to_xml() + dep.comm.barrier() + openmc.lib.init(intracomm=dep.comm) + + def clear_C_api(self): + """Finalize simulation and free memory allocated for the C-API""" + openmc.lib.finalize() + def deplete(self, timesteps, chain_file=None, method='cecm', - fission_q=None, **kwargs): + fission_q=None, final_step=True, **kwargs): """Deplete model using specified timesteps/power Parameters @@ -169,26 +270,75 @@ class Model: fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. + final_step : bool, optional + Indicate whether or not a transport solve should be run at the end + of the last timestep. + + .. versionadded:: 0.12.3 **kwargs Keyword arguments passed to integration function (e.g., :func:`openmc.deplete.integrator.cecm`) """ - # Import the depletion module. This is done here rather than the module - # header to delay importing openmc.lib (through openmc.deplete) which - # can be tough to install properly. - import openmc.deplete as dep - # Create OpenMC transport operator - op = dep.Operator( - self.geometry, self.settings, chain_file, - fission_q=fission_q, - ) + if self.C_init and self.depletion_operator is not None: + # Then the user has properly initialized the information and we can + # just carry forward + pass + elif self.C_init and self.depletion_operator is None: + # Then the user has initialzed the C-API but without the depletion + # isotopes loaded. We would have to reset and reload data, but + # doing so could lose user information. Therefore let us just + # provide an error and quit. + msg = "Model.deplete(...) cannot be called after " \ + "Model.init_C_api(...) if the use_depletion_operator " \ + "argument to Model.init_C_api(...) is False." + raise SetupError(msg) + else: + # To get here, the C-API is not initialized. So we can do that now + # To keep Model.deplete(...) API compatibility, we will allow the + # chain_file and fission_q params to be set since we havent loaded + # the API anyways + if chain_file is not None: + self.chain_file = chain_file + warnings.warn("The chain_file argument of Model.deplete(...) " + "has been deprecated and may be removed in a " + "future version. The Model.chain_file should be" + "used instead.", DeprecationWarning) + if fission_q is not None: + warnings.warn("The fission_q argument of Model.deplete(...) " + "has been deprecated and may be removed in a " + "future version. The Model.fission_q should be" + "used instead.", DeprecationWarning) + self.fission_q = fission_q + self.init_C_api(use_depletion_operator=True) - # Perform depletion - check_value('method', method, ('cecm', 'predictor', 'cf4', 'epc_rk4', - 'si_celi', 'si_leqi', 'celi', 'leqi')) - getattr(dep.integrator, method)(op, timesteps, **kwargs) + # Set up the integrator + integrator_class = dep.integrators.integrator_factory(method) + integrator = integrator_class(self.depletion_operator, + timesteps, **kwargs) + + # Now perform the depletion + integrator.integrate(final_step) + + # If we did not perform a transport calculation on the final step, then + # make the code update the C-API material inventory + if not final_step: + self.depletion_operator._update_materials() + + # Now make the python Materials match the C-API material data + for mat_id, mat in self._materials_by_id.items(): + if mat.depletable: + # Get the C data + c_mat = openmc.lib.materials[mat_id] + nuclides, densities = c_mat._get_densities() + # And now we can remove isotopes and add these ones in + atom_density = 0. + for nuc, density in zip(nuclides, densities): + mat.remove_nuclide(nuc) # Replace if it's there + mat.add_nuclide(nuc, density) + atom_density += density + mat.set_density('atom/b-cm', atom_density) def export_to_xml(self, directory='.'): """Export model to XML files. @@ -269,13 +419,18 @@ class Model: materials[mat_id].set_density('atom/b-cm', atom_density) def run(self, **kwargs): - """Creates the XML files, runs OpenMC, and returns the path to the last + """Runs OpenMC. If the C-API has been initialized, then the C-API is + used, otherwise, this method creates the XML files and runs OpenMC via + a system cal. In both cases this method returns the path to the last statepoint file generated. .. versionchanged:: 0.12 Instead of returning the final k-effective value, this function now returns the path to the final statepoint written. + .. versionchanged:: 0.12.3 + This method can utilize the C-API for execution + Parameters ---------- **kwargs @@ -289,16 +444,20 @@ class Model: """ - self.export_to_xml() - - # Setting tstart here ensures we don't pick up any pre-existing statepoint - # files in the output directory + # Setting tstart here ensures we don't pick up any pre-existing + # statepoint files in the output directory tstart = time.time() last_statepoint = None - openmc.run(**kwargs) + if self.C_init: + # Then run using the C-API + openmc.lib.run() + else: + # Then run via the command line + self.export_to_xml() + openmc.run(**kwargs) - # Get output directory and return the last statepoint written by this run + # Get output directory and return the last statepoint written this run if self.settings.output and 'path' in self.settings.output: output_dir = Path(self.settings.output['path']) else: @@ -309,3 +468,78 @@ class Model: tstart = mtime last_statepoint = sp return last_statepoint + + def _move_cell(self, cell_names_or_ids, vector, attrib_name): + # Method to do the same work whether it is a rotation or translation + check_type('cell_names_or_ids', cell_names_or_ids, Iterable, + (np.int, int, str)) + check_type('vector', vector, Iterable, (np.float, float)) + check_length('vector', vector, 3) + check_value('attrib_name', attrib_name, ('rotation', 'translation')) + + # Get the list of cell ids to use y converting from names and accepting + # only values that have actual ids + cell_ids = [None] * len(cell_names_or_ids) + for c, cell_name_or_id in enumerate(cell_names_or_ids): + if isinstance(cell_name_or_id, (int, np.int)): + if cell_name_or_id in self._cells_by_id: + cell_ids[c] = int(cell_name_or_id) + msg = 'Cell ID {} is not present in the model!'.format( + cell_name_or_id) + raise InvalidIDError(msg) + elif isinstance(cell_name_or_id, str): + if cell_name_or_id in self._cells_by_name: + cell_ids[c] = self._cells_by_name[cell_name_or_id] + else: + msg = 'Cell {} is not present in the model!'.format( + cell_name_or_id) + raise InvalidIDError(msg) + + # Now perform the motion + for cell_id in cell_ids: + cell = self._cells_by_id[cell_id] + if attrib_name == 'rotation': + cell.rotation = vector + elif attrib_name == 'translation': + cell.translation = vector + # Next lets keep what is in C-API memory up to date as well + if self.C_init: + C_cell = openmc.lib.cells[cell_id] + if attrib_name == 'rotation': + C_cell.rotation = vector + elif attrib_name == 'translation': + C_cell.translation = vector + + def rotate_cells(self, cell_names_or_ids, vector): + """Rotate the identified cell(s) by the specified rotation vector. + The rotation is only applied to cells filled with a universe. + + Parameters + ---------- + cell_names_or_ids : Iterable of str or int + The cell names (if str) or id (if int) that are to be translated + or rotated. This parameter can include a mix of names and ids. + vector : Iterable of float + The rotation vector of length 3 to apply. This array specifies the + angles in degrees about the x, y, and z axes, respectively. + + """ + + self._move_cell(cell_names_or_ids, vector, 'rotation') + + def translate_cells(self, cell_names_or_ids, vector): + """Translate the identified cell(s) by the specified translation vector. + The translation is only applied to cells filled with a universe. + + Parameters + ---------- + cell_names_or_ids : Iterable of str or int + The cell names (if str) or id (if int) that are to be translated + or rotated. This parameter can include a mix of names and ids. + vector : Iterable of float + The translation vector of length 3 to apply. This array specifies + the x, y, and z dimensions of the translation. + + """ + + self._move_cell(cell_names_or_ids, vector, 'translation') From c15564b8260f00ada383d90e654d085b9f26d077 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 24 Sep 2021 16:29:53 -0500 Subject: [PATCH 22/59] Update copyright to include UChicago Argonne, LLC --- LICENSE | 3 ++- docs/source/conf.py | 2 +- docs/source/license.rst | 3 ++- man/man1/openmc.1 | 4 ++-- src/output.cpp | 20 ++++++++++---------- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/LICENSE b/LICENSE index 848a23845..79f0c96f0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Copyright (c) 2011-2021 Massachusetts Institute of Technology and OpenMC contributors +Copyright (c) 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +LLC, and OpenMC 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 diff --git a/docs/source/conf.py b/docs/source/conf.py index 52d872abf..33fb515a5 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-2021, Massachusetts Institute of Technology and OpenMC contributors' +copyright = '2011-2021, 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 40ab106ad..5a3d40763 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,8 @@ License Agreement ================= -Copyright © 2011-2021 Massachusetts Institute of Technology and OpenMC contributors +Copyright © 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +LLC, and OpenMC 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 diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index 8a93af514..989e846e0 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -57,8 +57,8 @@ 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-2021 Massachusetts Institute of Technology and OpenMC -contributors. +Copyright \(co 2011-2021 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 this software and associated documentation files (the "Software"), to deal in diff --git a/src/output.cpp b/src/output.cpp index 73f3a0e9a..b78314823 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -72,26 +72,26 @@ void title() // Write version information fmt::print( - " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2021 MIT and OpenMC contributors\n" - " License | https://docs.openmc.org/en/latest/license.html\n" - " Version | {}.{}.{}{}\n", + " | The OpenMC Monte Carlo Code\n" + " Copyright | 2011-2021 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); + fmt::print(" Git SHA1 | {}\n", GIT_SHA1); #endif // Write the date and time - fmt::print(" Date/Time | {}\n", time_stamp()); + fmt::print(" Date/Time | {}\n", time_stamp()); #ifdef OPENMC_MPI // Write number of processors - fmt::print(" MPI Processes | {}\n", mpi::n_procs); + fmt::print(" MPI Processes | {}\n", mpi::n_procs); #endif #ifdef _OPENMP // Write number of OpenMP threads - fmt::print(" OpenMP Threads | {}\n", omp_get_max_threads()); + fmt::print(" OpenMP Threads | {}\n", omp_get_max_threads()); #endif std::cout << std::endl; } @@ -335,8 +335,8 @@ void print_version() #ifdef GIT_SHA1 fmt::print("Git SHA1: {}\n", GIT_SHA1); #endif - fmt::print("Copyright (c) 2011-2021 Massachusetts Institute of " - "Technology and OpenMC contributors\nMIT/X license at " + fmt::print("Copyright (c) 2011-2021 MIT, UChicago Argonne, LLC, and " + "contributors\nMIT/X license at " "\n"); } } From b0bb2493b989cdaaaadfd218ff9ee0ba4eb859b0 Mon Sep 17 00:00:00 2001 From: agnelson Date: Fri, 24 Sep 2021 16:33:47 -0500 Subject: [PATCH 23/59] Added generic ability to update Models volumes, temperatures, rotations, translations, densities for cells and materials as applicable. Other various changes based on local testing to make sure Model.deplete works and that it works when called in separate instances while keeping the model in C-API memory --- openmc/deplete/operator.py | 4 +- openmc/model/model.py | 292 +++++++++++++++++++++++-------------- 2 files changed, 189 insertions(+), 107 deletions(-) diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 9c67d3bec..8560f89e9 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -202,6 +202,7 @@ class Operator(TransportOperator): self.settings = settings self.geometry = geometry self.diff_burnable_mats = diff_burnable_mats + self.cleanup_when_done = True # Reduce the chain before we create more materials if reduce_chain: @@ -555,7 +556,8 @@ class Operator(TransportOperator): def finalize(self): """Finalize a depletion simulation and release resources.""" - openmc.lib.finalize() + if self.cleanup_when_done: + openmc.lib.finalize() def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" diff --git a/openmc/model/model.py b/openmc/model/model.py index 7f58e9589..d93f60f27 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,4 +1,5 @@ from collections.abc import Iterable +import os from pathlib import Path import time import warnings @@ -90,13 +91,13 @@ class Model: self.chain_file = chain_file self.fission_q = fission_q - self.depletion_operator = None - if self.materials is None: + if materials is None: mats = self.geometry.get_all_materials().values() else: mats = self.materials self._materials_by_id = {mat.id: mat for mat in mats} + self._materials_by_name = {mat.name: mat for mat in mats} cells = self.geometry.get_all_cells() self._cells_by_id = {cell.id: cell for cell in cells.values()} self._cells_by_name = {cell.name: cell for cell in cells.values()} @@ -129,10 +130,6 @@ class Model: def fission_q(self): return self._fission_q - @property - def depletion_operator(self): - return self._depletion_operator - @property def C_init(self): return openmc.lib.LIB_INIT @@ -181,21 +178,15 @@ class Model: def chain_file(self, chain_file): check_type('chain_file', chain_file, (type(None), str, Path)) if isinstance(chain_file, str): - self._chain_file = Path(chain_file) + self._chain_file = Path(chain_file).resolve() else: - self._chain_file = chain_file + self._chain_file = chain_file.resolve() @fission_q.setter def fission_q(self, fission_q): check_type('fission_q', fission_q, (type(None), dict)) self._fission_q = fission_q - @depletion_operator.setter - def depletion_operator(self, depletion_operator): - check_type('depletion_operator', depletion_operator, - (type(None), dep.Operator)) - self._depletion_operator = depletion_operator - @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): @@ -221,28 +212,11 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) - def init_C_api(self, use_depletion_operator=False): - """Initializes the model in memory via the C-API + def init_C_api(self): + """Initializes the model in memory via the C-API""" - Parameters - ---------- - directory : str - Directory to write XML files to. If it doesn't exist already, it - will be created. - use_depletion_operator : bool, optional - If True, the model will be loaded using the depletion operator - including all isotopes necessary from fission. This parameter will - use the :attr:`Model.chain_file` and :attr:`Model.fission_q` - attributes. - """ + self.clear_C_api() - if use_depletion_operator: - # Create OpenMC transport operator - self.depletion_operator = \ - dep.Operator(self.geometry, self.settings, - str(self.chain_file), fission_q=self.fission_q) - else: - openmc.lib.hard_reset() if dep.comm.rank == 0: self.export_to_xml() dep.comm.barrier() @@ -253,7 +227,8 @@ class Model: openmc.lib.finalize() def deplete(self, timesteps, chain_file=None, method='cecm', - fission_q=None, final_step=True, **kwargs): + fission_q=None, final_step=True, directory='.', + **kwargs): """Deplete model using specified timesteps/power Parameters @@ -265,57 +240,63 @@ class Model: Path to the depletion chain XML file. Defaults to the chain found under the ``depletion_chain`` in the :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. - method : str - Integration method used for depletion (e.g., 'cecm', 'predictor') + method : str, optional + Integration method used for depletion (e.g., 'cecm', 'predictor'). + Defaults to 'cecm'. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. + Defaults to pulling from the ``chain_file``. final_step : bool, optional Indicate whether or not a transport solve should be run at the end - of the last timestep. - - .. versionadded:: 0.12.3 + of the last timestep. Defaults to running this transport solve. + directory : str, optional + Directory to write XML files to. If it doesn't exist already, it + will be created. Defaults to the current working directory **kwargs Keyword arguments passed to integration function (e.g., :func:`openmc.deplete.integrator.cecm`) """ - if self.C_init and self.depletion_operator is not None: - # Then the user has properly initialized the information and we can - # just carry forward - pass - elif self.C_init and self.depletion_operator is None: - # Then the user has initialzed the C-API but without the depletion - # isotopes loaded. We would have to reset and reload data, but - # doing so could lose user information. Therefore let us just - # provide an error and quit. - msg = "Model.deplete(...) cannot be called after " \ - "Model.init_C_api(...) if the use_depletion_operator " \ - "argument to Model.init_C_api(...) is False." - raise SetupError(msg) + # To keep Model.deplete(...) API compatibility, we will allow the + # chain_file and fission_q params to be set if provided while we set + # the depletion operator + if chain_file is not None: + this_chain_file = Path(chain_file).resolve() + warnings.warn("The chain_file argument of Model.deplete(...) " + "has been deprecated and may be removed in a " + "future version. The Model.chain_file should be" + "used instead.", DeprecationWarning) else: - # To get here, the C-API is not initialized. So we can do that now - # To keep Model.deplete(...) API compatibility, we will allow the - # chain_file and fission_q params to be set since we havent loaded - # the API anyways - if chain_file is not None: - self.chain_file = chain_file - warnings.warn("The chain_file argument of Model.deplete(...) " - "has been deprecated and may be removed in a " - "future version. The Model.chain_file should be" - "used instead.", DeprecationWarning) - if fission_q is not None: - warnings.warn("The fission_q argument of Model.deplete(...) " - "has been deprecated and may be removed in a " - "future version. The Model.fission_q should be" - "used instead.", DeprecationWarning) - self.fission_q = fission_q - self.init_C_api(use_depletion_operator=True) + this_chain_file = self.chain_file + if fission_q is not None: + warnings.warn("The fission_q argument of Model.deplete(...) " + "has been deprecated and may be removed in a " + "future version. The Model.fission_q should be" + "used instead.", DeprecationWarning) + this_fission_q = fission_q + else: + this_fission_q = fission_q + + # Create directory if required + d = Path(directory) + if not d.is_dir(): + d.mkdir(parents=True) + start_dir = Path.cwd() + os.chdir(d) + + depletion_operator = \ + dep.Operator(self.geometry, self.settings, + str(this_chain_file.absolute()), + fission_q=this_fission_q) + # Tell depletion_operator.finalize NOT to clear C-API memory when it is + # done + depletion_operator.cleanup_when_done = False # Set up the integrator integrator_class = dep.integrators.integrator_factory(method) - integrator = integrator_class(self.depletion_operator, + integrator = integrator_class(depletion_operator, timesteps, **kwargs) # Now perform the depletion @@ -324,7 +305,7 @@ class Model: # If we did not perform a transport calculation on the final step, then # make the code update the C-API material inventory if not final_step: - self.depletion_operator._update_materials() + depletion_operator._update_materials() # Now make the python Materials match the C-API material data for mat_id, mat in self._materials_by_id.items(): @@ -340,6 +321,8 @@ class Model: atom_density += density mat.set_density('atom/b-cm', atom_density) + os.chdir(start_dir) + def export_to_xml(self, directory='.'): """Export model to XML files. @@ -376,6 +359,9 @@ class Model: def import_properties(self, filename): """Import physical properties + .. versionchanged:: 0.12.3 + This method now updates values as loaded in memory with the C-API + Parameters ---------- filename : str @@ -404,6 +390,9 @@ class Model: cell = cells[cell_id] if cell.fill_type in ('material', 'distribmat'): cell.temperature = group['temperature'][()] + if self.C_init: + C_cell = openmc.lib.cells[cell_id] + C_cell.set_temperature(group['temperature'][()]) # Make sure number of materials matches mats_group = fh['materials'] @@ -417,6 +406,9 @@ class Model: mat_id = int(name.split()[1]) atom_density = group.attrs['atom_density'] materials[mat_id].set_density('atom/b-cm', atom_density) + if self.C_init: + C_mat = openmc.lib.materials[mat_id] + C_mat.set_density(atom_density, 'atom/b-cm') def run(self, **kwargs): """Runs OpenMC. If the C-API has been initialized, then the C-API is @@ -469,54 +461,92 @@ class Model: last_statepoint = sp return last_statepoint - def _move_cell(self, cell_names_or_ids, vector, attrib_name): - # Method to do the same work whether it is a rotation or translation - check_type('cell_names_or_ids', cell_names_or_ids, Iterable, - (np.int, int, str)) - check_type('vector', vector, Iterable, (np.float, float)) - check_length('vector', vector, 3) - check_value('attrib_name', attrib_name, ('rotation', 'translation')) + def _change_py_C_attribs(self, names_or_ids, value, obj_type, attrib_name, + density_units='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, (np.int, int, str)) + check_type('obj_type', obj_type, str) + obj_type = obj_type.lower() + check_value('obj_type', obj_type, ('material', 'cell')) + check_value('attrib_name', attrib_name, + ('temperature', 'volume', 'density', 'rotation', + 'translation')) + # The C-API only allows setting density units of atom/b-cm and g/cm3 + check_value('density_units', density_units, ('atom/b-cm', 'g/cm3')) + # The C-API has no way to set cell volume so lets raise an exception + if obj_type == 'cell' and attrib_name == 'volume': + raise NotImplementedError( + 'Setting a Cell volume is not yet supported!') + # And some items just dont make sense + if obj_type == 'cell' and attrib_name == 'density': + raise ValueError('Cannot set a Cell density!') + if obj_type == 'material' and attrib_name in ('rotation', + 'translation'): + raise ValueError('Cannot set a material rotation/translation!') - # Get the list of cell ids to use y converting from names and accepting + # Set the + if obj_type == 'cell': + by_name = self._cells_by_name + by_id = self._cells_by_id + C_by_id = openmc.lib.cells + else: + by_name = self._materials_by_name + by_id = self._materials_by_id + C_by_id = openmc.lib.materials + + # Get the list of ids to use if converting from names and accepting # only values that have actual ids - cell_ids = [None] * len(cell_names_or_ids) - for c, cell_name_or_id in enumerate(cell_names_or_ids): - if isinstance(cell_name_or_id, (int, np.int)): - if cell_name_or_id in self._cells_by_id: - cell_ids[c] = int(cell_name_or_id) - msg = 'Cell ID {} is not present in the model!'.format( - cell_name_or_id) + ids = [None] * len(names_or_ids) + for i, name_or_id in enumerate(names_or_ids): + if isinstance(name_or_id, (int, np.int)): + if name_or_id in by_id: + ids[i] = int(name_or_id) + msg = '{} ID {} is not present in the model!'.format( + obj_type.capitalize(), name_or_id) raise InvalidIDError(msg) - elif isinstance(cell_name_or_id, str): - if cell_name_or_id in self._cells_by_name: - cell_ids[c] = self._cells_by_name[cell_name_or_id] + elif isinstance(name_or_id, str): + if name_or_id in by_name: + ids[i] = by_name[name_or_id].id else: - msg = 'Cell {} is not present in the model!'.format( - cell_name_or_id) + msg = '{} {} is not present in the model!'.format( + obj_type.capitalize(), name_or_id) raise InvalidIDError(msg) - # Now perform the motion - for cell_id in cell_ids: - cell = self._cells_by_id[cell_id] + # Now perform the change to both python and C-API + for id_ in ids: + obj = by_id[id_] if attrib_name == 'rotation': - cell.rotation = vector + obj.rotation = value elif attrib_name == 'translation': - cell.translation = vector + obj.translation = value + elif attrib_name == 'volume': + obj.volume = value + elif attrib_name == 'temperature': + obj.temperature = value + elif attrib_name == 'density': + obj.set_density(value, density_units) # Next lets keep what is in C-API memory up to date as well if self.C_init: - C_cell = openmc.lib.cells[cell_id] + C_obj = C_by_id[id_] if attrib_name == 'rotation': - C_cell.rotation = vector + C_obj.rotation = value elif attrib_name == 'translation': - C_cell.translation = vector + C_obj.translation = value + elif attrib_name == 'volume': + C_obj.set_volume = value + elif attrib_name == 'temperature': + C_obj.set_temperature = value + elif attrib_name == 'density': + C_obj.set_density(value, density_units) - def rotate_cells(self, cell_names_or_ids, vector): + def rotate_cells(self, names_or_ids, vector): """Rotate the identified cell(s) by the specified rotation vector. The rotation is only applied to cells filled with a universe. Parameters ---------- - cell_names_or_ids : Iterable of str or int + names_or_ids : Iterable of str or int The cell names (if str) or id (if int) that are to be translated or rotated. This parameter can include a mix of names and ids. vector : Iterable of float @@ -525,15 +555,15 @@ class Model: """ - self._move_cell(cell_names_or_ids, vector, 'rotation') + self._change_py_C_attribs(names_or_ids, vector, 'cell', 'rotation') - def translate_cells(self, cell_names_or_ids, vector): + def translate_cells(self, names_or_ids, vector): """Translate the identified cell(s) by the specified translation vector. The translation is only applied to cells filled with a universe. Parameters ---------- - cell_names_or_ids : Iterable of str or int + names_or_ids : Iterable of str or int The cell names (if str) or id (if int) that are to be translated or rotated. This parameter can include a mix of names and ids. vector : Iterable of float @@ -542,4 +572,54 @@ class Model: """ - self._move_cell(cell_names_or_ids, vector, 'translation') + self._change_py_C_attribs(names_or_ids, vector, 'cell', 'translation') + + def update_densities(self, names_or_ids, density, density_units): + """Update the density of a given set of materials to a new value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The material names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + density : float + The density to apply in the units specified by `density_units` + density_units : {'atom/b-cm', 'g/cm3'} + Units for `density` + + """ + + self._change_py_C_attribs(names_or_ids, density, 'material', 'density', + density_units) + + def update_cell_temperatures(self, names_or_ids, temperature): + """Update the temperature of a set of cells to the given value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The cell names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + temperature : float + The temperature to apply in units of Kelvin + + """ + + self._change_py_C_attribs(names_or_ids, temperature, 'cell', + 'temperature') + + def update_material_temperatures(self, names_or_ids, temperature): + """Update the temperature of a set of materials to the given value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The material names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + temperature : float + The temperature to apply in units of Kelvin + + """ + + self._change_py_C_attribs(names_or_ids, temperature, 'material', + 'temperature') From 24bca0631099a594e44afeee620e7c093b60f38b Mon Sep 17 00:00:00 2001 From: agnelson Date: Sat, 25 Sep 2021 08:17:02 -0500 Subject: [PATCH 24/59] Correcting application of Path.resolve --- openmc/model/model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index d93f60f27..ae0936183 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -179,8 +179,10 @@ class Model: check_type('chain_file', chain_file, (type(None), str, Path)) if isinstance(chain_file, str): self._chain_file = Path(chain_file).resolve() - else: + elif isinstance(chain_file, Path): self._chain_file = chain_file.resolve() + else: + self._chain_file = None @fission_q.setter def fission_q(self, fission_q): From e63c414e219c781e9dad8934ca124c4ea4ad0daf Mon Sep 17 00:00:00 2001 From: April Novak Date: Sat, 25 Sep 2021 09:18:31 -0500 Subject: [PATCH 25/59] Apply suggestions from code review Co-authored-by: Patrick Shriwise --- openmc/mesh.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 6dde9277c..372069cbb 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -215,7 +215,8 @@ class RegularMesh(MeshBase): mesh.lower_left = group['lower_left'][()] mesh.upper_right = group['upper_right'][()] mesh.width = group['width'][()] - mesh.length_multiplier = group['length_multiplier'][()] if 'length_multiplier' in group else 1.0 + if 'length_multiplier' in group: + mesh.length_multiplier = group['length_multiplier'][()] return mesh @@ -647,7 +648,8 @@ class UnstructuredMesh(MeshBase): An iterable of element centroid coordinates, e.g. [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0), ...] """ - def __init__(self, filename, library, mesh_id=None, name='', length_multiplier=1.0): + def __init__(self, filename, library, mesh_id=None, name='', + length_multiplier=1.0): super().__init__(mesh_id, name) self.filename = filename self._volumes = None @@ -729,7 +731,9 @@ class UnstructuredMesh(MeshBase): @length_multiplier.setter def length_multiplier(self, length_multiplier): - cv.check_type("Unstructured mesh length multiplier", length_multiplier, Real) + cv.check_type("Unstructured mesh length multiplier", + length_multiplier, + Real) self._length_multiplier = length_multiplier if (self._length_multiplier != 1.0): From 0d2f923be804cde98b8184d1660ced09926bdc33 Mon Sep 17 00:00:00 2001 From: agnelson Date: Mon, 27 Sep 2021 08:18:54 -0500 Subject: [PATCH 26/59] Added tests of the openmc.Model class. Still need to do tests of deplete and the methods which change python and C attributes. Pushing this now to verify the CI shows passes for the MPI configs --- openmc/model/model.py | 6 +- tests/unit_tests/conftest.py | 120 +++++++++++++++++++++ tests/unit_tests/test_model.py | 185 ++++++++++++++++++++++++++++++++- 3 files changed, 308 insertions(+), 3 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index ae0936183..aa00d40b4 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -193,6 +193,8 @@ class Model: def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): """Create model from existing XML files + When initializing this way, the user must manually load plots, tallies, + the chain_file and fission_q attributes. Parameters ---------- @@ -428,7 +430,9 @@ class Model: Parameters ---------- **kwargs - Keyword arguments passed to :func:`openmc.run` + Keyword arguments passed to :func:`openmc.run`. Note that these are + ignored if running via the C-API. Instead the parameters should be + set via the Settings object. Returns ------- diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 51d1b19a3..a1c4a24da 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -1,9 +1,129 @@ +from math import pi import openmc import pytest from tests.regression_tests import config +@pytest.fixture(scope='module') +def pin_model_attributes(): + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.29769) + uo2.add_element('U', 1., enrichment=2.4) + uo2.add_element('O', 2.) + + zirc = openmc.Material(name='Zirc') + zirc.set_density('g/cm3', 6.55) + zirc.add_element('Zr', 1.) + + borated_water = openmc.Material(name='Borated water') + borated_water.set_density('g/cm3', 0.740582) + borated_water.add_element('B', 4.0e-5) + 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') + + mats = openmc.Materials([uo2, zirc, borated_water]) + + pitch = 1.25984 + fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') + clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') + box = openmc.model.rectangular_prism(pitch, pitch, boundary_type='reflective') + + # Define cells + fuel = openmc.Cell(name='fuel', fill=uo2, region=-fuel_or) + clad = openmc.Cell(fill=zirc, region=+fuel_or & -clad_or) + water = openmc.Cell(fill=borated_water, region=+clad_or & box) + + # Define overall geometry + geom = openmc.Geometry([fuel, clad, water]) + uo2.volume = pi * fuel_or.r**2 + + settings = openmc.Settings() + settings.batches = 100 + settings.inactive = 10 + 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.source.Source(space=uniform_dist) + + entropy_mesh = openmc.RegularMesh() + entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] + entropy_mesh.upper_right = [0.39218, 0.39218, 1.e50] + entropy_mesh.dimension = [10, 10, 1] + settings.entropy_mesh = entropy_mesh + + tals = openmc.Tallies() + tal = openmc.Tally(name='test') + tal.scores = ['flux'] + tals.append(tal) + + plot = openmc.Plot() + plot.origin = (0., 0., 0.) + plot.width = (pitch, pitch) + plot.pixels = (300, 300) + plot.color_by = 'material' + plots = openmc.Plots((plot,)) + + chain = './chain_simple.xml' + fission_q = {'U235': 200e6} + + chain_file_xml = """ + + + + + + + + + + + + + + + + + + + + + 2.53000e-02 + + Gd157 Gd156 I135 Xe135 Xe136 Cs135 + 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 4.356330e-05 + + + + + + + 2.53000e-02 + + Gd157 Gd156 I135 Xe135 Xe136 Cs135 + 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 4.9097e-6 + + + + + + + 2.53000e-02 + + Gd157 Gd156 I135 Xe135 Xe136 Cs135 + 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 3.7100E-07 + + + + +""" + + return (mats, geom, settings, tals, plots, chain, fission_q, + chain_file_xml) + @pytest.fixture(scope='module') def mpi_intracomm(): if config['mpi']: diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4b658d5ab..b616856b0 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,18 +1,162 @@ +from typing import Type import pytest +from pathlib import Path import openmc import openmc.lib +def test_init(run_in_tmpdir, pin_model_attributes): + mats, geom, settings, tals, plots, chain, fission_q, _ = \ + pin_model_attributes + + openmc.reset_auto_ids() + # Check blank initialization of a model + test_model = openmc.Model() + assert test_model.geometry.root_universe is None + assert len(test_model.materials) == 0 + ref_settings = openmc.Settings() + assert sorted(test_model.settings.__dict__.keys()) == \ + sorted(ref_settings.__dict__.keys()) + for ref_k, ref_v in ref_settings.__dict__.items(): + assert test_model.settings.__dict__[ref_k] == ref_v + assert len(test_model.tallies) == 0 + assert len(test_model.plots) == 0 + assert test_model.chain_file is None + assert test_model.fission_q is None + assert test_model._materials_by_id == {} + assert test_model._materials_by_name == {} + assert test_model._cells_by_id == {} + assert test_model._cells_by_name == {} + assert test_model.C_init is False + + # Now check proper init of an actual model. Assume no interference between + # parameters and so we can apply them all at once instead of testing one + # parameter initialization at a time + test_model = openmc.Model(geom, mats, settings, tals, plots, chain, + fission_q) + assert test_model.geometry is geom + assert test_model.materials is mats + assert test_model.settings is settings + assert test_model.tallies is tals + assert test_model.plots is plots + assert test_model.chain_file == Path(chain).resolve() + assert test_model.fission_q is fission_q + assert test_model._materials_by_id == {1: mats[0], 2: mats[1], 3: mats[2]} + assert test_model._materials_by_name == {'UO2': mats[0], 'Zirc': mats[1], + 'Borated water': mats[2]} + assert test_model._cells_by_id == {1: geom.root_universe.cells[1], + 2: geom.root_universe.cells[2], + 3: geom.root_universe.cells[3]} + # No cell name for 2 and 3, so we expect a blank name to be assigned to + # cell 3 due to overwriting + assert test_model._cells_by_name == { + 'fuel': geom.root_universe.cells[1], '': geom.root_universe.cells[3]} + assert test_model.C_init is False + + # Finally test the parameter type checking by passing bad types and + # obtaining the right exception types + def_params = [geom, mats, settings, tals, plots, chain, fission_q] + for i in range(len(def_params)): + args = def_params.copy() + # Try an integer, as that is a bad type for all arguments + args[i] = i + with pytest.raises(TypeError): + test_model = openmc.Model(*args) + + +def test_from_xml(run_in_tmpdir, pin_model_attributes): + mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes + + # This test will write the individual files to xml and then init that way + # and run the same sort of test as in test_init + mats.export_to_xml() + geom.export_to_xml() + settings.export_to_xml() + tals.export_to_xml() + plots.export_to_xml() + + # This from_xml method cannot load chain and fission_q + test_model = openmc.Model.from_xml() + assert test_model.geometry.root_universe.cells.keys() == \ + geom.root_universe.cells.keys() + assert [c.fill.name for c in test_model.geometry.root_universe.cells.values()] == \ + [c.fill.name for c in geom.root_universe.cells.values()] + assert [mat.name for mat in test_model.materials] == [mat.name for mat in mats] + # We will assume the attributes of settings that are custom objects are + # OK if the others are so we dotn need to implement explicit comparisons + no_test = ['_source', '_entropy_mesh'] + assert sorted(k for k in test_model.settings.__dict__.keys() if k not in no_test) == \ + sorted(k for k in settings.__dict__.keys() if k not in no_test) + keys = sorted(k for k in settings.__dict__.keys() if k not in no_test) + for ref_k in keys: + assert test_model.settings.__dict__[ref_k] == settings.__dict__[ref_k] + assert len(test_model.tallies) == 0 + assert len(test_model.plots) == 0 + assert test_model.chain_file is None + assert test_model.fission_q is None + assert test_model._materials_by_id == \ + {1: test_model.materials[0], 2: test_model.materials[1], + 3: test_model.materials[2]} + assert test_model._materials_by_name == { + 'UO2': test_model.materials[0], 'Zirc': test_model.materials[1], + 'Borated water': test_model.materials[2]} + assert test_model._cells_by_id == {\ + 1: test_model.geometry.root_universe.cells[1], + 2: test_model.geometry.root_universe.cells[2], + 3: test_model.geometry.root_universe.cells[3]} + # No cell name for 2 and 3, so we expect a blank name to be assigned to + # cell 3 due to overwriting + assert test_model._cells_by_name == { + 'fuel': test_model.geometry.root_universe.cells[1], + '': test_model.geometry.root_universe.cells[3]} + assert test_model.C_init is False + + +def test_init_clear_C_api(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + # We are going to init and then make sure data is loaded + mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots) + # Set the depletion module to use the test intracomm as the depletion mods + # intracomm is the one that init uses. We will not perturb system state + # outside of this test by re-setting the openmc.deplete.comm to what it was + # before when we exist + if mpi_intracomm is not None: + orig_comm = openmc.deplete.comm + openmc.deplete.comm = mpi_intracomm + test_model.init_C_api() + + # First check that the API is advertised as initialized + assert openmc.lib.LIB_INIT is True + assert test_model.C_init is True + # Now make sure it actually is initialized by making a call to the lib + c_mat = openmc.lib.find_material((0.6, 0., 0.)) + # This should be Borated water + assert c_mat.name == 'Borated water' + assert c_mat.id == 3 + + # Ok, now lets test that we can clear the data and check that it is cleared + test_model.clear_C_api() + + # First check that the API is advertised as initialized + assert openmc.lib.LIB_INIT is False + assert test_model.C_init is False + # Note we cant actually test that a sys call fails because we should get a + # seg fault + + # And before done, reset the deplete communicator + if mpi_intracomm is not None: + openmc.deplete.comm = orig_comm + + def test_import_properties(run_in_tmpdir, mpi_intracomm): """Test importing properties on the Model class """ # Create PWR pin cell model and write XML files openmc.reset_auto_ids() model = openmc.examples.pwr_pin_cell() - model.export_to_xml() + model.init_C_api() # Change fuel temperature and density and export properties - openmc.lib.init(intracomm=mpi_intracomm) cell = openmc.lib.cells[1] cell.set_temperature(600.0) cell.fill.set_density(5.0, 'g/cm3') @@ -32,3 +176,40 @@ 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.fill.get_mass_density() == pytest.approx(5.0) + + +def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots) + # Set the depletion module to use the test intracomm as the depletion mods + # intracomm is the one that init uses. We will not perturb system state + # outside of this test by re-setting the openmc.deplete.comm to what it was + # before when we exist + + # This case will run by getting the k-eff and tallies for command-line and + # C-API execution modes and ensuring they give the same result. + sp_path = test_model.run(output=False) + with openmc.StatePoint(sp_path) as sp: + cli_keff = sp.k_combined + cli_flux = sp.get_tally(id=1).get_values()[0, 0, 0] + + if mpi_intracomm is not None: + orig_comm = openmc.deplete.comm + openmc.deplete.comm = mpi_intracomm + test_model.settings.verbosity = 1 + test_model.init_C_api() + sp_path = test_model.run() + with openmc.StatePoint(sp_path) as sp: + C_keff = sp.k_combined + C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] + test_model.clear_C_api() + + # and lets compare results + assert (C_keff - cli_keff) < 1e-15 + assert (C_flux - cli_flux) < 1e-15 + + # And before done, reset the deplete communicator + if mpi_intracomm is not None: + openmc.deplete.comm = orig_comm + + From f03b98ac0f3429468efa7c7c40df35ae0fceb280 Mon Sep 17 00:00:00 2001 From: agnelson Date: Mon, 27 Sep 2021 12:32:25 -0500 Subject: [PATCH 27/59] Updating model.init_C_api and model.run to handle the kwargs that have been assigned to model.run for the CLI interface. Also updated tests --- openmc/model/model.py | 120 +++++++++++++++++++++++++++++++-- tests/unit_tests/test_model.py | 15 +++-- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index aa00d40b4..3b7a64da9 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,6 +1,7 @@ from collections.abc import Iterable import os from pathlib import Path +from numbers import Integral import time import warnings @@ -216,15 +217,62 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) - def init_C_api(self): - """Initializes the model in memory via the C-API""" + def init_C_api(self, threads=None, geometry_debug=False, restart_file=None, + tracks=False, output=True, event_based=False): + """Initializes the model in memory via the C-API + + Parameters + ---------- + threads : int, optional + Number of OpenMP threads. If OpenMC is compiled with OpenMP threading + enabled, the default is implementation-dependent but is usually equal to + the number of hardware threads available (or a value set by the + :envvar:`OMP_NUM_THREADS` environment variable). + geometry_debug : bool, optional + Turn on geometry debugging during simulation. Defaults to False. + restart_file : str, optional + Path to restart file to use + tracks : bool, optional + Write tracks for all particles. Defaults to False. + output : bool + Capture OpenMC output from standard out + event_based : bool, optional + Turns on event-based parallelism, instead of default history-based + """ + + # TODO: right now the only way to set most of the above parameters via + # the C-API are at initialization time despite use-cases existing to + # set them for individual runs. For now this functionality is exposed + # where it exists (here in init), but in the future the functionality + # should be exposed so that it can be accessed via model.run(...) + + # TODO: the output flag is not yet implemented for the openmc.lib.init + # command. This will be the subject of future work. + + args = [] + + if isinstance(threads, Integral) and threads > 0: + args += ['-s', str(threads)] + + if geometry_debug: + args.append('-g') + + if event_based: + args.append('-e') + + if isinstance(restart_file, str): + args += ['-r', restart_file] + + if tracks: + args.append('-t') self.clear_C_api() if dep.comm.rank == 0: self.export_to_xml() dep.comm.barrier() - openmc.lib.init(intracomm=dep.comm) + # TODO: Implement the output flag somewhere on the C++ side + openmc.lib.init(args=args, intracomm=dep.comm) def clear_C_api(self): """Finalize simulation and free memory allocated for the C-API""" @@ -429,10 +477,31 @@ class Model: Parameters ---------- - **kwargs - Keyword arguments passed to :func:`openmc.run`. Note that these are - ignored if running via the C-API. Instead the parameters should be - set via the Settings object. + particles : int, optional + Number of particles to simulate per generation. + threads : int, optional + Number of OpenMP threads. If OpenMC is compiled with OpenMP + threading enabled, the default is implementation-dependent but is + usually equal to the number of hardware threads available (or a + value set by the :envvar:`OMP_NUM_THREADS` environment variable). + geometry_debug : bool, optional + Turn on geometry debugging during simulation. Defaults to False. + restart_file : str, optional + Path to restart file to use + tracks : bool, optional + Write tracks for all particles. Defaults to False. + output : bool + Capture OpenMC output from standard out + cwd : str, optional + Path to working directory to run in. Defaults to the current + working directory. + openmc_exec : str, optional + Path to OpenMC executable. Defaults to 'openmc'. + mpi_args : list of str, optional + MPI execute command and any additional MPI arguments to pass, + e.g. ['mpiexec', '-n', '8']. + event_based : bool, optional + Turns on event-based parallelism, instead of default history-based Returns ------- @@ -448,8 +517,45 @@ class Model: last_statepoint = None if self.C_init: + # Handle the openmc.run kwargs + # First dont allow ones that must be set via init + via_args = ['threads', 'geometry_debug', 'restart_file', 'tracks', + 'cwd'] + provided_args = [k for k in kwargs.keys()] + if any([key in via_args for key in provided_args]): + msg = "Argument {} must be set via Model.c_init(...)" + raise ValueError(msg) + + if 'particles' in kwargs: + init_particles = openmc.lib.settings.particles + if isinstance(kwargs['particles'], Integral) and \ + kwargs['particles'] > 0: + openmc.lib.settings.particles = kwargs['particles'] + + # Now lets handle the ones we can handle + if 'output' in kwargs: + init_verbosity = openmc.lib.settings.verbosity + if not kwargs['output']: + openmc.lib.settings.verbosity = 1 + + # Event-based can be set at init-time or on a case-basis. Handle + # the case-basis here. + if 'event_based' in kwargs: + # TODO: However, the event based flag isnt exposed in the C-API + # yet. Support for actual handling will be added in future work + msg = "Setting event-based mode directly via the C-API is " \ + "not yet implemented" + raise NotImplementedError(msg) + # Then run using the C-API openmc.lib.run() + + # Reset changes for the openmc.run kwargs handling + if 'particles' in kwargs: + openmc.lib.settings.particles = init_particles + if 'output' in kwargs: + openmc.lib.settings.verbosity = init_verbosity + else: # Then run via the command line self.export_to_xml() diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index b616856b0..0c389ac62 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -147,6 +147,8 @@ def test_init_clear_C_api(run_in_tmpdir, pin_model_attributes, mpi_intracomm): if mpi_intracomm is not None: openmc.deplete.comm = orig_comm + # TODO: in above test, tests are necessary for all the arguments of init + def test_import_properties(run_in_tmpdir, mpi_intracomm): """Test importing properties on the Model class """ @@ -196,17 +198,22 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): if mpi_intracomm is not None: orig_comm = openmc.deplete.comm openmc.deplete.comm = mpi_intracomm - test_model.settings.verbosity = 1 test_model.init_C_api() - sp_path = test_model.run() + sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: C_keff = sp.k_combined C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] - test_model.clear_C_api() # and lets compare results assert (C_keff - cli_keff) < 1e-15 - assert (C_flux - cli_flux) < 1e-15 + assert (C_flux - cli_flux) < 1e-13 + + # Now we should make sure the event-based flag gives us our not implemented + # error + with pytest.raises(NotImplementedError): + test_model.run(output=False, event_based=True) + + test_model.clear_C_api() # And before done, reset the deplete communicator if mpi_intracomm is not None: From d45af5a4ef1c47c5a747c208e0dd85e998ef9081 Mon Sep 17 00:00:00 2001 From: agnelson Date: Mon, 27 Sep 2021 13:45:31 -0500 Subject: [PATCH 28/59] Changes to the model.run interface again for clarity and for test passing --- openmc/model/model.py | 47 +++++++++++++++++----------------- tests/unit_tests/test_model.py | 16 +++++++++--- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 3b7a64da9..84174eb5e 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -462,10 +462,12 @@ class Model: C_mat = openmc.lib.materials[mat_id] C_mat.set_density(atom_density, 'atom/b-cm') - def run(self, **kwargs): + 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=False): """Runs OpenMC. If the C-API has been initialized, then the C-API is used, otherwise, this method creates the XML files and runs OpenMC via - a system cal. In both cases this method returns the path to the last + a system call. In both cases this method returns the path to the last statepoint file generated. .. versionchanged:: 0.12 @@ -490,7 +492,7 @@ class Model: Path to restart file to use tracks : bool, optional Write tracks for all particles. Defaults to False. - output : bool + output : bool, optional Capture OpenMC output from standard out cwd : str, optional Path to working directory to run in. Defaults to the current @@ -519,47 +521,44 @@ class Model: if self.C_init: # Handle the openmc.run kwargs # First dont allow ones that must be set via init - via_args = ['threads', 'geometry_debug', 'restart_file', 'tracks', - 'cwd'] - provided_args = [k for k in kwargs.keys()] - if any([key in via_args for key in provided_args]): - msg = "Argument {} must be set via Model.c_init(...)" - raise ValueError(msg) + for arg_name, arg, default in zip( + ['threads', 'geometry_debug', 'restart_file', 'tracks', 'cwd'], + [threads, geometry_debug, restart_file, tracks, cwd], + [None, False, None, False, '.']): + if arg != default: + msg = "{} must be set via Model.c_init(...)".format( + arg_name) + raise ValueError(msg) - if 'particles' in kwargs: + if particles is not None: init_particles = openmc.lib.settings.particles - if isinstance(kwargs['particles'], Integral) and \ - kwargs['particles'] > 0: - openmc.lib.settings.particles = kwargs['particles'] + if isinstance(particles, Integral) and particles > 0: + openmc.lib.settings.particles = particles # Now lets handle the ones we can handle - if 'output' in kwargs: + if not output: init_verbosity = openmc.lib.settings.verbosity - if not kwargs['output']: + if not output: openmc.lib.settings.verbosity = 1 # Event-based can be set at init-time or on a case-basis. Handle # the case-basis here. - if 'event_based' in kwargs: - # TODO: However, the event based flag isnt exposed in the C-API - # yet. Support for actual handling will be added in future work - msg = "Setting event-based mode directly via the C-API is " \ - "not yet implemented" - raise NotImplementedError(msg) + # TODO This will be dealt with in a future change to the C-API # Then run using the C-API openmc.lib.run() # Reset changes for the openmc.run kwargs handling - if 'particles' in kwargs: + if particles is not None: openmc.lib.settings.particles = init_particles - if 'output' in kwargs: + if output is not None: openmc.lib.settings.verbosity = init_verbosity else: # Then run via the command line self.export_to_xml() - openmc.run(**kwargs) + openmc.run(particles, threads, geometry_debug, restart_file, + tracks, output, cwd, openmc_exec, mpi_args, event_based) # Get output directory and return the last statepoint written this run if self.settings.output and 'path' in self.settings.output: diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 0c389ac62..a068ef781 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -208,10 +208,18 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert (C_keff - cli_keff) < 1e-15 assert (C_flux - cli_flux) < 1e-13 - # Now we should make sure the event-based flag gives us our not implemented - # error - with pytest.raises(NotImplementedError): - test_model.run(output=False, event_based=True) + # Now we should make sure that the flags for items which should be handled + # by init are properly set + with pytest.raises(ValueError): + test_model.run(threads=1) + with pytest.raises(ValueError): + test_model.run(geometry_debug=True) + with pytest.raises(ValueError): + test_model.run(restart_file='1.h5') + with pytest.raises(ValueError): + test_model.run(tracks=True) + with pytest.raises(ValueError): + test_model.run(cwd='hi') test_model.clear_C_api() From e62ba9b85af208d15cb959cd0c7d83b3724f15d4 Mon Sep 17 00:00:00 2001 From: agnelson Date: Tue, 28 Sep 2021 08:44:31 -0500 Subject: [PATCH 29/59] Upgrading tests of model, and adding better file management, Model.calculate_volumes method and Model.plot_geometry method. These last two still need to be tested as well as Model.deplete --- openmc/executor.py | 15 ++ openmc/lib/core.py | 2 +- openmc/model/model.py | 282 ++++++++++++++++++++++++++------- tests/unit_tests/conftest.py | 7 +- tests/unit_tests/test_model.py | 177 ++++++++++++++++++--- 5 files changed, 396 insertions(+), 87 deletions(-) diff --git a/openmc/executor.py b/openmc/executor.py index e8f0de2c7..de5604c9d 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -1,10 +1,25 @@ from collections.abc import Iterable from numbers import Integral import subprocess +from contextlib import contextmanager +from pathlib import Path +import os import openmc +@contextmanager +def change_directory(working_dir): + """A context manager for executing in a provided working directory""" + start_dir = Path().absolute() + try: + Path.mkdir(working_dir, exist_ok=True) + os.chdir(working_dir) + yield + finally: + os.chdir(start_dir) + + def _run(args, output, cwd): # Launch a subprocess p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, diff --git a/openmc/lib/core.py b/openmc/lib/core.py index e9601c332..03c6c41fc 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -215,7 +215,7 @@ def import_properties(filename): See Also -------- openmc.lib.export_properties - +mat """ _dll.openmc_properties_import(filename.encode()) diff --git a/openmc/model/model.py b/openmc/model/model.py index 84174eb5e..e32876a6c 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -4,6 +4,7 @@ from pathlib import Path from numbers import Integral import time import warnings +import subprocess import h5py import numpy as np @@ -450,8 +451,8 @@ class Model: mats_group = fh['materials'] n_cells = mats_group.attrs['n_materials'] if n_cells != len(materials): - raise ValueError("Number of materials in properties file doesn't " - "match current model.") + raise ValueError("Number of materials in properties file " + "doesn't match current model.") # Update material densities for name, group in mats_group.items(): @@ -518,65 +519,208 @@ class Model: tstart = time.time() last_statepoint = None - if self.C_init: - # Handle the openmc.run kwargs - # First dont allow ones that must be set via init - for arg_name, arg, default in zip( - ['threads', 'geometry_debug', 'restart_file', 'tracks', 'cwd'], - [threads, geometry_debug, restart_file, tracks, cwd], - [None, False, None, False, '.']): - if arg != default: - msg = "{} must be set via Model.c_init(...)".format( - arg_name) + # Operate in the provided working directory + with openmc.change_directory(Path(cwd)): + if self.C_init: + # Handle the run options as applicable + # First dont allow ones that must be set via init + for arg_name, arg, default in zip( + ['threads', 'geometry_debug', 'restart_file', 'tracks'], + [threads, geometry_debug, restart_file, tracks], + [None, False, None, False]): + if arg != default: + msg = "{} must be set via Model.c_init(...)".format( + arg_name) + raise ValueError(msg) + + if particles is not None: + init_particles = openmc.lib.settings.particles + if isinstance(particles, Integral) and particles > 0: + openmc.lib.settings.particles = particles + + # If we dont want output, make the verbosity quiet + if not output: + init_verbosity = openmc.lib.settings.verbosity + if not output: + openmc.lib.settings.verbosity = 1 + + # Event-based can be set at init-time or on a case-basis. + # Handle the argument here. + # TODO This will be dealt with in a future change to the C-API + + # Then run using the C-API + openmc.lib.run() + + # Reset changes for the openmc.run kwargs handling + if particles is not None: + openmc.lib.settings.particles = init_particles + if not output: + # Then re-set the initial verbosity + openmc.lib.settings.verbosity = init_verbosity + + else: + # Then run via the command line + self.export_to_xml() + openmc.run(particles, threads, geometry_debug, restart_file, + tracks, output, Path('.'), openmc_exec, mpi_args, + event_based) + + # Get output directory and return the last statepoint written + if self.settings.output and 'path' in self.settings.output: + output_dir = Path(self.settings.output['path']) + else: + output_dir = Path.cwd() + for sp in output_dir.glob('statepoint.*.h5'): + mtime = sp.stat().st_mtime + if mtime >= tstart: # >= allows for poor clock resolution + tstart = mtime + last_statepoint = sp + return last_statepoint + + def calculate_volume(self, threads=None, output=True, cwd='.', + openmc_exec='openmc', mpi_args=None, + apply_volumes=True): + """Runs an OpenMC stochastic volume calculation and, if requested, + applies volumes to the model + + Parameters + ---------- + threads : int, optional + Number of OpenMP threads. If OpenMC is compiled with OpenMP + threading enabled, the default is implementation-dependent but is + usually equal to the number of hardware threads available (or a + value set by the :envvar:`OMP_NUM_THREADS` environment variable). + This currenty only applies to the case when not using the C-API. + output : bool, optional + Capture OpenMC output from standard out + openmc_exec : str, optional + Path to OpenMC executable. Defaults to 'openmc'. + This only applies to the case when not using the C-API. + mpi_args : list of str, optional + MPI execute command and any additional MPI arguments to pass, + e.g. ['mpiexec', '-n', '8']. + This only applies to the case when not using the C-API. + cwd : str, optional + Path to working directory to run in. Defaults to the current + working directory. + apply_volumes : bool, optional + Whether apply the volume calculation results from this calculation + to the model. Defaults to applying the volumes. + """ + + if len(self.settings.volume_calculation) == 0: + # Then there is no volume calculation specified + raise ValueError("The Settings.volume_calculation attribute must" + " be specified before executing this method!") + + with openmc.change_directory(Path(cwd)): + if self.C_init: + if threads is not None: + msg = "Threads must be set via Model.c_init(...)" + raise ValueError(msg) + if mpi_args is not None: + msg = "The MPI environment must be set otherwise such as" \ + "with the call to mpi4py" raise ValueError(msg) - if particles is not None: - init_particles = openmc.lib.settings.particles - if isinstance(particles, Integral) and particles > 0: - openmc.lib.settings.particles = particles - - # Now lets handle the ones we can handle - if not output: - init_verbosity = openmc.lib.settings.verbosity + # Apply the output settings if not output: - openmc.lib.settings.verbosity = 1 + init_verbosity = openmc.lib.settings.verbosity + if not output: + openmc.lib.settings.verbosity = 1 - # Event-based can be set at init-time or on a case-basis. Handle - # the case-basis here. - # TODO This will be dealt with in a future change to the C-API + # Compute the volumes + openmc.lib.calculate_volumes() - # Then run using the C-API - openmc.lib.run() + # Reset the output verbosity + if not output: + openmc.lib.settings.verbosity = init_verbosity + else: + openmc.calculate_volumes(threads=threads, output=output, + openmc_exec=openmc_exec, + mpi_args=mpi_args) - # Reset changes for the openmc.run kwargs handling - if particles is not None: - openmc.lib.settings.particles = init_particles - if output is not None: - openmc.lib.settings.verbosity = init_verbosity + # Now we apply the volumes + if apply_volumes: + # Load the results + f_names = \ + ["volume_{}.h5".format(i + 1) + for i in range(len(self.settings.volume_calculations))] + vol_calcs = [openmc.VolumeCalculation.load_results(f_name) + for f_name in f_names] + # And now we can add them to the model + for vol_calc in vol_calcs: + # First add them to the Python side + self.geometry.add_volume_information(vol_calc) - else: - # Then run via the command line - self.export_to_xml() - openmc.run(particles, threads, geometry_debug, restart_file, - tracks, output, cwd, openmc_exec, mpi_args, event_based) + # And now repeat for the C-API + if vol_calc.domain == 'material': + # Then we can do this in the C-API + for domain_id in vol_calc.domains: + self.update_material_volumes( + [domain_id], vol_calc.volumes[domain_id]) - # Get output directory and return the last statepoint written this run - if self.settings.output and 'path' in self.settings.output: - output_dir = Path(self.settings.output['path']) - else: - output_dir = Path.cwd() - for sp in output_dir.glob('statepoint.*.h5'): - mtime = sp.stat().st_mtime - if mtime >= tstart: # >= allows for poor clock resolution - tstart = mtime - last_statepoint = sp - return last_statepoint + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', + convert=True, convert_exec='convert'): + """Creates plot images as specified by the Model.plots attribute + + If convert is True, this function requires that a program is installed + to convert PPM files to PNG files. Typically, that would be + `ImageMagick `_ which includes a + `convert` command. + + Parameters + ---------- + output : bool, optional + Capture OpenMC output from standard out + cwd : str, optional + Path to working directory to run in. Defaults to the current + working directory. + openmc_exec : str, optional + Path to OpenMC executable. Defaults to 'openmc'. + This only applies to the case when not using the C-API. + convert : bool, optional + Whether or not to attempt to convert from PPM to PNG + convert_exec : str, optional + Command that can convert PPM files into PNG files + """ + + if len(self.plots) == 0: + # Then there is no volume calculation specified + raise ValueError("The Model.plots attribute must be specified " + "before executing this method!") + + with openmc.change_directory(Path(cwd)): + if self.C_init: + # Apply the output settings + if not output: + init_verbosity = openmc.lib.settings.verbosity + if not output: + openmc.lib.settings.verbosity = 1 + + # Compute the volumes + openmc.lib.plot_geometry() + + # Reset the output verbosity + if not output: + openmc.lib.settings.verbosity = init_verbosity + else: + openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + + if convert: + for p in self.plots: + if p.filename is not None: + ppm_file = f'{p.filename}.ppm' + else: + ppm_file = f'plot_{p.id}.ppm' + png_file = ppm_file.replace('.ppm', '.png') + subprocess.check_call([convert_exec, ppm_file, png_file]) def _change_py_C_attribs(self, names_or_ids, value, obj_type, attrib_name, density_units='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, (np.int, int, str)) + check_type('names_or_ids', names_or_ids, Iterable, (Integral, str)) check_type('obj_type', obj_type, str) obj_type = obj_type.lower() check_value('obj_type', obj_type, ('material', 'cell')) @@ -588,7 +732,11 @@ class Model: # The C-API has no way to set cell volume so lets raise an exception if obj_type == 'cell' and attrib_name == 'volume': raise NotImplementedError( - 'Setting a Cell volume is not yet supported!') + 'Setting a Cell volume is not supported!') + # Same with setting temperatures, TODO: update C-API for this + if obj_type == 'material' and attrib_name == 'temperature': + raise NotImplementedError( + 'Setting a Material temperature is not yet supported!') # And some items just dont make sense if obj_type == 'cell' and attrib_name == 'density': raise ValueError('Cannot set a Cell density!') @@ -610,12 +758,13 @@ class Model: # only values that have actual ids ids = [None] * len(names_or_ids) for i, name_or_id in enumerate(names_or_ids): - if isinstance(name_or_id, (int, np.int)): + if isinstance(name_or_id, Integral): if name_or_id in by_id: ids[i] = int(name_or_id) - msg = '{} ID {} is not present in the model!'.format( - obj_type.capitalize(), name_or_id) - raise InvalidIDError(msg) + else: + msg = '{} ID {} is not present in the model!'.format( + obj_type.capitalize(), name_or_id) + raise InvalidIDError(msg) elif isinstance(name_or_id, str): if name_or_id in by_name: ids[i] = by_name[name_or_id].id @@ -636,7 +785,7 @@ class Model: elif attrib_name == 'temperature': obj.temperature = value elif attrib_name == 'density': - obj.set_density(value, density_units) + obj.set_density(density_units, value) # Next lets keep what is in C-API memory up to date as well if self.C_init: C_obj = C_by_id[id_] @@ -645,9 +794,9 @@ class Model: elif attrib_name == 'translation': C_obj.translation = value elif attrib_name == 'volume': - C_obj.set_volume = value + C_obj.volume = value elif attrib_name == 'temperature': - C_obj.set_temperature = value + C_obj.set_temperature(value) elif attrib_name == 'density': C_obj.set_density(value, density_units) @@ -685,7 +834,7 @@ class Model: self._change_py_C_attribs(names_or_ids, vector, 'cell', 'translation') - def update_densities(self, names_or_ids, density, density_units): + def update_densities(self, names_or_ids, density, density_units='atom/b-cm'): """Update the density of a given set of materials to a new value Parameters @@ -695,8 +844,8 @@ class Model: This parameter can include a mix of names and ids. density : float The density to apply in the units specified by `density_units` - density_units : {'atom/b-cm', 'g/cm3'} - Units for `density` + density_units : {'atom/b-cm', 'g/cm3'}, optional + Units for `density`. Defaults to 'atom/b-cm' """ @@ -734,3 +883,18 @@ class Model: self._change_py_C_attribs(names_or_ids, temperature, 'material', 'temperature') + + def update_material_volumes(self, names_or_ids, volume): + """Update the volume of a set of materials to the given value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The material names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + volume : float + The volume to apply in units of cm^3 + + """ + + self._change_py_C_attribs(names_or_ids, volume, 'material', 'volume') diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index a1c4a24da..d6f94e82e 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -28,10 +28,13 @@ def pin_model_attributes(): pitch = 1.25984 fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') - box = openmc.model.rectangular_prism(pitch, pitch, boundary_type='reflective') + box = openmc.model.rectangular_prism(pitch, pitch, + boundary_type='reflective') # Define cells - fuel = openmc.Cell(name='fuel', fill=uo2, region=-fuel_or) + fuel_inf_cell = openmc.Cell(name='inf fuel', fill=uo2) + fuel_inf_univ = openmc.Universe(cells=[fuel_inf_cell]) + fuel = openmc.Cell(name='fuel', fill=fuel_inf_univ, region=-fuel_or) clad = openmc.Cell(fill=zirc, region=+fuel_or & -clad_or) water = openmc.Cell(fill=borated_water, region=+clad_or & box) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index a068ef781..0ee8568e3 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,4 +1,4 @@ -from typing import Type +import numpy as np import pytest from pathlib import Path import openmc @@ -6,7 +6,7 @@ import openmc.lib def test_init(run_in_tmpdir, pin_model_attributes): - mats, geom, settings, tals, plots, chain, fission_q, _ = \ + mats, geom, settings, tals, plots, chain, fission_q, _ = \ pin_model_attributes openmc.reset_auto_ids() @@ -44,13 +44,16 @@ def test_init(run_in_tmpdir, pin_model_attributes): assert test_model._materials_by_id == {1: mats[0], 2: mats[1], 3: mats[2]} assert test_model._materials_by_name == {'UO2': mats[0], 'Zirc': mats[1], 'Borated water': mats[2]} - assert test_model._cells_by_id == {1: geom.root_universe.cells[1], - 2: geom.root_universe.cells[2], - 3: geom.root_universe.cells[3]} + # The last cell is the one that contains the infinite fuel + assert test_model._cells_by_id == \ + {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], + 4: geom.root_universe.cells[4], + 1: geom.root_universe.cells[2].fill.cells[1]} # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': geom.root_universe.cells[1], '': geom.root_universe.cells[3]} + 'fuel': geom.root_universe.cells[2], '': geom.root_universe.cells[4], + 'inf fuel': geom.root_universe.cells[2].fill.cells[1]} assert test_model.C_init is False # Finally test the parameter type checking by passing bad types and @@ -79,13 +82,16 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): test_model = openmc.Model.from_xml() assert test_model.geometry.root_universe.cells.keys() == \ geom.root_universe.cells.keys() - assert [c.fill.name for c in test_model.geometry.root_universe.cells.values()] == \ + assert [c.fill.name for c in + test_model.geometry.root_universe.cells.values()] == \ [c.fill.name for c in geom.root_universe.cells.values()] - assert [mat.name for mat in test_model.materials] == [mat.name for mat in mats] + assert [mat.name for mat in test_model.materials] == \ + [mat.name for mat in mats] # We will assume the attributes of settings that are custom objects are # OK if the others are so we dotn need to implement explicit comparisons no_test = ['_source', '_entropy_mesh'] - assert sorted(k for k in test_model.settings.__dict__.keys() if k not in no_test) == \ + assert sorted(k for k in test_model.settings.__dict__.keys() + if k not in no_test) == \ sorted(k for k in settings.__dict__.keys() if k not in no_test) keys = sorted(k for k in settings.__dict__.keys() if k not in no_test) for ref_k in keys: @@ -100,15 +106,17 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): assert test_model._materials_by_name == { 'UO2': test_model.materials[0], 'Zirc': test_model.materials[1], 'Borated water': test_model.materials[2]} - assert test_model._cells_by_id == {\ - 1: test_model.geometry.root_universe.cells[1], + assert test_model._cells_by_id == { 2: test_model.geometry.root_universe.cells[2], - 3: test_model.geometry.root_universe.cells[3]} + 3: test_model.geometry.root_universe.cells[3], + 4: test_model.geometry.root_universe.cells[4], + 1: test_model.geometry.root_universe.cells[2].fill.cells[1]} # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': test_model.geometry.root_universe.cells[1], - '': test_model.geometry.root_universe.cells[3]} + 'fuel': test_model.geometry.root_universe.cells[2], + '': test_model.geometry.root_universe.cells[4], + 'inf fuel': test_model.geometry.root_universe.cells[2].fill.cells[1]} assert test_model.C_init is False @@ -163,13 +171,26 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): cell.set_temperature(600.0) cell.fill.set_density(5.0, 'g/cm3') openmc.lib.export_properties() + + # Import properties to existing model + model.import_properties("properties.h5") + + # 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.fill.get_mass_density() == pytest.approx(5.0) + # Now C + assert openmc.lib.cells[1].get_temperature() == 600. + assert openmc.lib.materials[1].get_density('g/cm3') == pytest.approx(5.0) + + # Clear the C-API openmc.lib.finalize() - # Import properties to existing model and re-export to new directory - model.import_properties("properties.h5") + # Verify the attributes survived by exporting to XML and re-creating model.export_to_xml("with_properties") - # Load model with properties and confirm temperature/density has been changed + # Load model with properties and confirm temperature/density changed model_with_properties = openmc.Model.from_xml( 'with_properties/geometry.xml', 'with_properties/materials.xml', @@ -183,10 +204,6 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots) - # Set the depletion module to use the test intracomm as the depletion mods - # intracomm is the one that init uses. We will not perturb system state - # outside of this test by re-setting the openmc.deplete.comm to what it was - # before when we exist # This case will run by getting the k-eff and tallies for command-line and # C-API execution modes and ensuring they give the same result. @@ -196,6 +213,10 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): cli_flux = sp.get_tally(id=1).get_values()[0, 0, 0] if mpi_intracomm is not None: + # Set the depletion module to use the test intracomm as the depletion + # module's intracomm is the one that init uses. We will not perturb + # system state outside of this test by re-setting the + # openmc.deplete.comm to what it was before when we exist orig_comm = openmc.deplete.comm openmc.deplete.comm = mpi_intracomm test_model.init_C_api() @@ -205,8 +226,8 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] # and lets compare results - assert (C_keff - cli_keff) < 1e-15 - assert (C_flux - cli_flux) < 1e-13 + assert abs(C_keff - cli_keff) < 1e-15 + assert abs(C_flux - cli_flux) < 1e-13 # Now we should make sure that the flags for items which should be handled # by init are properly set @@ -218,8 +239,6 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.run(restart_file='1.h5') with pytest.raises(ValueError): test_model.run(tracks=True) - with pytest.raises(ValueError): - test_model.run(cwd='hi') test_model.clear_C_api() @@ -228,3 +247,111 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): openmc.deplete.comm = orig_comm +def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots) + + if mpi_intracomm is not None: + # Set the depletion module to use the test intracomm as the depletion + # module's intracomm is the one that init uses. We will not perturb + # system state outside of this test by re-setting the + # openmc.deplete.comm to what it was before when we exist + orig_comm = openmc.deplete.comm + openmc.deplete.comm = mpi_intracomm + test_model.init_C_api() + + # Now we can call rotate_cells, translate_cells, update_densities, + # update_cell_temperatures, and update_material_temperatures and make sure + # the changes have taken hold. + # For each we will first try bad inputs to make sure we get the right + # errors and then we do a good one which calls the material by name and + # then id to make sure it worked + + # The rotate_cells and translate_cells will work on the cell named fill, as + # it is filled with a universe and thus the operation will be valid + + # First rotate_cells + with pytest.raises(TypeError): + # Make sure it tells us we have a bad names_or_ids type + test_model.rotate_cells(None, (0, 0, 90)) + with pytest.raises(TypeError): + test_model.rotate_cells([None], (0, 0, 90)) + with pytest.raises(openmc.exceptions.InvalidIDError): + # Make sure it tells us we had a bad id + test_model.rotate_cells([7200], (0, 0, 90)) + with pytest.raises(openmc.exceptions.InvalidIDError): + # Make sure it tells us we had a bad id + test_model.rotate_cells(['bad_name'], (0, 0, 90)) + # Now a good one + assert np.all(openmc.lib.cells[2].rotation == (0., 0., 0.)) + test_model.rotate_cells([2], (0, 0, 90)) + assert np.all(openmc.lib.cells[2].rotation == (0., 0., 90.)) + + # And same thing by name + test_model.rotate_cells(['fuel'], (0, 0, 180)) + + # Now translate_cells. We dont need to re-check the TypeErrors/bad ids, + # because the other functions use the same hidden method as rotate_cells + assert np.all(openmc.lib.cells[2].translation == (0., 0., 0.)) + test_model.translate_cells([2], (0, 0, 10)) + assert np.all(openmc.lib.cells[2].translation == (0., 0., 10.)) + + # Now lets do the density updates. + # Check initial conditions + assert abs(openmc.lib.materials[1].get_density( + 'atom/b-cm') - 0.06891296988603757) < 1e-13 + mat_a_dens = np.sum( + [v[1] for v in test_model.materials[0]. + get_nuclide_atom_densities().values()]) + assert abs(mat_a_dens - 0.06891296988603757) < 1e-8 + # Change the density + test_model.update_densities(['UO2'], 2.) + assert abs(openmc.lib.materials[1].get_density('atom/b-cm') - 2.) < 1e-13 + mat_a_dens = np.sum( + [v[1] for v in test_model.materials[0]. + get_nuclide_atom_densities().values()]) + assert abs(mat_a_dens - 2.) < 1e-8 + + # Now lets do the cell temperature updates. + # Check initial conditions + + assert test_model._cells_by_id == \ + {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], + 4: geom.root_universe.cells[4], + 1: geom.root_universe.cells[2].fill.cells[1]} + assert abs(openmc.lib.cells[3].get_temperature() - 293.6) < 1e-13 + assert test_model.geometry.root_universe.cells[3].temperature is None + # Change the temperature + test_model.update_cell_temperatures([3], 600.) + assert abs(openmc.lib.cells[3].get_temperature() - 600.) < 1e-13 + assert abs(test_model.geometry.root_universe.cells[3].temperature - + 600.) < 1e-13 + + # Now lets do the material temperature updates. + # Check initial conditions + with pytest.raises(NotImplementedError): + test_model.update_material_temperatures(['UO2'], 600.) + # TODO: When C-API material.set_temperature is implemented, uncomment below + # assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 + # # The temperature on the material will be None because its just the + # # default assert test_model.materials[0].temperature is None + # # Change the temperature + # test_model.update_material_temperatures(['UO2'], 600.) + # assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 + # assert abs(test_model.materials[0].temperature - 600.) < 1e-13 + + # And finally material volume + # import pdb; pdb.set_trace() + assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 + # The temperature on the material will be None because its just the default + assert abs(test_model.materials[0].volume - 0.4831931368640985) < 1e-13 + # Change the temperature + test_model.update_material_volumes(['UO2'], 2.) + assert abs(openmc.lib.materials[1].volume - 2.) < 1e-13 + assert abs(test_model.materials[0].volume - 2.) < 1e-13 + + test_model.clear_C_api() + + # And before done, reset the deplete communicator + if mpi_intracomm is not None: + openmc.deplete.comm = orig_comm From 25d0a5f9af9d597803c7d65bed51f483d5b57602 Mon Sep 17 00:00:00 2001 From: aprilnovak Date: Sat, 25 Sep 2021 09:20:33 -0500 Subject: [PATCH 30/59] Change ordering in doc string. Refs #1872 --- openmc/mesh.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 372069cbb..4e1e9565a 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -215,8 +215,6 @@ class RegularMesh(MeshBase): mesh.lower_left = group['lower_left'][()] mesh.upper_right = group['upper_right'][()] mesh.width = group['width'][()] - if 'length_multiplier' in group: - mesh.length_multiplier = group['length_multiplier'][()] return mesh @@ -613,14 +611,12 @@ class UnstructuredMesh(MeshBase): ---------- filename : str Location of the unstructured mesh file - length_multiplier: float - Constant multiplier to apply to mesh coordinates mesh_id : int Unique identifier for the mesh name : str Name of the mesh - size : int - Number of elements in the unstructured mesh + length_multiplier: float + Constant multiplier to apply to mesh coordinates Attributes ---------- @@ -732,7 +728,7 @@ class UnstructuredMesh(MeshBase): @length_multiplier.setter def length_multiplier(self, length_multiplier): cv.check_type("Unstructured mesh length multiplier", - length_multiplier, + length_multiplier, Real) self._length_multiplier = length_multiplier @@ -843,6 +839,9 @@ class UnstructuredMesh(MeshBase): mesh.centroids = np.reshape(centroids, (vol_data.shape[0], 3)) mesh.size = mesh.volumes.size + if 'length_multiplier' in group: + mesh.length_multiplier = group['length_multiplier'][()] + return mesh def to_xml_element(self): From 6612b4ba0479cfad4aea711cd640fba314ed0412 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 08:59:43 +0000 Subject: [PATCH 31/59] updated unit_test for Mixture distribution --- tests/unit_tests/test_stats.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index e15958478..9e6237ba1 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -101,8 +101,12 @@ def test_mixture(): assert mix.distribution == [d1, d2] assert len(mix) == 4 - with pytest.raises(NotImplementedError): - mix.to_xml_element('distribution') + elem = mix.to_xml_element('distribution') + + d = openmc.stats.Mixture.from_xml_element(elem) + assert d.probability == p + assert d.distribution == [d1, d2] + assert len(d) == 4 def test_polar_azimuthal(): From d7bf830028d20bb91ac9178b4d462b7bda81b24b Mon Sep 17 00:00:00 2001 From: agnelson Date: Wed, 29 Sep 2021 08:22:16 -0500 Subject: [PATCH 32/59] Added test of Model.plot_geometry and made changes to reflect that openmc.lib.plot_geometry wont work if openmc.lib.init wasnt told that the run mode is to plot because then plots.xml isnt read in --- openmc/model/model.py | 36 +++++++++++++++---------- tests/unit_tests/conftest.py | 18 ++++++++----- tests/unit_tests/test_model.py | 49 ++++++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index e32876a6c..5e0c1a2bc 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -272,7 +272,7 @@ class Model: if dep.comm.rank == 0: self.export_to_xml() dep.comm.barrier() - # TODO: Implement the output flag somewhere on the C++ side + openmc.lib.init(args=args, intracomm=dep.comm) def clear_C_api(self): @@ -636,6 +636,7 @@ class Model: if not output: openmc.lib.settings.verbosity = init_verbosity else: + self.export_to_xml() openmc.calculate_volumes(threads=threads, output=output, openmc_exec=openmc_exec, mpi_args=mpi_args) @@ -691,21 +692,28 @@ class Model: "before executing this method!") with openmc.change_directory(Path(cwd)): - if self.C_init: - # Apply the output settings - if not output: - init_verbosity = openmc.lib.settings.verbosity - if not output: - openmc.lib.settings.verbosity = 1 + # TODO: openmc_init doesnt read plots.xml unless it is in plot mode + # so the following will not work. Commented out for now and + # replacing with non-C-API code + self.export_to_xml() + openmc.plot_geometry(output=output, openmc_exec=openmc_exec) - # Compute the volumes - openmc.lib.plot_geometry() + # if self.C_init: + # # Apply the output settings + # if not output: + # init_verbosity = openmc.lib.settings.verbosity + # if not output: + # openmc.lib.settings.verbosity = 1 - # Reset the output verbosity - if not output: - openmc.lib.settings.verbosity = init_verbosity - else: - openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + # # Compute the volumes + # openmc.lib.plot_geometry() + + # # Reset the output verbosity + # if not output: + # openmc.lib.settings.verbosity = init_verbosity + # else: + # self.export_to_xml() + # openmc.plot_geometry(output=output, openmc_exec=openmc_exec) if convert: for p in self.plots: diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index d6f94e82e..ff5340249 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -63,12 +63,18 @@ def pin_model_attributes(): tal.scores = ['flux'] tals.append(tal) - plot = openmc.Plot() - plot.origin = (0., 0., 0.) - plot.width = (pitch, pitch) - plot.pixels = (300, 300) - plot.color_by = 'material' - plots = openmc.Plots((plot,)) + plot1 = openmc.Plot() + plot1.origin = (0., 0., 0.) + plot1.width = (pitch, pitch) + plot1.pixels = (300, 300) + plot1.color_by = 'material' + plot1.filename = 'test' + plot2 = openmc.Plot() + plot2.origin = (0., 0., 0.) + plot2.width = (pitch, pitch) + plot2.pixels = (300, 300) + plot2.color_by = 'cell' + plots = openmc.Plots((plot1, plot2)) chain = './chain_simple.xml' fission_q = {'U235': 200e6} diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 0ee8568e3..dd6242fe1 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,6 +1,7 @@ import numpy as np import pytest from pathlib import Path +from shutil import which import openmc import openmc.lib @@ -226,7 +227,7 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] # and lets compare results - assert abs(C_keff - cli_keff) < 1e-15 + assert abs(C_keff - cli_keff) < 1e-13 assert abs(C_flux - cli_flux) < 1e-13 # Now we should make sure that the flags for items which should be handled @@ -247,6 +248,51 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): openmc.deplete.comm = orig_comm +def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots) + + if mpi_intracomm is not None: + # Set the depletion module to use the test intracomm as the depletion + # module's intracomm is the one that init uses. We will not perturb + # system state outside of this test by re-setting the + # openmc.deplete.comm to what it was before when we exist + orig_comm = openmc.deplete.comm + openmc.deplete.comm = mpi_intracomm + + # This test cannot check the correctness of the plot, but it can + # check that a plot was made and that the expected ppm and png files are + # there + + # We will only test convert if it is on the system, so as not to add an + # extra dependency just for tests + convert = which('convert') is not None + if convert: + exts = ['ppm', 'png'] + else: + exts = ['ppm'] + + # We will run the test twice, the first time without C-API, the second with + for i in range(2): + if i == 1: + test_model.init_C_api() + test_model.plot_geometry(output=True, convert=convert) + + # Now look for the files, expect to find test.ppm, plot_2.ppm, and if + # convert is True, test.png, plot_2.png + for fname in ['test.', 'plot_2.']: + for ext in exts: + test_file = Path('./{}{}'.format(fname, ext)) + assert test_file.exists() + test_file.unlink() + + test_model.clear_C_api() + + # And before done, reset the deplete communicator + if mpi_intracomm is not None: + openmc.deplete.comm = orig_comm + + def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots) @@ -314,7 +360,6 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Now lets do the cell temperature updates. # Check initial conditions - assert test_model._cells_by_id == \ {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], 4: geom.root_universe.cells[4], From f93887aa21c8231e807bcd830f133dd1ab0de9be Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:07:31 +0200 Subject: [PATCH 33/59] Update docs/source/io_formats/settings.rst Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index b76b23e63..e73bcff28 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -619,7 +619,7 @@ 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 producess + "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). From 9123ee9239761cbb56167e8dac8876b263ff74a6 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:08:49 +0200 Subject: [PATCH 34/59] Update src/distribution.cpp Co-authored-by: Paul Romano --- src/distribution.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/distribution.cpp b/src/distribution.cpp index e4775a5b7..f5b413e0d 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -293,9 +293,6 @@ double Equiprobable::sample(uint64_t* seed) const Mixture::Mixture(pugi::xml_node node) { - // Read and initialize tabular distribution - //auto params = get_node_array(node, "parameters"); - double cumsum = 0.0; for (pugi::xml_node pair = node.child("pair"); pair; pair = pair.next_sibling("pair")) { From cb27271afcc4872c7d81241da05abb97084862e1 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:09:17 +0200 Subject: [PATCH 35/59] Update src/distribution.cpp replace with range based for loop Co-authored-by: Paul Romano --- src/distribution.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/distribution.cpp b/src/distribution.cpp index f5b413e0d..a95f6bef8 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -294,8 +294,7 @@ double Equiprobable::sample(uint64_t* seed) const Mixture::Mixture(pugi::xml_node node) { double cumsum = 0.0; - for (pugi::xml_node pair = node.child("pair"); pair; - pair = pair.next_sibling("pair")) { + for (pugi::xml_node pair : node.children("pair")) { // Check that required data exists if (!pair.attribute("probability")) openmc::fatal_error("Mixture pair element does not have probability."); if (!pair.child("dist")) openmc::fatal_error("Mixture pair element does not have a distribution."); From d7622c1b5db6395ad7f094d8f47ce2768f591210 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:09:42 +0200 Subject: [PATCH 36/59] Update src/distribution.cpp Co-authored-by: Paul Romano --- src/distribution.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/distribution.cpp b/src/distribution.cpp index a95f6bef8..2573695bf 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -296,8 +296,8 @@ Mixture::Mixture(pugi::xml_node node) double cumsum = 0.0; for (pugi::xml_node pair : node.children("pair")) { // Check that required data exists - if (!pair.attribute("probability")) openmc::fatal_error("Mixture pair element does not have probability."); - if (!pair.child("dist")) openmc::fatal_error("Mixture pair element does not have a distribution."); + if (!pair.attribute("probability")) fatal_error("Mixture pair element does not have probability."); + if (!pair.child("dist")) fatal_error("Mixture pair element does not have a distribution."); // cummulative sum of probybilities cumsum += std::stod(pair.attribute("probability").value()); From 3fde9412f4f28494866624cbef40a7d2fe8a911d Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:10:11 +0200 Subject: [PATCH 37/59] Update src/distribution.cpp Co-authored-by: Paul Romano --- src/distribution.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/distribution.cpp b/src/distribution.cpp index 2573695bf..d9505d3b2 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -323,10 +323,7 @@ double Mixture::sample(uint64_t* seed) const p, [](const DistPair& pair, double p) { return pair.first < p; }); // This should not happen. Catch it - // TODO: Remove this check, when the code is trusted to always operate - if (it == distribution_.cend()) { - fatal_error("Bad Sampling in Mixture Distribution."); - } + Ensures(it != distribution_.cend()); // Sample the choosen distribution return it->second->sample(seed); From 1c47fb205917c28bd67da200a925ebc466d70ba3 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:10:34 +0200 Subject: [PATCH 38/59] Update src/distribution.cpp Co-authored-by: Paul Romano --- src/distribution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/distribution.cpp b/src/distribution.cpp index d9505d3b2..af3aac5af 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -325,7 +325,7 @@ double Mixture::sample(uint64_t* seed) const // This should not happen. Catch it Ensures(it != distribution_.cend()); - // Sample the choosen distribution + // Sample the chosen distribution return it->second->sample(seed); } From 9dc9b2af458f305c251d4572e1c1e1a335b3ea87 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:10:49 +0200 Subject: [PATCH 39/59] Update tests/regression_tests/source/test.py Co-authored-by: Paul Romano --- tests/regression_tests/source/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression_tests/source/test.py b/tests/regression_tests/source/test.py index 229ac8383..006358ca7 100644 --- a/tests/regression_tests/source/test.py +++ b/tests/regression_tests/source/test.py @@ -55,7 +55,7 @@ class SourceTestHarness(PyAPITestHarness): energy1 = openmc.stats.Maxwell(1.2895e6) energy2 = openmc.stats.Watt(0.988e6, 2.249e-6) energy3 = openmc.stats.Tabular(E, p, interpolation='histogram') - energy4 = openmc.stats.Mixture([1,2,3], [energy1, energy2, energy3]) + energy4 = openmc.stats.Mixture([1, 2, 3], [energy1, energy2, energy3]) source1 = openmc.Source(spatial1, angle1, energy1, strength=0.5) source2 = openmc.Source(spatial2, angle2, energy2, strength=0.3) From 0b3cd2544baa0046f9dbf2bcffca5aca42887bf4 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 17:11:30 +0200 Subject: [PATCH 40/59] Update openmc/stats/univariate.py better naming Co-authored-by: Paul Romano --- openmc/stats/univariate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index b5997b1cd..576b2247c 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -850,11 +850,10 @@ class Mixture(Univariate): Mixture distribution generated from XML element """ - P = [] - D = [] - for pair in elem: - if pair.tag == "pair": - P.append( float(get_text(pair, 'probability')) ) - D.append( Univariate.from_xml_element(pair.find("dist")) ) + probability = [] + distribution = [] + for pair in elem.findall('pair'): + probability.append(float(get_text(pair, 'probability'))) + distribution.append(Univariate.from_xml_element(pair.find("dist"))) - return cls(P,D) + return cls(probability, distribution) From fef3c2dc9d2e5d46c3c84f23b514c9d93fbc95cb Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Wed, 29 Sep 2021 19:54:18 +0000 Subject: [PATCH 41/59] update settings.rst --- docs/source/io_formats/settings.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index e73bcff28..8a83de7ac 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -650,19 +650,24 @@ variable and whose sub-elements/attributes are as follows: number :math:`a` that parameterizes the distribution :math:`p(x) dx = c x e^{-x/a} dx`. - For a "mixture" distribution, ``parameters`` provide the :math:'(p,d)' pairs - connecting the probabilites :math:'p' with the different sub-distributions - :math:'d'. All probabilities :math:'p' are given first followed by the corresponding - distributions :math:'d'. - .. note:: The above format should be used even when using the multi-group :ref:`energy_mode`. + :interpolation: For a "tabular" distribution, ``interpolation`` can be set to "histogram" or "linear-linear" thereby specifying how tabular points are to be interpolated. *Default*: histogram +:pair: + For a "mixture" distribution, this element provides a distribution and its corresponding probability.a + + :probability: + An attribute or ``pair`` that provides the probability of a univatiate distribution within a "mixture" distribution. + + :dist: + This sub-element of a ``pair`` element provides information on the corresponding univariate distribution. + ------------------------- ```` Element ------------------------- From dc80b799ac518ed6fb7c3ba97691ca5788f69cfc Mon Sep 17 00:00:00 2001 From: agnelson Date: Wed, 29 Sep 2021 18:51:00 -0500 Subject: [PATCH 42/59] Resolving comments from @paulromano and updating tests accordingly --- openmc/deplete/integrators.py | 46 +--- openmc/deplete/operator.py | 6 +- openmc/executor.py | 122 ++++++---- openmc/lib/__init__.py | 3 +- openmc/lib/core.py | 6 +- openmc/model/model.py | 419 +++++++++++++++++---------------- tests/unit_tests/conftest.py | 4 +- tests/unit_tests/test_model.py | 152 +++++------- 8 files changed, 373 insertions(+), 385 deletions(-) diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index 3055561be..585deabba 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -583,40 +583,12 @@ class SILEQIIntegrator(SIIntegrator): return proc_time, [eos_conc, inter_conc], [res_bar] -def integrator_factory(method): - """This method is a factor for the integrator sub-classes - - Params - ------ - method : str - The type of integrator method to use. Valid values are: 'cecm', - 'predictor', 'cf4', 'epc_rk4', 'si_celi', 'si_leqi', 'celi', and 'leqi' - - Returns - ------- - integrator : Integrator - The type of integrator - - """ - - if method == 'cecm': - integrator = CECMIntegrator - elif method == 'predictor': - integrator = PredictorIntegrator - elif method == 'cf4': - integrator = CF4Integrator - elif method == 'epc_rk4': - integrator = EPCRK4Integrator - elif method == 'si_celi': - integrator = SICELIIntegrator - elif method == 'si_leqi': - integrator = SILEQIIntegrator - elif method == 'celi': - integrator = CELIIntegrator - elif method == 'leqi': - integrator = LEQIIntegrator - else: - msg = "Invalid integrator method: {}!".format(method) - raise ValueError(msg) - - return integrator +integrator_by_name = { + 'cecm': CECMIntegrator, + 'predictor': PredictorIntegrator, + 'cf4': CF4Integrator, + 'epc_rk4': EPCRK4Integrator, + 'si_celi': SICELIIntegrator, + 'si_leqi': SILEQIIntegrator, + 'celi': CELIIntegrator, + 'leqi': LEQIIntegrator} diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 8560f89e9..5b000a389 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -202,7 +202,7 @@ class Operator(TransportOperator): self.settings = settings self.geometry = geometry self.diff_burnable_mats = diff_burnable_mats - self.cleanup_when_done = True + self._cleanup_when_done = True # Reduce the chain before we create more materials if reduce_chain: @@ -537,7 +537,7 @@ class Operator(TransportOperator): # Initialize OpenMC library comm.barrier() - if not openmc.lib.LIB_INIT: + if not openmc.lib.is_initialized: openmc.lib.init(intracomm=comm) # Generate tallies in memory @@ -556,7 +556,7 @@ class Operator(TransportOperator): def finalize(self): """Finalize a depletion simulation and release resources.""" - if self.cleanup_when_done: + if self._cleanup_when_done: openmc.lib.finalize() def _update_materials(self): diff --git a/openmc/executor.py b/openmc/executor.py index de5604c9d..fa55e1366 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -1,23 +1,79 @@ from collections.abc import Iterable from numbers import Integral import subprocess -from contextlib import contextmanager -from pathlib import Path -import os import openmc -@contextmanager -def change_directory(working_dir): - """A context manager for executing in a provided working directory""" - start_dir = Path().absolute() - try: - Path.mkdir(working_dir, exist_ok=True) - os.chdir(working_dir) - yield - finally: - os.chdir(start_dir) +def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, + plot=False, restart_file=None, threads=None, + tracks=False, event_based=False, + openmc_exec='openmc', mpi_args=None): + """Run an OpenMC simulation. + + Parameters + ---------- + volume : bool, optional + Run in stochastic volume calculation mode. Defaults to False. + geometry_debug : bool, optional + Turn on geometry debugging during simulation. Defaults to False. + particles : int, optional + Number of particles to simulate per generation. + plot : bool, optional + Run in plotting mode. Defaults to False. + restart_file : str, optional + Path to restart file to use + threads : int, optional + Number of OpenMP threads. If OpenMC is compiled with OpenMP threading + enabled, the default is implementation-dependent but is usually equal + to the number of hardware threads available (or a value set by the + :envvar:`OMP_NUM_THREADS` environment variable). + tracks : bool, optional + Write tracks for all particles. Defaults to False. + event_based : bool, optional + Turns on event-based parallelism, instead of default history-based + openmc_exec : str, optional + Path to OpenMC executable. Defaults to 'openmc'. + mpi_args : list of str, optional + MPI execute command and any additional MPI arguments to pass, + e.g. ['mpiexec', '-n', '8']. + + .. versionadded:: 0.13.0 + + Returns + ------- + args : Iterable of str + The runtime flags converted to CLI arguments of the OpenMC executable + + """ + + args = [openmc_exec] + + if volume: + args += ['--volume'] + + if isinstance(particles, Integral) and particles > 0: + args += ['-n', str(particles)] + + if isinstance(threads, Integral) and threads > 0: + args += ['-s', str(threads)] + + if geometry_debug: + args.append('-g') + + if event_based: + args.append('-e') + + if isinstance(restart_file, str): + args += ['-r', restart_file] + + if tracks: + args.append('-t') + + if mpi_args is not None: + args = mpi_args + args + + return args def _run(args, output, cwd): @@ -141,8 +197,8 @@ def calculate_volumes(threads=None, output=True, cwd='.', ---------- threads : int, optional Number of OpenMP threads. If OpenMC is compiled with OpenMP threading - enabled, the default is implementation-dependent but is usually equal to - the number of hardware threads available (or a value set by the + enabled, the default is implementation-dependent but is usually equal + to the number of hardware threads available (or a value set by the :envvar:`OMP_NUM_THREADS` environment variable). output : bool, optional Capture OpenMC output from standard out @@ -165,12 +221,9 @@ def calculate_volumes(threads=None, output=True, cwd='.', openmc.VolumeCalculation """ - args = [openmc_exec, '--volume'] - if isinstance(threads, Integral) and threads > 0: - args += ['-s', str(threads)] - if mpi_args is not None: - args = mpi_args + args + args = process_CLI_arguments(volume=True, threads=threads, + openmc_exec=openmc_exec, mpi_args=mpi_args) _run(args, output, cwd) @@ -186,8 +239,8 @@ def run(particles=None, threads=None, geometry_debug=False, Number of particles to simulate per generation. threads : int, optional Number of OpenMP threads. If OpenMC is compiled with OpenMP threading - enabled, the default is implementation-dependent but is usually equal to - the number of hardware threads available (or a value set by the + enabled, the default is implementation-dependent but is usually equal + to the number of hardware threads available (or a value set by the :envvar:`OMP_NUM_THREADS` environment variable). geometry_debug : bool, optional Turn on geometry debugging during simulation. Defaults to False. @@ -216,27 +269,10 @@ def run(particles=None, threads=None, geometry_debug=False, If the `openmc` executable returns a non-zero status """ - args = [openmc_exec] - if isinstance(particles, Integral) and particles > 0: - args += ['-n', str(particles)] - - if isinstance(threads, Integral) and threads > 0: - args += ['-s', str(threads)] - - if geometry_debug: - args.append('-g') - - if event_based: - args.append('-e') - - if isinstance(restart_file, str): - args += ['-r', restart_file] - - if tracks: - args.append('-t') - - if mpi_args is not None: - args = mpi_args + args + args = process_CLI_arguments( + volume=False, geometry_debug=geometry_debug, particles=particles, + restart_file=restart_file, threads=threads, tracks=tracks, + event_based=event_based, openmc_exec=openmc_exec, mpi_args=mpi_args) _run(args, output, cwd) diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 7e766a93b..f1b9c291f 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -60,4 +60,5 @@ from .settings import settings from .math import * from .plot import * -LIB_INIT = False +# Flag to denote whether or not openmc.lib.init has been called +is_initialized = False diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 03c6c41fc..6b72dde55 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -148,7 +148,7 @@ def export_properties(filename=None): def finalize(): """Finalize simulation and free memory""" _dll.openmc_finalize() - openmc.lib.LIB_INIT = False + openmc.lib.is_initialized = False def find_cell(xyz): @@ -215,7 +215,7 @@ def import_properties(filename): See Also -------- openmc.lib.export_properties -mat + """ _dll.openmc_properties_import(filename.encode()) @@ -255,7 +255,7 @@ def init(args=None, intracomm=None): intracomm = c_void_p(address) _dll.openmc_init(argc, argv, intracomm) - openmc.lib.LIB_INIT = True + openmc.lib.is_initialized = True def is_statepoint_batch(): diff --git a/openmc/model/model.py b/openmc/model/model.py index 5e0c1a2bc..e560ef9fb 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,20 +1,30 @@ from collections.abc import Iterable +import operator import os from pathlib import Path from numbers import Integral import time import warnings import subprocess +from contextlib import contextmanager import h5py -import numpy as np import openmc -from openmc.checkvalue import check_type, check_value, check_iterable_type, \ - check_length -import openmc.deplete as dep -from openmc.data.library import DataLibrary -from openmc.exceptions import DataError, InvalidIDError, SetupError +from openmc.checkvalue import check_type, check_value +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, exist_ok=True) + os.chdir(working_dir) + try: + yield + finally: + os.chdir(start_dir) class Model: @@ -28,6 +38,8 @@ class Model: not set, it will attempt to create a ``materials.xml`` file based on all materials appearing in the geometry. + .. versionchanged:: 0.13.0 + Parameters ---------- geometry : openmc.Geometry, optional @@ -40,14 +52,8 @@ class Model: Tallies information plots : openmc.Plots, optional Plot information - chain_file : str or Path, optional - Path to the depletion chain XML file. Defaults to the chain - found under the ``depletion_chain`` in the - :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. If a - str is provided it will be converted to a Path object. - fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. - If not given, values will be pulled from the ``chain_file``. + intracomm : mpi4py.MPI.Intracomm or None, optional + MPI intracommunicator Attributes ---------- @@ -61,19 +67,13 @@ class Model: Tallies information plots : openmc.Plots Plot information - chain_file : str or Path - Path to the depletion chain XML file. Defaults to the chain - found under the ``depletion_chain`` in the - :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. If a - str is provided it will be converted to a Path object. - fission_q : dict - Dictionary of nuclides and their fission Q values [eV]. - If not given, values will be pulled from the ``chain_file``. + intracomm : mpi4py.MPI.Intracomm or None + MPI intracommunicator """ def __init__(self, geometry=None, materials=None, settings=None, - tallies=None, plots=None, chain_file=None, fission_q=None): + tallies=None, plots=None, intracomm=None): self.geometry = openmc.Geometry() self.materials = openmc.Materials() self.settings = openmc.Settings() @@ -91,18 +91,33 @@ class Model: if plots is not None: self.plots = plots - self.chain_file = chain_file - self.fission_q = fission_q + self.intracomm = intracomm + # Store dictionaries to the materials and cells by ID and names if materials is None: mats = self.geometry.get_all_materials().values() else: mats = self.materials - self._materials_by_id = {mat.id: mat for mat in mats} - self._materials_by_name = {mat.name: mat for mat in mats} cells = self.geometry.get_all_cells() + # Get the ID maps + self._materials_by_id = {mat.id: mat for mat in mats} self._cells_by_id = {cell.id: cell for cell in cells.values()} - self._cells_by_name = {cell.name: cell for cell in cells.values()} + + # Get the names maps, but since names are not unique, store a list 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. + self._cells_by_name = {} + for cell in cells.values(): + if cell.name not in self._cells_by_name: + self._cells_by_name[cell.name] = [cell] + else: + self._cells_by_name[cell.name].append(cell) + self._materials_by_name = {} + for mat in mats: + if mat.name not in self._materials_by_name: + self._materials_by_name[mat.name] = [mat] + else: + self._materials_by_name[mat.name].append(mat) @property def geometry(self): @@ -125,16 +140,14 @@ class Model: return self._plots @property - def chain_file(self): - return self._chain_file + def intracomm(self): + return self._intracomm @property - def fission_q(self): - return self._fission_q - - @property - def C_init(self): - return openmc.lib.LIB_INIT + def is_initialized(self): + # TODO: Replace openmc.lib.is_initialized with a direct ctypes access + # to simulation::initialized + return openmc.lib.is_initialized @geometry.setter def geometry(self, geometry): @@ -176,20 +189,20 @@ class Model: for plot in plots: self._plots.append(plot) - @chain_file.setter - def chain_file(self, chain_file): - check_type('chain_file', chain_file, (type(None), str, Path)) - if isinstance(chain_file, str): - self._chain_file = Path(chain_file).resolve() - elif isinstance(chain_file, Path): - self._chain_file = chain_file.resolve() + @intracomm.setter + def intracomm(self, intracomm): + try: + from mpi4py import MPI + mpi_avail = True + except ImportError: + mpi_avail = False + if intracomm is None or not mpi_avail: + # TODO: move dummy_comm from openmc.deplete to openmc + from openmc.deplete.dummy_comm import DummyCommunicator + self._intracomm = DummyCommunicator() else: - self._chain_file = None - - @fission_q.setter - def fission_q(self, fission_q): - check_type('fission_q', fission_q, (type(None), dict)) - self._fission_q = fission_q + check_type('intracomm', intracomm, MPI.Comm) + self._intracomm = intracomm @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', @@ -218,17 +231,20 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) - def init_C_api(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=False): - """Initializes the model in memory via the C-API + def init_lib(self, threads=None, geometry_debug=False, restart_file=None, + tracks=False, output=True, event_based=False): + """Initializes the model in memory via the C API + + .. versionadded:: 0.13.0 Parameters ---------- threads : int, optional - Number of OpenMP threads. If OpenMC is compiled with OpenMP threading - enabled, the default is implementation-dependent but is usually equal to - the number of hardware threads available (or a value set by the - :envvar:`OMP_NUM_THREADS` environment variable). + Number of OpenMP threads. If OpenMC is compiled with OpenMP + threading enabled, the default is implementation-dependent but is + usually equal to the number of hardware threads available + (or a value set by the :envvar:`OMP_NUM_THREADS` environment + variable). geometry_debug : bool, optional Turn on geometry debugging during simulation. Defaults to False. restart_file : str, optional @@ -242,7 +258,7 @@ class Model: """ # TODO: right now the only way to set most of the above parameters via - # the C-API are at initialization time despite use-cases existing to + # the C API are at initialization time despite use-cases existing to # set them for individual runs. For now this functionality is exposed # where it exists (here in init), but in the future the functionality # should be exposed so that it can be accessed via model.run(...) @@ -267,42 +283,46 @@ class Model: if tracks: args.append('-t') - self.clear_C_api() + self.finalize_lib() - if dep.comm.rank == 0: + if self.intracomm.rank == 0: self.export_to_xml() - dep.comm.barrier() + self.intracomm.barrier() - openmc.lib.init(args=args, intracomm=dep.comm) + openmc.lib.init(args=args, intracomm=self.intracomm) + + def finalize_lib(self): + """Finalize simulation and free memory allocated for the C API + + .. versionadded:: 0.13.0 + + """ - def clear_C_api(self): - """Finalize simulation and free memory allocated for the C-API""" openmc.lib.finalize() - def deplete(self, timesteps, chain_file=None, method='cecm', - fission_q=None, final_step=True, directory='.', - **kwargs): + def deplete(self, timesteps, method='cecm', final_step=True, + operator_kwargs=None, integrator_kwargs=None, directory='.'): """Deplete model using specified timesteps/power + .. versionchanged:: 0.13.0 + Parameters ---------- timesteps : iterable of float Array of timesteps in units of [s]. Note that values are not cumulative. - chain_file : str, optional - Path to the depletion chain XML file. Defaults to the chain - found under the ``depletion_chain`` in the - :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. method : str, optional Integration method used for depletion (e.g., 'cecm', 'predictor'). Defaults to 'cecm'. - fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. - If not given, values will be pulled from the ``chain_file``. - Defaults to pulling from the ``chain_file``. final_step : bool, optional Indicate whether or not a transport solve should be run at the end of the last timestep. Defaults to running this transport solve. + operator_kwargs : dict + Keyword arguments passed to the depletion Operator initializer + (e.g., :func:`openmc.deplete.Operator`) + integrator_kwargs : dict + Keyword arguments passed to the depletion Operator initializer + (e.g., :func:`openmc.deplete.integrator.cecm`) directory : str, optional Directory to write XML files to. If it doesn't exist already, it will be created. Defaults to the current working directory @@ -312,69 +332,43 @@ class Model: """ - # To keep Model.deplete(...) API compatibility, we will allow the - # chain_file and fission_q params to be set if provided while we set - # the depletion operator - if chain_file is not None: - this_chain_file = Path(chain_file).resolve() - warnings.warn("The chain_file argument of Model.deplete(...) " - "has been deprecated and may be removed in a " - "future version. The Model.chain_file should be" - "used instead.", DeprecationWarning) - else: - this_chain_file = self.chain_file - if fission_q is not None: - warnings.warn("The fission_q argument of Model.deplete(...) " - "has been deprecated and may be removed in a " - "future version. The Model.fission_q should be" - "used instead.", DeprecationWarning) - this_fission_q = fission_q - else: - this_fission_q = fission_q + # Import openmc.deplete here so the Model can be used even if the + # shared library is unavailable. + import openmc.deplete as dep - # Create directory if required - d = Path(directory) - if not d.is_dir(): - d.mkdir(parents=True) - start_dir = Path.cwd() - os.chdir(d) + with _change_directory(Path(directory)): + depletion_operator = \ + dep.Operator(self.geometry, self.settings, **operator_kwargs) + # Tell depletion_operator.finalize NOT to clear C API memory when + # it is done + depletion_operator.cleanup_when_done = False - depletion_operator = \ - dep.Operator(self.geometry, self.settings, - str(this_chain_file.absolute()), - fission_q=this_fission_q) - # Tell depletion_operator.finalize NOT to clear C-API memory when it is - # done - depletion_operator.cleanup_when_done = False + # Set up the integrator + 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) - # Set up the integrator - integrator_class = dep.integrators.integrator_factory(method) - integrator = integrator_class(depletion_operator, - timesteps, **kwargs) + # Now perform the depletion + integrator.integrate(final_step) - # Now perform the depletion - integrator.integrate(final_step) + # If we did not perform a transport calculation on the final step, + # then make the code update the C API material inventory + if not final_step: + depletion_operator._update_materials() - # If we did not perform a transport calculation on the final step, then - # make the code update the C-API material inventory - if not final_step: - depletion_operator._update_materials() - - # Now make the python Materials match the C-API material data - for mat_id, mat in self._materials_by_id.items(): - if mat.depletable: - # Get the C data - c_mat = openmc.lib.materials[mat_id] - nuclides, densities = c_mat._get_densities() - # And now we can remove isotopes and add these ones in - atom_density = 0. - for nuc, density in zip(nuclides, densities): - mat.remove_nuclide(nuc) # Replace if it's there - mat.add_nuclide(nuc, density) - atom_density += density - mat.set_density('atom/b-cm', atom_density) - - os.chdir(start_dir) + # Now make the python Materials match the C API material data + for mat_id, mat in self._materials_by_id.items(): + if mat.depletable: + # Get the C data + c_mat = openmc.lib.materials[mat_id] + nuclides, densities = c_mat._get_densities() + # And now we can remove isotopes and add these ones in + mat.nuclides.clear() + for nuc, density in zip(nuclides, densities): + mat.add_nuclide(nuc, density) + mat.set_density('atom/b-cm', sum(densities)) def export_to_xml(self, directory='.'): """Export model to XML files. @@ -412,8 +406,8 @@ class Model: def import_properties(self, filename): """Import physical properties - .. versionchanged:: 0.12.3 - This method now updates values as loaded in memory with the C-API + .. versionchanged:: 0.13.0 + This method now updates values as loaded in memory with the C API Parameters ---------- @@ -443,9 +437,9 @@ class Model: cell = cells[cell_id] if cell.fill_type in ('material', 'distribmat'): cell.temperature = group['temperature'][()] - if self.C_init: - C_cell = openmc.lib.cells[cell_id] - C_cell.set_temperature(group['temperature'][()]) + if self.is_initialized: + lib_cell = openmc.lib.cells[cell_id] + lib_cell.set_temperature(group['temperature'][()]) # Make sure number of materials matches mats_group = fh['materials'] @@ -459,14 +453,14 @@ class Model: mat_id = int(name.split()[1]) atom_density = group.attrs['atom_density'] materials[mat_id].set_density('atom/b-cm', atom_density) - if self.C_init: + if self.is_initialized: 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=False): - """Runs OpenMC. If the C-API has been initialized, then the C-API is + """Runs OpenMC. If the C API has been initialized, then the C API is used, otherwise, this method creates the XML files and runs OpenMC via a system call. In both cases this method returns the path to the last statepoint file generated. @@ -475,8 +469,8 @@ class Model: Instead of returning the final k-effective value, this function now returns the path to the final statepoint written. - .. versionchanged:: 0.12.3 - This method can utilize the C-API for execution + .. versionchanged:: 0.13.0 + This method can utilize the C API for execution Parameters ---------- @@ -520,8 +514,8 @@ class Model: last_statepoint = None # Operate in the provided working directory - with openmc.change_directory(Path(cwd)): - if self.C_init: + with _change_directory(Path(cwd)): + if self.is_initialized: # Handle the run options as applicable # First dont allow ones that must be set via init for arg_name, arg, default in zip( @@ -529,8 +523,7 @@ class Model: [threads, geometry_debug, restart_file, tracks], [None, False, None, False]): if arg != default: - msg = "{} must be set via Model.c_init(...)".format( - arg_name) + msg = f"{arg_name} must be set via Model.is_initialized(...)" raise ValueError(msg) if particles is not None: @@ -546,9 +539,9 @@ class Model: # Event-based can be set at init-time or on a case-basis. # Handle the argument here. - # TODO This will be dealt with in a future change to the C-API + # TODO This will be dealt with in a future change to the C API - # Then run using the C-API + # Then run using the C API openmc.lib.run() # Reset changes for the openmc.run kwargs handling @@ -577,12 +570,14 @@ class Model: last_statepoint = sp return last_statepoint - def calculate_volume(self, threads=None, output=True, cwd='.', - openmc_exec='openmc', mpi_args=None, - apply_volumes=True): + def calculate_volumes(self, threads=None, output=True, cwd='.', + openmc_exec='openmc', mpi_args=None, + apply_volumes=True): """Runs an OpenMC stochastic volume calculation and, if requested, applies volumes to the model + .. versionadded:: 0.13.0 + Parameters ---------- threads : int, optional @@ -590,16 +585,16 @@ class Model: threading enabled, the default is implementation-dependent but is usually equal to the number of hardware threads available (or a value set by the :envvar:`OMP_NUM_THREADS` environment variable). - This currenty only applies to the case when not using the C-API. + This currenty only applies to the case when not using the C API. output : bool, optional Capture OpenMC output from standard out openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. - This only applies to the case when not using the C-API. + This only applies to the case when not using the C API. mpi_args : list of str, optional MPI execute command and any additional MPI arguments to pass, e.g. ['mpiexec', '-n', '8']. - This only applies to the case when not using the C-API. + This only applies to the case when not using the C API. cwd : str, optional Path to working directory to run in. Defaults to the current working directory. @@ -613,10 +608,10 @@ class Model: raise ValueError("The Settings.volume_calculation attribute must" " be specified before executing this method!") - with openmc.change_directory(Path(cwd)): - if self.C_init: + with _change_directory(Path(cwd)): + if self.is_initialized: if threads is not None: - msg = "Threads must be set via Model.c_init(...)" + msg = "Threads must be set via Model.is_initialized(...)" raise ValueError(msg) if mpi_args is not None: msg = "The MPI environment must be set otherwise such as" \ @@ -645,7 +640,7 @@ class Model: if apply_volumes: # Load the results f_names = \ - ["volume_{}.h5".format(i + 1) + [f"volume_{i + 1}.h5" for i in range(len(self.settings.volume_calculations))] vol_calcs = [openmc.VolumeCalculation.load_results(f_name) for f_name in f_names] @@ -654,9 +649,9 @@ class Model: # First add them to the Python side self.geometry.add_volume_information(vol_calc) - # And now repeat for the C-API + # And now repeat for the C API if vol_calc.domain == 'material': - # Then we can do this in the C-API + # Then we can do this in the C API for domain_id in vol_calc.domains: self.update_material_volumes( [domain_id], vol_calc.volumes[domain_id]) @@ -670,6 +665,8 @@ class Model: `ImageMagick `_ which includes a `convert` command. + .. versionadded:: 0.13.0 + Parameters ---------- output : bool, optional @@ -679,7 +676,7 @@ class Model: working directory. openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. - This only applies to the case when not using the C-API. + This only applies to the case when not using the C API. convert : bool, optional Whether or not to attempt to convert from PPM to PNG convert_exec : str, optional @@ -691,14 +688,14 @@ class Model: raise ValueError("The Model.plots attribute must be specified " "before executing this method!") - with openmc.change_directory(Path(cwd)): - # TODO: openmc_init doesnt read plots.xml unless it is in plot mode + with _change_directory(Path(cwd)): + # TODO: openmis_initialized doesnt read plots.xml unless it is in plot mode # so the following will not work. Commented out for now and - # replacing with non-C-API code + # replacing with non-C API code self.export_to_xml() openmc.plot_geometry(output=output, openmc_exec=openmc_exec) - # if self.C_init: + # if self.is_initialized: # # Apply the output settings # if not output: # init_verbosity = openmc.lib.settings.verbosity @@ -735,13 +732,13 @@ class Model: check_value('attrib_name', attrib_name, ('temperature', 'volume', 'density', 'rotation', 'translation')) - # The C-API only allows setting density units of atom/b-cm and g/cm3 + # The C API only allows setting density units of atom/b-cm and g/cm3 check_value('density_units', density_units, ('atom/b-cm', 'g/cm3')) - # The C-API has no way to set cell volume so lets raise an exception + # The C API has no way to set cell volume so lets raise an exception if obj_type == 'cell' and attrib_name == 'volume': raise NotImplementedError( 'Setting a Cell volume is not supported!') - # Same with setting temperatures, TODO: update C-API for this + # Same with setting temperatures, TODO: update C API for this if obj_type == 'material' and attrib_name == 'temperature': raise NotImplementedError( 'Setting a Material temperature is not yet supported!') @@ -756,62 +753,61 @@ class Model: if obj_type == 'cell': by_name = self._cells_by_name by_id = self._cells_by_id - C_by_id = openmc.lib.cells + obj_by_id = openmc.lib.cells else: by_name = self._materials_by_name by_id = self._materials_by_id - C_by_id = openmc.lib.materials + obj_by_id = openmc.lib.materials # Get the list of ids to use if converting from names and accepting # only values that have actual ids - ids = [None] * len(names_or_ids) - for i, name_or_id in enumerate(names_or_ids): + ids = [] + for name_or_id in names_or_ids: if isinstance(name_or_id, Integral): if name_or_id in by_id: - ids[i] = int(name_or_id) + ids.append(int(name_or_id)) else: - msg = '{} ID {} is not present in the model!'.format( - obj_type.capitalize(), name_or_id) + cap_obj = obj_type.capitalize() + msg = f'{cap_obj} ID {name_or_id} " \ + "is not present in the model!' raise InvalidIDError(msg) elif isinstance(name_or_id, str): if name_or_id in by_name: - ids[i] = by_name[name_or_id].id + # Then by_name[name_or_id] is a list so we need to add all + # entries + ids.extend([obj.id for obj in by_name[name_or_id]]) else: - msg = '{} {} is not present in the model!'.format( - obj_type.capitalize(), name_or_id) + cap_obj = obj_type.capitalize() + msg = f'{cap_obj} {name_or_id} " \ + "is not present in the model!' raise InvalidIDError(msg) - # Now perform the change to both python and C-API + # Now perform the change to both python and C API for id_ in ids: obj = by_id[id_] - if attrib_name == 'rotation': - obj.rotation = value - elif attrib_name == 'translation': - obj.translation = value - elif attrib_name == 'volume': - obj.volume = value - elif attrib_name == 'temperature': - obj.temperature = value - elif attrib_name == 'density': + if attrib_name == 'density': obj.set_density(density_units, value) - # Next lets keep what is in C-API memory up to date as well - if self.C_init: - C_obj = C_by_id[id_] - if attrib_name == 'rotation': - C_obj.rotation = value - elif attrib_name == 'translation': - C_obj.translation = value - elif attrib_name == 'volume': - C_obj.volume = value - elif attrib_name == 'temperature': - C_obj.set_temperature(value) + else: + setattr(obj, attrib_name, value) + # Next lets keep what is in C API memory up to date as well + if self.is_initialized: + lib_obj = obj_by_id[id_] + if attrib_name == 'temperature': + lib_obj.set_temperature(value) elif attrib_name == 'density': - C_obj.set_density(value, density_units) + lib_obj.set_density(value, density_units) + else: + setattr(lib_obj, attrib_name, value) def rotate_cells(self, names_or_ids, vector): """Rotate the identified cell(s) by the specified rotation vector. The rotation is only applied to cells filled with a universe. + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -829,6 +825,11 @@ class Model: """Translate the identified cell(s) by the specified translation vector. The translation is only applied to cells filled with a universe. + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -845,6 +846,11 @@ class Model: def update_densities(self, names_or_ids, density, density_units='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 + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -863,6 +869,11 @@ class Model: def update_cell_temperatures(self, names_or_ids, temperature): """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 + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -879,6 +890,11 @@ class Model: def update_material_temperatures(self, names_or_ids, temperature): """Update the temperature of a set of materials to the given value + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -895,6 +911,11 @@ class Model: def update_material_volumes(self, names_or_ids, volume): """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 + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index ff5340249..7ddca6dd7 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -129,9 +129,9 @@ def pin_model_attributes(): """ + operator_kwargs = {'chain_file': chain, 'fission_q': fission_q} - return (mats, geom, settings, tals, plots, chain, fission_q, - chain_file_xml) + return (mats, geom, settings, tals, plots, operator_kwargs, chain_file_xml) @pytest.fixture(scope='module') def mpi_intracomm(): diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index dd6242fe1..6dc135056 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -4,10 +4,11 @@ from pathlib import Path from shutil import which import openmc import openmc.lib +from openmc.deplete.dummy_comm import DummyCommunicator -def test_init(run_in_tmpdir, pin_model_attributes): - mats, geom, settings, tals, plots, chain, fission_q, _ = \ +def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, _, _ = \ pin_model_attributes openmc.reset_auto_ids() @@ -22,29 +23,25 @@ def test_init(run_in_tmpdir, pin_model_attributes): assert test_model.settings.__dict__[ref_k] == ref_v assert len(test_model.tallies) == 0 assert len(test_model.plots) == 0 - assert test_model.chain_file is None - assert test_model.fission_q is None assert test_model._materials_by_id == {} assert test_model._materials_by_name == {} assert test_model._cells_by_id == {} assert test_model._cells_by_name == {} - assert test_model.C_init is False + assert test_model.is_initialized is False + assert isinstance(test_model.intracomm, DummyCommunicator) # Now check proper init of an actual model. Assume no interference between # parameters and so we can apply them all at once instead of testing one # parameter initialization at a time - test_model = openmc.Model(geom, mats, settings, tals, plots, chain, - fission_q) + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) assert test_model.geometry is geom assert test_model.materials is mats assert test_model.settings is settings assert test_model.tallies is tals assert test_model.plots is plots - assert test_model.chain_file == Path(chain).resolve() - assert test_model.fission_q is fission_q assert test_model._materials_by_id == {1: mats[0], 2: mats[1], 3: mats[2]} - assert test_model._materials_by_name == {'UO2': mats[0], 'Zirc': mats[1], - 'Borated water': mats[2]} + assert test_model._materials_by_name == { + 'UO2': [mats[0]], 'Zirc': [mats[1]], 'Borated water': [mats[2]]} # The last cell is the one that contains the infinite fuel assert test_model._cells_by_id == \ {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], @@ -53,13 +50,21 @@ def test_init(run_in_tmpdir, pin_model_attributes): # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': geom.root_universe.cells[2], '': geom.root_universe.cells[4], - 'inf fuel': geom.root_universe.cells[2].fill.cells[1]} - assert test_model.C_init is False + 'fuel': [geom.root_universe.cells[2]], + '': [geom.root_universe.cells[3], geom.root_universe.cells[4]], + 'inf fuel': [geom.root_universe.cells[2].fill.cells[1]]} + assert test_model.is_initialized is False + if mpi_intracomm is None: + assert isinstance(test_model.intracomm, DummyCommunicator) + else: + assert test_model.intracomm == mpi_intracomm # Finally test the parameter type checking by passing bad types and # obtaining the right exception types - def_params = [geom, mats, settings, tals, plots, chain, fission_q] + if mpi_intracomm is not None: + def_params = [geom, mats, settings, tals, plots, mpi_intracomm] + else: + def_params = [geom, mats, settings, tals, plots] for i in range(len(def_params)): args = def_params.copy() # Try an integer, as that is a bad type for all arguments @@ -69,7 +74,7 @@ def test_init(run_in_tmpdir, pin_model_attributes): def test_from_xml(run_in_tmpdir, pin_model_attributes): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes + mats, geom, settings, tals, plots, _, _ = pin_model_attributes # This test will write the individual files to xml and then init that way # and run the same sort of test as in test_init @@ -99,14 +104,12 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): assert test_model.settings.__dict__[ref_k] == settings.__dict__[ref_k] assert len(test_model.tallies) == 0 assert len(test_model.plots) == 0 - assert test_model.chain_file is None - assert test_model.fission_q is None assert test_model._materials_by_id == \ {1: test_model.materials[0], 2: test_model.materials[1], 3: test_model.materials[2]} assert test_model._materials_by_name == { - 'UO2': test_model.materials[0], 'Zirc': test_model.materials[1], - 'Borated water': test_model.materials[2]} + 'UO2': [test_model.materials[0]], 'Zirc': [test_model.materials[1]], + 'Borated water': [test_model.materials[2]]} assert test_model._cells_by_id == { 2: test_model.geometry.root_universe.cells[2], 3: test_model.geometry.root_universe.cells[3], @@ -115,28 +118,23 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': test_model.geometry.root_universe.cells[2], - '': test_model.geometry.root_universe.cells[4], - 'inf fuel': test_model.geometry.root_universe.cells[2].fill.cells[1]} - assert test_model.C_init is False + 'fuel': [test_model.geometry.root_universe.cells[2]], + '': [test_model.geometry.root_universe.cells[3], + test_model.geometry.root_universe.cells[4]], + 'inf fuel': [test_model.geometry.root_universe.cells[2].fill.cells[1]]} + assert test_model.is_initialized is False + assert isinstance(test_model.intracomm, DummyCommunicator) -def test_init_clear_C_api(run_in_tmpdir, pin_model_attributes, mpi_intracomm): +def test_init_finalize_lib(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # We are going to init and then make sure data is loaded - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) - # Set the depletion module to use the test intracomm as the depletion mods - # intracomm is the one that init uses. We will not perturb system state - # outside of this test by re-setting the openmc.deplete.comm to what it was - # before when we exist - if mpi_intracomm is not None: - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm - test_model.init_C_api() + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model.init_lib() # First check that the API is advertised as initialized - assert openmc.lib.LIB_INIT is True - assert test_model.C_init is True + assert openmc.lib.is_initialized is True + assert test_model.is_initialized is True # Now make sure it actually is initialized by making a call to the lib c_mat = openmc.lib.find_material((0.6, 0., 0.)) # This should be Borated water @@ -144,20 +142,14 @@ def test_init_clear_C_api(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert c_mat.id == 3 # Ok, now lets test that we can clear the data and check that it is cleared - test_model.clear_C_api() + test_model.finalize_lib() # First check that the API is advertised as initialized - assert openmc.lib.LIB_INIT is False - assert test_model.C_init is False + assert openmc.lib.is_initialized is False + assert test_model.is_initialized is False # Note we cant actually test that a sys call fails because we should get a # seg fault - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm - - # TODO: in above test, tests are necessary for all the arguments of init - def test_import_properties(run_in_tmpdir, mpi_intracomm): """Test importing properties on the Model class """ @@ -165,7 +157,7 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # Create PWR pin cell model and write XML files openmc.reset_auto_ids() model = openmc.examples.pwr_pin_cell() - model.init_C_api() + model.init_lib() # Change fuel temperature and density and export properties cell = openmc.lib.cells[1] @@ -185,7 +177,7 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): assert openmc.lib.cells[1].get_temperature() == 600. assert openmc.lib.materials[1].get_density('g/cm3') == pytest.approx(5.0) - # Clear the C-API + # Clear the C API openmc.lib.finalize() # Verify the attributes survived by exporting to XML and re-creating @@ -203,24 +195,17 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) # This case will run by getting the k-eff and tallies for command-line and - # C-API execution modes and ensuring they give the same result. + # C API execution modes and ensuring they give the same result. sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: cli_keff = sp.k_combined cli_flux = sp.get_tally(id=1).get_values()[0, 0, 0] - if mpi_intracomm is not None: - # Set the depletion module to use the test intracomm as the depletion - # module's intracomm is the one that init uses. We will not perturb - # system state outside of this test by re-setting the - # openmc.deplete.comm to what it was before when we exist - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm - test_model.init_C_api() + test_model.init_lib() sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: C_keff = sp.k_combined @@ -241,24 +226,12 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): with pytest.raises(ValueError): test_model.run(tracks=True) - test_model.clear_C_api() - - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm + test_model.finalize_lib() def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) - - if mpi_intracomm is not None: - # Set the depletion module to use the test intracomm as the depletion - # module's intracomm is the one that init uses. We will not perturb - # system state outside of this test by re-setting the - # openmc.deplete.comm to what it was before when we exist - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) # This test cannot check the correctness of the plot, but it can # check that a plot was made and that the expected ppm and png files are @@ -272,10 +245,10 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): else: exts = ['ppm'] - # We will run the test twice, the first time without C-API, the second with + # We will run the test twice, the first time without C API, the second with for i in range(2): if i == 1: - test_model.init_C_api() + test_model.init_lib() test_model.plot_geometry(output=True, convert=convert) # Now look for the files, expect to find test.ppm, plot_2.ppm, and if @@ -286,25 +259,14 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_file.exists() test_file.unlink() - test_model.clear_C_api() - - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm + test_model.finalize_lib() def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) - if mpi_intracomm is not None: - # Set the depletion module to use the test intracomm as the depletion - # module's intracomm is the one that init uses. We will not perturb - # system state outside of this test by re-setting the - # openmc.deplete.comm to what it was before when we exist - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm - test_model.init_C_api() + test_model.init_lib() # Now we can call rotate_cells, translate_cells, update_densities, # update_cell_temperatures, and update_material_temperatures and make sure @@ -376,7 +338,7 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Check initial conditions with pytest.raises(NotImplementedError): test_model.update_material_temperatures(['UO2'], 600.) - # TODO: When C-API material.set_temperature is implemented, uncomment below + # TODO: When C API material.set_temperature is implemented, uncomment below # assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 # # The temperature on the material will be None because its just the # # default assert test_model.materials[0].temperature is None @@ -395,8 +357,4 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert abs(openmc.lib.materials[1].volume - 2.) < 1e-13 assert abs(test_model.materials[0].volume - 2.) < 1e-13 - test_model.clear_C_api() - - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm + test_model.finalize_lib() From ad483b3fea16587d9a6cae35e684d0fcdb085825 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 11:18:35 -0500 Subject: [PATCH 43/59] Moved the dummy communicator to being within openmc vice openmc.deplete, added test of model.calculate_volumes and model.deplete, fixed issues identified from those tests, and added a DLL quieting method (still needs to be propagated to integrator.integrate --- openmc/__init__.py | 2 + openmc/deplete/__init__.py | 11 - openmc/deplete/abc.py | 2 +- openmc/deplete/helpers.py | 2 +- openmc/deplete/operator.py | 19 +- openmc/deplete/results.py | 2 +- openmc/lib/core.py | 103 +++++++-- openmc/model/model.py | 161 ++++++-------- openmc/{deplete/dummy_comm.py => mpi.py} | 8 + tests/unit_tests/conftest.py | 129 ----------- tests/unit_tests/test_deplete_chain.py | 3 +- tests/unit_tests/test_deplete_integrator.py | 8 +- tests/unit_tests/test_lib.py | 7 +- tests/unit_tests/test_model.py | 234 ++++++++++++++++++-- 14 files changed, 407 insertions(+), 284 deletions(-) rename openmc/{deplete/dummy_comm.py => mpi.py} (79%) diff --git a/openmc/__init__.py b/openmc/__init__.py index 8985099ba..7366e8383 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -30,8 +30,10 @@ from openmc.plotter import * from openmc.search import * from openmc.polynomial import * from . import examples +from openmc.mpi import DummyCommunicator, MPI, comm # Import a few names from the model module from openmc.model import rectangular_prism, hexagonal_prism, Model + __version__ = '0.13.0-dev' diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 00b29e319..b8c1cdfff 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -4,17 +4,6 @@ openmc.deplete A depletion front-end tool. """ -import sys -from unittest.mock import Mock - -from .dummy_comm import DummyCommunicator - -try: - from mpi4py import MPI - comm = MPI.COMM_WORLD -except ImportError: - MPI = Mock() - comm = DummyCommunicator() from .nuclide import * from .chain import * diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index ca16926ea..bf9f4da82 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -22,7 +22,7 @@ from uncertainties import ufloat from openmc.data import DataLibrary from openmc.lib import MaterialFilter, Tally from openmc.checkvalue import check_type, check_greater_than -from . import comm +from openmc import comm from .results import Results from .chain import Chain from .results_list import ResultsList diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 6c3badc09..a8cf5212e 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -10,7 +10,7 @@ import sys from numpy import dot, zeros, newaxis, asarray -from . import comm +from openmc import comm from openmc.checkvalue import check_type, check_greater_than from openmc.data import JOULE_PER_EV, REACTION_MT from openmc.lib import ( diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 5b000a389..e4db13f95 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -20,7 +20,7 @@ from uncertainties import ufloat import openmc from openmc.checkvalue import check_value import openmc.lib -from . import comm +from openmc import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates @@ -176,6 +176,9 @@ class Operator(TransportOperator): results are to be used. diff_burnable_mats : bool Whether to differentiate burnable materials with multiple instances + cleanup_when_done : bool + Whether to finalize and clear the shared library memory when the + depletion operation is complete. Defaults to clearing the library. """ _fission_helpers = { "average": AveragedFissionYieldHelper, @@ -202,7 +205,7 @@ class Operator(TransportOperator): self.settings = settings self.geometry = geometry self.diff_burnable_mats = diff_burnable_mats - self._cleanup_when_done = True + self.cleanup_when_done = True # Reduce the chain before we create more materials if reduce_chain: @@ -318,6 +321,10 @@ class Operator(TransportOperator): # Reset results in OpenMC openmc.lib.reset() + # Update the number densities regardless of the source rate + self.number.set_density(vec) + self._update_materials() + # If the source rate is zero, return zero reaction rates without running # a transport solve if source_rate == 0.0: @@ -328,11 +335,7 @@ class Operator(TransportOperator): # Prevent OpenMC from complaining about re-creating tallies openmc.reset_auto_ids() - # Update status - self.number.set_density(vec) - - # Update material compositions and tally nuclides - self._update_materials() + # Update tally nuclides data in preparation for transport solve nuclides = self._get_tally_nuclides() self._rate_helper.nuclides = nuclides self._normalization_helper.nuclides = nuclides @@ -556,7 +559,7 @@ class Operator(TransportOperator): def finalize(self): """Finalize a depletion simulation and release resources.""" - if self._cleanup_when_done: + if self.cleanup_when_done: openmc.lib.finalize() def _update_materials(self): diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index ce8331e82..05d78f629 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -9,7 +9,7 @@ import copy import h5py import numpy as np -from . import comm, MPI +from openmc import comm, MPI from .reaction_rates import ReactionRates VERSION_RESULTS = (1, 1) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 6b72dde55..6f08f76b2 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -2,6 +2,7 @@ from contextlib import contextmanager from ctypes import (c_bool, c_int, c_int32, c_int64, c_double, c_char_p, c_char, POINTER, Structure, c_void_p, create_string_buffer) import sys +import os import numpy as np from numpy.ctypeslib import as_array @@ -110,9 +111,14 @@ def global_bounding_box(): return llc, urc -def calculate_volumes(): +def calculate_volumes(output=True): """Run stochastic volume calculation""" - _dll.openmc_calculate_volumes() + + if output: + _dll.openmc_calculate_volumes() + else: + with quiet_dll(): + _dll.openmc_calculate_volumes() def current_batch(): @@ -127,13 +133,17 @@ def current_batch(): return c_int.in_dll(_dll, 'current_batch').value -def export_properties(filename=None): +def export_properties(filename=None, output=True): """Export physical properties. + .. versionchanged:: 0.13.0 + Parameters ---------- filename : str or None Filename to export properties to (defaults to "properties.h5") + output: bool, optional + Whether or not to show output. Defaults to showing output See Also -------- @@ -142,7 +152,11 @@ def export_properties(filename=None): """ if filename is not None: filename = c_char_p(filename.encode()) - _dll.openmc_properties_export(filename) + if output: + _dll.openmc_properties_export(filename) + else: + with quiet_dll(): + _dll.openmc_properties_export(filename) def finalize(): @@ -220,15 +234,19 @@ def import_properties(filename): _dll.openmc_properties_import(filename.encode()) -def init(args=None, intracomm=None): +def init(args=None, intracomm=None, output=True): """Initialize OpenMC + .. versionchanged:: 0.13.0 + Parameters ---------- - args : list of str + args : list of str, optional Command-line arguments - intracomm : mpi4py.MPI.Intracomm or None + intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator + output: bool, optional + Whether or not to show output. Defaults to showing output """ if args is not None: @@ -254,7 +272,11 @@ def init(args=None, intracomm=None): address = MPI._addressof(intracomm) intracomm = c_void_p(address) - _dll.openmc_init(argc, argv, intracomm) + if output: + _dll.openmc_init(argc, argv, intracomm) + else: + with quiet_dll(): + _dll.openmc_init(argc, argv, intracomm) openmc.lib.is_initialized = True @@ -345,9 +367,22 @@ def next_batch(): return status.value -def plot_geometry(): - """Plot geometry""" - _dll.openmc_plot_geometry() +def plot_geometry(output=True): + """Plot geometry + + .. versionchanged:: 0.13.0 + + Parameters + ---------- + output: bool, optional + Whether or not to show output. Defaults to showing output + """ + + if output: + _dll.openmc_plot_geometry() + else: + with quiet_dll(): + _dll.openmc_plot_geometry() def reset(): @@ -360,9 +395,22 @@ def reset_timers(): _dll.openmc_reset_timers() -def run(): - """Run simulation""" - _dll.openmc_run() +def run(output=True): + """Run simulation + + .. versionchanged:: 0.13.0 + + Parameters + ---------- + output: bool, optional + Whether or not to show output. Defaults to showing output + """ + + if output: + _dll.openmc_run() + else: + with quiet_dll(): + _dll.openmc_run() def simulation_init(): @@ -478,3 +526,30 @@ class _FortranObjectWithID(_FortranObject): # assigned. If the array index of the object is out of bounds, an # OutOfBoundsError will be raised here by virtue of referencing self.id self.id + + +@contextmanager +def quiet_dll(): + """This context manager allows us to suppress standard output from DLLs""" + sys.stdout.flush() + # Save the initial file descriptor states + initial_stdout = sys.stdout + initial_stdout_fno = os.dup(sys.stdout.fileno()) + # Get a garbage descriptor so we can throw away output + devnull = os.open(os.devnull, os.O_WRONLY) + + # Get the current stdout stream and make a duplicate of it + new_stdout = os.dup(1) + # Copy the garbage output to the stdout stream + os.dup2(devnull, 1) + os.close(devnull) + # Now point stdout to the re-defined stdout + sys.stdout = os.fdopen(new_stdout, 'w') + + try: + yield + finally: + # Now we just clean up after ourselves and reset the streams + sys.stdout = initial_stdout + sys.stdout.flush() + os.dup2(initial_stdout_fno, 1) diff --git a/openmc/model/model.py b/openmc/model/model.py index e560ef9fb..c65470e3e 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -191,17 +191,11 @@ class Model: @intracomm.setter def intracomm(self, intracomm): - try: - from mpi4py import MPI - mpi_avail = True - except ImportError: - mpi_avail = False - if intracomm is None or not mpi_avail: - # TODO: move dummy_comm from openmc.deplete to openmc - from openmc.deplete.dummy_comm import DummyCommunicator - self._intracomm = DummyCommunicator() + if intracomm is None: + self._intracomm = openmc.comm else: - check_type('intracomm', intracomm, MPI.Comm) + check_type('intracomm', intracomm, + (openmc.MPI.Comm, openmc.DummyCommunicator)) self._intracomm = intracomm @classmethod @@ -263,25 +257,12 @@ class Model: # where it exists (here in init), but in the future the functionality # should be exposed so that it can be accessed via model.run(...) - # TODO: the output flag is not yet implemented for the openmc.lib.init - # command. This will be the subject of future work. - - args = [] - - if isinstance(threads, Integral) and threads > 0: - args += ['-s', str(threads)] - - if geometry_debug: - args.append('-g') - - if event_based: - args.append('-e') - - if isinstance(restart_file, str): - args += ['-r', restart_file] - - if tracks: - args.append('-t') + args = openmc.process_CLI_arguments( + volume=False, geometry_debug=geometry_debug, + restart_file=restart_file, threads=threads, tracks=tracks, + event_based=event_based) + # Args adds the openmc_exec command in the first entry; remove it + args = args[1:] self.finalize_lib() @@ -289,7 +270,13 @@ class Model: self.export_to_xml() self.intracomm.barrier() - openmc.lib.init(args=args, intracomm=self.intracomm) + if isinstance(self.intracomm, openmc.DummyCommunicator): + # openmc.lib.init does not accept DummyCommunicator, and importing + # the DummyCommunicator class is overkill. Filter it here + intracomm = None + else: + intracomm = self.intracomm + openmc.lib.init(args=args, intracomm=intracomm, output=output) def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -301,7 +288,7 @@ class Model: openmc.lib.finalize() def deplete(self, timesteps, method='cecm', final_step=True, - operator_kwargs=None, integrator_kwargs=None, directory='.'): + operator_kwargs=None, directory='.', **integrator_kwargs): """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 @@ -320,25 +307,34 @@ class Model: operator_kwargs : dict Keyword arguments passed to the depletion Operator initializer (e.g., :func:`openmc.deplete.Operator`) - integrator_kwargs : dict - Keyword arguments passed to the depletion Operator initializer - (e.g., :func:`openmc.deplete.integrator.cecm`) directory : str, optional Directory to write XML files to. If it doesn't exist already, it will be created. Defaults to the current working directory - **kwargs - Keyword arguments passed to integration function (e.g., - :func:`openmc.deplete.integrator.cecm`) + integrator_kwargs : dict + Remaining keyword arguments passed to the depletion Integrator + initializer (e.g., :func:`openmc.deplete.integrator.cecm`). """ + if operator_kwargs is None: + op_kwargs = {} + elif isinstance(operator_kwargs, dict): + op_kwargs = operator_kwargs + else: + msg = "operator_kwargs must be a dict or None" + raise ValueError(msg) + # Import openmc.deplete here so the Model can be used even if the # shared library is unavailable. import openmc.deplete as dep + # Store whether or not the library was initialized when we started + started_initialized = self.is_initialized + with _change_directory(Path(directory)): depletion_operator = \ - dep.Operator(self.geometry, self.settings, **operator_kwargs) + dep.Operator(self.geometry, self.settings, **op_kwargs) + # Tell depletion_operator.finalize NOT to clear C API memory when # it is done depletion_operator.cleanup_when_done = False @@ -351,13 +347,9 @@ class Model: **integrator_kwargs) # Now perform the depletion + # TODO: add output parameter to integrate integrator.integrate(final_step) - # If we did not perform a transport calculation on the final step, - # then make the code update the C API material inventory - if not final_step: - depletion_operator._update_materials() - # Now make the python Materials match the C API material data for mat_id, mat in self._materials_by_id.items(): if mat.depletable: @@ -370,6 +362,11 @@ class Model: mat.add_nuclide(nuc, density) mat.set_density('atom/b-cm', sum(densities)) + # If we didnt start intialized, we should cleanup after ourselves + if not started_initialized: + depletion_operator.cleanup_when_done = True + depletion_operator.finalize() + def export_to_xml(self, directory='.'): """Export model to XML files. @@ -531,25 +528,16 @@ class Model: if isinstance(particles, Integral) and particles > 0: openmc.lib.settings.particles = particles - # If we dont want output, make the verbosity quiet - if not output: - init_verbosity = openmc.lib.settings.verbosity - if not output: - openmc.lib.settings.verbosity = 1 - # Event-based can be set at init-time or on a case-basis. # Handle the argument here. # TODO This will be dealt with in a future change to the C API # Then run using the C API - openmc.lib.run() + openmc.lib.run(output) # Reset changes for the openmc.run kwargs handling if particles is not None: openmc.lib.settings.particles = init_particles - if not output: - # Then re-set the initial verbosity - openmc.lib.settings.verbosity = init_verbosity else: # Then run via the command line @@ -603,7 +591,7 @@ class Model: to the model. Defaults to applying the volumes. """ - if len(self.settings.volume_calculation) == 0: + if len(self.settings.volume_calculations) == 0: # Then there is no volume calculation specified raise ValueError("The Settings.volume_calculation attribute must" " be specified before executing this method!") @@ -618,18 +606,9 @@ class Model: "with the call to mpi4py" raise ValueError(msg) - # Apply the output settings - if not output: - init_verbosity = openmc.lib.settings.verbosity - if not output: - openmc.lib.settings.verbosity = 1 - # Compute the volumes - openmc.lib.calculate_volumes() + openmc.lib.calculate_volumes(output) - # Reset the output verbosity - if not output: - openmc.lib.settings.verbosity = init_verbosity else: self.export_to_xml() openmc.calculate_volumes(threads=threads, output=output, @@ -638,23 +617,19 @@ class Model: # Now we apply the volumes if apply_volumes: - # Load the results - f_names = \ - [f"volume_{i + 1}.h5" - for i in range(len(self.settings.volume_calculations))] - vol_calcs = [openmc.VolumeCalculation.load_results(f_name) - for f_name in f_names] - # And now we can add them to the model - for vol_calc in vol_calcs: + # Load the results and add them to the model + for i, vol_calc in enumerate(self.settings.volume_calculations): + f_name = f"volume_{i + 1}.h5" + vol_calc.load_results(f_name) # First add them to the Python side self.geometry.add_volume_information(vol_calc) # And now repeat for the C API - if vol_calc.domain == 'material': + if self.is_initialized and vol_calc.domain_type == 'material': # Then we can do this in the C API - for domain_id in vol_calc.domains: - self.update_material_volumes( - [domain_id], vol_calc.volumes[domain_id]) + for domain_id in vol_calc.ids: + openmc.lib.materials[domain_id].volume = \ + vol_calc.volumes[domain_id].n def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', convert=True, convert_exec='convert'): @@ -689,25 +664,15 @@ class Model: "before executing this method!") with _change_directory(Path(cwd)): - # TODO: openmis_initialized doesnt read plots.xml unless it is in plot mode - # so the following will not work. Commented out for now and - # replacing with non-C API code + # TODO: openmc.is_initialized doesnt read plots.xml unless it is + # in plot mode so the following will not work. Commented out for + # now and replacing with non-C API code self.export_to_xml() openmc.plot_geometry(output=output, openmc_exec=openmc_exec) # if self.is_initialized: - # # Apply the output settings - # if not output: - # init_verbosity = openmc.lib.settings.verbosity - # if not output: - # openmc.lib.settings.verbosity = 1 - # # Compute the volumes - # openmc.lib.plot_geometry() - - # # Reset the output verbosity - # if not output: - # openmc.lib.settings.verbosity = init_verbosity + # openmc.lib.plot_geometry(output) # else: # self.export_to_xml() # openmc.plot_geometry(output=output, openmc_exec=openmc_exec) @@ -721,8 +686,8 @@ class Model: png_file = ppm_file.replace('.ppm', '.png') subprocess.check_call([convert_exec, ppm_file, png_file]) - def _change_py_C_attribs(self, names_or_ids, value, obj_type, attrib_name, - density_units='atom/b-cm'): + def _change_py_lib_attribs(self, names_or_ids, value, obj_type, + attrib_name, density_units='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)) @@ -819,7 +784,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, vector, 'cell', 'rotation') + self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'rotation') def translate_cells(self, names_or_ids, vector): """Translate the identified cell(s) by the specified translation vector. @@ -841,7 +806,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, vector, 'cell', 'translation') + self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'translation') def update_densities(self, names_or_ids, density, density_units='atom/b-cm'): """Update the density of a given set of materials to a new value @@ -863,7 +828,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, density, 'material', 'density', + self._change_py_lib_attribs(names_or_ids, density, 'material', 'density', density_units) def update_cell_temperatures(self, names_or_ids, temperature): @@ -884,7 +849,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, temperature, 'cell', + self._change_py_lib_attribs(names_or_ids, temperature, 'cell', 'temperature') def update_material_temperatures(self, names_or_ids, temperature): @@ -905,7 +870,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, temperature, 'material', + self._change_py_lib_attribs(names_or_ids, temperature, 'material', 'temperature') def update_material_volumes(self, names_or_ids, volume): @@ -926,4 +891,4 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, volume, 'material', 'volume') + self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume') diff --git a/openmc/deplete/dummy_comm.py b/openmc/mpi.py similarity index 79% rename from openmc/deplete/dummy_comm.py rename to openmc/mpi.py index 7ac9be6c3..b64a84bc6 100644 --- a/openmc/deplete/dummy_comm.py +++ b/openmc/mpi.py @@ -1,4 +1,5 @@ import sys +from unittest.mock import Mock class DummyCommunicator: @@ -31,3 +32,10 @@ class DummyCommunicator: def Abort(self, exit_code_or_msg): sys.exit(exit_code_or_msg) + +try: + from mpi4py import MPI + comm = MPI.COMM_WORLD +except ImportError: + MPI = Mock() + comm = DummyCommunicator() diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 7ddca6dd7..51d1b19a3 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -1,138 +1,9 @@ -from math import pi import openmc import pytest from tests.regression_tests import config -@pytest.fixture(scope='module') -def pin_model_attributes(): - uo2 = openmc.Material(name='UO2') - uo2.set_density('g/cm3', 10.29769) - uo2.add_element('U', 1., enrichment=2.4) - uo2.add_element('O', 2.) - - zirc = openmc.Material(name='Zirc') - zirc.set_density('g/cm3', 6.55) - zirc.add_element('Zr', 1.) - - borated_water = openmc.Material(name='Borated water') - borated_water.set_density('g/cm3', 0.740582) - borated_water.add_element('B', 4.0e-5) - 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') - - mats = openmc.Materials([uo2, zirc, borated_water]) - - pitch = 1.25984 - fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') - clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') - box = openmc.model.rectangular_prism(pitch, pitch, - boundary_type='reflective') - - # Define cells - fuel_inf_cell = openmc.Cell(name='inf fuel', fill=uo2) - fuel_inf_univ = openmc.Universe(cells=[fuel_inf_cell]) - fuel = openmc.Cell(name='fuel', fill=fuel_inf_univ, region=-fuel_or) - clad = openmc.Cell(fill=zirc, region=+fuel_or & -clad_or) - water = openmc.Cell(fill=borated_water, region=+clad_or & box) - - # Define overall geometry - geom = openmc.Geometry([fuel, clad, water]) - uo2.volume = pi * fuel_or.r**2 - - settings = openmc.Settings() - settings.batches = 100 - settings.inactive = 10 - 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.source.Source(space=uniform_dist) - - entropy_mesh = openmc.RegularMesh() - entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] - entropy_mesh.upper_right = [0.39218, 0.39218, 1.e50] - entropy_mesh.dimension = [10, 10, 1] - settings.entropy_mesh = entropy_mesh - - tals = openmc.Tallies() - tal = openmc.Tally(name='test') - tal.scores = ['flux'] - tals.append(tal) - - plot1 = openmc.Plot() - plot1.origin = (0., 0., 0.) - plot1.width = (pitch, pitch) - plot1.pixels = (300, 300) - plot1.color_by = 'material' - plot1.filename = 'test' - plot2 = openmc.Plot() - plot2.origin = (0., 0., 0.) - plot2.width = (pitch, pitch) - plot2.pixels = (300, 300) - plot2.color_by = 'cell' - plots = openmc.Plots((plot1, plot2)) - - chain = './chain_simple.xml' - fission_q = {'U235': 200e6} - - chain_file_xml = """ - - - - - - - - - - - - - - - - - - - - - 2.53000e-02 - - Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 4.356330e-05 - - - - - - - 2.53000e-02 - - Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 4.9097e-6 - - - - - - - 2.53000e-02 - - Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 3.7100E-07 - - - - -""" - operator_kwargs = {'chain_file': chain, 'fission_q': fission_q} - - return (mats, geom, settings, tals, plots, operator_kwargs, chain_file_xml) - @pytest.fixture(scope='module') def mpi_intracomm(): if config['mpi']: diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index fa90b35f2..0904ff39a 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -7,7 +7,8 @@ import os from pathlib import Path import numpy as np -from openmc.deplete import comm, Chain, reaction_rates, nuclide, cram, pool +from openmc import comm +from openmc.deplete import Chain, reaction_rates, nuclide, cram, pool import pytest from tests import cdtemp diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 3195c5976..ec424b05b 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -14,11 +14,11 @@ import numpy as np from uncertainties import ufloat import pytest +from openmc import comm from openmc.deplete import ( - ReactionRates, Results, ResultsList, comm, OperatorResult, - PredictorIntegrator, CECMIntegrator, CF4Integrator, CELIIntegrator, - EPCRK4Integrator, LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, - cram) + ReactionRates, Results, ResultsList, OperatorResult, PredictorIntegrator, + CECMIntegrator, CF4Integrator, CELIIntegrator, EPCRK4Integrator, + LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram) from tests import dummy_operator diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 41613832f..079b4f9eb 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -713,7 +713,6 @@ def test_trigger_set_n_batches(uo2_trigger_model, mpi_intracomm): def test_cell_translation(pincell_model_w_univ, mpi_intracomm): openmc.lib.finalize() openmc.lib.init(intracomm=mpi_intracomm) - openmc.lib.simulation_init() # Cell 1 is filled with a material so it has a translation, but we can't # set it. cell = openmc.lib.cells[1] @@ -727,9 +726,12 @@ def test_cell_translation(pincell_model_w_univ, mpi_intracomm): # This time we *can* set it cell.translation = (1., 0., -1.) assert cell.translation == pytest.approx([1., 0., -1.]) + openmc.lib.finalize() -def test_cell_rotation(pincell_model_w_univ): +def test_cell_rotation(pincell_model_w_univ, mpi_intracomm): + openmc.lib.finalize() + openmc.lib.init(intracomm=mpi_intracomm) # Cell 1 is filled with a material so we cannot rotate it, but we can get # its rotation matrix (which will be the identity matrix) cell = openmc.lib.cells[1] @@ -742,3 +744,4 @@ def test_cell_rotation(pincell_model_w_univ): assert cell.rotation == pytest.approx([0., 0., 0.]) cell.rotation = (180., 0., 0.) assert cell.rotation == pytest.approx([180., 0., 0.]) + openmc.lib.finalize() diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 6dc135056..4ab4c7eb7 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,10 +1,110 @@ +from math import pi import numpy as np import pytest from pathlib import Path from shutil import which import openmc import openmc.lib -from openmc.deplete.dummy_comm import DummyCommunicator +from openmc.mpi import DummyCommunicator + + +@pytest.fixture(scope='function') +def pin_model_attributes(): + uo2 = openmc.Material(material_id=1, name='UO2') + uo2.set_density('g/cm3', 10.29769) + uo2.add_element('U', 1., enrichment=2.4) + uo2.add_element('O', 2.) + uo2.depletable = True + + zirc = openmc.Material(material_id=2, name='Zirc') + zirc.set_density('g/cm3', 6.55) + zirc.add_element('Zr', 1.) + zirc.depletable = False + + borated_water = openmc.Material(material_id=3, name='Borated water') + borated_water.set_density('g/cm3', 0.740582) + borated_water.add_element('B', 4.0e-5) + 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') + borated_water.depletable = False + + mats = openmc.Materials([uo2, zirc, borated_water]) + + pitch = 1.25984 + fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') + clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') + box = openmc.model.rectangular_prism(pitch, pitch, + boundary_type='reflective') + + # Define cells + fuel_inf_cell = openmc.Cell(cell_id=1, name='inf fuel', fill=uo2) + fuel_inf_univ = openmc.Universe(universe_id=1, cells=[fuel_inf_cell]) + fuel = openmc.Cell(cell_id=2, name='fuel', + fill=fuel_inf_univ, region=-fuel_or) + clad = openmc.Cell(cell_id=3, fill=zirc, region=+fuel_or & -clad_or) + water = openmc.Cell(cell_id=4, fill=borated_water, region=+clad_or & box) + + # Define overall geometry + geom = openmc.Geometry([fuel, clad, water]) + uo2.volume = pi * fuel_or.r**2 + + settings = openmc.Settings() + settings.batches = 100 + settings.inactive = 10 + settings.particles = 1000 + + # 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.source.Source(space=uniform_dist) + + entropy_mesh = openmc.RegularMesh() + entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] + entropy_mesh.upper_right = [0.39218, 0.39218, 1.e50] + entropy_mesh.dimension = [10, 10, 1] + settings.entropy_mesh = entropy_mesh + + tals = openmc.Tallies() + tal = openmc.Tally(tally_id=1, name='test') + tal.filters = [openmc.MaterialFilter(bins=[uo2])] + tal.scores = ['flux', 'fission'] + tals.append(tal) + + plot1 = openmc.Plot(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.origin = (0., 0., 0.) + plot2.width = (pitch, pitch) + plot2.pixels = (300, 300) + plot2.color_by = 'cell' + plots = openmc.Plots((plot1, plot2)) + + chain = './test_chain.xml' + + chain_file_xml = """ + + + + + + 2.53000e-02 + + Xe136 + 1.0 + + + + +""" + operator_kwargs = {'chain_file': chain} + + return (mats, geom, settings, tals, plots, operator_kwargs, chain_file_xml) def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): @@ -130,7 +230,7 @@ def test_init_finalize_lib(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # We are going to init and then make sure data is loaded mats, geom, settings, tals, plots, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) - test_model.init_lib() + test_model.init_lib(output=False) # First check that the API is advertised as initialized assert openmc.lib.is_initialized is True @@ -157,13 +257,13 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # Create PWR pin cell model and write XML files openmc.reset_auto_ids() model = openmc.examples.pwr_pin_cell() - model.init_lib() + model.init_lib(output=False) # Change fuel temperature and density and export properties cell = openmc.lib.cells[1] cell.set_temperature(600.0) cell.fill.set_density(5.0, 'g/cm3') - openmc.lib.export_properties() + openmc.lib.export_properties(output=False) # Import properties to existing model model.import_properties("properties.h5") @@ -203,17 +303,20 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: cli_keff = sp.k_combined - cli_flux = sp.get_tally(id=1).get_values()[0, 0, 0] + cli_flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] + cli_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] - test_model.init_lib() + test_model.init_lib(output=False) sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: - C_keff = sp.k_combined - C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] + lib_keff = sp.k_combined + lib_flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] + lib_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] # and lets compare results - assert abs(C_keff - cli_keff) < 1e-13 - assert abs(C_flux - cli_flux) < 1e-13 + assert abs(lib_keff - cli_keff) < 1e-13 + assert abs(lib_flux - cli_flux) < 1e-13 + assert abs(lib_fiss - cli_fiss) < 1e-13 # Now we should make sure that the flags for items which should be handled # by init are properly set @@ -248,8 +351,8 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # We will run the test twice, the first time without C API, the second with for i in range(2): if i == 1: - test_model.init_lib() - test_model.plot_geometry(output=True, convert=convert) + test_model.init_lib(output=False) + test_model.plot_geometry(output=False, convert=convert) # Now look for the files, expect to find test.ppm, plot_2.ppm, and if # convert is True, test.png, plot_2.png @@ -262,11 +365,11 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.finalize_lib() -def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): +def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) - test_model.init_lib() + test_model.init_lib(output=False) # Now we can call rotate_cells, translate_cells, update_densities, # update_cell_temperatures, and update_material_temperatures and make sure @@ -358,3 +461,106 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert abs(test_model.materials[0].volume - 2.) < 1e-13 test_model.finalize_lib() + + +# def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): +# mats, geom, settings, tals, plots, op_kwargs, chain_file_xml = \ +# pin_model_attributes +# with open('test_chain.xml', 'w') as f: +# f.write(chain_file_xml) +# test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + +# initial_mat = mats[0].clone() +# initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] + +# # Note that the chain file includes only U-235 fission to a stable Xe136 w/ +# # a yield of 100%. Thus all the U235 we lose becomes Xe136 + +# # 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, +# operator_kwargs=op_kwargs, +# power=1.) +# # Get the new Xe136 and U235 atom densities +# after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] +# after_u = mats[0].get_nuclide_atom_densities()['U235'][1] +# assert abs((after_xe + after_u) - initial_u) < 1e-15 +# assert test_model.is_initialized is False + +# # Reset the initial material densities +# mats[0].nuclides.clear() +# densities = initial_mat.get_nuclide_atom_densities() +# tot_density = 0. +# for nuc, density in densities.values(): +# mats[0].add_nuclide(nuc, density) +# tot_density += density +# mats[0].set_density('atom/b-cm', tot_density) + +# # Now we can re-run with the pre-initialized API +# test_model.init_lib(output=False) +# test_model.deplete([1e6], 'predictor', final_step=False, +# operator_kwargs=op_kwargs, +# power=1.) +# # Get the new Xe136 and U235 atom densities +# after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] +# after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] +# assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 +# assert test_model.is_initialized is True + +# # And end by comparing to the previous case +# assert abs(after_xe - after_lib_xe) < 1e-15 +# assert abs(after_u - after_lib_u) < 1e-15 + +# test_model.finalize_lib() + + +def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + + # With no vol calcs, it should fail + with pytest.raises(ValueError): + test_model.calculate_volumes(output=False) + + # Add a cell and mat volume calc + material_vol_calc = openmc.VolumeCalculation( + [mats[2]], samples=1000, lower_left=(-.63, -.63, -100.), + upper_right=(.63, .63, 100.)) + cell_vol_calc = openmc.VolumeCalculation( + [geom.root_universe.cells[3]], samples=1000, + lower_left=(-.63, -.63, -100.), upper_right=(.63, .63, 100.)) + test_model.settings.volume_calculations = \ + [material_vol_calc, cell_vol_calc] + + # Now lets compute the volumes and check to see if it was applied + # First lets do without using the C-API + # Make sure the volumes are unassigned first + assert mats[2].volume is None + assert geom.root_universe.cells[3].volume is None + test_model.calculate_volumes(output=False, apply_volumes=True) + + # Now let's test that we have volumes assigned; we arent checking the + # value, just that the value was changed + assert mats[2].volume > 0. + assert geom.root_universe.cells[3].volume > 0. + + # Now reset the values + mats[2].volume = None + geom.root_universe.cells[3].volume = None + + # And do again with an initialized library + for file in ['volume_1.h5', 'volume_2.h5']: + file = Path(file) + file.unlink() + test_model.init_lib(output=False) + test_model.calculate_volumes(output=False, apply_volumes=True) + assert mats[2].volume > 0. + assert geom.root_universe.cells[3].volume > 0. + assert openmc.lib.materials[3].volume == mats[2].volume + + test_model.finalize_lib() + + + From 61d067f1f5a83b4167c9e9f6ea3efc4ec36aac17 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 11:19:11 -0500 Subject: [PATCH 44/59] oops; had test_model.py:test_deplete commented out. fixed --- tests/unit_tests/test_model.py | 86 +++++++++++++++++----------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4ab4c7eb7..07a356097 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -463,56 +463,56 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.finalize_lib() -# def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): -# mats, geom, settings, tals, plots, op_kwargs, chain_file_xml = \ -# pin_model_attributes -# with open('test_chain.xml', 'w') as f: -# f.write(chain_file_xml) -# test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) +def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, op_kwargs, chain_file_xml = \ + pin_model_attributes + with open('test_chain.xml', 'w') as f: + f.write(chain_file_xml) + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) -# initial_mat = mats[0].clone() -# initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] + initial_mat = mats[0].clone() + initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] -# # Note that the chain file includes only U-235 fission to a stable Xe136 w/ -# # a yield of 100%. Thus all the U235 we lose becomes Xe136 + # Note that the chain file includes only U-235 fission to a stable Xe136 w/ + # a yield of 100%. Thus all the U235 we lose becomes Xe136 -# # 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, -# operator_kwargs=op_kwargs, -# power=1.) -# # Get the new Xe136 and U235 atom densities -# after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] -# after_u = mats[0].get_nuclide_atom_densities()['U235'][1] -# assert abs((after_xe + after_u) - initial_u) < 1e-15 -# assert test_model.is_initialized is False + # 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, + operator_kwargs=op_kwargs, + power=1.) + # Get the new Xe136 and U235 atom densities + after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] + after_u = mats[0].get_nuclide_atom_densities()['U235'][1] + assert abs((after_xe + after_u) - initial_u) < 1e-15 + assert test_model.is_initialized is False -# # Reset the initial material densities -# mats[0].nuclides.clear() -# densities = initial_mat.get_nuclide_atom_densities() -# tot_density = 0. -# for nuc, density in densities.values(): -# mats[0].add_nuclide(nuc, density) -# tot_density += density -# mats[0].set_density('atom/b-cm', tot_density) + # Reset the initial material densities + mats[0].nuclides.clear() + densities = initial_mat.get_nuclide_atom_densities() + tot_density = 0. + for nuc, density in densities.values(): + mats[0].add_nuclide(nuc, density) + tot_density += density + mats[0].set_density('atom/b-cm', tot_density) -# # Now we can re-run with the pre-initialized API -# test_model.init_lib(output=False) -# test_model.deplete([1e6], 'predictor', final_step=False, -# operator_kwargs=op_kwargs, -# power=1.) -# # Get the new Xe136 and U235 atom densities -# after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] -# after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] -# assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 -# assert test_model.is_initialized is True + # Now we can re-run with the pre-initialized API + test_model.init_lib(output=False) + test_model.deplete([1e6], 'predictor', final_step=False, + operator_kwargs=op_kwargs, + power=1.) + # Get the new Xe136 and U235 atom densities + after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] + after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] + assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 + assert test_model.is_initialized is True -# # And end by comparing to the previous case -# assert abs(after_xe - after_lib_xe) < 1e-15 -# assert abs(after_u - after_lib_u) < 1e-15 + # And end by comparing to the previous case + assert abs(after_xe - after_lib_xe) < 1e-15 + assert abs(after_u - after_lib_u) < 1e-15 -# test_model.finalize_lib() + test_model.finalize_lib() def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): From b335f82792e60aaf7da9cd02fbe2921121405aeb Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 14:39:53 -0500 Subject: [PATCH 45/59] Made integrator quiet and fixed failing test caused by MPICommunicator types --- openmc/lib/core.py | 79 +++++++++++++++++----------------- openmc/model/model.py | 34 +++++++-------- tests/unit_tests/test_model.py | 4 +- 3 files changed, 58 insertions(+), 59 deletions(-) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 6f08f76b2..3b691721c 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -114,11 +114,8 @@ def global_bounding_box(): def calculate_volumes(output=True): """Run stochastic volume calculation""" - if output: + with quiet_dll(output): _dll.openmc_calculate_volumes() - else: - with quiet_dll(): - _dll.openmc_calculate_volumes() def current_batch(): @@ -152,11 +149,9 @@ def export_properties(filename=None, output=True): """ if filename is not None: filename = c_char_p(filename.encode()) - if output: + + with quiet_dll(output): _dll.openmc_properties_export(filename) - else: - with quiet_dll(): - _dll.openmc_properties_export(filename) def finalize(): @@ -272,11 +267,8 @@ def init(args=None, intracomm=None, output=True): address = MPI._addressof(intracomm) intracomm = c_void_p(address) - if output: + with quiet_dll(output): _dll.openmc_init(argc, argv, intracomm) - else: - with quiet_dll(): - _dll.openmc_init(argc, argv, intracomm) openmc.lib.is_initialized = True @@ -378,11 +370,8 @@ def plot_geometry(output=True): Whether or not to show output. Defaults to showing output """ - if output: + with quiet_dll(output): _dll.openmc_plot_geometry() - else: - with quiet_dll(): - _dll.openmc_plot_geometry() def reset(): @@ -406,11 +395,8 @@ def run(output=True): Whether or not to show output. Defaults to showing output """ - if output: + with quiet_dll(output): _dll.openmc_run() - else: - with quiet_dll(): - _dll.openmc_run() def simulation_init(): @@ -529,27 +515,40 @@ class _FortranObjectWithID(_FortranObject): @contextmanager -def quiet_dll(): - """This context manager allows us to suppress standard output from DLLs""" - sys.stdout.flush() - # Save the initial file descriptor states - initial_stdout = sys.stdout - initial_stdout_fno = os.dup(sys.stdout.fileno()) - # Get a garbage descriptor so we can throw away output - devnull = os.open(os.devnull, os.O_WRONLY) +def quiet_dll(output=True): + """This context manager allows us to suppress standard output from DLLs - # Get the current stdout stream and make a duplicate of it - new_stdout = os.dup(1) - # Copy the garbage output to the stdout stream - os.dup2(devnull, 1) - os.close(devnull) - # Now point stdout to the re-defined stdout - sys.stdout = os.fdopen(new_stdout, 'w') + Parameters + ---------- + output : bool + Denotes whether the output should be displayed (True) or not (False) - try: + .. versionadded:: 0.13.0 + + """ + + if output: yield - finally: - # Now we just clean up after ourselves and reset the streams - sys.stdout = initial_stdout + else: sys.stdout.flush() - os.dup2(initial_stdout_fno, 1) + # Save the initial file descriptor states + initial_stdout = sys.stdout + initial_stdout_fno = os.dup(sys.stdout.fileno()) + # Get a garbage descriptor so we can throw away output + devnull = os.open(os.devnull, os.O_WRONLY) + + # Get the current stdout stream and make a duplicate of it + new_stdout = os.dup(1) + # Copy the garbage output to the stdout stream + os.dup2(devnull, 1) + os.close(devnull) + # Now point stdout to the re-defined stdout + sys.stdout = os.fdopen(new_stdout, 'w') + + try: + yield + finally: + # Now we just clean up after ourselves and reset the streams + sys.stdout = initial_stdout + sys.stdout.flush() + os.dup2(initial_stdout_fno, 1) diff --git a/openmc/model/model.py b/openmc/model/model.py index c65470e3e..b35f50c87 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -52,8 +52,6 @@ class Model: Tallies information plots : openmc.Plots, optional Plot information - intracomm : mpi4py.MPI.Intracomm or None, optional - MPI intracommunicator Attributes ---------- @@ -67,8 +65,10 @@ class Model: Tallies information plots : openmc.Plots Plot information - intracomm : mpi4py.MPI.Intracomm or None - MPI intracommunicator + intracomm : mpi4py.MPI.Intracomm or openmc.DummyCommunicator + MPI intracommunicator; this defaults to the mpi4py world communicator + from if present, or a DummyCommunicator otherwise. If an alternative + communicator is desired, this parameter should be modified accordingly. """ @@ -91,7 +91,7 @@ class Model: if plots is not None: self.plots = plots - self.intracomm = intracomm + self.intracomm = openmc.comm # Store dictionaries to the materials and cells by ID and names if materials is None: @@ -191,12 +191,8 @@ class Model: @intracomm.setter def intracomm(self, intracomm): - if intracomm is None: - self._intracomm = openmc.comm - else: - check_type('intracomm', intracomm, - (openmc.MPI.Comm, openmc.DummyCommunicator)) - self._intracomm = intracomm + check_type('intracomm', intracomm, type(openmc.comm)) + self._intracomm = intracomm @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', @@ -272,7 +268,7 @@ class Model: if isinstance(self.intracomm, openmc.DummyCommunicator): # openmc.lib.init does not accept DummyCommunicator, and importing - # the DummyCommunicator class is overkill. Filter it here + # the DummyCommunicator class there is overkill. Filter it here intracomm = None else: intracomm = self.intracomm @@ -288,7 +284,8 @@ class Model: openmc.lib.finalize() def deplete(self, timesteps, method='cecm', final_step=True, - operator_kwargs=None, directory='.', **integrator_kwargs): + operator_kwargs=None, directory='.', output=True, + **integrator_kwargs): """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 @@ -310,6 +307,8 @@ class Model: directory : str, 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`). @@ -332,8 +331,9 @@ class Model: started_initialized = self.is_initialized with _change_directory(Path(directory)): - depletion_operator = \ - dep.Operator(self.geometry, self.settings, **op_kwargs) + with openmc.lib.quiet_dll(output): + depletion_operator = \ + dep.Operator(self.geometry, self.settings, **op_kwargs) # Tell depletion_operator.finalize NOT to clear C API memory when # it is done @@ -347,8 +347,8 @@ class Model: **integrator_kwargs) # Now perform the depletion - # TODO: add output parameter to integrate - integrator.integrate(final_step) + with openmc.lib.quiet_dll(output): + integrator.integrate(final_step) # Now make the python Materials match the C API material data for mat_id, mat in self._materials_by_id.items(): diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 07a356097..2760eadb4 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -481,7 +481,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # and make sure we get the same answer test_model.deplete([1e6], 'predictor', final_step=False, operator_kwargs=op_kwargs, - power=1.) + power=1., output=False) # Get the new Xe136 and U235 atom densities after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_u = mats[0].get_nuclide_atom_densities()['U235'][1] @@ -501,7 +501,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.init_lib(output=False) test_model.deplete([1e6], 'predictor', final_step=False, operator_kwargs=op_kwargs, - power=1.) + power=1., output=False) # Get the new Xe136 and U235 atom densities after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] From 20c8043c1c1046d4be2b45b17e7fbe2d1519f8c2 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 15:15:58 -0500 Subject: [PATCH 46/59] (1) Made OpenMC load plots.xml upon initialization even if not in the plot runmode; (2) Made the finalize method clear plotting data; (3) allowed the openmc.lib and openmc.model package to call plot_geometry now that plots.xml would have been loaded --- include/openmc/plot.h | 7 ++++--- openmc/model/model.py | 18 ++++++------------ src/finalize.cpp | 3 +++ src/initialize.cpp | 4 +++- src/plot.cpp | 12 ++++++++++-- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/include/openmc/plot.h b/include/openmc/plot.h index aa15277aa..3d526cad6 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -30,9 +30,7 @@ namespace model { extern std::unordered_map plot_map; //!< map of plot ids to index extern vector plots; //!< Plot instance container -extern uint64_t - plotter_prn_seeds[N_STREAMS]; // Random number seeds used for plotter -extern int plotter_stream; // Stream index used by the plotter +extern uint64_t plotter_seed; // Stream index used by the plotter } // namespace model @@ -274,6 +272,9 @@ void voxel_finalize(hid_t dspace, hid_t dset, hid_t memspace); //! Read plot specifications from a plots.xml file void read_plots_xml(); +//! Clear memory +void free_memory_plot(); + //! Create a ppm image for a plot object //! \param[in] plot object void create_ppm(Plot const& pl); diff --git a/openmc/model/model.py b/openmc/model/model.py index b35f50c87..44479d7e7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -664,18 +664,12 @@ class Model: "before executing this method!") with _change_directory(Path(cwd)): - # TODO: openmc.is_initialized doesnt read plots.xml unless it is - # in plot mode so the following will not work. Commented out for - # now and replacing with non-C API code - self.export_to_xml() - openmc.plot_geometry(output=output, openmc_exec=openmc_exec) - - # if self.is_initialized: - # # Compute the volumes - # openmc.lib.plot_geometry(output) - # else: - # self.export_to_xml() - # openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + if self.is_initialized: + # Compute the volumes + openmc.lib.plot_geometry(output) + else: + self.export_to_xml() + openmc.plot_geometry(output=output, openmc_exec=openmc_exec) if convert: for p in self.plots: diff --git a/src/finalize.cpp b/src/finalize.cpp index b1e09a37e..2f6c65aaf 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -15,6 +15,7 @@ #include "openmc/message_passing.h" #include "openmc/nuclide.h" #include "openmc/photon.h" +#include "openmc/plot.h" #include "openmc/random_lcg.h" #include "openmc/settings.h" #include "openmc/simulation.h" @@ -45,6 +46,7 @@ void free_memory() free_memory_mesh(); free_memory_tally(); free_memory_bank(); + free_memory_plot(); if (mpi::master) { free_memory_cmfd(); } @@ -128,6 +130,7 @@ int openmc_finalize() data::temperature_min = 0.0; data::temperature_max = INFTY; model::root_universe = -1; + model::plotter_seed = 1; openmc::openmc_set_seed(DEFAULT_SEED); // Deallocate arrays diff --git a/src/initialize.cpp b/src/initialize.cpp index e53767691..75ec20518 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -305,9 +305,11 @@ void read_input_xml() // Initialize distribcell_filters prepare_distribcell(); + // Read the plots.xml regardless of plot mode in case plots are requested + // via the API + read_plots_xml(); if (settings::run_mode == RunMode::PLOTTING) { // Read plots.xml if it exists - read_plots_xml(); if (mpi::master && settings::verbosity >= 5) print_plot(); diff --git a/src/plot.cpp b/src/plot.cpp index 789437691..217b09839 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -119,9 +119,11 @@ extern "C" int openmc_plot_geometry() void read_plots_xml() { - // Check if plots.xml exists + // Check if plots.xml exists; this is only necessary when the plot runmode is + // initiated. Otherwise, we want to read plots.xml because it may be called + // later via the API. In that case, its ok for a plots.xml to not exist std::string filename = settings::path_input + "plots.xml"; - if (!file_exists(filename)) { + if (!file_exists(filename) && settings::run_mode == RunMode::PLOTTING) { fatal_error(fmt::format("Plots XML file '{}' does not exist!", filename)); } @@ -138,6 +140,12 @@ void read_plots_xml() } } +void free_memory_plot() +{ + model::plots.clear(); + model::plot_map.clear(); +} + //============================================================================== // CREATE_PPM creates an image based on user input from a plots.xml // specification in the portable pixmap format (PPM) From e8081207d7df265f495779a5735cdfe5b34f704a Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 16:34:58 -0500 Subject: [PATCH 47/59] Modified the C API to support setting material temperatures and to support setting the event based flag. Fixed failing MPI test. --- include/openmc/capi.h | 1 + include/openmc/material.h | 8 ++--- include/openmc/settings.h | 2 +- openmc/executor.py | 17 ++++++---- openmc/lib/__init__.py | 2 ++ openmc/lib/material.py | 7 ++++ openmc/lib/settings.py | 1 + openmc/model/model.py | 52 ++++++++++++++---------------- src/material.cpp | 12 +++++++ tests/unit_tests/test_lib.py | 6 ++++ tests/unit_tests/test_model.py | 59 +++++++++++++++++----------------- 11 files changed, 100 insertions(+), 67 deletions(-) diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 0929a11f7..679919dfa 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -88,6 +88,7 @@ int openmc_material_set_densities( 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_temperature(int32_t index, double temperature); int openmc_material_set_volume(int32_t index, double volume); int openmc_material_filter_get_bins( int32_t index, const int32_t** bins, size_t* n); diff --git a/include/openmc/material.h b/include/openmc/material.h index 709d20573..b13cfe330 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -158,6 +158,10 @@ 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] + + double temperature_ {-1}; //!< Default temperature for material + //!< A negative indicates no default + //!< temperature was specified. bool fissionable_ { false}; //!< Does this material contain fissionable nuclides bool depletable_ {false}; //!< Is the material depletable? @@ -195,10 +199,6 @@ private: // Private data members gsl::index index_; - //! \brief Default temperature for cells containing this material. - //! - //! A negative value indicates no default temperature was specified. - double temperature_ {-1}; }; //============================================================================== diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 2d0261960..b96ab91fd 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -32,7 +32,7 @@ extern "C" bool cmfd_run; //!< is a CMFD run? extern bool delayed_photon_scaling; //!< Scale fission photon yield to include delayed extern "C" bool entropy_on; //!< calculate Shannon entropy? -extern bool event_based; //!< use event-based mode (instead of history-based) +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? diff --git a/openmc/executor.py b/openmc/executor.py index fa55e1366..ef1bc25dc 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -7,8 +7,8 @@ import openmc def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, plot=False, restart_file=None, threads=None, - tracks=False, event_based=False, - openmc_exec='openmc', mpi_args=None): + tracks=False, event_based=None, openmc_exec='openmc', + mpi_args=None): """Run an OpenMC simulation. Parameters @@ -30,8 +30,9 @@ def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, :envvar:`OMP_NUM_THREADS` environment variable). tracks : bool, optional Write tracks for all particles. Defaults to False. - event_based : bool, optional - Turns on event-based parallelism, instead of default history-based + event_based : None or bool, optional + Turns on event-based parallelism if True. If None, the value in + the Settings will be used. openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. mpi_args : list of str, optional @@ -61,8 +62,9 @@ def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, if geometry_debug: args.append('-g') - if event_based: - args.append('-e') + if event_based is not None: + if event_based: + args.append('-e') if isinstance(restart_file, str): args += ['-r', restart_file] @@ -70,6 +72,9 @@ def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, if tracks: args.append('-t') + if plot: + args.append('-p') + if mpi_args is not None: args = mpi_args + args diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index f1b9c291f..c14b0d9c2 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -61,4 +61,6 @@ from .math import * from .plot 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 +# openmc_init and openmc_finalize methods is_initialized = False diff --git a/openmc/lib/material.py b/openmc/lib/material.py index fde197d3d..c1800cb46 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -57,6 +57,9 @@ _dll.openmc_material_get_name.errcheck = _error_handler _dll.openmc_material_set_name.argtypes = [c_int32, c_char_p] _dll.openmc_material_set_name.restype = c_int _dll.openmc_material_set_name.errcheck = _error_handler +_dll.openmc_material_set_temperature.argtypes = [c_int32, c_double] +_dll.openmc_material_set_temperature.restype = c_int +_dll.openmc_material_set_temperature.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 @@ -165,6 +168,10 @@ class Material(_FortranObjectWithID): return None return volume.value + @temperature.setter + def temperature(self, temperature): + _dll.openmc_material_set_temperature(self._index, temperature) + @volume.setter def volume(self, volume): _dll.openmc_material_set_volume(self._index, volume) diff --git a/openmc/lib/settings.py b/openmc/lib/settings.py index 1eab4aa8a..2e9dd18df 100644 --- a/openmc/lib/settings.py +++ b/openmc/lib/settings.py @@ -34,6 +34,7 @@ class _Settings: restart_run = _DLLGlobal(c_bool, 'restart_run') run_CE = _DLLGlobal(c_bool, 'run_CE') verbosity = _DLLGlobal(c_int, 'verbosity') + event_based = _DLLGlobal(c_bool, 'event_based') @property def run_mode(self): diff --git a/openmc/model/model.py b/openmc/model/model.py index 44479d7e7..465ad3064 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -73,7 +73,7 @@ class Model: """ def __init__(self, geometry=None, materials=None, settings=None, - tallies=None, plots=None, intracomm=None): + tallies=None, plots=None): self.geometry = openmc.Geometry() self.materials = openmc.Materials() self.settings = openmc.Settings() @@ -145,8 +145,6 @@ class Model: @property def is_initialized(self): - # TODO: Replace openmc.lib.is_initialized with a direct ctypes access - # to simulation::initialized return openmc.lib.is_initialized @geometry.setter @@ -222,7 +220,7 @@ class Model: return cls(geometry, materials, settings) def init_lib(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=False): + tracks=False, output=True, event_based=None): """Initializes the model in memory via the C API .. versionadded:: 0.13.0 @@ -243,8 +241,9 @@ class Model: Write tracks for all particles. Defaults to False. output : bool Capture OpenMC output from standard out - event_based : bool, optional - Turns on event-based parallelism, instead of default history-based + event_based : None or bool, optional + Turns on event-based parallelism if True. If None, the value in + the Settings will be used. """ # TODO: right now the only way to set most of the above parameters via @@ -456,7 +455,7 @@ 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=False): + openmc_exec='openmc', mpi_args=None, event_based=None): """Runs OpenMC. If the C API has been initialized, then the C API is used, otherwise, this method creates the XML files and runs OpenMC via a system call. In both cases this method returns the path to the last @@ -494,8 +493,9 @@ class Model: mpi_args : list of str, optional MPI execute command and any additional MPI arguments to pass, e.g. ['mpiexec', '-n', '8']. - event_based : bool, optional - Turns on event-based parallelism, instead of default history-based + event_based : None or bool, optional + Turns on event-based parallelism if True. If None, the value in + the Settings will be used. Returns ------- @@ -523,21 +523,21 @@ class Model: msg = f"{arg_name} must be set via Model.is_initialized(...)" raise ValueError(msg) + init_particles = openmc.lib.settings.particles if particles is not None: - init_particles = openmc.lib.settings.particles if isinstance(particles, Integral) and particles > 0: openmc.lib.settings.particles = particles - # Event-based can be set at init-time or on a case-basis. - # Handle the argument here. - # TODO This will be dealt with in a future change to the C API + init_event_based = openmc.lib.settings.event_based + if event_based is not None: + openmc.lib.settings.event_based = event_based # Then run using the C API openmc.lib.run(output) # Reset changes for the openmc.run kwargs handling - if particles is not None: - openmc.lib.settings.particles = init_particles + openmc.lib.settings.particles = init_particles + openmc.lib.settings.event_based = init_event_based else: # Then run via the command line @@ -697,10 +697,7 @@ class Model: if obj_type == 'cell' and attrib_name == 'volume': raise NotImplementedError( 'Setting a Cell volume is not supported!') - # Same with setting temperatures, TODO: update C API for this - if obj_type == 'material' and attrib_name == 'temperature': - raise NotImplementedError( - 'Setting a Material temperature is not yet supported!') + # And some items just dont make sense if obj_type == 'cell' and attrib_name == 'density': raise ValueError('Cannot set a Cell density!') @@ -751,10 +748,10 @@ class Model: # Next lets keep what is in C API memory up to date as well if self.is_initialized: lib_obj = obj_by_id[id_] - if attrib_name == 'temperature': - lib_obj.set_temperature(value) - elif attrib_name == 'density': + if attrib_name == 'density': lib_obj.set_density(value, density_units) + elif attrib_name == 'temperature' and obj_type == 'cell': + lib_obj.set_temperature(value) else: setattr(lib_obj, attrib_name, value) @@ -800,7 +797,8 @@ class Model: """ - self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'translation') + self._change_py_lib_attribs(names_or_ids, vector, 'cell', + 'translation') def update_densities(self, names_or_ids, density, density_units='atom/b-cm'): """Update the density of a given set of materials to a new value @@ -822,8 +820,8 @@ class Model: """ - self._change_py_lib_attribs(names_or_ids, density, 'material', 'density', - density_units) + self._change_py_lib_attribs(names_or_ids, density, 'material', + 'density', density_units) def update_cell_temperatures(self, names_or_ids, temperature): """Update the temperature of a set of cells to the given value @@ -844,7 +842,7 @@ class Model: """ self._change_py_lib_attribs(names_or_ids, temperature, 'cell', - 'temperature') + 'temperature') def update_material_temperatures(self, names_or_ids, temperature): """Update the temperature of a set of materials to the given value @@ -865,7 +863,7 @@ class Model: """ self._change_py_lib_attribs(names_or_ids, temperature, 'material', - 'temperature') + 'temperature') def update_material_volumes(self, names_or_ids, volume): """Update the volume of a set of materials to the given value diff --git a/src/material.cpp b/src/material.cpp index ca29ce740..fb4b2675e 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1480,6 +1480,18 @@ extern "C" int openmc_material_set_name(int32_t index, const char* name) return 0; } +extern "C" int openmc_material_set_temperature(int32_t index, double temperature) +{ + if (index >= 0 && index < model::materials.size()) { + auto& m {model::materials[index]}; + m->temperature_ = temperature; + return 0; + } else { + set_errmsg("Index in materials array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } +} + extern "C" int openmc_material_set_volume(int32_t index, double volume) { if (index >= 0 && index < model::materials.size()) { diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 079b4f9eb..c21f85633 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -207,6 +207,11 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" + assert m.temperature == 293.6 + m.temperature = 400. + assert m.temperature == 400. + m.temperature == 293.6 + def test_properties_density(lib_init): m = openmc.lib.materials[1] @@ -263,6 +268,7 @@ def test_settings(lib_init): assert settings.generations_per_batch == 1 assert settings.particles == 100 assert settings.seed == 1 + assert settings.event_based is False settings.seed = 11 diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 2760eadb4..092ccfc3c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -5,7 +5,6 @@ from pathlib import Path from shutil import which import openmc import openmc.lib -from openmc.mpi import DummyCommunicator @pytest.fixture(scope='function') @@ -128,12 +127,14 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_model._cells_by_id == {} assert test_model._cells_by_name == {} assert test_model.is_initialized is False - assert isinstance(test_model.intracomm, DummyCommunicator) + assert hasattr(test_model.intracomm, 'rank') # Now check proper init of an actual model. Assume no interference between # parameters and so we can apply them all at once instead of testing one # parameter initialization at a time - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm assert test_model.geometry is geom assert test_model.materials is mats assert test_model.settings is settings @@ -154,17 +155,10 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): '': [geom.root_universe.cells[3], geom.root_universe.cells[4]], 'inf fuel': [geom.root_universe.cells[2].fill.cells[1]]} assert test_model.is_initialized is False - if mpi_intracomm is None: - assert isinstance(test_model.intracomm, DummyCommunicator) - else: - assert test_model.intracomm == mpi_intracomm # Finally test the parameter type checking by passing bad types and # obtaining the right exception types - if mpi_intracomm is not None: - def_params = [geom, mats, settings, tals, plots, mpi_intracomm] - else: - def_params = [geom, mats, settings, tals, plots] + def_params = [geom, mats, settings, tals, plots] for i in range(len(def_params)): args = def_params.copy() # Try an integer, as that is a bad type for all arguments @@ -223,13 +217,14 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): test_model.geometry.root_universe.cells[4]], 'inf fuel': [test_model.geometry.root_universe.cells[2].fill.cells[1]]} assert test_model.is_initialized is False - assert isinstance(test_model.intracomm, DummyCommunicator) def test_init_finalize_lib(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # We are going to init and then make sure data is loaded mats, geom, settings, tals, plots, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm test_model.init_lib(output=False) # First check that the API is advertised as initialized @@ -257,6 +252,8 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # Create PWR pin cell model and write XML files openmc.reset_auto_ids() model = openmc.examples.pwr_pin_cell() + if mpi_intracomm is not None: + model.intracomm = mpi_intracomm model.init_lib(output=False) # Change fuel temperature and density and export properties @@ -296,7 +293,9 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm # This case will run by getting the k-eff and tallies for command-line and # C API execution modes and ensuring they give the same result. @@ -334,7 +333,9 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm # This test cannot check the correctness of the plot, but it can # check that a plot was made and that the expected ppm and png files are @@ -367,7 +368,9 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm test_model.init_lib(output=False) @@ -439,19 +442,13 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Now lets do the material temperature updates. # Check initial conditions - with pytest.raises(NotImplementedError): - test_model.update_material_temperatures(['UO2'], 600.) - # TODO: When C API material.set_temperature is implemented, uncomment below - # assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 - # # The temperature on the material will be None because its just the - # # default assert test_model.materials[0].temperature is None - # # Change the temperature - # test_model.update_material_temperatures(['UO2'], 600.) - # assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 - # assert abs(test_model.materials[0].temperature - 600.) < 1e-13 + assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 + # Change the temperature + test_model.update_material_temperatures(['UO2'], 600.) + assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 + assert abs(test_model.materials[0].temperature - 600.) < 1e-13 # And finally material volume - # import pdb; pdb.set_trace() assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 # The temperature on the material will be None because its just the default assert abs(test_model.materials[0].volume - 0.4831931368640985) < 1e-13 @@ -468,7 +465,9 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): pin_model_attributes with open('test_chain.xml', 'w') as f: f.write(chain_file_xml) - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm initial_mat = mats[0].clone() initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] @@ -518,7 +517,9 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm # With no vol calcs, it should fail with pytest.raises(ValueError): From fe9d929ed0a40cc191faa6d5210b02e13681f2f9 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Fri, 1 Oct 2021 12:17:47 +0200 Subject: [PATCH 48/59] Apply suggestions from code review Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 4 ++-- openmc/stats/univariate.py | 2 +- src/distribution.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 8a83de7ac..c797212c1 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -660,10 +660,10 @@ 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.a + For a "mixture" distribution, this element provides a distribution and its corresponding probability. :probability: - An attribute or ``pair`` that provides the probability of a univatiate 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. diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 576b2247c..af6c9a6d1 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -828,7 +828,7 @@ class Mixture(Univariate): element = ET.Element(element_name) element.set("type", "mixture") - for p,d in zip(self.probability, self.distribution): + for p, d in zip(self.probability, self.distribution): data = ET.SubElement(element, "pair") data.set("probability", str(p)) data.append(d.to_xml_element("dist")) diff --git a/src/distribution.cpp b/src/distribution.cpp index af3aac5af..c7952aba3 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -358,7 +358,7 @@ UPtrDist distribution_from_xml(pugi::xml_node node) } else if (type == "tabular") { dist = UPtrDist {new Tabular(node)}; } else if (type == "mixture") { - dist = UPtrDist{new Mixture(node)}; + dist = UPtrDist {new Mixture(node)}; } else { openmc::fatal_error("Invalid distribution type: " + type); } From 8991a104322554104006d769abce87499cc19f32 Mon Sep 17 00:00:00 2001 From: Olaf Schumann Date: Fri, 1 Oct 2021 10:23:39 +0000 Subject: [PATCH 49/59] need to include gsl-lite.hpp for Ensures() --- src/distribution.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/distribution.cpp b/src/distribution.cpp index c7952aba3..62296cb25 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -7,6 +7,8 @@ #include // for runtime_error #include // for string, stod +#include + #include "openmc/error.h" #include "openmc/math_functions.h" #include "openmc/random_dist.h" From e1365bd0364862a018fe84397d2bcbe3982f3200 Mon Sep 17 00:00:00 2001 From: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Date: Fri, 1 Oct 2021 06:09:24 -0500 Subject: [PATCH 50/59] Applied suggested changes per @paulromano's code review Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 2 +- openmc/deplete/helpers.py | 2 +- openmc/deplete/operator.py | 2 +- openmc/deplete/results.py | 2 +- openmc/lib/core.py | 2 ++ openmc/model/model.py | 18 ++++++++---------- tests/unit_tests/test_deplete_chain.py | 2 +- tests/unit_tests/test_deplete_integrator.py | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index bf9f4da82..b9198696d 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -22,7 +22,7 @@ from uncertainties import ufloat from openmc.data import DataLibrary from openmc.lib import MaterialFilter, Tally from openmc.checkvalue import check_type, check_greater_than -from openmc import comm +from openmc.mpi import comm from .results import Results from .chain import Chain from .results_list import ResultsList diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index a8cf5212e..f22382923 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -10,7 +10,7 @@ import sys from numpy import dot, zeros, newaxis, asarray -from openmc import comm +from openmc.mpi import comm from openmc.checkvalue import check_type, check_greater_than from openmc.data import JOULE_PER_EV, REACTION_MT from openmc.lib import ( diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index e4db13f95..29370eb8c 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -20,7 +20,7 @@ from uncertainties import ufloat import openmc from openmc.checkvalue import check_value import openmc.lib -from openmc import comm +from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 05d78f629..111c8e638 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -9,7 +9,7 @@ import copy import h5py import numpy as np -from openmc import comm, MPI +from openmc.mpi import comm, MPI from .reaction_rates import ReactionRates VERSION_RESULTS = (1, 1) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 3b691721c..92cad3330 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -363,6 +363,7 @@ def plot_geometry(output=True): """Plot geometry .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- @@ -388,6 +389,7 @@ def run(output=True): """Run simulation .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- diff --git a/openmc/model/model.py b/openmc/model/model.py index 465ad3064..ac54763c4 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -109,15 +109,13 @@ class Model: self._cells_by_name = {} for cell in cells.values(): if cell.name not in self._cells_by_name: - self._cells_by_name[cell.name] = [cell] - else: - self._cells_by_name[cell.name].append(cell) + self._cells_by_name[cell.name] = set() + self._cells_by_name[cell.name].add(cell) self._materials_by_name = {} for mat in mats: if mat.name not in self._materials_by_name: - self._materials_by_name[mat.name] = [mat] - else: - self._materials_by_name[mat.name].append(mat) + self._materials_by_name[mat.name] = set() + self._materials_by_name[mat.name].add(mat) @property def geometry(self): @@ -196,8 +194,7 @@ class Model: def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): """Create model from existing XML files - When initializing this way, the user must manually load plots, tallies, - the chain_file and fission_q attributes. + When initializing this way, the user must manually load plots and tallies. Parameters ---------- @@ -288,6 +285,8 @@ class Model: """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 + The *final_step*, *operator_kwargs*, *directory*, and *output* + arguments were added. Parameters ---------- @@ -319,8 +318,7 @@ class Model: elif isinstance(operator_kwargs, dict): op_kwargs = operator_kwargs else: - msg = "operator_kwargs must be a dict or None" - raise ValueError(msg) + raise ValueError("operator_kwargs must be a dict or None") # Import openmc.deplete here so the Model can be used even if the # shared library is unavailable. diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 0904ff39a..9769a49e3 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -7,7 +7,7 @@ import os from pathlib import Path import numpy as np -from openmc import comm +from openmc.mpi import comm from openmc.deplete import Chain, reaction_rates, nuclide, cram, pool import pytest diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index ec424b05b..a08d8738c 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -14,7 +14,7 @@ import numpy as np from uncertainties import ufloat import pytest -from openmc import comm +from openmc.mpi import comm from openmc.deplete import ( ReactionRates, Results, ResultsList, OperatorResult, PredictorIntegrator, CECMIntegrator, CF4Integrator, CELIIntegrator, EPCRK4Integrator, From e4476fc0b7b8e2f22ce7f6d7cf32c3c76276fb59 Mon Sep 17 00:00:00 2001 From: agnelson Date: Fri, 1 Oct 2021 09:21:41 -0500 Subject: [PATCH 51/59] Delayed mpi4py import until the user clearly indicated intent to use a communicator (or they already have created one and thus have already imported it); corrected versionchanged doc statements to include comments about what changed; added stackoverflow reference --- include/openmc/capi.h | 1 - include/openmc/material.h | 8 ++-- openmc/__init__.py | 1 - openmc/dummy_comm.py | 33 ++++++++++++++ openmc/executor.py | 17 +++---- openmc/lib/core.py | 9 +++- openmc/lib/material.py | 7 --- openmc/model/model.py | 82 ++++++++++++---------------------- openmc/mpi.py | 37 +-------------- src/material.cpp | 12 ----- tests/unit_tests/test_lib.py | 5 --- tests/unit_tests/test_model.py | 65 ++++++++------------------- 12 files changed, 102 insertions(+), 175 deletions(-) create mode 100644 openmc/dummy_comm.py diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 679919dfa..0929a11f7 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -88,7 +88,6 @@ int openmc_material_set_densities( 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_temperature(int32_t index, double temperature); int openmc_material_set_volume(int32_t index, double volume); int openmc_material_filter_get_bins( int32_t index, const int32_t** bins, size_t* n); diff --git a/include/openmc/material.h b/include/openmc/material.h index b13cfe330..709d20573 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -158,10 +158,6 @@ 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] - - double temperature_ {-1}; //!< Default temperature for material - //!< A negative indicates no default - //!< temperature was specified. bool fissionable_ { false}; //!< Does this material contain fissionable nuclides bool depletable_ {false}; //!< Is the material depletable? @@ -199,6 +195,10 @@ private: // Private data members gsl::index index_; + //! \brief Default temperature for cells containing this material. + //! + //! A negative value indicates no default temperature was specified. + double temperature_ {-1}; }; //============================================================================== diff --git a/openmc/__init__.py b/openmc/__init__.py index 7366e8383..d118e6a05 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -30,7 +30,6 @@ from openmc.plotter import * from openmc.search import * from openmc.polynomial import * from . import examples -from openmc.mpi import DummyCommunicator, MPI, comm # Import a few names from the model module from openmc.model import rectangular_prism, hexagonal_prism, Model diff --git a/openmc/dummy_comm.py b/openmc/dummy_comm.py new file mode 100644 index 000000000..7ac9be6c3 --- /dev/null +++ b/openmc/dummy_comm.py @@ -0,0 +1,33 @@ +import sys + + +class DummyCommunicator: + rank = 0 + size = 1 + + def allgather(self, sendobj): + return [sendobj] + + def allreduce(self, sendobj, op=None): + return sendobj + + def barrier(self): + pass + + def bcast(self, obj, root=0): + return obj + + def gather(self, sendobj, root=0): + return [sendobj] + + def py2f(self): + return 0 + + def reduce(self, sendobj, op=None, root=0): + return sendobj + + def scatter(self, sendobj, root=0): + return sendobj[0] + + def Abort(self, exit_code_or_msg): + sys.exit(exit_code_or_msg) diff --git a/openmc/executor.py b/openmc/executor.py index ef1bc25dc..c2974722c 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -5,11 +5,12 @@ import subprocess import openmc -def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, - plot=False, restart_file=None, threads=None, - tracks=False, event_based=None, openmc_exec='openmc', - mpi_args=None): - """Run an OpenMC simulation. +def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, + plot=False, restart_file=None, threads=None, + tracks=False, event_based=None, + openmc_exec='openmc', mpi_args=None): + """Converts user-readable flags in to command-line arguments to be run with + the OpenMC executable via subprocess. Parameters ---------- @@ -227,8 +228,8 @@ def calculate_volumes(threads=None, output=True, cwd='.', """ - args = process_CLI_arguments(volume=True, threads=threads, - openmc_exec=openmc_exec, mpi_args=mpi_args) + args = _process_CLI_arguments(volume=True, threads=threads, + openmc_exec=openmc_exec, mpi_args=mpi_args) _run(args, output, cwd) @@ -275,7 +276,7 @@ def run(particles=None, threads=None, geometry_debug=False, """ - args = process_CLI_arguments( + args = _process_CLI_arguments( volume=False, geometry_debug=geometry_debug, particles=particles, restart_file=restart_file, threads=threads, tracks=tracks, event_based=event_based, openmc_exec=openmc_exec, mpi_args=mpi_args) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 92cad3330..136e5a6d8 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -134,6 +134,7 @@ def export_properties(filename=None, output=True): """Export physical properties. .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- @@ -233,6 +234,7 @@ def init(args=None, intracomm=None, output=True): """Initialize OpenMC .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- @@ -363,7 +365,7 @@ def plot_geometry(output=True): """Plot geometry .. versionchanged:: 0.13.0 - The *output* argument was added. + The *output* argument was added. Parameters ---------- @@ -389,7 +391,7 @@ def run(output=True): """Run simulation .. versionchanged:: 0.13.0 - The *output* argument was added. + The *output* argument was added. Parameters ---------- @@ -529,6 +531,9 @@ def quiet_dll(output=True): """ + # This contextmanager is modified from that provided here: + # https://stackoverflow.com/a/14797594 + if output: yield else: diff --git a/openmc/lib/material.py b/openmc/lib/material.py index c1800cb46..fde197d3d 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -57,9 +57,6 @@ _dll.openmc_material_get_name.errcheck = _error_handler _dll.openmc_material_set_name.argtypes = [c_int32, c_char_p] _dll.openmc_material_set_name.restype = c_int _dll.openmc_material_set_name.errcheck = _error_handler -_dll.openmc_material_set_temperature.argtypes = [c_int32, c_double] -_dll.openmc_material_set_temperature.restype = c_int -_dll.openmc_material_set_temperature.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 @@ -168,10 +165,6 @@ class Material(_FortranObjectWithID): return None return volume.value - @temperature.setter - def temperature(self, temperature): - _dll.openmc_material_set_temperature(self._index, temperature) - @volume.setter def volume(self, volume): _dll.openmc_material_set_volume(self._index, volume) diff --git a/openmc/model/model.py b/openmc/model/model.py index ac54763c4..904c60746 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -11,6 +11,8 @@ from contextlib import contextmanager import h5py import openmc +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 @@ -39,6 +41,8 @@ class Model: materials appearing in the geometry. .. versionchanged:: 0.13.0 + The model information can now be loaded in to OpenMC directly via + openmc.lib Parameters ---------- @@ -65,10 +69,6 @@ class Model: Tallies information plots : openmc.Plots Plot information - intracomm : mpi4py.MPI.Intracomm or openmc.DummyCommunicator - MPI intracommunicator; this defaults to the mpi4py world communicator - from if present, or a DummyCommunicator otherwise. If an alternative - communicator is desired, this parameter should be modified accordingly. """ @@ -91,8 +91,6 @@ class Model: if plots is not None: self.plots = plots - self.intracomm = openmc.comm - # Store dictionaries to the materials and cells by ID and names if materials is None: mats = self.geometry.get_all_materials().values() @@ -137,10 +135,6 @@ class Model: def plots(self): return self._plots - @property - def intracomm(self): - return self._intracomm - @property def is_initialized(self): return openmc.lib.is_initialized @@ -185,16 +179,12 @@ class Model: for plot in plots: self._plots.append(plot) - @intracomm.setter - def intracomm(self, intracomm): - check_type('intracomm', intracomm, type(openmc.comm)) - self._intracomm = intracomm - @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): """Create model from existing XML files - When initializing this way, the user must manually load plots and tallies. + When initializing this way, the user must manually load plots and + tallies. Parameters ---------- @@ -217,7 +207,7 @@ class Model: return cls(geometry, materials, settings) def init_lib(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=None): + tracks=False, output=True, event_based=None, intracomm=None): """Initializes the model in memory via the C API .. versionadded:: 0.13.0 @@ -241,6 +231,8 @@ class Model: event_based : None or bool, optional Turns on event-based parallelism if True. If None, the value in the Settings will be used. + intracomm : DummyCommnicator, mpi4py.MPI.Intracomm or None, optional + MPI intracommunicator """ # TODO: right now the only way to set most of the above parameters via @@ -249,7 +241,7 @@ class Model: # where it exists (here in init), but in the future the functionality # should be exposed so that it can be accessed via model.run(...) - args = openmc.process_CLI_arguments( + args = _process_CLI_arguments( volume=False, geometry_debug=geometry_debug, restart_file=restart_file, threads=threads, tracks=tracks, event_based=event_based) @@ -258,17 +250,18 @@ class Model: self.finalize_lib() - if self.intracomm.rank == 0: - self.export_to_xml() - self.intracomm.barrier() - - if isinstance(self.intracomm, openmc.DummyCommunicator): - # openmc.lib.init does not accept DummyCommunicator, and importing - # the DummyCommunicator class there is overkill. Filter it here - intracomm = None + # The Model object needs to be aware of the communicator so it can + # use it in certain cases, therefore lets store the communicator + if intracomm is not None: + self._intracomm = intracomm else: - intracomm = self.intracomm - openmc.lib.init(args=args, intracomm=intracomm, output=output) + self._intracomm = DummyCommunicator() + + if self._intracomm.rank == 0: + self.export_to_xml() + self._intracomm.barrier() + + openmc.lib.init(args=args, intracomm=self._intracomm, output=output) def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -285,8 +278,8 @@ class Model: """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 - The *final_step*, *operator_kwargs*, *directory*, and *output* - arguments were added. + The *final_step*, *operator_kwargs*, *directory*, and *output* + arguments were added. Parameters ---------- @@ -691,10 +684,14 @@ class Model: 'translation')) # The C API only allows setting density units of atom/b-cm and g/cm3 check_value('density_units', density_units, ('atom/b-cm', 'g/cm3')) - # The C API has no way to set cell volume so lets raise an exception + # The C API has no way to set cell volume or material temperature + # so lets raise exceptions as needed if obj_type == 'cell' and attrib_name == 'volume': raise NotImplementedError( 'Setting a Cell volume is not supported!') + if obj_type == 'material' and attrib_name == 'temperature': + raise NotImplementedError( + 'Setting a material temperature is not supported!') # And some items just dont make sense if obj_type == 'cell' and attrib_name == 'density': @@ -748,7 +745,7 @@ class Model: lib_obj = obj_by_id[id_] if attrib_name == 'density': lib_obj.set_density(value, density_units) - elif attrib_name == 'temperature' and obj_type == 'cell': + elif attrib_name == 'temperature': lib_obj.set_temperature(value) else: setattr(lib_obj, attrib_name, value) @@ -842,27 +839,6 @@ class Model: self._change_py_lib_attribs(names_or_ids, temperature, 'cell', 'temperature') - def update_material_temperatures(self, names_or_ids, temperature): - """Update the temperature of a set of materials to the given value - - .. note:: If applying this change to a name that is not unique, then - the change will be applied to all objects of that name. - - .. versionadded:: 0.13.0 - - Parameters - ---------- - names_or_ids : Iterable of str or int - The material names (if str) or id (if int) that are to be updated. - This parameter can include a mix of names and ids. - temperature : float - The temperature to apply in units of Kelvin - - """ - - self._change_py_lib_attribs(names_or_ids, temperature, 'material', - 'temperature') - def update_material_volumes(self, names_or_ids, volume): """Update the volume of a set of materials to the given value diff --git a/openmc/mpi.py b/openmc/mpi.py index b64a84bc6..cfc10c0d1 100644 --- a/openmc/mpi.py +++ b/openmc/mpi.py @@ -1,41 +1,8 @@ -import sys -from unittest.mock import Mock - - -class DummyCommunicator: - rank = 0 - size = 1 - - def allgather(self, sendobj): - return [sendobj] - - def allreduce(self, sendobj, op=None): - return sendobj - - def barrier(self): - pass - - def bcast(self, obj, root=0): - return obj - - def gather(self, sendobj, root=0): - return [sendobj] - - def py2f(self): - return 0 - - def reduce(self, sendobj, op=None, root=0): - return sendobj - - def scatter(self, sendobj, root=0): - return sendobj[0] - - def Abort(self, exit_code_or_msg): - sys.exit(exit_code_or_msg) - try: from mpi4py import MPI comm = MPI.COMM_WORLD except ImportError: + from unittest.mock import Mock MPI = Mock() + from openmc.dummy_comm import DummyCommunicator comm = DummyCommunicator() diff --git a/src/material.cpp b/src/material.cpp index fb4b2675e..ca29ce740 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1480,18 +1480,6 @@ extern "C" int openmc_material_set_name(int32_t index, const char* name) return 0; } -extern "C" int openmc_material_set_temperature(int32_t index, double temperature) -{ - if (index >= 0 && index < model::materials.size()) { - auto& m {model::materials[index]}; - m->temperature_ = temperature; - return 0; - } else { - set_errmsg("Index in materials array is out of bounds."); - return OPENMC_E_OUT_OF_BOUNDS; - } -} - extern "C" int openmc_material_set_volume(int32_t index, double volume) { if (index >= 0 && index < model::materials.size()) { diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index c21f85633..ccff33ac3 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -207,11 +207,6 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" - assert m.temperature == 293.6 - m.temperature = 400. - assert m.temperature == 400. - m.temperature == 293.6 - def test_properties_density(lib_init): m = openmc.lib.materials[1] diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 092ccfc3c..cffa5c249 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -127,14 +127,11 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_model._cells_by_id == {} assert test_model._cells_by_name == {} assert test_model.is_initialized is False - assert hasattr(test_model.intracomm, 'rank') # Now check proper init of an actual model. Assume no interference between # parameters and so we can apply them all at once instead of testing one # parameter initialization at a time test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm assert test_model.geometry is geom assert test_model.materials is mats assert test_model.settings is settings @@ -142,7 +139,7 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_model.plots is plots assert test_model._materials_by_id == {1: mats[0], 2: mats[1], 3: mats[2]} assert test_model._materials_by_name == { - 'UO2': [mats[0]], 'Zirc': [mats[1]], 'Borated water': [mats[2]]} + 'UO2': {mats[0]}, 'Zirc': {mats[1]}, 'Borated water': {mats[2]}} # The last cell is the one that contains the infinite fuel assert test_model._cells_by_id == \ {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], @@ -151,9 +148,9 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': [geom.root_universe.cells[2]], - '': [geom.root_universe.cells[3], geom.root_universe.cells[4]], - 'inf fuel': [geom.root_universe.cells[2].fill.cells[1]]} + 'fuel': {geom.root_universe.cells[2]}, + '': {geom.root_universe.cells[3], geom.root_universe.cells[4]}, + 'inf fuel': {geom.root_universe.cells[2].fill.cells[1]}} assert test_model.is_initialized is False # Finally test the parameter type checking by passing bad types and @@ -202,8 +199,8 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): {1: test_model.materials[0], 2: test_model.materials[1], 3: test_model.materials[2]} assert test_model._materials_by_name == { - 'UO2': [test_model.materials[0]], 'Zirc': [test_model.materials[1]], - 'Borated water': [test_model.materials[2]]} + 'UO2': {test_model.materials[0]}, 'Zirc': {test_model.materials[1]}, + 'Borated water': {test_model.materials[2]}} assert test_model._cells_by_id == { 2: test_model.geometry.root_universe.cells[2], 3: test_model.geometry.root_universe.cells[3], @@ -212,10 +209,10 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': [test_model.geometry.root_universe.cells[2]], - '': [test_model.geometry.root_universe.cells[3], - test_model.geometry.root_universe.cells[4]], - 'inf fuel': [test_model.geometry.root_universe.cells[2].fill.cells[1]]} + 'fuel': {test_model.geometry.root_universe.cells[2]}, + '': {test_model.geometry.root_universe.cells[3], + test_model.geometry.root_universe.cells[4]}, + 'inf fuel': {test_model.geometry.root_universe.cells[2].fill.cells[1]}} assert test_model.is_initialized is False @@ -223,9 +220,7 @@ def test_init_finalize_lib(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # We are going to init and then make sure data is loaded mats, geom, settings, tals, plots, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) # First check that the API is advertised as initialized assert openmc.lib.is_initialized is True @@ -252,9 +247,7 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # Create PWR pin cell model and write XML files openmc.reset_auto_ids() model = openmc.examples.pwr_pin_cell() - if mpi_intracomm is not None: - model.intracomm = mpi_intracomm - model.init_lib(output=False) + model.init_lib(output=False, intracomm=mpi_intracomm) # Change fuel temperature and density and export properties cell = openmc.lib.cells[1] @@ -294,8 +287,6 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm # This case will run by getting the k-eff and tallies for command-line and # C API execution modes and ensuring they give the same result. @@ -305,7 +296,7 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): cli_flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] cli_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: lib_keff = sp.k_combined @@ -334,8 +325,6 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm # This test cannot check the correctness of the plot, but it can # check that a plot was made and that the expected ppm and png files are @@ -352,7 +341,7 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # We will run the test twice, the first time without C API, the second with for i in range(2): if i == 1: - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) test_model.plot_geometry(output=False, convert=convert) # Now look for the files, expect to find test.ppm, plot_2.ppm, and if @@ -369,14 +358,11 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) # Now we can call rotate_cells, translate_cells, update_densities, - # update_cell_temperatures, and update_material_temperatures and make sure - # the changes have taken hold. + # and update_cell_temperatures and make sure the changes have taken hold. # For each we will first try bad inputs to make sure we get the right # errors and then we do a good one which calls the material by name and # then id to make sure it worked @@ -440,14 +426,6 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert abs(test_model.geometry.root_universe.cells[3].temperature - 600.) < 1e-13 - # Now lets do the material temperature updates. - # Check initial conditions - assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 - # Change the temperature - test_model.update_material_temperatures(['UO2'], 600.) - assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 - assert abs(test_model.materials[0].temperature - 600.) < 1e-13 - # And finally material volume assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 # The temperature on the material will be None because its just the default @@ -466,8 +444,6 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): with open('test_chain.xml', 'w') as f: f.write(chain_file_xml) test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm initial_mat = mats[0].clone() initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] @@ -497,7 +473,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats[0].set_density('atom/b-cm', tot_density) # Now we can re-run with the pre-initialized API - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) test_model.deplete([1e6], 'predictor', final_step=False, operator_kwargs=op_kwargs, power=1., output=False) @@ -518,8 +494,6 @@ def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats, geom, settings, tals, plots, _, _ = pin_model_attributes test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm # With no vol calcs, it should fail with pytest.raises(ValueError): @@ -555,13 +529,10 @@ def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): for file in ['volume_1.h5', 'volume_2.h5']: file = Path(file) file.unlink() - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) test_model.calculate_volumes(output=False, apply_volumes=True) assert mats[2].volume > 0. assert geom.root_universe.cells[3].volume > 0. assert openmc.lib.materials[3].volume == mats[2].volume test_model.finalize_lib() - - - From 2cadd1d3ed2f64800ddb38eaf000d3d5f423f094 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 4 Oct 2021 15:39:00 -0500 Subject: [PATCH 52/59] Remove comma in UChicago Argonne, LLC Co-authored-by: Patrick Shriwise --- 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 79f0c96f0..3523dd370 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +Copyright (c) 2011-2021 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 33fb515a5..a10494260 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-2021, Massachusetts Institute of Technology, UChicago Argonne, LLC, and OpenMC contributors' +copyright = '2011-2021, 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 5a3d40763..d8ea319aa 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +Copyright © 2011-2021 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 989e846e0..f99389069 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -58,7 +58,7 @@ section libraries if the user has not specified the tag in .I materials.xml\fP. .SH LICENSE Copyright \(co 2011-2021 Massachusetts Institute of Technology, UChicago -Argonne, LLC, and OpenMC contributors. +Argonne LLC, and OpenMC contributors. .PP 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 diff --git a/src/output.cpp b/src/output.cpp index b78314823..7bc012296 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -73,7 +73,7 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2021 MIT, UChicago Argonne, LLC and contributors\n" + " Copyright | 2011-2021 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" : ""); @@ -335,7 +335,7 @@ void print_version() #ifdef GIT_SHA1 fmt::print("Git SHA1: {}\n", GIT_SHA1); #endif - fmt::print("Copyright (c) 2011-2021 MIT, UChicago Argonne, LLC, and " + fmt::print("Copyright (c) 2011-2021 MIT, UChicago Argonne LLC, and " "contributors\nMIT/X license at " "\n"); } From 6eea13043f82c40f352e975206098937d8ce5e73 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 30 Sep 2021 16:28:05 -0500 Subject: [PATCH 53/59] Add support for PNG plots using libpng --- CMakeLists.txt | 10 +++ cmake/OpenMCConfig.cmake.in | 2 + include/openmc/plot.h | 13 ++- src/plot.cpp | 80 +++++++++++++++++-- tests/regression_tests/plot/results_true.dat | 2 +- tests/regression_tests/plot/test.py | 8 +- .../plot_overlaps/results_true.dat | 2 +- tests/regression_tests/plot_overlaps/test.py | 8 +- tests/regression_tests/plot_voxel/test.py | 2 +- 9 files changed, 107 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 75c66a233..d0c2569c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,11 @@ if(libmesh) find_package(LIBMESH REQUIRED) endif() +#=============================================================================== +# libpng +#=============================================================================== +find_package(PNG) + #=============================================================================== # HDF5 for binary output #=============================================================================== @@ -423,6 +428,11 @@ if(libmesh) target_link_libraries(libopenmc PkgConfig::LIBMESH) endif() +if (PNG_FOUND) + target_compile_definitions(libopenmc PRIVATE USE_LIBPNG) + target_link_libraries(libopenmc PNG::PNG) +endif() + #=============================================================================== # openmc executable #=============================================================================== diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index fa7ed7cc1..b12e95f23 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -16,6 +16,8 @@ if(@libmesh@) pkg_check_modules(LIBMESH REQUIRED @LIBMESH_PC_FILE@>=1.6.0 IMPORTED_TARGET) endif() +find_package(PNG) + if(NOT TARGET OpenMC::libopenmc) include("${OpenMC_CMAKE_DIR}/OpenMCTargets.cmake") endif() diff --git a/include/openmc/plot.h b/include/openmc/plot.h index aa15277aa..93a5f42ad 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -239,11 +239,18 @@ public: //! \param[out] image data associated with the plot object void draw_mesh_lines(Plot const& pl, ImageData& data); -//! Write a ppm image to file using a plot object's image data +//! Write a PPM image using a plot object's image data //! \param[in] plot object //! \param[out] image data associated with the plot object void output_ppm(Plot const& pl, const ImageData& data); +#ifdef USE_LIBPNG +//! Write a PNG image using a plot object's image data +//! \param[in] plot object +//! \param[out] image data associated with the plot object +void output_png(Plot const& pl, const ImageData& data); +#endif + //! Initialize a voxel file //! \param[in] id of an open hdf5 file //! \param[in] dimensions of the voxel file (dx, dy, dz) @@ -274,9 +281,9 @@ void voxel_finalize(hid_t dspace, hid_t dset, hid_t memspace); //! Read plot specifications from a plots.xml file void read_plots_xml(); -//! Create a ppm image for a plot object +//! Create an image for a plot object //! \param[in] plot object -void create_ppm(Plot const& pl); +void create_image(Plot const& pl); //! Create an hdf5 voxel file for a plot object //! \param[in] plot object diff --git a/src/plot.cpp b/src/plot.cpp index 789437691..27a01bc07 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,12 +1,16 @@ #include "openmc/plot.h" #include +#include #include #include #include "xtensor/xview.hpp" #include #include +#ifdef USE_LIBPNG +#include +#endif #include "openmc/constants.h" #include "openmc/error.h" @@ -103,17 +107,19 @@ uint64_t plotter_seed = 1; extern "C" int openmc_plot_geometry() { + for (auto& pl : model::plots) { write_message(5, "Processing plot {}: {}...", pl.id_, pl.path_plot_); if (PlotType::slice == pl.type_) { // create 2D image - create_ppm(pl); + create_image(pl); } else if (PlotType::voxel == pl.type_) { // create voxel file for 3D viewing create_voxel(pl); } } + return 0; } @@ -139,11 +145,11 @@ void read_plots_xml() } //============================================================================== -// CREATE_PPM creates an image based on user input from a plots.xml -// specification in the portable pixmap format (PPM) +// CREATE_IMAGE creates an image based on user input from a plots.xml +// specification in the PNG/PPM format //============================================================================== -void create_ppm(Plot const& pl) +void create_image(Plot const& pl) { size_t width = pl.pixels_[0]; @@ -184,8 +190,12 @@ void create_ppm(Plot const& pl) draw_mesh_lines(pl, data); } - // write ppm data to file +// create image file +#ifdef USE_LIBPNG + output_png(pl, data); +#else output_ppm(pl, data); +#endif } void Plot::set_id(pugi::xml_node plot_node) @@ -238,7 +248,11 @@ void Plot::set_output_path(pugi::xml_node plot_node) // add appropriate file extension to name switch (type_) { case PlotType::slice: +#ifdef USE_LIBPNG + filename.append(".png"); +#else filename.append(".ppm"); +#endif break; case PlotType::voxel: filename.append(".h5"); @@ -673,6 +687,60 @@ void output_ppm(Plot const& pl, const ImageData& data) of << "\n"; } +//============================================================================== +// OUTPUT_PNG writes out a previously generated image to a PNG file +//============================================================================== + +#ifdef USE_LIBPNG +void output_png(Plot const& pl, const ImageData& data) +{ + // Open PNG file for writing + std::string fname = pl.path_plot_; + fname = strtrim(fname); + auto fp = std::fopen(fname.c_str(), "wb"); + + // Initialize write and info structures + auto png_ptr = + png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + auto info_ptr = png_create_info_struct(png_ptr); + + // Setup exception handling + if (setjmp(png_jmpbuf(png_ptr))) + fatal_error("Error during png creation"); + + png_init_io(png_ptr, fp); + + // Write header (8 bit colour depth) + int width = pl.pixels_[0]; + int height = pl.pixels_[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); + + // Allocate memory for one row (3 bytes per pixel - RGB) + std::vector row(3 * width); + + // Write color for each pixel + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + RGBColor rgb = data(x, y); + row[3 * x] = rgb.red; + row[3 * x + 1] = rgb.green; + row[3 * x + 2] = rgb.blue; + } + png_write_row(png_ptr, row.data()); + } + + // End write + png_write_end(png_ptr, nullptr); + + // Clean up data structures + std::fclose(fp); + png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); + png_destroy_write_struct(&png_ptr, nullptr); +} +#endif + //============================================================================== // DRAW_MESH_LINES draws mesh line boundaries on an image //============================================================================== @@ -777,7 +845,7 @@ void draw_mesh_lines(Plot const& pl, ImageData& data) //============================================================================== // CREATE_VOXEL outputs a binary file that can be input into silomesh for 3D -// geometry visualization. It works the same way as create_ppm by dragging a +// geometry visualization. It works the same way as create_image by dragging a // particle across the geometry for the specified number of voxels. The first 3 // int's in the binary are the number of x, y, and z voxels. The next 3 // double's are the widths of the voxels in the x, y, and z directions. The diff --git a/tests/regression_tests/plot/results_true.dat b/tests/regression_tests/plot/results_true.dat index 54b6a253f..9174202fb 100644 --- a/tests/regression_tests/plot/results_true.dat +++ b/tests/regression_tests/plot/results_true.dat @@ -1 +1 @@ -a8192f6029cf99748816fab2618fa48e180656eba313940ccdfff3e56890a5dadd13bf92cd7e3be6a4b535bca5e2a58a905fcfba018fb922273f8be6dfc19859 \ No newline at end of file +f85c20735a0c08525fe48b19a8e075c074539ee6c8860268fa0cb515d842496b7086e5b94305ef78dcf2106bb193abdf438259ac8ff1d0245a2782eb6f5af873 \ No newline at end of file diff --git a/tests/regression_tests/plot/test.py b/tests/regression_tests/plot/test.py index a6f7306a0..2fd6a5b79 100644 --- a/tests/regression_tests/plot/test.py +++ b/tests/regression_tests/plot/test.py @@ -19,7 +19,7 @@ class PlotTestHarness(TestHarness): openmc.plot_geometry(openmc_exec=config['exe']) def _test_output_created(self): - """Make sure *.ppm has been created.""" + """Make sure *.png has been created.""" for fname in self._plot_names: assert os.path.exists(fname), 'Plot output file does not exist.' @@ -34,8 +34,8 @@ class PlotTestHarness(TestHarness): outstr = bytes() for fname in self._plot_names: - if fname.endswith('.ppm'): - # Add PPM output to results + if fname.endswith('.png'): + # Add PNG output to results with open(fname, 'rb') as fh: outstr += fh.read() elif fname.endswith('.h5'): @@ -56,6 +56,6 @@ class PlotTestHarness(TestHarness): def test_plot(): - harness = PlotTestHarness(('plot_1.ppm', 'plot_2.ppm', 'plot_3.ppm', + harness = PlotTestHarness(('plot_1.png', 'plot_2.png', 'plot_3.png', 'plot_4.h5')) harness.main() diff --git a/tests/regression_tests/plot_overlaps/results_true.dat b/tests/regression_tests/plot_overlaps/results_true.dat index 90ec8924a..cb04daaa4 100644 --- a/tests/regression_tests/plot_overlaps/results_true.dat +++ b/tests/regression_tests/plot_overlaps/results_true.dat @@ -1 +1 @@ -566103831cb8273b0578565c39d30e479664e2b1783d877b45b42cf4f3af0b01671b6db423114b09a74bbe1ddf51a7db565ff2118d6d1ee987b52318773b719a \ No newline at end of file +926065ceb2a9b8292fe6270317c38c4373473cfea19d2a8392a32e5ece8e314c04b9f032921d987bd195ae4b6f674d359b0e38302e6ae4c93b4ac9573a384ac6 \ No newline at end of file diff --git a/tests/regression_tests/plot_overlaps/test.py b/tests/regression_tests/plot_overlaps/test.py index 62236081b..cf23abc27 100644 --- a/tests/regression_tests/plot_overlaps/test.py +++ b/tests/regression_tests/plot_overlaps/test.py @@ -19,7 +19,7 @@ class PlotTestHarness(TestHarness): openmc.plot_geometry(openmc_exec=config['exe']) def _test_output_created(self): - """Make sure *.ppm has been created.""" + """Make sure *.png has been created.""" for fname in self._plot_names: assert os.path.exists(fname), 'Plot output file does not exist.' @@ -34,8 +34,8 @@ class PlotTestHarness(TestHarness): outstr = bytes() for fname in self._plot_names: - if fname.endswith('.ppm'): - # Add PPM output to results + if fname.endswith('.png'): + # Add PNG output to results with open(fname, 'rb') as fh: outstr += fh.read() elif fname.endswith('.h5'): @@ -56,6 +56,6 @@ class PlotTestHarness(TestHarness): def test_plot_overlap(): - harness = PlotTestHarness(('plot_1.ppm', 'plot_2.ppm', 'plot_3.ppm', + harness = PlotTestHarness(('plot_1.png', 'plot_2.png', 'plot_3.png', 'plot_4.h5')) harness.main() diff --git a/tests/regression_tests/plot_voxel/test.py b/tests/regression_tests/plot_voxel/test.py index 3b7a6bef2..c4fa20c80 100644 --- a/tests/regression_tests/plot_voxel/test.py +++ b/tests/regression_tests/plot_voxel/test.py @@ -25,7 +25,7 @@ class PlotVoxelTestHarness(TestHarness): glob.glob('plot_4.h5')) def _test_output_created(self): - """Make sure *.ppm has been created.""" + """Make sure plots have been created.""" for fname in self._plot_names: assert os.path.exists(fname), 'Plot output file does not exist.' From d8043b6098bb9464b054c05649cba505874e0e17 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 1 Oct 2021 13:19:29 -0500 Subject: [PATCH 54/59] Support png files in plot_inline and Plot.to_ipython_image --- openmc/executor.py | 20 +++++-------------- openmc/plots.py | 50 ++++++++++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/openmc/executor.py b/openmc/executor.py index e8f0de2c7..882764730 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -3,6 +3,7 @@ from numbers import Integral import subprocess import openmc +from .plots import _get_plot_image def _run(args, output, cwd): @@ -62,10 +63,6 @@ def plot_geometry(output=True, openmc_exec='openmc', cwd='.'): def plot_inline(plots, openmc_exec='openmc', cwd='.', convert_exec='convert'): """Display plots inline in a Jupyter notebook. - This function requires that you have a program installed to convert PPM - files to PNG files. Typically, that would be `ImageMagick - `_ which includes a `convert` command. - Parameters ---------- plots : Iterable of openmc.Plot @@ -75,7 +72,8 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', convert_exec='convert'): cwd : str, optional Path to working directory to run in convert_exec : str, optional - Command that can convert PPM files into PNG files + Command that can convert PPM files into PNG files. Only used if your + OpenMC installation was not compiled against libpng. Raises ------ @@ -83,7 +81,7 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', convert_exec='convert'): If the `openmc` executable returns a non-zero status """ - from IPython.display import Image, display + from IPython.display import display if not isinstance(plots, Iterable): plots = [plots] @@ -94,16 +92,8 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', convert_exec='convert'): # Run OpenMC in geometry plotting mode plot_geometry(False, openmc_exec, cwd) - images = [] if plots is not None: - for p in plots: - if p.filename is not None: - ppm_file = f'{p.filename}.ppm' - else: - ppm_file = f'plot_{p.id}.ppm' - png_file = ppm_file.replace('.ppm', '.png') - subprocess.check_call([convert_exec, ppm_file, png_file]) - images.append(Image(png_file)) + images = [_get_plot_image(p, convert_exec) for p in plots] display(*images) diff --git a/openmc/plots.py b/openmc/plots.py index 0abff20d7..bf8be6dfc 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,6 +1,7 @@ from collections.abc import Iterable, Mapping from numbers import Real, Integral from pathlib import Path +import shutil import subprocess from xml.etree import ElementTree as ET @@ -165,6 +166,31 @@ _SVG_COLORS = { } +def _get_plot_image(plot, convert_exec): + from IPython.display import Image + + # Check for .png first + stem = plot.filename if plot.filename is not None else f'plot_{plot.id}' + png_file = f'{stem}.png' + if Path(png_file).exists(): + return Image(png_file) + + # If .png doesn't exist, try finding .ppm + ppm_file = f'{stem}.ppm' + if not Path(ppm_file).exists(): + raise FileNotFoundError(f"Could not find image for plot {plot.id}") + + # Check that 'convert' command exists + if shutil.which(convert_exec) is None: + raise RuntimeError( + f"ImageMagick is needed to convert {ppm_file} to .png format. Please " + "install ImageMagick and try again.") + + # Convert .ppm to .png and return image + subprocess.check_call([convert_exec, ppm_file, png_file]) + return Image(png_file) + + class Plot(IDManagerMixin): """Definition of a finite region of space to be plotted. @@ -675,11 +701,9 @@ class Plot(IDManagerMixin): convert_exec='convert'): """Render plot as an image - This method runs OpenMC in plotting mode to produce a bitmap image which - is then converted to a .png file and loaded in as an - :class:`IPython.display.Image` object. As such, it requires that your - model geometry, materials, and settings have already been exported to - XML. + This method runs OpenMC in plotting mode to produce a .png file. If PNG + supported is not enabled, try converting the fallback .ppm file to .png + using ImageMagick's convert command. Parameters ---------- @@ -688,7 +712,8 @@ class Plot(IDManagerMixin): cwd : str, optional Path to working directory to run in convert_exec : str, optional - Command that can convert PPM files into PNG files + Command that can convert PPM files into PNG files. Only used if your + OpenMC installation was not compiled against libpng. Returns ------- @@ -696,23 +721,14 @@ class Plot(IDManagerMixin): Image generated """ - from IPython.display import Image - # Create plots.xml Plots([self]).export_to_xml() # Run OpenMC in geometry plotting mode openmc.plot_geometry(False, openmc_exec, cwd) - # Convert to .png - if self.filename is not None: - ppm_file = f'{self.filename}.ppm' - else: - ppm_file = f'plot_{self.id}.ppm' - png_file = ppm_file.replace('.ppm', '.png') - subprocess.check_call([convert_exec, ppm_file, png_file]) - - return Image(png_file) + # Return produced image + return _get_plot_image(self, convert_exec) class Plots(cv.CheckedList): From c347e2163d0a1a990073e415a2c1733956be768e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 1 Oct 2021 13:20:12 -0500 Subject: [PATCH 55/59] Update ci.yml, Dockerfile, install instructions --- .github/workflows/ci.yml | 42 ++++++++++++++---------------- Dockerfile | 2 +- docs/source/quickinstall.rst | 2 +- docs/source/usersguide/install.rst | 9 +++++++ docs/source/usersguide/plots.rst | 27 +++++-------------- 5 files changed, 37 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1673af531..e65e630b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,6 @@ env: COVERALLS_PARALLEL: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: main: runs-on: ubuntu-20.04 @@ -61,10 +60,10 @@ jobs: python-version: 3.8 omp: n mpi: y - name: 'Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, + name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, mpi=${{ matrix.mpi }}, dagmc=${{ matrix.dagmc }}, libmesh=${{ matrix.libmesh }}, event=${{ matrix.event }} - vectfit=${{ matrix.vectfit }})' + vectfit=${{ matrix.vectfit }})" env: MPI: ${{ matrix.mpi }} @@ -78,24 +77,23 @@ jobs: steps: - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - - name: Environment Variables + + - 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 - - - name: Apt dependencies + - name: Apt dependencies shell: bash run: | sudo apt -y update - sudo apt install -y libmpich-dev \ + sudo apt install -y libpng-dev \ + libmpich-dev \ libnetcdf-dev \ libpnetcdf-dev \ libhdf5-serial-dev \ @@ -105,23 +103,21 @@ 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: install + - name: install shell: bash run: | echo "$HOME/NJOY2016/build" >> $GITHUB_PATH $GITHUB_WORKSPACE/tools/ci/gha-install.sh - - - name: before + + - name: before shell: bash run: $GITHUB_WORKSPACE/tools/ci/gha-before-script.sh - - - name: test + + - name: test shell: bash run: $GITHUB_WORKSPACE/tools/ci/gha-script.sh - - - name: after_success + - name: after_success shell: bash run: | cpp-coveralls -i src -i include --exclude-pattern "/usr/*" --dump cpp_cov.json @@ -131,8 +127,8 @@ jobs: needs: main runs-on: ubuntu-latest steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.github_token }} - parallel-finished: true + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/Dockerfile b/Dockerfile index 89fec615e..67ff84a5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN apt-get update -y && \ apt-get install -y \ python3-pip python-is-python3 wget git gfortran g++ cmake \ mpich libmpich-dev libhdf5-serial-dev libhdf5-mpich-dev \ - imagemagick && \ + libpng-dev && \ apt-get autoremove # Update system-provided pip diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index e416b3efd..e3138a53b 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -100,7 +100,7 @@ directly from the package manager: .. code-block:: sh - sudo apt install g++ cmake libhdf5-dev + 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. diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 5fc1fe639..3090ae69e 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -219,6 +219,15 @@ Prerequisites .. admonition:: Optional :class: note + * libpng_ official reference PNG library + + 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:: + + sudo apt install libpng-dev + * An MPI implementation for distributed-memory parallel runs To compile with support for parallel runs on a distributed-memory diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index 109a8b549..d57917a3d 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -80,26 +80,13 @@ assign them to a :class:`openmc.Plots` collection and export it to XML:: plots += [plot2, plot3] plots.export_to_xml() -To actually generate the plots, run the :func:`openmc.plot_geometry` -function. Alternatively, run the :ref:`scripts_openmc` executable with the -``--plot`` command-line flag. When that has finished, you will have one or more -``.ppm`` files, i.e., `portable pixmap -`_ files. On some Linux -distributions, these ``.ppm`` files are natively viewable. If you find that -you're unable to open them on your system (or you don't like the fact that they -are not compressed), you may want to consider converting them to another format. -This is easily accomplished with the ``convert`` command available on most Linux -distributions as part of the `ImageMagick -`_ package. (On Debian -derivatives: ``sudo apt install imagemagick``). Images are then converted like: - -.. code-block:: sh - - convert myplot.ppm myplot.png - -Alternatively, if you're working within a `Jupyter `_ -Notebook or QtConsole, you can use the :func:`openmc.plot_inline` to run OpenMC -in plotting mode and display the resulting plot within the notebook. +To actually generate the plots, run the :func:`openmc.plot_geometry` function. +Alternatively, run the :ref:`scripts_openmc` executable with the ``--plot`` +command-line flag. When that has finished, you will have one or more ``.png`` +files. Alternatively, if you're working within a `Jupyter +`_ Notebook or QtConsole, you can use the +:func:`openmc.plot_inline` to run OpenMC in plotting mode and display the +resulting plot within the notebook. .. _usersguide_voxel: From c9e91d42fbc0a8aca2e7cd70f464ee940c848a08 Mon Sep 17 00:00:00 2001 From: agnelson Date: Tue, 5 Oct 2021 08:39:24 -0500 Subject: [PATCH 56/59] Replacing manual absolute error tests with pytest.approx, improving documentation, and other small changes per the review by @paulroman. --- openmc/executor.py | 2 +- openmc/lib/core.py | 20 +++++++++---- openmc/model/model.py | 12 ++++---- tests/unit_tests/test_model.py | 53 +++++++++++++++++++--------------- 4 files changed, 53 insertions(+), 34 deletions(-) diff --git a/openmc/executor.py b/openmc/executor.py index c2974722c..2d4911ff2 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -52,7 +52,7 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, args = [openmc_exec] if volume: - args += ['--volume'] + args.append('--volume') if isinstance(particles, Integral) and particles > 0: args += ['-n', str(particles)] diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 136e5a6d8..e277d527a 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -112,7 +112,17 @@ def global_bounding_box(): def calculate_volumes(output=True): - """Run stochastic volume calculation""" + """Run stochastic volume calculation + + .. versionchanged:: 0.13.0 + The *output* argument was added. + + Parameters + ---------- + output : bool, optional + Whether or not to show output. Defaults to showing output + + """ with quiet_dll(output): _dll.openmc_calculate_volumes() @@ -140,7 +150,7 @@ def export_properties(filename=None, output=True): ---------- filename : str or None Filename to export properties to (defaults to "properties.h5") - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output See Also @@ -242,7 +252,7 @@ def init(args=None, intracomm=None, output=True): Command-line arguments intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output """ @@ -369,7 +379,7 @@ def plot_geometry(output=True): Parameters ---------- - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output """ @@ -395,7 +405,7 @@ def run(output=True): Parameters ---------- - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output """ diff --git a/openmc/model/model.py b/openmc/model/model.py index 904c60746..ad898fd0a 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -20,7 +20,7 @@ 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() + start_dir = Path.cwd() Path.mkdir(working_dir, exist_ok=True) os.chdir(working_dir) try: @@ -231,7 +231,7 @@ class Model: event_based : None or bool, optional Turns on event-based parallelism if True. If None, the value in the Settings will be used. - intracomm : DummyCommnicator, mpi4py.MPI.Intracomm or None, optional + intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator """ @@ -261,7 +261,10 @@ class Model: self.export_to_xml() self._intracomm.barrier() - openmc.lib.init(args=args, intracomm=self._intracomm, output=output) + # We cannot pass DummyCommunicator to openmc.lib.init so pass instead + # the user-provided intracomm which will either be None or an mpi4py + # communicator + openmc.lib.init(args=args, intracomm=intracomm, output=output) def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -610,8 +613,7 @@ class Model: if apply_volumes: # Load the results and add them to the model for i, vol_calc in enumerate(self.settings.volume_calculations): - f_name = f"volume_{i + 1}.h5" - vol_calc.load_results(f_name) + vol_calc.load_results(f"volume_{i + 1}.h5") # First add them to the Python side self.geometry.add_volume_information(vol_calc) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index cffa5c249..b1021a269 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,8 +1,10 @@ from math import pi -import numpy as np -import pytest from pathlib import Path from shutil import which + +import numpy as np +import pytest + import openmc import openmc.lib @@ -304,9 +306,9 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): lib_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] # and lets compare results - assert abs(lib_keff - cli_keff) < 1e-13 - assert abs(lib_flux - cli_flux) < 1e-13 - assert abs(lib_fiss - cli_fiss) < 1e-13 + assert lib_keff.n == pytest.approx(cli_keff.n, abs=1e-13) + assert lib_flux == pytest.approx(cli_flux, abs=1e-13) + assert lib_fiss == pytest.approx(cli_fiss, abs=1e-13) # Now we should make sure that the flags for items which should be handled # by init are properly set @@ -348,7 +350,7 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # convert is True, test.png, plot_2.png for fname in ['test.', 'plot_2.']: for ext in exts: - test_file = Path('./{}{}'.format(fname, ext)) + test_file = Path(f'./{fname}{ext}') assert test_file.exists() test_file.unlink() @@ -398,19 +400,20 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Now lets do the density updates. # Check initial conditions - assert abs(openmc.lib.materials[1].get_density( - 'atom/b-cm') - 0.06891296988603757) < 1e-13 + assert openmc.lib.materials[1].get_density('atom/b-cm') == \ + pytest.approx(0.06891296988603757, abs=1e-13) mat_a_dens = np.sum( [v[1] for v in test_model.materials[0]. get_nuclide_atom_densities().values()]) - assert abs(mat_a_dens - 0.06891296988603757) < 1e-8 + assert mat_a_dens == pytest.approx(0.06891296988603757, abs=1e-8) # Change the density test_model.update_densities(['UO2'], 2.) - assert abs(openmc.lib.materials[1].get_density('atom/b-cm') - 2.) < 1e-13 + assert openmc.lib.materials[1].get_density('atom/b-cm') == \ + pytest.approx(2., abs=1e-13) mat_a_dens = np.sum( [v[1] for v in test_model.materials[0]. get_nuclide_atom_densities().values()]) - assert abs(mat_a_dens - 2.) < 1e-8 + assert mat_a_dens == pytest.approx(2., abs=1e-8) # Now lets do the cell temperature updates. # Check initial conditions @@ -418,22 +421,26 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], 4: geom.root_universe.cells[4], 1: geom.root_universe.cells[2].fill.cells[1]} - assert abs(openmc.lib.cells[3].get_temperature() - 293.6) < 1e-13 + assert openmc.lib.cells[3].get_temperature() == \ + pytest.approx(293.6, abs=1e-13) assert test_model.geometry.root_universe.cells[3].temperature is None # Change the temperature test_model.update_cell_temperatures([3], 600.) - assert abs(openmc.lib.cells[3].get_temperature() - 600.) < 1e-13 - assert abs(test_model.geometry.root_universe.cells[3].temperature - - 600.) < 1e-13 + assert openmc.lib.cells[3].get_temperature() == \ + pytest.approx(600., abs=1e-13) + assert test_model.geometry.root_universe.cells[3].temperature == \ + pytest.approx(600., abs=1e-13) # And finally material volume - assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 + assert openmc.lib.materials[1].volume == \ + pytest.approx(0.4831931368640985, abs=1e-13) # The temperature on the material will be None because its just the default - assert abs(test_model.materials[0].volume - 0.4831931368640985) < 1e-13 + assert test_model.materials[0].volume == \ + pytest.approx(0.4831931368640985, abs=1e-13) # Change the temperature test_model.update_material_volumes(['UO2'], 2.) - assert abs(openmc.lib.materials[1].volume - 2.) < 1e-13 - assert abs(test_model.materials[0].volume - 2.) < 1e-13 + assert openmc.lib.materials[1].volume == pytest.approx(2., abs=1e-13) + assert test_model.materials[0].volume == pytest.approx(2., abs=1e-13) test_model.finalize_lib() @@ -460,7 +467,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Get the new Xe136 and U235 atom densities after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_u = mats[0].get_nuclide_atom_densities()['U235'][1] - assert abs((after_xe + after_u) - initial_u) < 1e-15 + assert after_xe + after_u == pytest.approx(initial_u, abs=1e-15) assert test_model.is_initialized is False # Reset the initial material densities @@ -480,12 +487,12 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Get the new Xe136 and U235 atom densities after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] - assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 + assert after_lib_xe + after_lib_u == pytest.approx(initial_u, abs=1e-15) assert test_model.is_initialized is True # And end by comparing to the previous case - assert abs(after_xe - after_lib_xe) < 1e-15 - assert abs(after_u - after_lib_u) < 1e-15 + assert after_xe == pytest.approx(after_lib_xe, abs=1e-15) + assert after_u == pytest.approx(after_lib_u, abs=1e-15) test_model.finalize_lib() From 7b070b715425d6169e34c560c497aa548a749f39 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 5 Oct 2021 13:42:27 -0500 Subject: [PATCH 57/59] Remove convert arguments from Model.plot_geometry --- openmc/model/model.py | 22 ++-------------------- tests/unit_tests/test_model.py | 22 ++++++---------------- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index ad898fd0a..bc0796ea7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -624,15 +624,9 @@ class Model: openmc.lib.materials[domain_id].volume = \ vol_calc.volumes[domain_id].n - def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', - convert=True, convert_exec='convert'): + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): """Creates plot images as specified by the Model.plots attribute - If convert is True, this function requires that a program is installed - to convert PPM files to PNG files. Typically, that would be - `ImageMagick `_ which includes a - `convert` command. - .. versionadded:: 0.13.0 Parameters @@ -645,10 +639,7 @@ 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. - convert : bool, optional - Whether or not to attempt to convert from PPM to PNG - convert_exec : str, optional - Command that can convert PPM files into PNG files + """ if len(self.plots) == 0: @@ -664,15 +655,6 @@ class Model: self.export_to_xml() openmc.plot_geometry(output=output, openmc_exec=openmc_exec) - if convert: - for p in self.plots: - if p.filename is not None: - ppm_file = f'{p.filename}.ppm' - else: - ppm_file = f'plot_{p.id}.ppm' - png_file = ppm_file.replace('.ppm', '.png') - subprocess.check_call([convert_exec, ppm_file, png_file]) - def _change_py_lib_attribs(self, names_or_ids, value, obj_type, attrib_name, density_units='atom/b-cm'): # Method to do the same work whether it is a cell or material and diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index b1021a269..994130f89 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -332,27 +332,17 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # check that a plot was made and that the expected ppm and png files are # there - # We will only test convert if it is on the system, so as not to add an - # extra dependency just for tests - convert = which('convert') is not None - if convert: - exts = ['ppm', 'png'] - else: - exts = ['ppm'] - # We will run the test twice, the first time without C API, the second with for i in range(2): if i == 1: test_model.init_lib(output=False, intracomm=mpi_intracomm) - test_model.plot_geometry(output=False, convert=convert) + test_model.plot_geometry(output=False) - # Now look for the files, expect to find test.ppm, plot_2.ppm, and if - # convert is True, test.png, plot_2.png - for fname in ['test.', 'plot_2.']: - for ext in exts: - test_file = Path(f'./{fname}{ext}') - assert test_file.exists() - test_file.unlink() + # Now look for the files + for fname in ('test.png', 'plot_2.png'): + test_file = Path(fname) + assert test_file.exists() + test_file.unlink() test_model.finalize_lib() From 613d6e3ba2bae20288b832a9eed51afd34f5fb0a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 6 Oct 2021 07:16:21 -0500 Subject: [PATCH 58/59] Fix docstring for Plot.to_ipython_image --- openmc/plots.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index bf8be6dfc..424a4eb84 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -701,9 +701,9 @@ class Plot(IDManagerMixin): convert_exec='convert'): """Render plot as an image - This method runs OpenMC in plotting mode to produce a .png file. If PNG - supported is not enabled, try converting the fallback .ppm file to .png - using ImageMagick's convert command. + This method runs OpenMC in plotting mode to produce a .png file. If your + installation of OpenMC was not compiled against libpng, try converting + the fallback .ppm file to .png using ImageMagick's convert command. Parameters ---------- From 66ef28a3854769ea3baec19191c1ec138dbad20c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 6 Oct 2021 11:51:38 -0500 Subject: [PATCH 59/59] Remove convert_exec arguments, update documentation --- .gitignore | 3 ++- docs/source/io_formats/plots.rst | 29 ++++++++----------------- openmc/executor.py | 12 ++++++----- openmc/plots.py | 37 ++++++++++---------------------- tests/unit_tests/test_model.py | 3 +-- 5 files changed, 30 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index f328d7b9a..88e59895d 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ scripts/*.tar.* scripts/G4EMLOW*/ # Images +*.png *.ppm *.voxel *.vti @@ -103,4 +104,4 @@ CMakeSettings.json .vscode/ # Python pickle files -*.pkl \ No newline at end of file +*.pkl diff --git a/docs/source/io_formats/plots.rst b/docs/source/io_formats/plots.rst index 966d2b802..e6b75eafc 100644 --- a/docs/source/io_formats/plots.rst +++ b/docs/source/io_formats/plots.rst @@ -10,9 +10,9 @@ of the plots.xml is simply ```` and any number output plots can be defined with ```` sub-elements. Two plot types are currently implemented in openMC: -* ``slice`` 2D pixel plot along one of the major axes. Produces a PPM image +* ``slice`` 2D pixel plot along one of the major axes. Produces a PNG image file. -* ``voxel`` 3D voxel data dump. Produces a binary file containing voxel xyz +* ``voxel`` 3D voxel data dump. Produces an HDF5 file containing voxel xyz position and cell or material id. @@ -68,20 +68,14 @@ sub-elements: :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 PPM file format. PPM files can be displayed in most viewers (e.g. the - default Gnome viewer, IrfanView, etc.). 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. These - datafiles 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. + 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. - .. note:: Since the PPM format is saved without any kind of compression, - the resulting file sizes can be quite large. Saving the image in - the PNG format can often times reduce the file size by orders of - magnitude without any loss of image quality. Likewise, - high-resolution voxel files produced by OpenMC can be quite large, + .. note:: High-resolution voxel files produced by OpenMC can be quite large, but the equivalent VTK files will be significantly smaller. *Default*: "slice" @@ -94,11 +88,6 @@ attribute or sub-element: directions for "slice" and "voxel" plots, respectively. Should be two or three integers separated by spaces. - .. warning:: The ``pixels`` input determines the output file size. For the - PPM format, 10 million pixels will result in a file just under - 30 MB in size. A 10 million voxel binary file will be around - 40 MB. - .. warning:: If the aspect ratio defined in ``pixels`` does not match the aspect ratio defined in ``width`` the plot may appear stretched or squeezed. diff --git a/openmc/executor.py b/openmc/executor.py index b64d50125..63653f4d0 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -137,9 +137,14 @@ def plot_geometry(output=True, openmc_exec='openmc', cwd='.'): _run([openmc_exec, '-p'], output, cwd) -def plot_inline(plots, openmc_exec='openmc', cwd='.', convert_exec='convert'): +def plot_inline(plots, openmc_exec='openmc', cwd='.'): """Display plots inline in a Jupyter notebook. + .. versionchanged:: 0.13.0 + The *convert_exec* argument was removed since OpenMC now produces + .png images directly. + + Parameters ---------- plots : Iterable of openmc.Plot @@ -148,9 +153,6 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', convert_exec='convert'): Path to OpenMC executable cwd : str, optional Path to working directory to run in - convert_exec : str, optional - Command that can convert PPM files into PNG files. Only used if your - OpenMC installation was not compiled against libpng. Raises ------ @@ -170,7 +172,7 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', convert_exec='convert'): plot_geometry(False, openmc_exec, cwd) if plots is not None: - images = [_get_plot_image(p, convert_exec) for p in plots] + images = [_get_plot_image(p) for p in plots] display(*images) diff --git a/openmc/plots.py b/openmc/plots.py index 424a4eb84..521be95ad 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -166,28 +166,15 @@ _SVG_COLORS = { } -def _get_plot_image(plot, convert_exec): +def _get_plot_image(plot): from IPython.display import Image - # Check for .png first + # Make sure .png file was created stem = plot.filename if plot.filename is not None else f'plot_{plot.id}' png_file = f'{stem}.png' - if Path(png_file).exists(): - return Image(png_file) + if not Path(png_file).exists(): + raise FileNotFoundError(f"Could not find .png image for plot {plot.id}") - # If .png doesn't exist, try finding .ppm - ppm_file = f'{stem}.ppm' - if not Path(ppm_file).exists(): - raise FileNotFoundError(f"Could not find image for plot {plot.id}") - - # Check that 'convert' command exists - if shutil.which(convert_exec) is None: - raise RuntimeError( - f"ImageMagick is needed to convert {ppm_file} to .png format. Please " - "install ImageMagick and try again.") - - # Convert .ppm to .png and return image - subprocess.check_call([convert_exec, ppm_file, png_file]) return Image(png_file) @@ -697,13 +684,14 @@ class Plot(IDManagerMixin): return element - def to_ipython_image(self, openmc_exec='openmc', cwd='.', - convert_exec='convert'): + def to_ipython_image(self, openmc_exec='openmc', cwd='.'): """Render plot as an image - This method runs OpenMC in plotting mode to produce a .png file. If your - installation of OpenMC was not compiled against libpng, try converting - the fallback .ppm file to .png using ImageMagick's convert command. + This method runs OpenMC in plotting mode to produce a .png file. + + .. versionchanged:: 0.13.0 + The *convert_exec* argument was removed since OpenMC now produces + .png images directly. Parameters ---------- @@ -711,9 +699,6 @@ class Plot(IDManagerMixin): Path to OpenMC executable cwd : str, optional Path to working directory to run in - convert_exec : str, optional - Command that can convert PPM files into PNG files. Only used if your - OpenMC installation was not compiled against libpng. Returns ------- @@ -728,7 +713,7 @@ class Plot(IDManagerMixin): openmc.plot_geometry(False, openmc_exec, cwd) # Return produced image - return _get_plot_image(self, convert_exec) + return _get_plot_image(self) class Plots(cv.CheckedList): diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 994130f89..d22485b3c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -329,8 +329,7 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model = openmc.Model(geom, mats, settings, tals, plots) # This test cannot check the correctness of the plot, but it can - # check that a plot was made and that the expected ppm and png files are - # there + # check that a plot was made and that the expected png files are there # We will run the test twice, the first time without C API, the second with for i in range(2):