Skip to content

Commit

Permalink
Compute material volumes in mesh elements based on raytracing (#3129)
Browse files Browse the repository at this point in the history
Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com>
Co-authored-by: Patrick Shriwise <pshriwise@gmail.com>
  • Loading branch information
3 people authored Feb 26, 2025
1 parent 865c80a commit e060534
Show file tree
Hide file tree
Showing 10 changed files with 814 additions and 238 deletions.
1 change: 1 addition & 0 deletions docs/source/pythonapi/base.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Constructing Tallies
openmc.CylindricalMesh
openmc.SphericalMesh
openmc.UnstructuredMesh
openmc.MeshMaterialVolumes
openmc.Trigger
openmc.TallyDerivative
openmc.Tally
Expand Down
4 changes: 4 additions & 0 deletions include/openmc/bounding_box.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <algorithm> // for min, max

#include "openmc/constants.h"
#include "openmc/position.h"

namespace openmc {

Expand Down Expand Up @@ -54,6 +55,9 @@ struct BoundingBox {
zmax = std::max(zmax, other.zmax);
return *this;
}

inline Position min() const { return {xmin, ymin, zmin}; }
inline Position max() const { return {xmax, ymax, zmax}; }
};

} // namespace openmc
Expand Down
4 changes: 2 additions & 2 deletions include/openmc/capi.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ int openmc_mesh_get_id(int32_t index, int32_t* id);
int openmc_mesh_set_id(int32_t index, int32_t id);
int openmc_mesh_get_n_elements(int32_t index, size_t* n);
int openmc_mesh_get_volumes(int32_t index, double* volumes);
int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin,
int result_size, void* result, int* hits, uint64_t* seed);
int openmc_mesh_material_volumes(int32_t index, int nx, int ny, int nz,
int max_mats, int32_t* materials, double* volumes);
int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh);
int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh);
int openmc_new_filter(const char* type, int32_t* index);
Expand Down
95 changes: 72 additions & 23 deletions include/openmc/mesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,70 @@ extern const libMesh::Parallel::Communicator* libmesh_comm;
} // namespace settings
#endif

class Mesh {
//==============================================================================
//! Helper class for keeping track of volume for each material in a mesh element
//
//! This class is used in Mesh::material_volumes to manage for each mesh element
//! a list of (material, volume) pairs. The openmc.lib.Mesh class allocates two
//! 2D arrays, one for materials and one for volumes. Because we don't know a
//! priori how many materials there are in each element but at the same time we
//! can't dynamically size an array at runtime for performance reasons, we
//! assume a maximum number of materials per element. For each element, the set
//! of material indices are stored in a hash table with twice as many slots as
//! the assumed maximum number of materials per element. Collision resolution is
//! handled by open addressing with linear probing.
//==============================================================================

namespace detail {

class MaterialVolumes {
public:
// Types, aliases
struct MaterialVolume {
int32_t material; //!< material index
double volume; //!< volume in [cm^3]
};
MaterialVolumes(int32_t* mats, double* vols, int table_size)
: materials_(mats), volumes_(vols), table_size_(table_size)
{}

//! Add volume for a given material in a mesh element
//
//! \param[in] index_elem Index of the mesh element
//! \param[in] index_material Index of the material within the model
//! \param[in] volume Volume to add
void add_volume(int index_elem, int index_material, double volume);
void add_volume_unsafe(int index_elem, int index_material, double volume);

// Accessors
int32_t& materials(int i, int j) { return materials_[i * table_size_ + j]; }
const int32_t& materials(int i, int j) const
{
return materials_[i * table_size_ + j];
}

double& volumes(int i, int j) { return volumes_[i * table_size_ + j]; }
const double& volumes(int i, int j) const
{
return volumes_[i * table_size_ + j];
}

bool table_full() const { return table_full_; }

private:
int32_t* materials_; //!< material index (bins, table_size)
double* volumes_; //!< volume in [cm^3] (bins, table_size)
int table_size_; //!< Size of hash table for each mesh element
bool table_full_ {false}; //!< Whether the hash table is full

// Value used to indicate an empty slot in the hash table. We use -2 because
// the value -1 is used to indicate a void material.
static constexpr int EMPTY {-2};
};

} // namespace detail

//==============================================================================
//! Base mesh class
//==============================================================================

class Mesh {
public:
// Constructors and destructor
Mesh() = default;
Mesh(pugi::xml_node node);
Expand Down Expand Up @@ -172,24 +228,17 @@ class Mesh {

virtual std::string get_mesh_type() const = 0;

//! Determine volume of materials within a single mesh elemenet
//
//! \param[in] n_sample Number of samples within each element
//! \param[in] bin Index of mesh element
//! \param[out] Array of (material index, volume) for desired element
//! \param[inout] seed Pseudorandom number seed
//! \return Number of materials within element
int material_volumes(
int n_sample, int bin, span<MaterialVolume> volumes, uint64_t* seed) const;

//! Determine volume of materials within a single mesh elemenet
//! Determine volume of materials within each mesh element
//
//! \param[in] n_sample Number of samples within each element
//! \param[in] bin Index of mesh element
//! \param[inout] seed Pseudorandom number seed
//! \return Vector of (material index, volume) for desired element
vector<MaterialVolume> material_volumes(
int n_sample, int bin, uint64_t* seed) const;
//! \param[in] nx Number of samples in x direction
//! \param[in] ny Number of samples in y direction
//! \param[in] nz Number of samples in z direction
//! \param[in] max_materials Maximum number of materials in a single mesh
//! element
//! \param[inout] materials Array storing material indices
//! \param[inout] volumes Array storing volumes
void material_volumes(int nx, int ny, int nz, int max_materials,
int32_t* materials, double* volumes) const;

//! Determine bounding box of mesh
//
Expand Down
135 changes: 76 additions & 59 deletions openmc/lib/mesh.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections.abc import Mapping, Sequence
from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure,
create_string_buffer, c_uint64, c_size_t)
from random import getrandbits
from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER,
create_string_buffer, c_size_t)
from math import sqrt
import sys
from weakref import WeakValueDictionary

Expand All @@ -10,24 +10,20 @@

from ..exceptions import AllocationError, InvalidIDError
from . import _dll
from .core import _FortranObjectWithID
from .core import _FortranObjectWithID, quiet_dll
from .error import _error_handler
from .material import Material
from .plot import _Position
from ..bounding_box import BoundingBox
from ..mesh import MeshMaterialVolumes

__all__ = [
'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh',
'SphericalMesh', 'UnstructuredMesh', 'meshes'
'SphericalMesh', 'UnstructuredMesh', 'meshes', 'MeshMaterialVolumes'
]


class _MaterialVolume(Structure):
_fields_ = [
("material", c_int32),
("volume", c_double)
]

arr_2d_int32 = np.ctypeslib.ndpointer(dtype=np.int32, ndim=2, flags='CONTIGUOUS')
arr_2d_double = np.ctypeslib.ndpointer(dtype=np.double, ndim=2, flags='CONTIGUOUS')

# Mesh functions
_dll.openmc_extend_meshes.argtypes = [c_int32, c_char_p, POINTER(c_int32),
Expand All @@ -51,8 +47,7 @@ class _MaterialVolume(Structure):
_dll.openmc_mesh_bounding_box.restype = c_int
_dll.openmc_mesh_bounding_box.errcheck = _error_handler
_dll.openmc_mesh_material_volumes.argtypes = [
c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume),
POINTER(c_int), POINTER(c_uint64)]
c_int32, c_int, c_int, c_int, c_int, arr_2d_int32, arr_2d_double]
_dll.openmc_mesh_material_volumes.restype = c_int
_dll.openmc_mesh_material_volumes.errcheck = _error_handler
_dll.openmc_mesh_get_plot_bins.argtypes = [
Expand Down Expand Up @@ -190,58 +185,81 @@ def bounding_box(self) -> BoundingBox:

def material_volumes(
self,
n_samples: int = 10_000,
prn_seed: int | None = None
) -> list[list[tuple[Material, float]]]:
"""Determine volume of materials in each mesh element
n_samples: int | tuple[int, int, int] = 10_000,
max_materials: int = 4,
output: bool = True,
) -> MeshMaterialVolumes:
"""Determine volume of materials in each mesh element.
This method works by raytracing repeatedly through the mesh to count the
estimated volume of each material in all mesh elements. Three sets of
rays are used: one set parallel to the x-axis, one parallel to the
y-axis, and one parallel to the z-axis.
.. versionadded:: 0.15.0
.. versionchanged:: 0.15.1
Material volumes are now determined by raytracing rather than by
point sampling.
Parameters
----------
n_samples : int
Number of samples in each mesh element
prn_seed : int
Pseudorandom number generator (PRNG) seed; if None, one will be
generated randomly.
n_samples : int or 3-tuple of int
Total number of rays to sample. The number of rays in each direction
is determined by the aspect ratio of the mesh bounding box. When
specified as a 3-tuple, it is interpreted as the number of rays in
the x, y, and z dimensions.
max_materials : int, optional
Estimated maximum number of materials in any given mesh element.
output : bool, optional
Whether or not to show output.
Returns
-------
List of tuple of (material, volume) for each mesh element. Void volume
is represented by having a value of None in the first element of a
tuple.
MeshMaterialVolumes
Dictionary-like object that maps material IDs to an array of volumes
equal in size to the number of mesh elements.
"""
if n_samples <= 0:
raise ValueError("Number of samples must be positive")
if prn_seed is None:
prn_seed = getrandbits(63)
prn_seed = c_uint64(prn_seed)

# Preallocate space for MaterialVolume results
size = 16
result = (_MaterialVolume * size)()

hits = c_int() # Number of materials hit in a given element
volumes = []
for i_element in range(self.n_elements):
while True:
try:
if isinstance(n_samples, int):
# Determine number of rays in each direction based on aspect ratios
# and using the relation (nx*ny + ny*nz + nx*nz) = n_samples
width_x, width_y, width_z = self.bounding_box.width
ax = width_x / width_z
ay = width_y / width_z
f = sqrt(n_samples/(ax*ay + ax + ay))
nx = round(f * ax)
ny = round(f * ay)
nz = round(f)
else:
nx, ny, nz = n_samples

# Value indicating an empty slot in the hash table (matches C++)
EMPTY_SLOT = -2

# Preallocate arrays for material indices and volumes
n = self.n_elements
slot_factor = 2
table_size = slot_factor*max_materials
materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32)
volumes = np.zeros((n, table_size), dtype=np.float64)

# Run material volume calculation
while True:
try:
with quiet_dll(output):
_dll.openmc_mesh_material_volumes(
self._index, n_samples, i_element, size, result, hits, prn_seed)
except AllocationError:
# Increase size of result array and try again
size *= 2
result = (_MaterialVolume * size)()
else:
# If no error, break out of loop
break
self._index, nx, ny, nz, table_size, materials, volumes)
except AllocationError:
# Increase size of result array and try again
table_size *= 2
materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32)
volumes = np.zeros((n, table_size), dtype=np.float64)
else:
# If no error, break out of loop
break

volumes.append([
(Material(index=r.material), r.volume)
for r in result[:hits.value]
])
return volumes
return MeshMaterialVolumes(materials, volumes)

def get_plot_bins(
self,
Expand Down Expand Up @@ -306,7 +324,7 @@ class RegularMesh(Mesh):
The lower-left corner of the structured mesh. If only two coordinate are
given, it is assumed that the mesh is an x-y mesh.
upper_right : numpy.ndarray
The upper-right corner of the structrued mesh. If only two coordinate
The upper-right corner of the structured mesh. If only two coordinate
are given, it is assumed that the mesh is an x-y mesh.
width : numpy.ndarray
The width of mesh cells in each direction.
Expand Down Expand Up @@ -395,7 +413,7 @@ class RectilinearMesh(Mesh):
lower_left : numpy.ndarray
The lower-left corner of the structured mesh.
upper_right : numpy.ndarray
The upper-right corner of the structrued mesh.
The upper-right corner of the structured mesh.
width : numpy.ndarray
The width of mesh cells in each direction.
n_elements : int
Expand Down Expand Up @@ -500,7 +518,7 @@ class CylindricalMesh(Mesh):
lower_left : numpy.ndarray
The lower-left corner of the structured mesh.
upper_right : numpy.ndarray
The upper-right corner of the structrued mesh.
The upper-right corner of the structured mesh.
width : numpy.ndarray
The width of mesh cells in each direction.
n_elements : int
Expand Down Expand Up @@ -605,7 +623,7 @@ class SphericalMesh(Mesh):
lower_left : numpy.ndarray
The lower-left corner of the structured mesh.
upper_right : numpy.ndarray
The upper-right corner of the structrued mesh.
The upper-right corner of the structured mesh.
width : numpy.ndarray
The width of mesh cells in each direction.
n_elements : int
Expand Down Expand Up @@ -719,7 +737,6 @@ def __getitem__(self, key):
raise KeyError(str(e))
return _get_mesh(index.value)


def __iter__(self):
for i in range(len(self)):
yield _get_mesh(i).id
Expand Down
Loading

0 comments on commit e060534

Please sign in to comment.