diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 7fb736a4f10..e30290647fb 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -99,8 +99,8 @@ int openmc_get_material_index(int32_t id, int32_t* index); int openmc_get_mesh_index(int32_t id, int32_t* index); int openmc_get_n_batches(int* n_batches, bool get_max_batches); int openmc_get_nuclide_index(const char name[], int* index); -int openmc_add_unstructured_mesh( - const char filename[], const char library[], int* id); +int openmc_add_unstructured_mesh(const char filename[], const char library[], + double length_multiplier, const char options[], int32_t id, int32_t* index); int64_t openmc_get_seed(); uint64_t openmc_get_stride(); int openmc_get_tally_index(int32_t id, int32_t* index); @@ -140,12 +140,18 @@ int openmc_mesh_filter_get_translation(int32_t index, double translation[3]); int openmc_mesh_filter_set_translation(int32_t index, double translation[3]); int openmc_mesh_get_id(int32_t index, int32_t* id); int openmc_mesh_set_id(int32_t index, int32_t id); +int openmc_mesh_get_name(int32_t index, const char** name); +int openmc_mesh_set_name(int32_t index, const char* name); int openmc_mesh_get_n_elements(int32_t index, size_t* n); int openmc_mesh_get_volumes(int32_t index, double* volumes); int openmc_mesh_material_volumes(int32_t index, int nx, int ny, int nz, int max_mats, int32_t* materials, double* volumes, double* bboxes); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh); +int openmc_cylindrical_mesh_get_origin(int32_t index, double origin[3]); +int openmc_cylindrical_mesh_set_origin(int32_t index, const double origin[3]); +int openmc_spherical_mesh_get_origin(int32_t index, double origin[3]); +int openmc_spherical_mesh_set_origin(int32_t index, const double origin[3]); int openmc_new_filter(const char* type, int32_t* index); int openmc_next_batch(int* status); int openmc_nuclide_name(int index, const char** name); diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 1c6044514bd..14aee7de5a9 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -207,6 +207,8 @@ class Mesh { const std::string& name() const { return name_; } + void set_name(const std::string& name) { name_ = name; } + //! Set the mesh ID void set_id(int32_t id = -1); @@ -475,6 +477,16 @@ class PeriodicStructuredMesh : public StructuredMesh { return r - origin_; }; + const Position& origin() const { return origin_; } + + virtual int set_grid() = 0; + + int set_origin(Position origin) + { + origin_ = origin; + return set_grid(); + } + // Data members Position origin_ {0.0, 0.0, 0.0}; //!< Origin of the mesh }; @@ -832,7 +844,8 @@ class MOABMesh : public UnstructuredMesh { MOABMesh() = default; MOABMesh(pugi::xml_node); MOABMesh(hid_t group); - MOABMesh(const std::string& filename, double length_multiplier = 1.0); + MOABMesh(const std::string& filename, double length_multiplier = 1.0, + const std::string& options = {}); MOABMesh(std::shared_ptr external_mbi); static const std::string mesh_lib_type; @@ -1002,7 +1015,8 @@ class LibMesh : public UnstructuredMesh { // Constructors LibMesh(pugi::xml_node node); LibMesh(hid_t group); - LibMesh(const std::string& filename, double length_multiplier = 1.0); + LibMesh(const std::string& filename, double length_multiplier = 1.0, + const std::string& options = {}); LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier = 1.0); static const std::string mesh_lib_type; diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index d0b385d169d..8d6692d01c3 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -109,6 +109,9 @@ class WeightWindows { //! Ready the weight window class for use void set_defaults(); + //! Replace the energy grid with defaults for the selected particle type + void reset_energy_bounds(); + //! Ensure the weight window lower bounds are properly allocated void allocate_ww_bounds(); diff --git a/openmc/checkvalue.py b/openmc/checkvalue.py index 5ff2cf9ac5a..cc747230bcb 100644 --- a/openmc/checkvalue.py +++ b/openmc/checkvalue.py @@ -1,6 +1,7 @@ import copy import os from collections.abc import Iterable +from numbers import Real import numpy as np @@ -80,7 +81,18 @@ def check_iterable_type(name, value, expected_type, min_depth=1, max_depth=1): max_depth : int The maximum number of layers of nested iterables there should be before reaching the ultimately contained items + + Notes + ----- + For NumPy floating-point arrays with an allowed number of dimensions, the + dtype guarantees the element type and the per-element scan is skipped when + *expected_type* is :class:`numbers.Real` or :class:`float`. """ + if (isinstance(value, np.ndarray) and value.dtype.kind == 'f' + and min_depth <= value.ndim <= max_depth + and expected_type in (Real, float)): + return + # Initialize the tree at the very first item. tree = [value] index = [0] diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 19e6f74d7ad..19b5dcfa115 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -2,6 +2,7 @@ from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, c_void_p, create_string_buffer, c_size_t) from math import sqrt +from pathlib import Path import sys from weakref import WeakValueDictionary @@ -14,7 +15,14 @@ from .error import _error_handler from .plot import _Position from ..bounding_box import BoundingBox -from ..mesh import MeshMaterialVolumes +from ..mesh import ( + CylindricalMesh as PythonCylindricalMesh, + MeshMaterialVolumes, + RectilinearMesh as PythonRectilinearMesh, + RegularMesh as PythonRegularMesh, + SphericalMesh as PythonSphericalMesh, + UnstructuredMesh as PythonUnstructuredMesh, +) __all__ = [ 'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh', @@ -36,6 +44,12 @@ _dll.openmc_mesh_set_id.argtypes = [c_int32, c_int32] _dll.openmc_mesh_set_id.restype = c_int _dll.openmc_mesh_set_id.errcheck = _error_handler +_dll.openmc_mesh_get_name.argtypes = [c_int32, POINTER(c_char_p)] +_dll.openmc_mesh_get_name.restype = c_int +_dll.openmc_mesh_get_name.errcheck = _error_handler +_dll.openmc_mesh_set_name.argtypes = [c_int32, c_char_p] +_dll.openmc_mesh_set_name.restype = c_int +_dll.openmc_mesh_set_name.errcheck = _error_handler _dll.openmc_mesh_get_n_elements.argtypes = [c_int32, POINTER(c_size_t)] _dll.openmc_mesh_get_n_elements.restype = c_int _dll.openmc_mesh_get_n_elements.errcheck = _error_handler @@ -97,6 +111,12 @@ c_int, POINTER(c_double), c_int, POINTER(c_double), c_int] _dll.openmc_cylindrical_mesh_set_grid.restype = c_int _dll.openmc_cylindrical_mesh_set_grid.errcheck = _error_handler +_dll.openmc_cylindrical_mesh_get_origin.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_cylindrical_mesh_get_origin.restype = c_int +_dll.openmc_cylindrical_mesh_get_origin.errcheck = _error_handler +_dll.openmc_cylindrical_mesh_set_origin.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_cylindrical_mesh_set_origin.restype = c_int +_dll.openmc_cylindrical_mesh_set_origin.errcheck = _error_handler _dll.openmc_spherical_mesh_get_grid.argtypes = [c_int32, POINTER(POINTER(c_double)), POINTER(c_int), POINTER(POINTER(c_double)), @@ -107,6 +127,17 @@ c_int, POINTER(c_double), c_int, POINTER(c_double), c_int] _dll.openmc_spherical_mesh_set_grid.restype = c_int _dll.openmc_spherical_mesh_set_grid.errcheck = _error_handler +_dll.openmc_spherical_mesh_get_origin.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_spherical_mesh_get_origin.restype = c_int +_dll.openmc_spherical_mesh_get_origin.errcheck = _error_handler +_dll.openmc_spherical_mesh_set_origin.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_spherical_mesh_set_origin.restype = c_int +_dll.openmc_spherical_mesh_set_origin.errcheck = _error_handler + +_dll.openmc_add_unstructured_mesh.argtypes = [ + c_char_p, c_char_p, c_double, c_char_p, c_int32, POINTER(c_int32)] +_dll.openmc_add_unstructured_mesh.restype = c_int +_dll.openmc_add_unstructured_mesh.errcheck = _error_handler class Mesh(_FortranObjectWithID): @@ -114,6 +145,7 @@ class Mesh(_FortranObjectWithID): """ __instances = WeakValueDictionary() + _python_type = None def __new__(cls, uid=None, new=True, index=None): mapping = meshes @@ -145,6 +177,59 @@ def __new__(cls, uid=None, new=True, index=None): return cls.__instances[index] + @classmethod + def from_python(cls, mesh, uid=None, base_dir=None): + """Create a shared-library mesh from a Python API mesh. + + The OpenMC shared library must be initialized before calling this + method. The returned object is tied to the active library session and + becomes invalid when that session is finalized. + + Parameters + ---------- + mesh : openmc.MeshBase + Python API mesh to convert. + uid : int, optional + ID to assign to the library mesh. If omitted, the ID of *mesh* is + used. + base_dir : path-like, optional + Directory used to resolve relative filenames for unstructured + meshes. If omitted, the current working directory is used. + + Returns + ------- + openmc.lib.Mesh + Corresponding mesh in the active library session. + + """ + import openmc.lib + + if not openmc.lib.is_initialized: + raise RuntimeError( + 'The OpenMC shared library must be initialized before ' + 'creating a library mesh.') + + if cls is Mesh: + for mesh_cls in _MESH_TYPE_MAP.values(): + if isinstance(mesh, mesh_cls._python_type): + cls = mesh_cls + break + else: + raise TypeError(f'Unsupported mesh type: {type(mesh)}') + elif not isinstance(mesh, cls._python_type): + raise TypeError( + f'{cls.__name__}.from_python cannot convert {type(mesh)}') + + uid = mesh.id if uid is None else uid + base_dir = Path.cwd() if base_dir is None else Path(base_dir) + lib_mesh = cls._from_python(mesh, uid, base_dir) + lib_mesh.name = mesh.name + return lib_mesh + + @classmethod + def _from_python(cls, mesh, uid, base_dir): + raise NotImplementedError + @property def id(self): mesh_id = c_int32() @@ -155,6 +240,16 @@ def id(self): def id(self, mesh_id): _dll.openmc_mesh_set_id(self._index, mesh_id) + @property + def name(self): + name = c_char_p() + _dll.openmc_mesh_get_name(self._index, name) + return name.value.decode() + + @name.setter + def name(self, name): + _dll.openmc_mesh_set_name(self._index, name.encode()) + @property def n_elements(self) -> int: n = c_size_t() @@ -357,10 +452,19 @@ class RegularMesh(Mesh): """ mesh_type = 'regular' + _python_type = PythonRegularMesh def __init__(self, uid=None, new=True, index=None): super().__init__(uid, new, index) + @classmethod + def _from_python(cls, mesh, uid, base_dir): + lib_mesh = cls(uid=uid) + lib_mesh.dimension = mesh.dimension + lib_mesh.set_parameters( + lower_left=mesh.lower_left, upper_right=mesh.upper_right) + return lib_mesh + @property def dimension(self): dims = POINTER(c_int)() @@ -445,10 +549,17 @@ class RectilinearMesh(Mesh): """ mesh_type = 'rectilinear' + _python_type = PythonRectilinearMesh def __init__(self, uid=None, new=True, index=None): super().__init__(uid, new, index) + @classmethod + def _from_python(cls, mesh, uid, base_dir): + lib_mesh = cls(uid=uid) + lib_mesh.set_grid(mesh.x_grid, mesh.y_grid, mesh.z_grid) + return lib_mesh + @property def lower_left(self): return self._get_parameters()[0] @@ -550,10 +661,18 @@ class CylindricalMesh(Mesh): """ mesh_type = 'cylindrical' + _python_type = PythonCylindricalMesh def __init__(self, uid=None, new=True, index=None): super().__init__(uid, new, index) + @classmethod + def _from_python(cls, mesh, uid, base_dir): + lib_mesh = cls(uid=uid) + lib_mesh.set_grid(mesh.r_grid, mesh.phi_grid, mesh.z_grid) + lib_mesh.origin = mesh.origin + return lib_mesh + @property def lower_left(self): return self._get_parameters()[0] @@ -621,6 +740,21 @@ def set_grid(self, r_grid, phi_grid, z_grid): _dll.openmc_cylindrical_mesh_set_grid(self._index, r_grid, nr, phi_grid, nphi, z_grid, nz) + @property + def origin(self): + origin = np.empty(3) + _dll.openmc_cylindrical_mesh_get_origin( + self._index, origin.ctypes.data_as(POINTER(c_double))) + return origin + + @origin.setter + def origin(self, origin): + origin = np.ascontiguousarray(origin, dtype=np.float64) + if origin.shape != (3,): + raise ValueError('Mesh origin must have three coordinates') + _dll.openmc_cylindrical_mesh_set_origin( + self._index, origin.ctypes.data_as(POINTER(c_double))) + class SphericalMesh(Mesh): """SphericalMesh stored internally. @@ -655,10 +789,18 @@ class SphericalMesh(Mesh): """ mesh_type = 'spherical' + _python_type = PythonSphericalMesh def __init__(self, uid=None, new=True, index=None): super().__init__(uid, new, index) + @classmethod + def _from_python(cls, mesh, uid, base_dir): + lib_mesh = cls(uid=uid) + lib_mesh.set_grid(mesh.r_grid, mesh.theta_grid, mesh.phi_grid) + lib_mesh.origin = mesh.origin + return lib_mesh + @property def lower_left(self): return self._get_parameters()[0] @@ -726,9 +868,65 @@ def set_grid(self, r_grid, theta_grid, phi_grid): _dll.openmc_spherical_mesh_set_grid(self._index, r_grid, nr, theta_grid, ntheta, phi_grid, nphi) + @property + def origin(self): + origin = np.empty(3) + _dll.openmc_spherical_mesh_get_origin( + self._index, origin.ctypes.data_as(POINTER(c_double))) + return origin + + @origin.setter + def origin(self, origin): + origin = np.ascontiguousarray(origin, dtype=np.float64) + if origin.shape != (3,): + raise ValueError('Mesh origin must have three coordinates') + _dll.openmc_spherical_mesh_set_origin( + self._index, origin.ctypes.data_as(POINTER(c_double))) + class UnstructuredMesh(Mesh): - pass + _python_type = PythonUnstructuredMesh + + @classmethod + def _from_python(cls, mesh, uid, base_dir): + filename = Path(mesh.filename) + if not filename.is_absolute(): + filename = base_dir / filename + return cls.from_file( + filename.resolve(), mesh.library, uid=uid, + length_multiplier=mesh.length_multiplier, options=mesh.options) + + @classmethod + def from_file(cls, filename, library, uid=None, length_multiplier=1.0, + options=None): + """Create an unstructured mesh from a file. + + Parameters + ---------- + filename : path-like + Path to the unstructured mesh file. + library : {'libmesh', 'moab'} + Library used to load the mesh. + uid : int, optional + Unique ID for the mesh. If omitted, an ID is assigned. + length_multiplier : float, optional + Multiplicative factor applied to mesh coordinates. + options : str, optional + Options used to construct spatial search data structures. + + Returns + ------- + openmc.lib.UnstructuredMesh + The newly allocated mesh. + + """ + index = c_int32() + mesh_id = -1 if uid is None else uid + options = None if options is None else options.encode() + _dll.openmc_add_unstructured_mesh( + str(filename).encode(), library.encode(), length_multiplier, + options, mesh_id, index) + return cls(index=index.value) _MESH_TYPE_MAP = { diff --git a/openmc/lib/weight_windows.py b/openmc/lib/weight_windows.py index 2b26d3b55f5..fd945d0c7b5 100644 --- a/openmc/lib/weight_windows.py +++ b/openmc/lib/weight_windows.py @@ -11,8 +11,7 @@ from .core import _FortranObjectWithID from .error import _error_handler from .filter import EnergyFilter, MeshFilter, ParticleFilter -from .mesh import _get_mesh -from .mesh import meshes +from .mesh import Mesh, _get_mesh, meshes __all__ = ['WeightWindows', 'weight_windows'] @@ -194,10 +193,15 @@ def energy_bounds(self): @energy_bounds.setter def energy_bounds(self, e_bounds): - e_bounds_arr = np.asarray(e_bounds, dtype=float) - e_bounds_ptr = e_bounds_arr.ctypes.data_as(POINTER(c_double)) + if e_bounds is None: + e_bounds_ptr = None + size = 0 + else: + e_bounds_arr = np.ascontiguousarray(e_bounds, dtype=np.float64) + e_bounds_ptr = e_bounds_arr.ctypes.data_as(POINTER(c_double)) + size = e_bounds_arr.size _dll.openmc_weight_windows_set_energy_bounds( - self._index, e_bounds_ptr, e_bounds_arr.size) + self._index, e_bounds_ptr, size) @property def particle(self): @@ -222,8 +226,8 @@ def bounds(self): @bounds.setter def bounds(self, bounds): - lower = np.asarray(bounds[0]) - upper = np.asarray(bounds[1]) + lower = np.ascontiguousarray(bounds[0], dtype=np.float64) + upper = np.ascontiguousarray(bounds[1], dtype=np.float64) lower_p = lower.ctypes.data_as(POINTER(c_double)) upper_p = upper.ctypes.data_as(POINTER(c_double)) @@ -293,6 +297,48 @@ def update_magic(self, tally, value='mean', threshold=1.0, ratio=5.0): threshold, ratio) + @classmethod + def from_python(cls, weight_windows, mesh=None, base_dir=None): + """Create shared-library weight windows from a Python API object. + + Parameters + ---------- + weight_windows : openmc.WeightWindows + Python API weight windows to convert. + mesh : openmc.lib.Mesh, optional + Previously converted library mesh. If omitted, the mesh associated + with *weight_windows* is converted automatically. + base_dir : path-like, optional + Directory used to resolve relative unstructured-mesh filenames + when *mesh* is omitted. + + Returns + ------- + openmc.lib.WeightWindows + Corresponding weight windows in the active library session. + + """ + if mesh is None: + mesh = Mesh.from_python(weight_windows.mesh, base_dir=base_dir) + + lib_ww = cls(weight_windows.id) + lib_ww.particle = weight_windows.particle_type + lib_ww.mesh = mesh + lib_ww.energy_bounds = weight_windows.energy_bounds + + lower = np.ascontiguousarray( + weight_windows.lower_ww_bounds.ravel(order='F'), dtype=np.float64) + upper = np.ascontiguousarray( + weight_windows.upper_ww_bounds.ravel(order='F'), dtype=np.float64) + lib_ww.bounds = lower, upper + + lib_ww.survival_ratio = weight_windows.survival_ratio + if weight_windows.max_lower_bound_ratio is not None: + lib_ww.max_lower_bound_ratio = weight_windows.max_lower_bound_ratio + lib_ww.max_split = weight_windows.max_split + lib_ww.weight_cutoff = weight_windows.weight_cutoff + return lib_ww + @classmethod def from_tally(cls, tally, particle=ParticleType.NEUTRON): """Create an instance of the WeightWindows class based on the specified tally. diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 63af2596efc..5d90e02a270 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -140,9 +140,7 @@ def __init__( "upper_bound_ratio must be present.") if upper_bound_ratio: - self.upper_ww_bounds = [ - lb * upper_bound_ratio for lb in self.lower_ww_bounds - ] + self.upper_ww_bounds = self.lower_ww_bounds * upper_bound_ratio if upper_ww_bounds is not None: self.upper_ww_bounds = upper_ww_bounds @@ -1074,18 +1072,20 @@ def export_to_hdf5(self, path: PathLike = 'weight_windows.h5', **init_kwargs): import openmc.lib cv.check_type('path', path, PathLike) - # Create a temporary model with the weight windows - model = openmc.Model() - sph = openmc.Sphere(boundary_type='vacuum') - cell = openmc.Cell(region=-sph) - model.geometry = openmc.Geometry([cell]) - model.settings.weight_windows = self - model.settings.particles = 100 - model.settings.batches = 1 - # Get absolute path before moving to temporary directory path = Path(path).resolve() + original_dir = Path.cwd() + + # Populate the C++ model directly and use its existing HDF5 writer. + with openmc.lib.TemporarySession(**init_kwargs): + lib_meshes = {} + for ww in self: + mesh = ww.mesh + if mesh.id not in lib_meshes: + lib_meshes[mesh.id] = openmc.lib.Mesh.from_python( + mesh, base_dir=original_dir) + + openmc.lib.WeightWindows.from_python( + ww, mesh=lib_meshes[mesh.id]) - # Load the model with openmc.lib and then export it to an HDF5 file - with openmc.lib.TemporarySession(model, **init_kwargs): openmc.lib.export_weight_windows(path) diff --git a/src/mesh.cpp b/src/mesh.cpp index 46698a6e390..4dac20c45b5 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2499,24 +2499,28 @@ extern "C" int openmc_extend_meshes( return 0; } -//! Adds a new unstructured mesh to OpenMC -extern "C" int openmc_add_unstructured_mesh( - const char filename[], const char library[], int* id) +//! Adds a new unstructured mesh to OpenMC with all supported properties +extern "C" int openmc_add_unstructured_mesh(const char filename[], + const char library[], double length_multiplier, const char options[], + int32_t id, int32_t* index) { std::string lib_name(library); std::string mesh_file(filename); + std::string mesh_options(options ? options : ""); bool valid_lib = false; #ifdef OPENMC_DAGMC_ENABLED if (lib_name == MOABMesh::mesh_lib_type) { - model::meshes.push_back(std::move(make_unique(mesh_file))); + model::meshes.push_back( + make_unique(mesh_file, length_multiplier, mesh_options)); valid_lib = true; } #endif #ifdef OPENMC_LIBMESH_ENABLED if (lib_name == LibMesh::mesh_lib_type) { - model::meshes.push_back(std::move(make_unique(mesh_file))); + model::meshes.push_back( + make_unique(mesh_file, length_multiplier, mesh_options)); valid_lib = true; } #endif @@ -2528,9 +2532,8 @@ extern "C" int openmc_add_unstructured_mesh( return OPENMC_E_INVALID_ARGUMENT; } - // auto-assign new ID - model::meshes.back()->set_id(-1); - *id = model::meshes.back()->id_; + model::meshes.back()->set_id(id); + *index = model::meshes.size() - 1; return 0; } @@ -2561,8 +2564,25 @@ extern "C" int openmc_mesh_set_id(int32_t index, int32_t id) { if (int err = check_mesh(index)) return err; - model::meshes[index]->id_ = id; - model::mesh_map[id] = index; + model::meshes[index]->set_id(id); + return 0; +} + +//! Return the name of a mesh +extern "C" int openmc_mesh_get_name(int32_t index, const char** name) +{ + if (int err = check_mesh(index)) + return err; + *name = model::meshes[index]->name().c_str(); + return 0; +} + +//! Set the name of a mesh +extern "C" int openmc_mesh_set_name(int32_t index, const char* name) +{ + if (int err = check_mesh(index)) + return err; + model::meshes[index]->set_name(name); return 0; } @@ -2882,6 +2902,59 @@ extern "C" int openmc_spherical_mesh_set_grid(int32_t index, index, grid_x, nx, grid_y, ny, grid_z, nz); } +template +int openmc_periodic_mesh_get_origin_impl(int32_t index, double origin[3]) +{ + if (int err = check_mesh(index)) + return err; + T* mesh = dynamic_cast(model::meshes[index].get()); + if (!mesh) { + set_errmsg("This mesh is not of the expected type."); + return OPENMC_E_INVALID_TYPE; + } + const auto& mesh_origin = mesh->origin(); + origin[0] = mesh_origin.x; + origin[1] = mesh_origin.y; + origin[2] = mesh_origin.z; + return 0; +} + +template +int openmc_periodic_mesh_set_origin_impl(int32_t index, const double origin[3]) +{ + if (int err = check_mesh(index)) + return err; + T* mesh = dynamic_cast(model::meshes[index].get()); + if (!mesh) { + set_errmsg("This mesh is not of the expected type."); + return OPENMC_E_INVALID_TYPE; + } + return mesh->set_origin({origin[0], origin[1], origin[2]}); +} + +extern "C" int openmc_cylindrical_mesh_get_origin( + int32_t index, double origin[3]) +{ + return openmc_periodic_mesh_get_origin_impl(index, origin); +} + +extern "C" int openmc_cylindrical_mesh_set_origin( + int32_t index, const double origin[3]) +{ + return openmc_periodic_mesh_set_origin_impl(index, origin); +} + +extern "C" int openmc_spherical_mesh_get_origin(int32_t index, double origin[3]) +{ + return openmc_periodic_mesh_get_origin_impl(index, origin); +} + +extern "C" int openmc_spherical_mesh_set_origin( + int32_t index, const double origin[3]) +{ + return openmc_periodic_mesh_set_origin_impl(index, origin); +} + #ifdef OPENMC_DAGMC_ENABLED const std::string MOABMesh::mesh_lib_type = "moab"; @@ -2896,11 +2969,13 @@ MOABMesh::MOABMesh(hid_t group) : UnstructuredMesh(group) initialize(); } -MOABMesh::MOABMesh(const std::string& filename, double length_multiplier) +MOABMesh::MOABMesh(const std::string& filename, double length_multiplier, + const std::string& options) : UnstructuredMesh() { n_dimension_ = 3; filename_ = filename; + options_ = options; set_length_multiplier(length_multiplier); initialize(); } @@ -3627,9 +3702,11 @@ LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) } // create the mesh from an input file -LibMesh::LibMesh(const std::string& filename, double length_multiplier) +LibMesh::LibMesh(const std::string& filename, double length_multiplier, + const std::string& options) { n_dimension_ = 3; + options_ = options; set_mesh_pointer_from_filename(filename); set_length_multiplier(length_multiplier); initialize(); diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index d3565eaaf63..081c49a7b8d 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -185,11 +185,25 @@ void WeightWindows::set_defaults() if (p_type == C_NONE) { fatal_error("Weight windows particle is not supported for transport."); } - energy_bounds_.push_back(data::energy_min[p_type]); - energy_bounds_.push_back(data::energy_max[p_type]); + double energy_min = data::energy_min[p_type]; + double energy_max = data::energy_max[p_type]; + if (energy_min >= energy_max) { + energy_min = 0.0; + energy_max = INFTY; + } + energy_bounds_.push_back(energy_min); + energy_bounds_.push_back(energy_max); } } +void WeightWindows::reset_energy_bounds() +{ + energy_bounds_.clear(); + set_defaults(); + if (mesh_idx_ != C_NONE) + allocate_ww_bounds(); +} + void WeightWindows::allocate_ww_bounds() { auto shape = bounds_size(); @@ -1156,7 +1170,11 @@ extern "C" int openmc_weight_windows_set_energy_bounds( if (int err = verify_ww_index(ww_idx)) return err; const auto& wws = variance_reduction::weight_windows.at(ww_idx); - wws->set_energy_bounds({e_bounds, e_bounds_size}); + if (e_bounds_size == 0) { + wws->reset_energy_bounds(); + } else { + wws->set_energy_bounds({e_bounds, e_bounds_size}); + } return 0; } diff --git a/tests/unit_tests/test_checkvalue.py b/tests/unit_tests/test_checkvalue.py new file mode 100644 index 00000000000..5e8081c71f0 --- /dev/null +++ b/tests/unit_tests/test_checkvalue.py @@ -0,0 +1,26 @@ +from numbers import Real + +import numpy as np +import pytest + +from openmc.checkvalue import check_iterable_type + + +@pytest.mark.parametrize('dtype', (np.float16, np.float32, np.float64)) +@pytest.mark.parametrize('expected_type', (Real, float)) +def test_check_iterable_type_float_array(dtype, expected_type): + values = np.ones((2, 3, 4), dtype=dtype) + check_iterable_type( + 'values', values, expected_type, min_depth=1, max_depth=3) + + +def test_check_iterable_type_float_array_depth(): + values = np.ones((2, 3)) + with pytest.raises(TypeError, match='maximum depth'): + check_iterable_type('values', values, Real, max_depth=1) + + +def test_check_iterable_type_nonfloat_array(): + values = np.ones(3, dtype=np.complex128) + with pytest.raises(TypeError, match='Items must be of type'): + check_iterable_type('values', values, Real) diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 8f1900eaa87..e59f52ceacd 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -130,6 +130,16 @@ def pincell_model_w_univ(): yield +def test_mesh_from_python_requires_init(): + mesh = openmc.RegularMesh(mesh_id=100) + mesh.dimension = (1, 1) + mesh.lower_left = (0.0, 0.0) + mesh.upper_right = (1.0, 1.0) + + with pytest.raises(RuntimeError, match='must be initialized'): + openmc.lib.Mesh.from_python(mesh) + + def test_cell_mapping(lib_init): cells = openmc.lib.cells assert isinstance(cells, Mapping) @@ -597,6 +607,8 @@ def test_find_material(lib_init): def test_regular_mesh(lib_init): mesh = openmc.lib.RegularMesh() + mesh.name = 'runtime mesh' + assert mesh.name == 'runtime mesh' mesh.dimension = (2, 3, 4) assert mesh.dimension == (2, 3, 4) with pytest.raises(exc.AllocationError): @@ -760,6 +772,11 @@ def test_cylindrical_mesh(lib_init): for k, _ in enumerate(np.diff(z_grid)): assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), 10)) + mesh.origin = (1.0, 2.0, 3.0) + np.testing.assert_allclose(mesh.origin, (1.0, 2.0, 3.0)) + assert np.all(mesh.dimension == (2, 2, 2)) + mesh.origin = (0.0, 0.0, 0.0) + np.testing.assert_allclose(mesh.volumes[::2], 10/360 * pi * 5**2 * 10) np.testing.assert_allclose(mesh.volumes[1::2], 10/360 * pi * (10**2 - 5**2) * 10) @@ -814,6 +831,11 @@ def test_spherical_mesh(lib_init): for k, _ in enumerate(np.diff(phi_grid)): assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), deg2rad(10))) + mesh.origin = (-1.0, -2.0, -3.0) + np.testing.assert_allclose(mesh.origin, (-1.0, -2.0, -3.0)) + assert np.all(mesh.dimension == (2, 2, 2)) + mesh.origin = (0.0, 0.0, 0.0) + dtheta = lambda d1, d2: np.cos(deg2rad(d1)) - np.cos(deg2rad(d2)) f = 1/3 * deg2rad(10.) np.testing.assert_allclose(mesh.volumes[::4], f * 5**3 * dtheta(0., 10.)) @@ -861,6 +883,87 @@ def test_spherical_mesh(lib_init): (0.5**3 - 0.25**3) / 3 * d_theta * d_phi * 2/pi) +def test_mesh_from_python(lib_init): + regular = openmc.RegularMesh(mesh_id=101, name='regular') + regular.dimension = (2, 3) + regular.lower_left = (0.0, 1.0) + regular.upper_right = (2.0, 4.0) + lib_regular = openmc.lib.Mesh.from_python(regular) + assert isinstance(lib_regular, openmc.lib.RegularMesh) + assert lib_regular.id == regular.id + assert lib_regular.name == regular.name + assert lib_regular.dimension == regular.dimension + np.testing.assert_allclose(lib_regular.lower_left, regular.lower_left) + np.testing.assert_allclose(lib_regular.upper_right, regular.upper_right) + + rectilinear = openmc.RectilinearMesh(mesh_id=102, name='rectilinear') + rectilinear.x_grid = (-2.0, 0.0, 3.0) + rectilinear.y_grid = (1.0, 4.0) + rectilinear.z_grid = (-5.0, 0.0, 5.0) + lib_rectilinear = openmc.lib.Mesh.from_python(rectilinear) + assert isinstance(lib_rectilinear, openmc.lib.RectilinearMesh) + assert lib_rectilinear.id == rectilinear.id + assert lib_rectilinear.name == rectilinear.name + assert tuple(lib_rectilinear.dimension) == rectilinear.dimension + np.testing.assert_allclose( + lib_rectilinear.lower_left, rectilinear.lower_left) + np.testing.assert_allclose( + lib_rectilinear.upper_right, rectilinear.upper_right) + + cylindrical = openmc.CylindricalMesh( + r_grid=(0.0, 1.0, 2.0), phi_grid=(0.0, np.pi), + z_grid=(-1.0, 1.0), origin=(1.0, 2.0, 3.0), mesh_id=103, + name='cylindrical') + lib_cylindrical = openmc.lib.Mesh.from_python(cylindrical) + assert isinstance(lib_cylindrical, openmc.lib.CylindricalMesh) + assert lib_cylindrical.id == cylindrical.id + assert lib_cylindrical.name == cylindrical.name + assert tuple(lib_cylindrical.dimension) == cylindrical.dimension + np.testing.assert_allclose(lib_cylindrical.origin, cylindrical.origin) + + spherical = openmc.SphericalMesh( + r_grid=(0.0, 1.0), theta_grid=(0.0, np.pi), + phi_grid=(0.0, 2.0 * np.pi), origin=(-1.0, -2.0, -3.0), + mesh_id=104, name='spherical') + lib_spherical = openmc.lib.Mesh.from_python(spherical) + assert isinstance(lib_spherical, openmc.lib.SphericalMesh) + assert lib_spherical.id == spherical.id + assert lib_spherical.name == spherical.name + assert tuple(lib_spherical.dimension) == spherical.dimension + np.testing.assert_allclose(lib_spherical.origin, spherical.origin) + + with pytest.raises(TypeError, match='cannot convert'): + openmc.lib.RegularMesh.from_python(rectilinear) + + +def test_weight_windows_from_python(lib_init): + mesh = openmc.RegularMesh(mesh_id=105, name='weight windows mesh') + mesh.dimension = (2, 2) + mesh.lower_left = (-1.0, -1.0) + mesh.upper_right = (1.0, 1.0) + lower = np.arange(1.0, 9.0) + ww = openmc.WeightWindows( + mesh, lower, upper_bound_ratio=5.0, + energy_bounds=(0.0, 1.0, 10.0), particle_type='photon', + survival_ratio=4.0, max_lower_bound_ratio=2.0, max_split=12, + weight_cutoff=1.0e-20, id=201) + + lib_ww = openmc.lib.WeightWindows.from_python(ww) + + assert lib_ww.id == ww.id + assert lib_ww.mesh.id == mesh.id + assert lib_ww.particle == openmc.ParticleType.PHOTON + np.testing.assert_allclose(lib_ww.energy_bounds, ww.energy_bounds) + np.testing.assert_allclose( + lib_ww.bounds[0], ww.lower_ww_bounds.ravel(order='F')) + np.testing.assert_allclose( + lib_ww.bounds[1], ww.upper_ww_bounds.ravel(order='F')) + assert lib_ww.survival_ratio == ww.survival_ratio + assert lib_ww.max_lower_bound_ratio == ww.max_lower_bound_ratio + assert lib_ww.max_split == ww.max_split + assert lib_ww.weight_cutoff == ww.weight_cutoff + + def test_restart(lib_init, mpi_intracomm): # Finalize and re-init to make internal state consistent with XML. openmc.lib.hard_reset() diff --git a/tests/unit_tests/weightwindows/test_ww_list.py b/tests/unit_tests/weightwindows/test_ww_list.py index d148f382a53..c6c82ebd17d 100644 --- a/tests/unit_tests/weightwindows/test_ww_list.py +++ b/tests/unit_tests/weightwindows/test_ww_list.py @@ -1,11 +1,21 @@ +import h5py +import numpy as np +import pytest + import openmc +import openmc.lib -def test_ww_roundtrip(request, run_in_tmpdir): +def test_ww_roundtrip(request, run_in_tmpdir, monkeypatch): # Load weight windows from a wwinp file wwinp_file = request.path.with_name('wwinp_n') wws = openmc.WeightWindowsList.from_wwinp(wwinp_file) + def fail_xml_export(*args, **kwargs): + pytest.fail('Weight windows should not be serialized to XML') + + monkeypatch.setattr(openmc.WeightWindows, 'to_xml_element', fail_xml_export) + # Roundtrip them, writing to HDF5 and reading back in wws.export_to_hdf5('ww.h5') wws_new = openmc.WeightWindowsList.from_hdf5('ww.h5') @@ -21,3 +31,69 @@ def test_ww_roundtrip(request, run_in_tmpdir): assert ww.max_split == ww_new.max_split assert ww.weight_cutoff == ww_new.weight_cutoff assert ww.mesh.id == ww_new.mesh.id + + +def test_export_hdf5_format(request, run_in_tmpdir): + # openmc_weight_windows_import expects this on-disk layout. + wws = openmc.WeightWindowsList.from_wwinp(request.path.with_name('wwinp_n')) + wws.export_to_hdf5('ww.h5') + + with h5py.File('ww.h5') as f: + assert f.attrs['filetype'] == b'weight_windows' + assert list(f.attrs['version']) == [1, 0] + wws_group = f['weight_windows'] + assert int(wws_group.attrs['n_weight_windows']) == len(wws) + for ww in wws: + group = wws_group[f'weight_windows_{ww.id}'] + assert group['lower_ww_bounds'].ndim == 2 + assert group['lower_ww_bounds'].shape[0] == ww.num_energy_bins + assert 'max_lower_bound_ratio' in group + + +def test_export_periodic_mesh_metadata(run_in_tmpdir): + cylindrical = openmc.CylindricalMesh( + r_grid=[0.0, 1.0, 2.0], phi_grid=[0.0, np.pi], + z_grid=[-1.0, 1.0], origin=(1.0, 2.0, 3.0), mesh_id=10, + name='cylindrical') + spherical = openmc.SphericalMesh( + r_grid=[0.0, 2.0], theta_grid=[0.0, np.pi], + phi_grid=[0.0, 2.0 * np.pi], origin=(-1.0, -2.0, -3.0), mesh_id=11, + name='spherical') + windows = openmc.WeightWindowsList([ + openmc.WeightWindows(cylindrical, [1.0, 2.0], + upper_bound_ratio=5.0, id=10), + openmc.WeightWindows(spherical, [3.0], + upper_bound_ratio=5.0, id=11), + ]) + + windows.export_to_hdf5('ww.h5') + roundtrip = openmc.WeightWindowsList.from_hdf5('ww.h5') + meshes = {ww.mesh.id: ww.mesh for ww in roundtrip} + + assert meshes[10].name == cylindrical.name + assert meshes[11].name == spherical.name + assert np.allclose(meshes[10].origin, cylindrical.origin) + assert np.allclose(meshes[11].origin, spherical.origin) + for ww in roundtrip: + assert ww.energy_bounds[0] == 0.0 + assert ww.energy_bounds[-1] == np.finfo(float).max + + +@pytest.mark.parametrize('library', ('libmesh', 'moab')) +def test_export_hdf5_unstructured_mesh(request, run_in_tmpdir, library): + if library == 'libmesh' and not openmc.lib.feature_enabled('libmesh'): + pytest.skip('LibMesh not enabled in this build.') + if library == 'moab' and not openmc.lib.feature_enabled('dagmc'): + pytest.skip('DAGMC (and MOAB) not enabled in this build.') + + mesh = openmc.UnstructuredMesh( + request.path.with_name('test_mesh_tets.exo'), library, mesh_id=20, + name='unstructured', length_multiplier=2.0) + ww = openmc.WeightWindows(mesh, np.ones(12_000), upper_bound_ratio=5.0) + openmc.WeightWindowsList([ww]).export_to_hdf5('ww.h5') + + with h5py.File('ww.h5') as f: + mesh_group = f['meshes'][f'mesh {mesh.id}'] + assert mesh_group['type'][()] == b'unstructured' + assert mesh_group['name'][()] == b'unstructured' + assert mesh_group['length_multiplier'][()] == 2.0