From 4f6a25e00aea8fe66e55bd182932193bf83061de Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 8 Jun 2026 16:08:08 -0500 Subject: [PATCH 1/8] Introduce new C API function for slice plots (#3806) --- include/openmc/capi.h | 5 + include/openmc/plot.h | 112 +++++---- openmc/lib/plot.py | 376 ++++++++++++++-------------- openmc/model/model.py | 155 ++++++++++-- src/plot.cpp | 202 ++++++++++++++- tests/unit_tests/test_lib.py | 36 ++- tests/unit_tests/test_slice_data.py | 168 +++++++++++++ 7 files changed, 763 insertions(+), 291 deletions(-) create mode 100644 tests/unit_tests/test_slice_data.py diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 911654d318f..6b78145a4c2 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -123,8 +123,13 @@ int openmc_new_filter(const char* type, int32_t* index); int openmc_next_batch(int* status); int openmc_nuclide_name(int index, const char** name); int openmc_plot_geometry(); +// Deprecated; use openmc_slice_data. int openmc_id_map(const void* slice, int32_t* data_out); +// Deprecated; use openmc_slice_data. int openmc_property_map(const void* slice, double* data_out); +int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool show_overlaps, int level, + int32_t filter_index, int32_t* geom_data, double* property_data); int openmc_get_plot_index(int32_t id, int32_t* index); int openmc_plot_get_id(int32_t index, int32_t* id); int openmc_plot_set_id(int32_t index, int32_t id); diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 6c00fc2f6ba..f97d313847f 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -18,6 +18,8 @@ #include "openmc/position.h" #include "openmc/random_lcg.h" #include "openmc/ray.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/filter_match.h" #include "openmc/xml_interface.h" namespace openmc { @@ -148,10 +150,11 @@ class PlottableInterface { struct IdData { // Constructor - IdData(size_t h_res, size_t v_res); + IdData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); void set_overlap(size_t y, size_t x); // Members @@ -160,16 +163,34 @@ struct IdData { struct PropertyData { // Constructor - PropertyData(size_t h_res, size_t v_res); + PropertyData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); void set_overlap(size_t y, size_t x); // Members tensor::Tensor data_; //!< 2D array of temperature & density data }; +struct RasterData { + // Constructor + RasterData(size_t h_res, size_t v_res, bool include_filter = false); + + // Methods + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x); + + // Members + tensor::Tensor + id_data_; //!< [v_res, h_res, 3 or 4]: cell, instance, mat, [filter_bin] + tensor::Tensor + property_data_; //!< [v_res, h_res, 2]: temperature, density + bool include_filter_; //!< Whether filter bin index is included +}; + //=============================================================================== // Plot class //=============================================================================== @@ -177,7 +198,7 @@ struct PropertyData { class SlicePlotBase { public: template - T get_map() const; + T get_map(int32_t filter_index = -1) const; enum class PlotBasis { xy = 1, xz = 2, yz = 3 }; @@ -188,70 +209,65 @@ class SlicePlotBase { // Members public: - Position origin_; //!< Plot origin in geometry - Position width_; //!< Plot width in geometry - PlotBasis basis_; //!< Plot basis (XY/XZ/YZ) - array pixels_; //!< Plot size in pixels - bool slice_color_overlaps_; //!< Show overlapping cells? - int slice_level_ {-1}; //!< Plot universe level + Position origin_; //!< Plot origin in geometry + Direction u_span_; //!< Full-width span vector in geometry + Direction v_span_; //!< Full-height span vector in geometry + array pixels_; //!< Plot size in pixels + bool show_overlaps_; //!< Show overlapping cells? + int slice_level_ {-1}; //!< Plot universe level private: }; template -T SlicePlotBase::get_map() const +T SlicePlotBase::get_map(int32_t filter_index) const { size_t width = pixels_[0]; size_t height = pixels_[1]; - // get pixel size - double in_pixel = (width_[0]) / static_cast(width); - double out_pixel = (width_[1]) / static_cast(height); + // Determine if filter is being used + bool include_filter = (filter_index >= 0); + Filter* filter = nullptr; + if (include_filter) { + filter = model::tally_filters[filter_index].get(); + } // size data array - T data(width, height); - - // setup basis indices and initial position centered on pixel - int in_i, out_i; - Position xyz = origin_; - switch (basis_) { - case PlotBasis::xy: - in_i = 0; - out_i = 1; - break; - case PlotBasis::xz: - in_i = 0; - out_i = 2; - break; - case PlotBasis::yz: - in_i = 1; - out_i = 2; - break; - default: - UNREACHABLE(); - } + T data(width, height, include_filter); - // set initial position - xyz[in_i] = origin_[in_i] - width_[0] / 2. + in_pixel / 2.; - xyz[out_i] = origin_[out_i] + width_[1] / 2. - out_pixel / 2.; + // compute pixel steps and top-left pixel center + Direction u_step = u_span_ / static_cast(width); + Direction v_step = v_span_ / static_cast(height); + + Position start = + origin_ - 0.5 * u_span_ + 0.5 * v_span_ + 0.5 * u_step - 0.5 * v_step; + + // Validate that span vectors define a valid plane + Position cross = u_span_.cross(v_span_); + if (cross.norm() == 0.0) { + fatal_error("Slice span vectors are invalid (zero area)."); + } - // arbitrary direction - Direction dir = {1. / std::sqrt(2.), 1. / std::sqrt(2.), 0.0}; + // Use an arbitrary direction that is not aligned with any coordinate axis. + // The direction has no physical meaning for plotting but is used by + // Surface::sense() to break ties when a pixel is coincident with a surface. + Direction dir = {1.0 / std::sqrt(2.0), 1.0 / std::sqrt(2.0), 0.0}; #pragma omp parallel { - GeometryState p; - p.r() = xyz; + Particle p; + p.r() = start; p.u() = dir; p.coord(0).universe() = model::root_universe; int level = slice_level_; int j {}; + FilterMatch match; #pragma omp for for (int y = 0; y < height; y++) { - p.r()[out_i] = xyz[out_i] - out_pixel * y; + Position row = start - v_step * static_cast(y); for (int x = 0; x < width; x++) { - p.r()[in_i] = xyz[in_i] + in_pixel * x; + p.r() = row + u_step * static_cast(x); p.n_coord() = 1; // local variables bool found_cell = exhaustive_find_cell(p); @@ -260,9 +276,9 @@ T SlicePlotBase::get_map() const j = level; } if (found_cell) { - data.set_value(y, x, p, j); + data.set_value(y, x, p, j, filter, &match); } - if (slice_color_overlaps_ && check_cell_overlap(p, false)) { + if (show_overlaps_ && check_cell_overlap(p, false)) { data.set_overlap(y, x); } } // inner for @@ -297,6 +313,8 @@ class Plot : public PlottableInterface, public SlicePlotBase { void print_info() const override; PlotType type_; //!< Plot type (Slice/Voxel) + Position width_; //!< Axis-aligned width from plot.xml + PlotBasis basis_; //!< Basis from plot.xml for slice plots int meshlines_width_; //!< Width of lines added to the plot int index_meshlines_mesh_ {-1}; //!< Index of the mesh to draw on the plot RGBColor meshlines_color_; //!< Color of meshlines on the plot diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 90af80d5b76..44d6ac273d3 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -9,6 +9,7 @@ from .error import _error_handler import numpy as np +import warnings class _Position(Structure): @@ -51,218 +52,209 @@ def __repr__(self): return f"({self.x}, {self.y}, {self.z})" -class _PlotBase(Structure): - """A structure defining a 2-D geometry slice with underlying c-types +def _extract_slice_data_args(plot): + """Convert a legacy plot-like object into slice_data keyword arguments.""" + try: + kwargs = { + 'origin': tuple(plot.origin), + 'width': (plot.width, plot.height), + 'basis': plot.basis, + 'pixels': (plot.h_res, plot.v_res), + 'show_overlaps': getattr(plot, 'color_overlaps', False), + 'level': getattr(plot, 'level', -1), + } + except AttributeError as exc: + raise TypeError( + "plot must be a legacy plot-like object with origin, width, " + "height, basis, h_res, and v_res attributes." + ) from exc + return kwargs + + +_dll.openmc_slice_data.argtypes = [ + POINTER(c_double * 3), # origin + POINTER(c_double * 3), # u_span + POINTER(c_double * 3), # v_span + POINTER(c_size_t * 2), # pixels + c_bool, # show_overlaps + c_int, # level + c_int32, # filter_index + POINTER(c_int32), # geom_data + POINTER(c_double), # property_data (can be None) +] +_dll.openmc_slice_data.restype = c_int +_dll.openmc_slice_data.errcheck = _error_handler + + +def slice_data(origin, width=None, basis='xy', u_span=None, v_span=None, + pixels=None, show_overlaps=False, level=-1, filter=None, + include_properties=True): + """Generate a 2D raster of geometry and property data for plotting. - C-Type Attributes - ----------------- - origin_ : openmc.lib.plot._Position - A position defining the origin of the plot. - width_ : openmc.lib.plot._Position - The width of the plot along the x, y, and z axes, respectively - basis_ : c_int - The axes basis of the plot view. - pixels_ : c_size_t[3] - The resolution of the plot in the horizontal and vertical dimensions - color_overlaps_ : c_bool - Whether to assign unique IDs (-3) to overlapping regions. - level_ : c_int - The universe level for the plot view - - Attributes + Parameters ---------- - origin : tuple or list of ndarray - Origin (center) of the plot - width : float - The horizontal dimension of the plot in geometry units (cm) - height : float - The vertical dimension of the plot in geometry units (cm) - basis : string - One of {'xy', 'xz', 'yz'} indicating the horizontal and vertical - axes of the plot. - h_res : int - The horizontal resolution of the plot in pixels - v_res : int - The vertical resolution of the plot in pixels - level : int - The universe level for the plot (default: -1 -> all universes shown) - """ - _fields_ = [('origin_', _Position), - ('width_', _Position), - ('basis_', c_int), - ('pixels_', 3*c_size_t), - ('color_overlaps_', c_bool), - ('level_', c_int)] - - def __init__(self): - self.level_ = -1 - self.basis_ = 1 - self.color_overlaps_ = False - - @property - def origin(self): - return self.origin_ - - @origin.setter - def origin(self, origin): - self.origin_.x = origin[0] - self.origin_.y = origin[1] - self.origin_.z = origin[2] - - @property - def width(self): - return self.width_.x - - @width.setter - def width(self, width): - self.width_.x = width - - @property - def height(self): - return self.width_.y - - @height.setter - def height(self, height): - self.width_.y = height + origin : sequence of float + Center position of the plot [x, y, z] + width : sequence of float + Width of the plot [horizontal, vertical]. Mutually exclusive with + u_span/v_span. + basis : {'xy', 'xz', 'yz'} or int + Plot basis. Ignored if u_span/v_span are provided. + u_span : sequence of float, optional + Full-width span vector for the horizontal axis (3 values). Mutually + exclusive with width. + v_span : sequence of float, optional + Full-height span vector for the vertical axis (3 values). Mutually + exclusive with width. + pixels : sequence of int + Number of pixels [horizontal, vertical] + show_overlaps : bool, optional + Whether to detect overlapping cells + level : int, optional + Universe level (-1 for deepest) + filter : openmc.lib.Filter, optional + Filter for bin index lookup + include_properties : bool, optional + Whether to compute temperature/density - @property - def basis(self): - if self.basis_ == 1: - return 'xy' - elif self.basis_ == 2: - return 'xz' - elif self.basis_ == 3: - return 'yz' - - raise ValueError(f"Plot basis {self.basis_} is invalid") - - @basis.setter - def basis(self, basis): + Returns + ------- + geom_data : numpy.ndarray + Array of shape (v_res, h_res, 3) or (v_res, h_res, 4) with int32 dtype. + Contains [cell_id, cell_instance, material_id] when no filter is provided, + or [cell_id, cell_instance, material_id, filter_bin] when a filter is provided. + property_data : numpy.ndarray or None + Array of shape (v_res, h_res, 2) with float64 dtype containing + [temperature, density], or None if include_properties=False + """ + if pixels is None: + raise ValueError("pixels must be specified.") + if len(pixels) != 2: + raise ValueError("pixels must be a length-2 sequence.") + + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") + + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + u_span = np.asarray(u_span, dtype=float) + v_span = np.asarray(v_span, dtype=float) + if u_span.shape != (3,) or v_span.shape != (3,): + raise ValueError("u_span and v_span must be length-3 sequences.") + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + if u_norm == 0.0 or v_norm == 0.0: + raise ValueError("u_span and v_span must be non-zero vectors.") + dot = float(np.dot(u_span, v_span)) + ortho_tol = 1.0e-10 * u_norm * v_norm + if abs(dot) > ortho_tol: + raise ValueError("u_span and v_span must be orthogonal.") + else: + if width is None: + raise ValueError("width must be provided when u_span/v_span are not set.") + if len(width) != 2: + raise ValueError("width must be a length-2 sequence.") + basis_map = {'xy': 1, 'xz': 2, 'yz': 3} if isinstance(basis, str): - valid_bases = ('xy', 'xz', 'yz') basis = basis.lower() - if basis not in valid_bases: + if basis not in basis_map: raise ValueError(f"{basis} is not a valid plot basis.") - - if basis == 'xy': - self.basis_ = 1 - elif basis == 'xz': - self.basis_ = 2 - elif basis == 'yz': - self.basis_ = 3 - return - - if isinstance(basis, int): - valid_bases = (1, 2, 3) - if basis not in valid_bases: + basis = basis_map[basis] + elif isinstance(basis, int): + if basis not in basis_map.values(): raise ValueError(f"{basis} is not a valid plot basis.") - self.basis_ = basis - return - - raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") - - @property - def h_res(self): - return self.pixels_[0] - - @h_res.setter - def h_res(self, h_res): - self.pixels_[0] = h_res - - @property - def v_res(self): - return self.pixels_[1] - - @v_res.setter - def v_res(self, v_res): - self.pixels_[1] = v_res - - @property - def level(self): - return int(self.level_) - - @level.setter - def level(self, level): - self.level_ = level - - @property - def color_overlaps(self): - return self.color_overlaps_ - - @color_overlaps.setter - def color_overlaps(self, color_overlaps): - self.color_overlaps_ = color_overlaps - - def __repr__(self): - out_str = ["-----", - "Plot:", - "-----", - f"Origin: {self.origin}", - f"Width: {self.width}", - f"Height: {self.height}", - f"Basis: {self.basis}", - f"HRes: {self.h_res}", - f"VRes: {self.v_res}", - f"Color Overlaps: {self.color_overlaps}", - f"Level: {self.level}"] - return '\n'.join(out_str) - - -_dll.openmc_id_map.argtypes = [POINTER(_PlotBase), POINTER(c_int32)] -_dll.openmc_id_map.restype = c_int -_dll.openmc_id_map.errcheck = _error_handler + else: + raise ValueError(f"{basis} is not a valid plot basis.") + + if basis == 1: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, width[1], 0.0], dtype=float) + elif basis == 2: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) + else: + u_span = np.array([0.0, width[0], 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) + + origin = np.asarray(origin, dtype=float) + if origin.shape != (3,): + raise ValueError("origin must be a length-3 sequence.") + + # Prepare ctypes arrays + origin_arr = (c_double * 3)(*origin) + u_span_arr = (c_double * 3)(*u_span) + v_span_arr = (c_double * 3)(*v_span) + pixels_arr = (c_size_t * 2)(*pixels) + + # Get internal filter index from filter ID if filter is provided + if filter is not None: + filter_index = c_int32() + _dll.openmc_get_filter_index(filter.id, filter_index) + filter_index = filter_index.value + else: + filter_index = -1 + + # Allocate output arrays with dynamic size based on filter + n_geom_fields = 4 if filter is not None else 3 + geom_data = np.zeros((pixels[1], pixels[0], n_geom_fields), dtype=np.int32) + if include_properties: + property_data = np.zeros((pixels[1], pixels[0], 2), dtype=np.float64) + prop_ptr = property_data.ctypes.data_as(POINTER(c_double)) + else: + property_data = None + prop_ptr = None + + _dll.openmc_slice_data( + origin_arr, + u_span_arr, + v_span_arr, + pixels_arr, + show_overlaps, + level, + filter_index, + geom_data.ctypes.data_as(POINTER(c_int32)), + prop_ptr + ) + + return geom_data, property_data def id_map(plot): - """ - Generate a 2-D map of cell and material IDs. Used for in-memory image - generation. - - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated - - Returns - ------- - id_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 3) of - OpenMC property ids with dtype int32. The last dimension of the array - contains, in order, cell IDs, cell instances, and material IDs. + """Deprecated compatibility wrapper for geometry ID maps. + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=False)` instead. """ - img_data = np.zeros((plot.v_res, plot.h_res, 3), - dtype=np.dtype('int32')) - _dll.openmc_id_map(plot, img_data.ctypes.data_as(POINTER(c_int32))) - return img_data + warnings.warn( + "openmc.lib.id_map is deprecated and will be removed in a future " + "release; use openmc.lib.slice_data(..., include_properties=False).", + FutureWarning, + ) - -_dll.openmc_property_map.argtypes = [POINTER(_PlotBase), POINTER(c_double)] -_dll.openmc_property_map.restype = c_int -_dll.openmc_property_map.errcheck = _error_handler + kwargs = _extract_slice_data_args(plot) + geom_data, _ = slice_data(include_properties=False, **kwargs) + return geom_data[:, :, :3] def property_map(plot): - """ - Generate a 2-D map of cell temperatures and material densities. Used for - in-memory image generation. - - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated - - Returns - ------- - property_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 2) of - OpenMC property ids with dtype float + """Deprecated compatibility wrapper for temperature/density maps. + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=True)` instead. """ - prop_data = np.zeros((plot.v_res, plot.h_res, 2)) - _dll.openmc_property_map(plot, prop_data.ctypes.data_as(POINTER(c_double))) + warnings.warn( + "openmc.lib.property_map is deprecated and will be removed in a " + "future release; use openmc.lib.slice_data(..., " + "include_properties=True).", + FutureWarning, + ) + + kwargs = _extract_slice_data_args(plot) + _, prop_data = slice_data(include_properties=True, **kwargs) return prop_data + _dll.openmc_get_plot_index.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_get_plot_index.restype = c_int _dll.openmc_get_plot_index.errcheck = _error_handler diff --git a/openmc/model/model.py b/openmc/model/model.py index 56c0e7a49fa..d927b65ae64 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -292,7 +292,7 @@ def _assign_fw_cadis_tally_IDs(self): id_next = reference_tal.id break - if id_next == None: + if id_next is None: raise RuntimeError( f'Local FW-CADIS target tally {tal.id} not found on model.tallies!') else: @@ -1127,28 +1127,148 @@ def id_map( array contains cell IDs, cell instances, and material IDs (in that order). """ + ids, _ = self.slice_data( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + show_overlaps=color_overlaps, + level=-1, + include_properties=False, + **init_kwargs, + ) + return ids + + def slice_data( + self, + origin: Sequence[float] | None = None, + width: Sequence[float] | None = None, + pixels: int | Sequence[int] = 40000, + basis: str = 'xy', + u_span: Sequence[float] | None = None, + v_span: Sequence[float] | None = None, + show_overlaps: bool = False, + level: int = -1, + filter: openmc.Filter | None = None, + include_properties: bool = True, + **init_kwargs + ) -> tuple[np.ndarray, np.ndarray | None]: + """Generate geometry and property data for a 2D plot slice. + + This method combines the functionality of :meth:`id_map` and property + mapping into a single call, avoiding duplicate geometry lookups. It also + supports filter bin index lookup for tally visualization. + + .. versionadded:: 0.16.0 + + Parameters + ---------- + origin : Sequence[float], optional + Origin of the plot. If unspecified, this argument defaults to the + center of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (0.0, 0.0, 0.0). + width : Sequence[float], optional + Width of the plot. If unspecified, this argument defaults to the + width of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (10.0, 10.0). + pixels : int | Sequence[int], optional + If an iterable of ints is provided then this directly sets the + number of pixels to use in each basis direction. If a single int is + provided then this sets the total number of pixels in the plot and + the number of pixels in each basis direction is calculated from this + total and the image aspect ratio based on the width argument. + basis : {'xy', 'yz', 'xz'}, optional + Basis of the plot. + u_span : Sequence[float], optional + Full-width span vector for an oriented slice (3 values). Mutually + exclusive with width. + v_span : Sequence[float], optional + Full-height span vector for an oriented slice (3 values). Mutually + exclusive with width. + show_overlaps : bool, optional + Whether to identify and assign unique IDs (-3) to overlapping + regions. If False, overlapping regions will be assigned the ID of + the lowest-numbered cell that occupies that region. Defaults to + False. + level : int, optional + Universe level to plot (-1 for deepest). Defaults to -1. + filter : openmc.Filter, optional + If provided, the information for each pixel also includes an index + in the filter corresponding to the pixel position. + include_properties : bool, optional + Whether to include temperature/density data. Defaults to True. + **init_kwargs + Keyword arguments passed to :meth:`Model.init_lib`. + + Returns + ------- + geom_data : numpy.ndarray + Shape (v_res, h_res, 3) or (v_res, h_res, 4) int32 array. Contains + [cell_id, cell_instance, material_id] when no filter, or [cell_id, + cell_instance, material_id, filter_bin] with filter. + property_data : numpy.ndarray or None + Shape (v_res, h_res, 2) float64 array with [temperature, density], + or None if include_properties=False. + """ import openmc.lib - origin, width, pixels = self._set_plot_defaults( - origin, width, pixels, basis) + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") - # initialize the openmc.lib.plot._PlotBase object - plot_obj = openmc.lib.plot._PlotBase() - plot_obj.origin = origin - plot_obj.width = width[0] - plot_obj.height = width[1] - plot_obj.h_res = pixels[0] - plot_obj.v_res = pixels[1] - plot_obj.basis = basis - plot_obj.color_overlaps = color_overlaps + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + if origin is None: + origin = (0.0, 0.0, 0.0) + if isinstance(pixels, int): + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + aspect_ratio = u_norm / v_norm + pixels_y = math.sqrt(pixels / aspect_ratio) + pixels = (int(pixels / pixels_y), int(pixels_y)) + else: + origin, width, pixels = self._set_plot_defaults( + origin, width, pixels, basis) # Silence output by default. Also set arguments to start in volume # calculation mode to avoid loading cross sections init_kwargs.setdefault('output', False) init_kwargs.setdefault('args', ['-c']) + # If filter does not already appear in the model, temporarily add a + # tally with the filter + original_length = len(self.tallies) + if filter is not None: + filter_ids = {f.id for t in self.tallies for f in t.filters} + if filter.id not in filter_ids: + # Create temporary tally while preserving ID assignment + next_id = openmc.Tally.next_id + temp_tally = openmc.Tally() + temp_tally.filters = [filter] + temp_tally.scores = ['flux'] + self.tallies.append(temp_tally) + openmc.Tally.used_ids.remove(temp_tally.id) + openmc.Tally.next_id = next_id + with openmc.lib.TemporarySession(self, **init_kwargs): - return openmc.lib.id_map(plot_obj) + geom_data, property_data = openmc.lib.slice_data( + origin=origin, + width=width, + basis=basis, + u_span=u_span, + v_span=v_span, + pixels=pixels, + show_overlaps=show_overlaps, + level=level, + filter=filter, + include_properties=include_properties, + ) + + # If filter was temporarily added, remove it + if len(self.tallies) > original_length: + self.tallies.pop() + + return geom_data, property_data @add_plot_params def plot( @@ -1216,13 +1336,14 @@ def plot( "openmc.config before plotting.") break - # Get ID map from the C API - id_map = self.id_map( + # Get plot IDs from the C API + id_map, _ = self.slice_data( origin=origin, width=width, pixels=pixels, basis=basis, - color_overlaps=show_overlaps + show_overlaps=show_overlaps, + include_properties=False, ) # Generate colors if not provided @@ -1748,7 +1869,7 @@ def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bo @staticmethod def _auto_generate_mgxs_lib( - model: openmc.model.model, + model: openmc.model.Model, groups: openmc.mgxs.EnergyGroups, correction: str | None, directory: PathLike, diff --git a/src/plot.cpp b/src/plot.cpp index 707d53dc2cb..b9a1136afdb 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -33,6 +33,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/string_utils.h" +#include "openmc/tallies/filter.h" namespace openmc { @@ -44,10 +45,12 @@ constexpr int PLOT_LEVEL_LOWEST {-1}; //!< lower bound on plot universe level constexpr int32_t NOT_FOUND {-2}; constexpr int32_t OVERLAP {-3}; -IdData::IdData(size_t h_res, size_t v_res) : data_({v_res, h_res, 3}, NOT_FOUND) +IdData::IdData(size_t h_res, size_t v_res, bool /*include_filter*/) + : data_({v_res, h_res, 3}, NOT_FOUND) {} -void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) +void IdData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { // set cell data if (p.n_coord() <= level) { @@ -64,7 +67,6 @@ void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) Cell* c = model::cells.at(p.lowest_coord().cell()).get(); if (p.material() == MATERIAL_VOID) { data_(y, x, 2) = MATERIAL_VOID; - return; } else if (c->type_ == Fill::MATERIAL) { Material* m = model::materials.at(p.material()).get(); data_(y, x, 2) = m->id_; @@ -77,12 +79,12 @@ void IdData::set_overlap(size_t y, size_t x) data_(y, x, k) = OVERLAP; } -PropertyData::PropertyData(size_t h_res, size_t v_res) +PropertyData::PropertyData(size_t h_res, size_t v_res, bool /*include_filter*/) : data_({v_res, h_res, 2}, NOT_FOUND) {} -void PropertyData::set_value( - size_t y, size_t x, const GeometryState& p, int level) +void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { Cell* c = model::cells.at(p.lowest_coord().cell()).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; @@ -97,6 +99,74 @@ void PropertyData::set_overlap(size_t y, size_t x) data_(y, x) = OVERLAP; } +//============================================================================== +// RasterData implementation +//============================================================================== + +RasterData::RasterData(size_t h_res, size_t v_res, bool include_filter) + : id_data_({v_res, h_res, include_filter ? 4u : 3u}, NOT_FOUND), + property_data_({v_res, h_res, 2}, static_cast(NOT_FOUND)), + include_filter_(include_filter) +{} + +void RasterData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter, FilterMatch* match) +{ + // set cell data + if (p.n_coord() <= level) { + id_data_(y, x, 0) = NOT_FOUND; + id_data_(y, x, 1) = NOT_FOUND; + } else { + id_data_(y, x, 0) = model::cells.at(p.coord(level).cell())->id_; + id_data_(y, x, 1) = level == p.n_coord() - 1 + ? p.cell_instance() + : cell_instance_at_level(p, level); + } + + // set material data + Cell* c = model::cells.at(p.lowest_coord().cell()).get(); + if (p.material() == MATERIAL_VOID) { + id_data_(y, x, 2) = MATERIAL_VOID; + } else if (c->type_ == Fill::MATERIAL) { + Material* m = model::materials.at(p.material()).get(); + id_data_(y, x, 2) = m->id_; + } + + // set filter index (only if filter is being used) + if (include_filter_ && filter) { + filter->get_all_bins(p, TallyEstimator::COLLISION, *match); + if (match->bins_.empty()) { + id_data_(y, x, 3) = -1; + } else { + id_data_(y, x, 3) = match->bins_[0]; + } + match->bins_.clear(); + match->weights_.clear(); + } + + // set temperature (in K) + property_data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; + + // set density (g/cm³) + if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) { + Material* m = model::materials.at(p.material()).get(); + property_data_(y, x, 1) = m->density_gpcc_; + } +} + +void RasterData::set_overlap(size_t y, size_t x) +{ + // Set cell, instance, and material to OVERLAP, but preserve filter bin + id_data_(y, x, 0) = OVERLAP; + id_data_(y, x, 1) = OVERLAP; + id_data_(y, x, 2) = OVERLAP; + // Note: id_data_(y, x, 3) is NOT overwritten - preserves filter bin for tally + // plotting + + property_data_(y, x, 0) = OVERLAP; + property_data_(y, x, 1) = OVERLAP; +} + //============================================================================== // Global variables //============================================================================== @@ -450,6 +520,22 @@ void Plot::set_width(pugi::xml_node plot_node) if (pl_width.size() == 2) { width_.x = pl_width[0]; width_.y = pl_width[1]; + switch (basis_) { + case PlotBasis::xy: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, width_.y, 0.0}; + break; + case PlotBasis::xz: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + case PlotBasis::yz: + u_span_ = {0.0, width_.x, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + default: + UNREACHABLE(); + } } else { fatal_error( fmt::format(" must be length 2 in slice plot {}", id())); @@ -765,7 +851,7 @@ Plot::Plot(pugi::xml_node plot_node, PlotType type) set_width(plot_node); set_meshlines(plot_node); slice_level_ = level_; // Copy level employed in SlicePlotBase::get_map - slice_color_overlaps_ = color_overlaps_; + show_overlaps_ = color_overlaps_; } //============================================================================== @@ -862,23 +948,39 @@ void Plot::draw_mesh_lines(ImageData& data) const rgb = meshlines_color_; int ax1, ax2; + Position expected_u {}; + Position expected_v {}; switch (basis_) { case PlotBasis::xy: ax1 = 0; ax2 = 1; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, width_[1], 0.0}; break; case PlotBasis::xz: ax1 = 0; ax2 = 2; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; case PlotBasis::yz: ax1 = 1; ax2 = 2; + expected_u = {0.0, width_[0], 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; default: UNREACHABLE(); } + // Meshlines rely on axis-aligned indexing in global coordinates. + constexpr double rel_tol {1e-12}; + double span_tol = rel_tol * (1.0 + u_span_.norm() + v_span_.norm()); + if ((u_span_ - expected_u).norm() > span_tol || + (v_span_ - expected_v).norm() > span_tol) { + fatal_error("Meshlines are only supported for axis-aligned slice plots."); + } + Position ll_plot {origin_}; Position ur_plot {origin_}; @@ -1008,11 +1110,11 @@ void Plot::create_voxel() const voxel_init(file_id, &(dims[0]), &dspace, &dset, &memspace); SlicePlotBase pltbase; - pltbase.width_ = width_; pltbase.origin_ = origin_; - pltbase.basis_ = PlotBasis::xy; + pltbase.u_span_ = {width_.x, 0.0, 0.0}; + pltbase.v_span_ = {0.0, width_.y, 0.0}; pltbase.pixels() = pixels(); - pltbase.slice_color_overlaps_ = color_overlaps_; + pltbase.show_overlaps_ = color_overlaps_; ProgressBar pb; for (int z = 0; z < pixels()[2]; z++) { @@ -1794,6 +1896,12 @@ void PhongRay::on_intersection() extern "C" int openmc_id_map(const void* plot, int32_t* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_id_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { @@ -1801,7 +1909,7 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1815,14 +1923,20 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) extern "C" int openmc_property_map(const void* plot, double* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_property_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { - set_errmsg("Invalid slice pointer passed to openmc_id_map"); + set_errmsg("Invalid slice pointer passed to openmc_property_map"); return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1834,6 +1948,68 @@ extern "C" int openmc_property_map(const void* plot, double* data_out) return 0; } +extern "C" int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool color_overlaps, + int level, int32_t filter_index, int32_t* geom_data, double* property_data) +{ + // Validate span vectors + Direction u_span_pos {u_span[0], u_span[1], u_span[2]}; + Direction v_span_pos {v_span[0], v_span[1], v_span[2]}; + double u_norm = u_span_pos.norm(); + double v_norm = v_span_pos.norm(); + if (u_norm == 0.0 || v_norm == 0.0) { + set_errmsg("Slice span vectors must be non-zero."); + return OPENMC_E_INVALID_ARGUMENT; + } + + constexpr double ORTHO_REL_TOL = 1e-10; + double dot = u_span_pos.dot(v_span_pos); + if (std::abs(dot) > ORTHO_REL_TOL * u_norm * v_norm) { + set_errmsg("Slice span vectors must be orthogonal."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Validate filter index if provided + if (filter_index >= 0) { + if (int err = verify_filter(filter_index)) + return err; + } + + // Initialize overlap check vector if needed + if (color_overlaps && model::overlap_check_count.size() == 0) { + model::overlap_check_count.resize(model::cells.size()); + } + + try { + // Create a temporary SlicePlotBase object to reuse get_map logic + SlicePlotBase plot_params; + plot_params.origin_ = Position {origin[0], origin[1], origin[2]}; + plot_params.u_span_ = u_span_pos; + plot_params.v_span_ = v_span_pos; + plot_params.pixels_[0] = pixels[0]; + plot_params.pixels_[1] = pixels[1]; + plot_params.show_overlaps_ = color_overlaps; + plot_params.slice_level_ = level; + + // Use get_map to generate data + auto data = plot_params.get_map(filter_index); + + // Copy geometry data + std::copy(data.id_data_.begin(), data.id_data_.end(), geom_data); + + // Copy property data if requested + if (property_data != nullptr) { + std::copy( + data.property_data_.begin(), data.property_data_.end(), property_data); + } + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_UNASSIGNED; + } + + return 0; +} + extern "C" int openmc_get_plot_index(int32_t id, int32_t* index) { auto it = model::plot_map.find(id); diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 6926bc11b18..f62306e834d 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -895,22 +895,23 @@ def test_load_nuclide(lib_init): openmc.lib.load_nuclide('Pu3') +class LegacySlicePlot: + origin = (0.0, 0.0, 0.0) + width = 1.26 + height = 1.26 + basis = 'xy' + h_res = 3 + v_res = 3 + level = -1 + + def test_id_map(lib_init): expected_ids = np.array([[(3, 0, 3), (2, 0, 2), (3, 0, 3)], [(2, 0, 2), (1, 0, 1), (2, 0, 2)], [(3, 0, 3), (2, 0, 2), (3, 0, 3)]], dtype='int32') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - ids = openmc.lib.plot.id_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + ids = openmc.lib.id_map(LegacySlicePlot()) assert np.array_equal(expected_ids, ids) @@ -920,17 +921,8 @@ def test_property_map(lib_init): [ (293.6, 6.55), (293.6, 10.29769), (293.6, 6.55)], [(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)]], dtype='float') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - properties = openmc.lib.plot.property_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + properties = openmc.lib.property_map(LegacySlicePlot()) assert np.allclose(expected_properties, properties, atol=1e-04) diff --git a/tests/unit_tests/test_slice_data.py b/tests/unit_tests/test_slice_data.py new file mode 100644 index 00000000000..cc5fb047473 --- /dev/null +++ b/tests/unit_tests/test_slice_data.py @@ -0,0 +1,168 @@ +import numpy as np +import openmc +from openmc.examples import pwr_pin_cell + + +def test_slice_data_basic(run_in_tmpdir): + """Test basic slice_data functionality.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(100, 100), + basis='xy' + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (100, 100, 3) + assert geom_data.dtype == np.int32 + assert prop_data.shape == (100, 100, 2) + assert prop_data.dtype == np.float64 + + # Check we have valid geometry + assert np.any(geom_data[:, :, 0] >= 0) # Valid cell IDs + assert np.any(prop_data[:, :, 0] > 0) # Valid temperatures + + +def test_slice_data_no_properties(run_in_tmpdir): + """Test slice_data without property data.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + assert prop_data is None + + +def test_slice_data_with_filter(run_in_tmpdir): + """Test slice_data with a cell filter.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + include_properties=False + ) + + # With filter, should have 4 fields + assert geom_data.shape == (50, 50, 4) + + # Filter bin index should be populated where cells exist + filter_bins = geom_data[:, :, 3] + valid_cells = geom_data[:, :, 0] >= 0 + assert np.any(filter_bins[valid_cells] >= 0) + + +def test_slice_data_overlaps(run_in_tmpdir): + """Test slice_data with overlap detection.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + show_overlaps=True, + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + # Check for overlap markers (-3) if any exist + # Note: This test may pass without finding overlaps if geometry is correct + + +def test_slice_data_overlaps_with_filter(run_in_tmpdir): + """Test that overlaps don't overwrite filter bin data.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + show_overlaps=True, + include_properties=False + ) + + assert geom_data.shape == (50, 50, 4) + + # If any overlaps exist, verify filter bin is still valid (not -3) + overlap_pixels = geom_data[:, :, 0] == -3 + if np.any(overlap_pixels): + # Filter bins at overlap locations should NOT be -3 + filter_bins_at_overlaps = geom_data[overlap_pixels, 3] + assert not np.all(filter_bins_at_overlaps == -3), \ + "Filter bins should be preserved even where overlaps are detected" + + +def test_slice_data_different_bases(run_in_tmpdir): + """Test slice_data with different basis planes.""" + model = pwr_pin_cell() + + for basis in ['xy', 'xz', 'yz']: + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(25, 25), + basis=basis + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_oriented_spans(run_in_tmpdir): + """Test slice_data with oriented span vectors.""" + model = pwr_pin_cell() + + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + u_span=(1.0, 0.0, 0.0), + v_span=(0.0, 0.0, 1.0), + pixels=(25, 25) + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_level(run_in_tmpdir): + """Test slice_data with specific universe level.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + level=0, # Root universe only + include_properties=False + ) + + assert geom_data.shape == (50, 50, 3) + + +def test_id_map_reverted(run_in_tmpdir): + """Test that id_map returns 3D array without filter support.""" + model = pwr_pin_cell() + id_data = model.id_map( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + basis='xy' + ) + + # Should have 3 fields (cell_id, cell_instance, material_id) + assert id_data.shape == (50, 50, 3) + assert id_data.dtype == np.int32 + + # Check valid data + assert np.any(id_data[:, :, 0] >= 0) # Valid cell IDs From ea6ba328c9960a9e1b1cc85ab3b4f1ddd91ed3a1 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 8 Jun 2026 18:37:34 -0500 Subject: [PATCH 2/8] Fix collision track feature for photon transport (#3946) Co-authored-by: Paul Romano --- docs/source/io_formats/collision_track.rst | 4 +- docs/source/io_formats/settings.rst | 31 +++--- docs/source/usersguide/settings.rst | 5 + include/openmc/constants.h | 2 +- include/openmc/nuclide.h | 3 + src/collision_track.cpp | 12 ++- .../case_1_Reactions/collision_track_true.h5 | Bin 0 -> 2960 bytes .../case_1_Reactions/inputs_true.dat | 6 +- .../case_1_Reactions/results_true.dat | 2 - .../case_2_Cell_ID/collision_track_true.h5 | Bin 0 -> 9744 bytes .../case_2_Cell_ID/inputs_true.dat | 6 +- .../case_2_Cell_ID/results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 9744 bytes .../case_3_Material_ID/inputs_true.dat | 6 +- .../case_3_Material_ID/results_true.dat | 2 - .../case_4_Nuclide_ID/collision_track_true.h5 | Bin 0 -> 9232 bytes .../case_4_Nuclide_ID/inputs_true.dat | 6 +- .../case_4_Nuclide_ID/results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 9744 bytes .../case_5_Universe_ID/inputs_true.dat | 6 +- .../case_5_Universe_ID/results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 6160 bytes .../inputs_true.dat | 6 +- .../results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 6032 bytes .../inputs_true.dat | 6 +- .../results_true.dat | 2 - .../case_8_2threads/inputs_true.dat | 57 ---------- .../case_8_2threads/results_true.dat | 2 - .../regression_tests/collision_track/test.py | 51 ++------- tests/testing_harness.py | 99 +++++++++++------- tests/unit_tests/test_collision_track.py | 43 +++++++- 32 files changed, 168 insertions(+), 197 deletions(-) create mode 100644 tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_1_Reactions/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat delete mode 100644 tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat delete mode 100644 tests/regression_tests/collision_track/case_8_2threads/results_true.dat diff --git a/docs/source/io_formats/collision_track.rst b/docs/source/io_formats/collision_track.rst index 8e1e00ffb8e..4123fdac6b0 100644 --- a/docs/source/io_formats/collision_track.rst +++ b/docs/source/io_formats/collision_track.rst @@ -10,7 +10,7 @@ may also be written after each batch when multiple files are requested (``collision_track.N.h5``) or when the run is performed in parallel. The file contains the information needed to reconstruct each recorded collision. -The current revision of the collision track file format is 1.1. +The current revision of the collision track file format is 1.2. **/** @@ -33,7 +33,7 @@ The current revision of the collision track file format is 1.1. - ``event_mt`` (*int*) -- ENDF MT number identifying the reaction. - ``delayed_group`` (*int*) -- Delayed neutron group index (non-zero for delayed events). - ``cell_id`` (*int*) -- ID of the cell in which the collision occurred. - - ``nuclide_id`` (*int*) -- ZA identifier of the nuclide (ZZZAAAM format). + - ``nuclide_id`` (*int*) -- PDG number of the nuclide (100ZZZAAAM). - ``material_id`` (*int*) -- ID of the material containing the collision site. - ``universe_id`` (*int*) -- ID of the universe containing the collision site. - ``n_collision`` (*int*) -- Collision counter for the particle history. diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 1a436a75ede..fb02159169e 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -98,6 +98,11 @@ sub-elements: A list of strings representing the nuclide, to define specific define specific target nuclide collisions to be banked. + .. note:: + Electron and positron collision-track events are not associated with + a specific nuclide. If a ``nuclides`` entry is specified, these events + are omitted. + *Default*: None :reactions: @@ -606,30 +611,30 @@ found in the :ref:`random ray user guide `. *Default*: None :adjoint_source: - Specifies an adjoint fixed source for adjoint transport simulations, and - follows the format for :ref:`source_element`. The distributions which make - up the adjoint source are subject to the same restrictions as forward + Specifies an adjoint fixed source for adjoint transport simulations, and + follows the format for :ref:`source_element`. The distributions which make + up the adjoint source are subject to the same restrictions as forward fixed sources in Random Ray mode. *Default*: None - + :adjoint: - Specifies whether to perform adjoint transport. The default is 'False', + Specifies whether to perform adjoint transport. The default is 'False', corresponding to forward transport. *Default*: None - + :volume_estimator: - Specifies choice of volume estimator for the random ray solver. Options + Specifies choice of volume estimator for the random ray solver. Options are 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. *Default*: None :volume_normalized_flux_tallies: - Specifies whether to normalize flux tallies by volume (bool). The - default is 'False'. When enabled, flux tallies will be reported in units - of cm/cm^3. When disabled, flux tallies will be reported in units of cm - (i.e., total distance traveled by neutrons in the spatial tally + Specifies whether to normalize flux tallies by volume (bool). The + default is 'False'. When enabled, flux tallies will be reported in units + of cm/cm^3. When disabled, flux tallies will be reported in units of cm + (i.e., total distance traveled by neutrons in the spatial tally region). *Default*: None @@ -1757,11 +1762,11 @@ mesh-based weight windows. The ratio of the lower to upper weight window bounds. *Default*: 5.0 - + For FW-CADIS: :targets: - A sequence of IDs corresponding to the tallies which cover phase + A sequence of IDs corresponding to the tallies which cover phase space regions of interest for local variance reduction. *Default*: None diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 8ac07f3c892..e8514b8561a 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -792,6 +792,11 @@ collision_track.h5 file at the end of the simulation. The file contains 300 recorded collisions that occurred in materials with IDs 1 or 2, involving fission or (n,2n) reactions on the nuclides U-238 or O-16, within cells with IDs 5 and 12. + +.. note:: + Electron and positron collision-track events are not associated with a + specific nuclide. If a ``nuclides`` entry is specified, these events are omitted. + The file can be read using :func:`openmc.read_collision_track_file`. The example below shows how to extract the data from the collision_track feature and displays the fields stored in the file: diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 0b425a673dc..7baed25c5b0 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -35,7 +35,7 @@ constexpr array VERSION_VOXEL {2, 0}; constexpr array VERSION_MGXS_LIBRARY {1, 0}; constexpr array VERSION_PROPERTIES {1, 1}; constexpr array VERSION_WEIGHT_WINDOWS {1, 0}; -constexpr array VERSION_COLLISION_TRACK {1, 1}; +constexpr array VERSION_COLLISION_TRACK {1, 2}; // ============================================================================ // ADJUSTABLE PARAMETERS diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 7a8b2acadd9..ae39a53ddf5 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -84,6 +84,9 @@ class Nuclide { double collapse_rate(int MT, double temperature, span energy, span flux) const; + //! Return a ParticleType object representing this nuclide + ParticleType particle_type() const { return {Z_, A_, metastable_}; } + //============================================================================ // Data members std::string name_; //!< Name of nuclide, e.g. "U235" diff --git a/src/collision_track.cpp b/src/collision_track.cpp index 03cbc32b7b2..2e749007ff5 100644 --- a/src/collision_track.cpp +++ b/src/collision_track.cpp @@ -200,8 +200,13 @@ void collision_track_record(Particle& particle) return; int cell_id = model::cells[cell_index]->id_; - const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); - std::string nuclide = nuclide_ptr->name_; + std::string nuclide {}; + int nuclide_id = 0; + if (particle.event_nuclide() != NUCLIDE_NONE) { + const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); + nuclide = nuclide_ptr->name_; + nuclide_id = nuclide_ptr->particle_type().pdg_number(); + } int universe_id = model::universes[particle.lowest_coord().universe()]->id_; double delta_E = particle.E_last() - particle.E(); int material_index = particle.material(); @@ -224,8 +229,7 @@ void collision_track_record(Particle& particle) site.event_mt = particle.event_mt(); site.delayed_group = particle.delayed_group(); site.cell_id = cell_id; - site.nuclide_id = - 10000 * nuclide_ptr->Z_ + 10 * nuclide_ptr->A_ + nuclide_ptr->metastable_; + site.nuclide_id = nuclide_id; site.material_id = material_id; site.universe_id = universe_id; site.n_collision = particle.n_collision(); diff --git a/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 b/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..c315462134217a7b6f4f23b5831d8097fe92d024 GIT binary patch literal 2960 zcmeHJ&1(};5PzEw*tV%nDnxBB!Gm6UP^efeWFvGXDr#vX7OJFiv&L;Uo3P!)MlXU0 z6+A>i1hFV0*grrJK~0PxMJl58AS&oVdb3sVB$fC+<4dw>yo!BccjnFfelzcLvX{a` z`#Rfu+5u{!2wcF6RradTR_GuW)@S|+st8ohP&wxyiU1=b-)3s}()a-+-)^fv~)H7)^ML&N@KlJUwT$3zW33Yg1a_(_`!H4Co8yE0^5QYkn>w&i;r@+rcK& zMB1%SoRSBcaG2SSYiTKb=0gnBMA~uBBrJ>$H{md|9aqh%h80bb01$|a10&c-{$9mZ zJ(gGF(S(`KW*|0VgXc{=S5b96s>Q2yh{y3d&oQ!!uEkYE!|gZ^w9$EPDrTvs7Q;Ca zbixMDn|N;4&`?>aCZ_3mc#Y0;jcB!EVa(|oWMhZtW@4tLDLOtE2&p&1Yb6nRs`NJZ=W1J zd}K&~5y)5oDADhmwZ^{gYg~b?pLcy-{PevTf0y)4b^esjeH(vr@M2k-k<0J*tlX6j zl&;)5`f|CbZR*+L+xA)#pFTcz^Zo;gMbhpCl@sN~AnSKi#Si83$GJhYUx(o1gHH%< zRv(n8q5o>sR-U~YAC*_7!j0k5{PRb}or7aJSMLw$!sWin7nPFqW>vX9a%ZV{^+V*^ k!tN#M%fxK%S8$=4f7a=K80dQ2GC#_P^7rDb0BoT0Hw#|g3IG5A literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat index 7533616c059..005a5602058 100644 --- a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ (n,fission) 101 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..850bba8415ef0534522e94487a08473e1fae182b GIT binary patch literal 9744 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 E1#}!hp#T5? literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat index 55fb835de03..8d46c3d3b97 100644 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 22 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..777d91f088810687d7762eaba930c31b7caffe2a GIT binary patch literal 9744 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 w>p+y eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 1 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..527705297feba53a95173def72577be38f8954f8 GIT binary patch literal 9232 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 N=I?(5e+d%)zX2?eSMmS= literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat index 8960dde5cbd..6202bcacc75 100644 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ O16 U235 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..65419d5ead9739ee3000454f40eeafe20497d129 GIT binary patch literal 9744 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 eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -52,7 +52,7 @@ 22 77 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..507665a60fdac4d894511918d0492b9509172c8e GIT binary patch literal 6160 zcmeHKc|4U_AAc^cu7)Bdv}qI?`&x+eoDe3W)Fd?_Zd}XND@zo~v}&0q&CoIvm8rBZ zS`5xjs!5VHrA-N0(?+(u&vWndm^%+%-ap^ZJLi7x{hf1u%lG_#=XZYR-0cpwbLB>= zj>O2u;b2mjys%6E^ioY;Su{hBX}%ZLVNrA&MNdeGIxNB>$)nT>>mvJVk|KVJh+vBr z+Sy@*ByFFj6dQAp5%oCq#6R@^Dgq96(3N!9o$^|ZqJL6(Mh!8akf0#m8eRzBGc45G zC(v`HH$RZ%(fK)x791v3pz@WO*Rq(%5(|^TIK*caFNhl!wVF$tRU|s2i7$=ISQjRX z;X+Xsp({(AjtFijxoS+FNN_13M@__Mi7e1=(v+Mf`z(wt#8}G~iW7a&Nn=rZ6GYP( zO(ijTHmR#b9K$$*hEn{Yh!SyLNy}@85~TPFYJl*JY&d4}6=u>)=toh!q0aC?y3v&G z->VZfDMkj;4X5~`$cPCuy8p{Q62(*43>ZE!hXcjOmr|a^6lRPE(hcJU3v(W4$TEim z=|=j63B??k!i@1iy4(mZKg=^&WPp$t76*o&_)z>@`ErB2qqx4FexV`Zs|le+;(`P8 z#dUqSK|!88-@ZCDfVgl53F^B{b@i@AEP){X_xa0{1z>8^ihACMEOc!*%S5mF723M# zo0ax^FM8($jKcJMT-P??e%8+gJsQmTGkCCa?V)vSnwZL;;a+{x5397QbG=074R_PM#W!$ifXQ{=pAvm4sG!FIM=OZ0F$CUGVNOQ z;K|CV+hcAm=)`#T5?`w1@FRQjo^b8k&{)~M4FTHKU`@ss zqssd=$Z;YoKP}`YIG?j;;kNo(WMBDO{dxFwxIw~VY0XA;cCpw+d{Cjy0Kth)E{7$XW9L$3N^@PiAwb2!?i&6x0W2K%|&QL z;@L%4lPT6v+jiqy2L0yT|@@WZV|HhQFy6joI#jEgXFtJ^2{X`gl|=YBjTXu5-@>rX^@7J&iSnBYhV=a}S)= zhfmubEdRa{cb@3K*V&Z4?(#e2s-h#WzKtc2U2Dg8I@k!HxslYDefQ9|(+0dko!h`| zWqb1Pf)?Q9^@Z0GGY9UQ-pbd@w}#)1d+6-~jfagtkAc53WM;-`cS%8k{dt4TRaM~U zF73!k=?y4p;!eKx;bJuYlef9Ujuxc&;0+k#nvFWA##x&*xWY$LcIT3GZJ?okNvvCh z=CJWq82CmB??xT{7aQMq&8phar7E+N=eK za&Lg>J=(G#_Tw-%*5b{+w-(ST`jq=A34>wd|LsyC)-rom)WL7<Ib_4o8*X;C-(Cf%NHsWBavlZm$@lO?BG=zo@S-pvm zP5a>wcK(?DpUm>oZ&Uk>4!r(-@dvqj@WxKr({+3YICxj%c9Q-xge@<-*LkHFa9nG5 zNGLR-tjM{Nq5Hpu^2xeI@ln%Z_U)Jp>nhFq;gjb~P4ZB``m4@}e}Rpk=H%=maG)em z`v$uh1zRC+A2Q@z%!`=32W%5uP zy1&#a@$xnr(tq*}BJ2Ms8knLJ7MDDcHDe3(iu2AkpYKBFrB<*cnlzI=dQC+(-G4tIAi=Mf36c zk1Xm?=k4k8n;ul7n*}K)(4kK=9Y{;?jZ}z&~oT;q01b^rrhMN0nU;O5Kf= zWY;;sJfjrlFYl&7pT^D|zQ(Yh|IGR$P2iIIPy5fN)}i=O+CA=Q4_I=kt>(3A8E~4q zeX>`mB(4|sL_6qIA~+#&ZsV@fX3%!|1xH4+qA&jo?dy&l-);#N*J!!8YYspECo eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 550000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..b7ff67c48e1c0261d34de2a9a61179e569671da2 GIT binary patch literal 6032 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat index 005d9feb271..1449c3596fb 100644 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -56,7 +56,7 @@ 1 11 U238 U235 H1 U234 100000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat b/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat deleted file mode 100644 index 514932c1a69..00000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - eigenvalue - 100 - 5 - 1 - - - -2.0 -2.0 -2.0 2.0 2.0 2.0 - - - true - - - - 200 - - 1 - - diff --git a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat b/tests/regression_tests/collision_track/case_8_2threads/results_true.dat deleted file mode 100644 index d4d1d1e5ad4..00000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/test.py b/tests/regression_tests/collision_track/test.py index 00e1e3de410..2f9de9ac00e 100644 --- a/tests/regression_tests/collision_track/test.py +++ b/tests/regression_tests/collision_track/test.py @@ -59,28 +59,13 @@ """ -import os - import openmc -import openmc.lib import pytest from tests.testing_harness import CollisionTrackTestHarness from tests.regression_tests import config -@pytest.fixture(scope="function") -def two_threads(monkeypatch): - """Set the number of OMP threads to 2 for the test.""" - monkeypatch.setenv("OMP_NUM_THREADS", "2") - - -@pytest.fixture(scope="function") -def single_process(monkeypatch): - """Set the number of MPI process to 1 for the test.""" - monkeypatch.setitem(config, "mpi_np", "1") - - @pytest.fixture(scope="module") def model_1(): """Cylindrical core contained in a first box which is contained in a larger box. @@ -181,9 +166,9 @@ def model_1(): # ============================================================================= model.settings = openmc.Settings() - model.settings.particles = 100 + model.settings.particles = 80 model.settings.batches = 5 - model.settings.inactive = 1 + model.settings.inactive = 4 model.settings.seed = 1 bounds = [ @@ -203,19 +188,19 @@ def model_1(): @pytest.mark.parametrize( "folder, model_name, parameter", - [("case_1_Reactions", "model_1", {"max_collisions": 300, "reactions": ["(n,fission)", 101]}), + [("case_1_Reactions", "model_1", {"max_collisions": 100, "reactions": ["(n,fission)", 101]}), ("case_2_Cell_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22]}), + "max_collisions": 100, "cell_ids": [22]}), ("case_3_Material_ID", "model_1", { - "max_collisions": 300, "material_ids": [1]}), + "max_collisions": 100, "material_ids": [1]}), ("case_4_Nuclide_ID", "model_1", { - "max_collisions": 300, "nuclides": ["O16", "U235"]}), + "max_collisions": 100, "nuclides": ["O16", "U235"]}), ("case_5_Universe_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22], "universe_ids": [77]}), + "max_collisions": 100, "cell_ids": [22], "universe_ids": [77]}), ("case_6_deposited_energy_threshold", "model_1", { - "max_collisions": 300, "deposited_E_threshold": 5.5e5}), + "max_collisions": 100, "deposited_E_threshold": 5.5e5}), ("case_7_all_parameters_used_together", "model_1", { - "max_collisions": 300, + "max_collisions": 100, "reactions": ["elastic", 18, "(n,disappear)"], "material_ids": [1, 11], "universe_ids": [77], @@ -235,21 +220,3 @@ def test_collision_track_several_cases( "statepoint.5.h5", model=model, workdir=folder ) harness.main() - - -@pytest.mark.skipif(config["event"], reason="Results from history-based mode.") -def test_collision_track_2threads(model_1, two_threads, single_process): - # This test checks that the `max_collisions` setting is honored: - # no collisions beyond the specified limit should be recorded. - # - # For the result to be reproducible, the number of threads and - # the transport mode (history vs. event) must remain fixed. - assert os.environ["OMP_NUM_THREADS"] == "2" - assert config["mpi_np"] == "1" - model_1.settings.collision_track = { - "max_collisions": 200 - } - harness = CollisionTrackTestHarness( - "statepoint.5.h5", model=model_1, workdir="case_8_2threads" - ) - harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 1ad91b7a89f..8d156bd6472 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -546,19 +546,19 @@ def __init__(self, statepoint_name, model=None, inputs_true=None, workdir=None): def _test_output_created(self): """Make sure collision_track.h5 has also been created.""" - super()._test_output_created() if self._model.settings.collision_track: assert os.path.exists( "collision_track.h5" ), "collision_track file has not been created." - def _compare_output(self): + def _compare_results(self): """Compare collision_track.h5 files.""" if self._model.settings.collision_track: collision_track_true = self._return_collision_track_data( "collision_track_true.h5") collision_track_test = self._return_collision_track_data( "collision_track.h5") + assert collision_track_true.shape == collision_track_test.shape np.testing.assert_allclose( collision_track_true, collision_track_test, rtol=1e-07) @@ -582,15 +582,18 @@ def build_inputs(self): def _overwrite_results(self): """Also add the 'collision_track.h5' file during overwriting.""" - super()._overwrite_results() if os.path.exists("collision_track.h5"): shutil.copyfile("collision_track.h5", "collision_track_true.h5") + def _write_results(self, results_string): + # The result file for this test are written by the OpenMC executable itself + pass + @staticmethod def _return_collision_track_data(filepath): """ - Read a collision_track file and return a sorted array composed - of flatten arrays of collision information. + Read a collision_track file and return a sorted array composed of + flattened collision records. Parameters ---------- @@ -600,42 +603,58 @@ def _return_collision_track_data(filepath): Returns ------- data : np.array - Sorted array composed of flatten arrays of collision_track data for - each collision information + Sorted array composed of flattened collision-track records. """ - data = [] - keys = [] - - # Read source file source = openmc.read_collision_track_file(filepath) - for src in source: - r = src['r'] - u = src['u'] - e = src['E'] - de = src['dE'] - time = src['time'] - wgt = src['wgt'] - delayed_group = src['delayed_group'] - cell_id = src['cell_id'] - nuclide_id = src['nuclide_id'] - material_id = src['material_id'] - universe_id = src['universe_id'] - n_collision = src['n_collision'] - event_mt = src['event_mt'] - key = ( - f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" - f"{e:.10e} {de:.10e} {time:.10e} {wgt:.10e} {event_mt} {delayed_group} {cell_id}" - f"{nuclide_id} {material_id} {universe_id} {n_collision} " - ) - keys.append(key) - values = [*r, *u, e, de, time, wgt, event_mt, - delayed_group, cell_id, nuclide_id, material_id, - universe_id, n_collision] - assert len(values) == 17 - data.append(values) - - data = np.array(data) - keys = np.array(keys) - sorted_idx = np.argsort(keys, kind='stable') + columns = [ + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + source['event_mt'], + source['delayed_group'], + source['cell_id'], + source['nuclide_id'], + source['material_id'], + source['universe_id'], + source['n_collision'], + source['particle'], + source['parent_id'], + source['progeny_id'], + ] + data = np.column_stack(columns) + + # Sort by the complete record, prioritizing stable integer identifiers + # before floating-point fields. This removes dependence on the order in + # which threads append otherwise reproducible collision records. + sort_columns = [ + source['parent_id'], + source['progeny_id'], + source['n_collision'], + source['particle'], + source['cell_id'], + source['material_id'], + source['universe_id'], + source['nuclide_id'], + source['event_mt'], + source['delayed_group'], + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + ] + sorted_idx = np.lexsort(tuple(reversed(sort_columns))) return data[sorted_idx] diff --git a/tests/unit_tests/test_collision_track.py b/tests/unit_tests/test_collision_track.py index 25344a3bd0c..96676e2cd5c 100644 --- a/tests/unit_tests/test_collision_track.py +++ b/tests/unit_tests/test_collision_track.py @@ -34,6 +34,7 @@ def geometry(): {"max_collisions": 200, "mcpl": True} ], + ids=str ) def test_xml_serialization(parameter, run_in_tmpdir): """Check that the different use cases can be written and read in XML.""" @@ -45,7 +46,7 @@ def test_xml_serialization(parameter, run_in_tmpdir): assert read_settings.collision_track == parameter -@pytest.fixture(scope="module") +@pytest.fixture def model(): """Simple hydrogen sphere divided in two hemispheres by a z-plane to form 2 cells.""" @@ -127,3 +128,43 @@ def test_format_similarity(run_in_tmpdir, model): np.testing.assert_allclose(data_h5, data_mcpl, rtol=1e-05) # tolerance not that low due to the strings that is saved in MCPL, # not enough precision! + + +def test_photon_particles(run_in_tmpdir, model): + """Test that the collision track can be used to track photon particles.""" + model.settings.collision_track = {"max_collisions": 200, "cell_ids": [1, 2]} + + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box(*model.geometry.bounding_box), + energy=openmc.stats.delta_function(1e5), + particle='photon' + ) + model.run() + + with h5py.File("collision_track.h5", "r") as f: + source = f["collision_track_bank"] + + assert len(source) < 200 + + allowed_particles = (openmc.ParticleType.PHOTON, openmc.ParticleType.ELECTRON) + + for point in source: + particle_type = openmc.ParticleType(point['particle']) + assert particle_type in allowed_particles + + if particle_type == openmc.ParticleType.ELECTRON: + assert point['nuclide_id'] == 0 + + +def test_collision_track_two_threads(model, run_in_tmpdir): + # This test checks that the `max_collisions` setting is honored: + # no collisions beyond the specified limit should be recorded. + # + # The exact set of events in the capped bank is not reproducible with + # multiple threads because the bank stores whichever thread appends first + # until capacity is reached. + model.settings.collision_track = {"max_collisions": 200} + model.run(threads=2, particles=500) + + collision_track = openmc.read_collision_track_hdf5("collision_track.h5") + assert len(collision_track) == 200 From 998565808ea2c4e85fcf45686de4f5eefac6b300 Mon Sep 17 00:00:00 2001 From: Logan Harbour Date: Mon, 8 Jun 2026 21:07:21 -0600 Subject: [PATCH 3/8] Use non-deprecated methods for getting libMesh node points (#3963) --- src/mesh.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 5ab7ac3988b..181af846694 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3670,7 +3670,7 @@ Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const // Get tet vertex coordinates from LibMesh std::array tet_verts; for (int i = 0; i < elem.n_nodes(); i++) { - auto node_ref = elem.node_ref(i); + const auto& node_ref = elem.node_ref(i); tet_verts[i] = {node_ref(0), node_ref(1), node_ref(2)}; } // Samples position within tet using Barycentric coordinates @@ -3700,7 +3700,7 @@ int LibMesh::n_vertices() const Position LibMesh::vertex(int vertex_id) const { - const auto node_ref = m_->node_ref(vertex_id); + const auto& node_ref = m_->node_ref(vertex_id); if (length_multiplier_ > 0.0) { return length_multiplier_ * Position(node_ref(0), node_ref(1), node_ref(2)); } else { From 02eb999af1e30b6a61c092baeebe2329ac5719c8 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:09:25 +0300 Subject: [PATCH 4/8] fix tmate start only on successful tests (#3954) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8c14279b92..c3da79c0482 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,7 @@ jobs: - name: Setup tmate debug session continue-on-error: true - if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} + if: ${{ failure() && contains(env.COMMIT_MESSAGE, '[gha-debug]') }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 10 From 09ee8308d089c6e4be8e025edc619b9d650a3acd Mon Sep 17 00:00:00 2001 From: stetsonschott Date: Wed, 17 Jun 2026 20:33:40 -0500 Subject: [PATCH 5/8] Replaced 'C0' by elemental carbon in `openmc/examples.py'. (#3974) --- openmc/examples.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/examples.py b/openmc/examples.py index 90a0bffe7b8..6895bd94b53 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -150,7 +150,7 @@ def pwr_core() -> openmc.Model: rpv_steel.add_nuclide('Ni60', 0.0026776, 'wo') rpv_steel.add_nuclide('Mn55', 0.01, 'wo') rpv_steel.add_nuclide('Cr52', 0.002092475, 'wo') - rpv_steel.add_nuclide('C0', 0.0025, 'wo') + rpv_steel.add_element('C', 0.0025, 'wo') rpv_steel.add_nuclide('Cu63', 0.0013696, 'wo') lower_rad_ref = openmc.Material(6, name='Lower radial reflector') @@ -1618,4 +1618,4 @@ def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): model.settings = settings model.tallies = tallies - return model \ No newline at end of file + return model From 608a1c3386db6af88d70d35cc2202d3955aea30b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 23 Jun 2026 02:00:47 -0500 Subject: [PATCH 6/8] Fix several issues related to independent operator depletion (#3977) --- openmc/deplete/independent_operator.py | 4 ++ openmc/deplete/microxs.py | 63 ++++++++++++++++++------ tests/unit_tests/test_deplete_microxs.py | 60 ++++++++++++++++++++++ 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index c12863956b9..bdfc32763be 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -339,8 +339,12 @@ def get_material_rates(self, mat_index, nuc_index, react_index): for i_nuc in nuc_index: nuc = self.nuc_ind_map[i_nuc] + if nuc not in xs._index_nuc: + continue for i_rx in react_index: rx = self.rx_ind_map[i_rx] + if rx not in xs._index_rx: + continue # Determine reaction rate by multiplying xs in [b] by flux # in [n-cm/src] to give [(reactions/src)*b-cm/atom] diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 687cf646f29..42bb958caf7 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -84,7 +84,12 @@ def get_microxs_and_flux( reactions listed in the depletion chain file are used. energies : iterable of float or str Energy group boundaries in [eV] or the name of the group structure. - If left as None energies will default to [0.0, 100e6] + If left as None, no energy filter is applied to the flux tally. When + `reaction_rate_mode` is "direct", these boundaries define the output + flux and microscopic cross section energy group structure. When + `reaction_rate_mode` is "flux", these boundaries define the multigroup + flux tally used to collapse continuous-energy cross sections; returned + fluxes and microscopic cross sections are one-group. reaction_rate_mode : {"direct", "flux"}, optional The "direct" method tallies reaction rates directly (per energy group). The "flux" method tallies a multigroup flux spectrum and then @@ -110,7 +115,9 @@ def get_microxs_and_flux( reaction_rate_opts : dict, optional When `reaction_rate_mode="flux"`, allows selecting a subset of nuclide/reaction pairs to be computed via direct reaction-rate tallies - (per energy group). Supported keys: "nuclides", "reactions". + over one energy bin spanning the full `energies` range. Supported keys: + "nuclides", "reactions". If "reactions" are specified without + "nuclides", all selected nuclides are used. Returns ------- @@ -139,10 +146,14 @@ def get_microxs_and_flux( nuclides = [nuc.name for nuc in chain.nuclides if nuc.name in nuclides_with_data] - # Set up the reaction rate and flux tallies + # Set up the reaction rate and flux tallies. When energies are omitted, no + # energy filter is needed for the transport calculation. A one-group energy + # range is still needed later if flux collapse is requested. + collapse_energies = energies if energies is None: - energies = [0.0, 100.0e6] - if isinstance(energies, str): + energy_filter = None + collapse_energies = [0.0, 100.0e6] + elif isinstance(energies, str): energy_filter = openmc.EnergyFilter.from_group_structure(energies) else: energy_filter = openmc.EnergyFilter(energies) @@ -172,8 +183,11 @@ def get_microxs_and_flux( rr_reactions = list(reactions) elif reaction_rate_mode == 'flux' and reaction_rate_opts: opts = reaction_rate_opts or {} - rr_nuclides = list(opts.get('nuclides', [])) rr_reactions = list(opts.get('reactions', [])) + if rr_reactions: + rr_nuclides = list(opts.get('nuclides', nuclides)) + else: + rr_nuclides = list(opts.get('nuclides', [])) # Keep only requested pairs within overall sets if rr_nuclides: rr_nuclides = [n for n in rr_nuclides if n in set(nuclides)] @@ -182,7 +196,7 @@ def get_microxs_and_flux( # Use 1-group energy filter for RR in flux mode has_rr = bool(rr_nuclides and rr_reactions) - if has_rr and reaction_rate_mode == 'flux': + if has_rr and reaction_rate_mode == 'flux' and energy_filter is not None: rr_energy_filter = openmc.EnergyFilter( [energy_filter.values[0], energy_filter.values[-1]]) else: @@ -194,14 +208,18 @@ def get_microxs_and_flux( model.tallies = [] for i, domain_filter in enumerate(domain_filters): flux_tally = openmc.Tally(name=f'MicroXS flux {i}') - flux_tally.filters = [domain_filter, energy_filter] + flux_tally.filters = [domain_filter] + if energy_filter is not None: + flux_tally.filters.append(energy_filter) flux_tally.scores = ['flux'] model.tallies.append(flux_tally) flux_tallies.append(flux_tally) if has_rr: rr_tally = openmc.Tally(name=f'MicroXS RR {i}') - rr_tally.filters = [domain_filter, rr_energy_filter] + rr_tally.filters = [domain_filter] + if rr_energy_filter is not None: + rr_tally.filters.append(rr_energy_filter) rr_tally.nuclides = rr_nuclides rr_tally.multiply_density = False rr_tally.scores = rr_reactions @@ -255,8 +273,12 @@ def get_microxs_and_flux( all_flux_arrays = [] for flux_tally in flux_tallies: # Get flux values and make energy groups last dimension - flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) - flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) + flux = flux_tally.get_reshaped_data() + if energy_filter is None: + flux = flux[..., np.newaxis] # (domains, 1, 1, groups) + else: + # (domains, groups, 1, 1) -> (domains, 1, 1, groups) + flux = np.moveaxis(flux, 1, -1) all_flux_arrays.append(flux) fluxes.extend(flux.squeeze((1, 2))) @@ -266,8 +288,15 @@ def get_microxs_and_flux( for flux_arr, rr_tally in zip(all_flux_arrays, rr_tallies): flux = flux_arr # Get reaction rates and make energy groups last dimension - reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) - reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) + reaction_rates = rr_tally.get_reshaped_data() + if rr_energy_filter is None: + # (domains, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = reaction_rates[..., np.newaxis] + else: + # (domains, groups, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = np.moveaxis(reaction_rates, 1, -1) # If RR is 1-group, sum flux over groups if reaction_rate_mode == "flux": @@ -279,16 +308,20 @@ def get_microxs_and_flux( direct_micros.extend( MicroXS(xs_i, rr_nuclides, rr_reactions) for xs_i in xs) - # If using flux mode, compute flux-collapsed microscopic XS if reaction_rate_mode == 'flux': + # Compute flux-collapsed microscopic XS flux_micros = [MicroXS.from_multigroup_flux( - energies=energies, + energies=collapse_energies, multigroup_flux=flux_i, chain_file=chain_file, nuclides=nuclides, reactions=reactions ) for flux_i in fluxes] + # We need to return one-group fluxes to match the microscopic cross + # sections, which are always one-group by virtue of the collapse + fluxes = [flux.sum(keepdims=True) for flux in fluxes] + # Decide which micros to use and merge if needed if reaction_rate_mode == 'flux' and rr_tallies: micros = [m1.merge(m2) for m1, m2 in zip(flux_micros, direct_micros)] diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 26529e6ce96..0b1937facd9 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -179,6 +179,66 @@ def capture_run(**kwargs): assert ef.values[0] == pytest.approx(energies[0]) assert ef.values[-1] == pytest.approx(energies[-1]) + +def _simple_model(): + model = openmc.Model() + mat = openmc.Material(components={'H1': 1.0, 'H2': 1.0}, + density=5.0, density_units='g/cm3') + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model.geometry = openmc.Geometry([cell]) + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.run_mode = 'fixed source' + return model, mat + + +def test_hybrid_tally_defaults_to_all_nuclides(run_in_tmpdir): + energies = [0., 0.625, 2.0e7] + kwargs = { + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)', '(n,gamma)'], + 'energies': energies, + 'reaction_rate_mode': 'flux', + 'chain_file': CHAIN_FILE, + } + + model, mat = _simple_model() + default_fluxes, default_micros = get_microxs_and_flux( + model, [mat], reaction_rate_opts={'reactions': ['(n,2n)']}, **kwargs + ) + + model, mat = _simple_model() + explicit_fluxes, explicit_micros = get_microxs_and_flux( + model, [mat], + reaction_rate_opts={ + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)'] + }, + **kwargs + ) + + np.testing.assert_allclose(default_fluxes[0], explicit_fluxes[0]) + np.testing.assert_allclose(default_micros[0].data, explicit_micros[0].data) + assert default_micros[0].nuclides == explicit_micros[0].nuclides + assert default_micros[0].reactions == explicit_micros[0].reactions + + +def test_flux_mode_returns_one_group_flux(run_in_tmpdir): + model, mat = _simple_model() + fluxes, micros = get_microxs_and_flux( + model, [mat], + nuclides=['H1'], + reactions=['(n,2n)'], + energies=[0., 0.625, 2.0e7], + reaction_rate_mode='flux', + chain_file=CHAIN_FILE, + ) + + assert fluxes[0].shape == (1,) + assert micros[0].data.shape == (1, 1, 1) + assert fluxes[0][0] > 0.0 + # --------------------------------------------------------------------------- # Tests for MicroXS.merge() # --------------------------------------------------------------------------- From 4d6244d93c78daafdb4db19bdff37fd204306666 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 25 Jun 2026 00:09:16 +0300 Subject: [PATCH 7/8] Fix for numpy 2.5.0 (#3981) --- tests/regression_tests/mg_temperature/build_2g.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression_tests/mg_temperature/build_2g.py b/tests/regression_tests/mg_temperature/build_2g.py index 1fb7234499b..bca87364e4b 100644 --- a/tests/regression_tests/mg_temperature/build_2g.py +++ b/tests/regression_tests/mg_temperature/build_2g.py @@ -227,7 +227,7 @@ def analytical_solution_2g_therm(xsmin, xsmax=None, wgt=1.0): L = np.array([sa[0] + ss12, 0.0, -ss12, sa[1]]).reshape(2, 2) Q = np.array([nsf[0], nsf[1], 0.0, 0.0]).reshape(2, 2) arr = np.linalg.inv(L).dot(Q) - return np.amax(np.linalg.eigvals(arr)) + return np.amax(np.linalg.eigvals(arr).real) def build_inf_model(xsnames, xslibname, temperature, tempmethod='nearest'): From d06fdee93a3b6b52a22d90bcee2f6382f6171ee6 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 30 Jun 2026 16:38:09 +0200 Subject: [PATCH 8/8] Preserve user material names in convert_to_multigroup and avoid overwriting material with same name bug fix (#3984) --- openmc/model/model.py | 17 +++++++---- .../infinite_medium/inputs_true.dat | 10 +++---- .../material_wise/inputs_true.dat | 10 +++---- .../stochastic_slab/inputs_true.dat | 10 +++---- .../infinite_medium/inputs_true.dat | 10 +++---- .../material_wise/inputs_true.dat | 10 +++---- .../stochastic_slab/inputs_true.dat | 10 +++---- .../infinite_medium/model/inputs_true.dat | 10 +++---- .../infinite_medium/user/inputs_true.dat | 10 +++---- .../stochastic_slab/model/inputs_true.dat | 10 +++---- .../stochastic_slab/user/inputs_true.dat | 10 +++---- .../infinite_medium/inputs_true.dat | 10 +++---- .../material_wise/inputs_true.dat | 10 +++---- .../stochastic_slab/inputs_true.dat | 10 +++---- .../inputs_true.dat | 10 +++---- tests/unit_tests/test_model.py | 29 +++++++++++++++++++ 16 files changed, 111 insertions(+), 75 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index d927b65ae64..3284942885e 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2772,12 +2772,15 @@ def convert_to_multigroup( self.settings.run_mode = original_run_mode break - # Make sure all materials have a name, and that the name is a valid HDF5 - # dataset name + # Temporarily replace each material's name with a unique, valid HDF5 + # dataset name (its name plus ID) for use as its MGXS library entry + # and macroscopic. The ID keeps the name unique even when materials + # share a name; the original names are restored at the end. + original_names = [material.name for material in self.materials] for material in self.materials: - if not material.name or not material.name.strip(): - material.name = f"material {material.id}" - material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) + base = material.name if material.name and material.name.strip() \ + else "material" + material.name = re.sub(r'[^a-zA-Z0-9]', '_', base) + f"_{material.id}" # If needed, generate the needed MGXS data library file if not Path(mgxs_path).is_file() or overwrite_mgxs_library: @@ -2809,6 +2812,10 @@ def convert_to_multigroup( self.settings.energy_mode = 'multi-group' + # Restore the user's original material names. + for material, name in zip(self.materials, original_names): + material.name = name + def convert_to_random_ray(self): """Convert a multigroup model to use random ray. diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat index 86d5ec4abd5..81f8c98cac7 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat index 86d5ec4abd5..81f8c98cac7 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat index 86d5ec4abd5..81f8c98cac7 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat index b00935ef38a..48b0e8256f8 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat index 472406fa882..be738c3e05d 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat index 472406fa882..be738c3e05d 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat index 15981f7fa5f..d2d8289ff2e 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat index 86d5ec4abd5..81f8c98cac7 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat index 15981f7fa5f..d2d8289ff2e 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat index 86d5ec4abd5..81f8c98cac7 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat index c60e6a04199..c49020d558a 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat index c60e6a04199..c49020d558a 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat index c60e6a04199..c49020d558a 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat index 11100e88e12..0ea8c017760 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 9234b2d2721..028a83b0bdc 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1038,3 +1038,32 @@ def test_id_map_to_rgb(): ) # Check that overlap region is green assert np.allclose(rgb_overlap[5:, 5:], [0.0, 1.0, 0.0]) + + +def test_convert_to_multigroup_preserves_material_names(run_in_tmpdir): + """convert_to_multigroup leaves the user's material names unchanged and keys + the MGXS library by a unique sanitised name + id, so distinct materials that + share a name do not collapse to a single cross section.""" + a = openmc.Material(name="Steel Plate #1") + a.add_element("Fe", 1.0) + a.set_density("g/cm3", 7.9) + b = openmc.Material(name="Steel Plate #1") # same name, distinct material + b.add_element("Fe", 1.0) + b.set_density("g/cm3", 7.9) + + s1 = openmc.Sphere(r=1.0) + s2 = openmc.Sphere(r=2.0, boundary_type="vacuum") + c1 = openmc.Cell(fill=a, region=-s1) + c2 = openmc.Cell(fill=b, region=+s1 & -s2) + model = openmc.Model(openmc.Geometry([c1, c2]), openmc.Materials([a, b])) + + # Pre-create the library so MGXS generation (and transport) is skipped. + Path("mgxs.h5").touch() + model.convert_to_multigroup(method="material_wise", mgxs_path="mgxs.h5") + + # User names are preserved, not sanitised or de-duplicated. + assert [m.name for m in model.materials] == ["Steel Plate #1", "Steel Plate #1"] + # Each material reads a unique, sanitised library entry (name + id). + macro = [m._macroscopic for m in model.materials] + assert macro == [f"Steel_Plate__1_{a.id}", f"Steel_Plate__1_{b.id}"] + assert len(set(macro)) == 2