2023-04-12 15:11:13 -04:00
|
|
|
|
from __future__ import annotations
|
2025-10-28 11:36:03 -05:00
|
|
|
|
from collections.abc import Callable, Iterable, Sequence
|
2025-04-01 21:47:49 -05:00
|
|
|
|
import copy
|
2025-10-28 11:36:03 -05:00
|
|
|
|
from dataclasses import dataclass, field
|
2025-09-26 12:27:05 -04:00
|
|
|
|
from functools import cache
|
2019-03-12 09:58:35 -05:00
|
|
|
|
from pathlib import Path
|
2025-01-31 10:12:24 -06:00
|
|
|
|
import math
|
|
|
|
|
|
from numbers import Integral, Real
|
2025-04-01 21:47:49 -05:00
|
|
|
|
import random
|
|
|
|
|
|
import re
|
2025-01-31 10:12:24 -06:00
|
|
|
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
2025-10-28 11:36:03 -05:00
|
|
|
|
from typing import Any, Protocol
|
2022-10-16 13:06:37 -04:00
|
|
|
|
import warnings
|
2017-04-06 21:20:29 -05:00
|
|
|
|
|
2021-06-24 14:50:05 +07:00
|
|
|
|
import h5py
|
2023-12-13 08:11:32 -06:00
|
|
|
|
import lxml.etree as ET
|
2024-09-27 00:20:40 +02:00
|
|
|
|
import numpy as np
|
2025-10-28 11:36:03 -05:00
|
|
|
|
from scipy.optimize import curve_fit
|
2021-06-24 14:50:05 +07:00
|
|
|
|
|
2017-03-18 16:34:19 -04:00
|
|
|
|
import openmc
|
2022-11-03 19:09:15 -05:00
|
|
|
|
import openmc._xml as xml
|
2021-10-01 09:21:41 -05:00
|
|
|
|
from openmc.dummy_comm import DummyCommunicator
|
|
|
|
|
|
from openmc.executor import _process_CLI_arguments
|
2026-07-09 23:55:32 +02:00
|
|
|
|
from openmc.checkvalue import (check_type, check_value, check_greater_than,
|
|
|
|
|
|
check_length, PathLike)
|
2021-09-29 18:51:00 -05:00
|
|
|
|
from openmc.exceptions import InvalidIDError
|
2025-12-17 21:44:53 +01:00
|
|
|
|
from openmc.plots import add_plot_params, _BASIS_INDICES, id_map_to_rgb
|
2024-05-02 12:47:10 -05:00
|
|
|
|
from openmc.utility_funcs import change_directory
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-28 11:36:03 -05:00
|
|
|
|
# Protocol for a function that is passed to search_keff
|
|
|
|
|
|
class ModelModifier(Protocol):
|
|
|
|
|
|
def __call__(self, val: float, **kwargs: Any) -> None:
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 17:02:14 +02:00
|
|
|
|
def _check_pixels(pixels: int | Sequence[int]) -> None:
|
|
|
|
|
|
if isinstance(pixels, Integral):
|
|
|
|
|
|
check_greater_than('pixels', pixels, 0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
check_length('pixels', pixels, 2)
|
|
|
|
|
|
for p in pixels:
|
|
|
|
|
|
check_greater_than('pixels', p, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-03-19 15:21:15 -05:00
|
|
|
|
class Model:
|
2017-04-06 21:20:29 -05:00
|
|
|
|
"""Model container.
|
|
|
|
|
|
|
|
|
|
|
|
This class can be used to store instances of :class:`openmc.Geometry`,
|
|
|
|
|
|
:class:`openmc.Materials`, :class:`openmc.Settings`,
|
2021-06-24 15:02:32 +07:00
|
|
|
|
:class:`openmc.Tallies`, and :class:`openmc.Plots`, thus making a complete
|
|
|
|
|
|
model. The :meth:`Model.export_to_xml` method will export XML files for all
|
|
|
|
|
|
attributes that have been set. If the :attr:`Model.materials` attribute is
|
|
|
|
|
|
not set, it will attempt to create a ``materials.xml`` file based on all
|
|
|
|
|
|
materials appearing in the geometry.
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. versionchanged:: 0.13.0
|
2021-10-01 09:21:41 -05:00
|
|
|
|
The model information can now be loaded in to OpenMC directly via
|
|
|
|
|
|
openmc.lib
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
2017-03-18 16:34:19 -04:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2017-04-06 21:20:29 -05:00
|
|
|
|
geometry : openmc.Geometry, optional
|
2017-03-18 16:34:19 -04:00
|
|
|
|
Geometry information
|
2017-04-06 21:20:29 -05:00
|
|
|
|
materials : openmc.Materials, optional
|
2017-03-18 16:34:19 -04:00
|
|
|
|
Materials information
|
2017-04-06 21:20:29 -05:00
|
|
|
|
settings : openmc.Settings, optional
|
2017-03-18 16:34:19 -04:00
|
|
|
|
Settings information
|
2017-04-06 21:20:29 -05:00
|
|
|
|
tallies : openmc.Tallies, optional
|
|
|
|
|
|
Tallies information
|
|
|
|
|
|
plots : openmc.Plots, optional
|
|
|
|
|
|
Plot information
|
2026-07-23 16:31:42 +02:00
|
|
|
|
description : str, optional
|
|
|
|
|
|
A description of the model
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
|
|
|
|
|
geometry : openmc.Geometry
|
|
|
|
|
|
Geometry information
|
|
|
|
|
|
materials : openmc.Materials
|
|
|
|
|
|
Materials information
|
|
|
|
|
|
settings : openmc.Settings
|
|
|
|
|
|
Settings information
|
|
|
|
|
|
tallies : openmc.Tallies
|
|
|
|
|
|
Tallies information
|
2017-03-20 21:10:04 -04:00
|
|
|
|
plots : openmc.Plots
|
|
|
|
|
|
Plot information
|
2026-07-23 16:31:42 +02:00
|
|
|
|
description : str
|
|
|
|
|
|
A description of the model
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
geometry: openmc.Geometry | None = None,
|
2025-07-29 05:34:03 -04:00
|
|
|
|
materials: openmc.Materials | None = None,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
settings: openmc.Settings | None = None,
|
|
|
|
|
|
tallies: openmc.Tallies | None = None,
|
|
|
|
|
|
plots: openmc.Plots | None = None,
|
2026-07-23 16:31:42 +02:00
|
|
|
|
description: str = '',
|
2025-05-08 08:10:33 +02:00
|
|
|
|
):
|
2025-01-13 07:19:07 +01:00
|
|
|
|
self.geometry = openmc.Geometry() if geometry is None else geometry
|
|
|
|
|
|
self.materials = openmc.Materials() if materials is None else materials
|
|
|
|
|
|
self.settings = openmc.Settings() if settings is None else settings
|
|
|
|
|
|
self.tallies = openmc.Tallies() if tallies is None else tallies
|
|
|
|
|
|
self.plots = openmc.Plots() if plots is None else plots
|
2026-07-23 16:31:42 +02:00
|
|
|
|
self.description = description
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
|
|
|
|
|
@property
|
2025-07-29 05:34:03 -04:00
|
|
|
|
def geometry(self) -> openmc.Geometry:
|
2017-03-18 16:34:19 -04:00
|
|
|
|
return self._geometry
|
|
|
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
|
@geometry.setter
|
|
|
|
|
|
def geometry(self, geometry):
|
|
|
|
|
|
check_type('geometry', geometry, openmc.Geometry)
|
|
|
|
|
|
self._geometry = geometry
|
|
|
|
|
|
|
2017-03-20 21:10:04 -04:00
|
|
|
|
@property
|
2025-07-29 05:34:03 -04:00
|
|
|
|
def materials(self) -> openmc.Materials:
|
2017-03-20 21:10:04 -04:00
|
|
|
|
return self._materials
|
|
|
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
|
@materials.setter
|
|
|
|
|
|
def materials(self, materials):
|
|
|
|
|
|
check_type('materials', materials, Iterable, openmc.Material)
|
|
|
|
|
|
if isinstance(materials, openmc.Materials):
|
|
|
|
|
|
self._materials = materials
|
|
|
|
|
|
else:
|
2025-07-29 05:34:03 -04:00
|
|
|
|
if not hasattr(self, '_materials'):
|
|
|
|
|
|
self._materials = openmc.Materials()
|
2023-06-17 04:34:02 +01:00
|
|
|
|
del self._materials[:]
|
|
|
|
|
|
for mat in materials:
|
|
|
|
|
|
self._materials.append(mat)
|
|
|
|
|
|
|
2017-03-20 21:10:04 -04:00
|
|
|
|
@property
|
2025-07-29 05:34:03 -04:00
|
|
|
|
def settings(self) -> openmc.Settings:
|
2017-03-20 21:10:04 -04:00
|
|
|
|
return self._settings
|
|
|
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
|
@settings.setter
|
|
|
|
|
|
def settings(self, settings):
|
|
|
|
|
|
check_type('settings', settings, openmc.Settings)
|
|
|
|
|
|
self._settings = settings
|
|
|
|
|
|
|
2017-03-20 21:10:04 -04:00
|
|
|
|
@property
|
2025-07-29 05:34:03 -04:00
|
|
|
|
def tallies(self) -> openmc.Tallies:
|
2017-03-20 21:10:04 -04:00
|
|
|
|
return self._tallies
|
|
|
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
|
@tallies.setter
|
|
|
|
|
|
def tallies(self, tallies):
|
|
|
|
|
|
check_type('tallies', tallies, Iterable, openmc.Tally)
|
|
|
|
|
|
if isinstance(tallies, openmc.Tallies):
|
|
|
|
|
|
self._tallies = tallies
|
|
|
|
|
|
else:
|
2025-07-29 05:34:03 -04:00
|
|
|
|
if not hasattr(self, '_tallies'):
|
|
|
|
|
|
self._tallies = openmc.Tallies()
|
2023-06-17 04:34:02 +01:00
|
|
|
|
del self._tallies[:]
|
|
|
|
|
|
for tally in tallies:
|
|
|
|
|
|
self._tallies.append(tally)
|
|
|
|
|
|
|
2017-03-20 21:10:04 -04:00
|
|
|
|
@property
|
2025-07-29 05:34:03 -04:00
|
|
|
|
def plots(self) -> openmc.Plots:
|
2017-03-20 21:10:04 -04:00
|
|
|
|
return self._plots
|
|
|
|
|
|
|
2023-06-17 04:34:02 +01:00
|
|
|
|
@plots.setter
|
|
|
|
|
|
def plots(self, plots):
|
2025-02-17 21:11:54 -06:00
|
|
|
|
check_type('plots', plots, Iterable, openmc.PlotBase)
|
2023-06-17 04:34:02 +01:00
|
|
|
|
if isinstance(plots, openmc.Plots):
|
|
|
|
|
|
self._plots = plots
|
|
|
|
|
|
else:
|
2025-07-29 05:34:03 -04:00
|
|
|
|
if not hasattr(self, '_plots'):
|
|
|
|
|
|
self._plots = openmc.Plots()
|
2023-06-17 04:34:02 +01:00
|
|
|
|
del self._plots[:]
|
|
|
|
|
|
for plot in plots:
|
|
|
|
|
|
self._plots.append(plot)
|
|
|
|
|
|
|
2026-07-23 16:31:42 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def description(self) -> str:
|
|
|
|
|
|
return self._description
|
|
|
|
|
|
|
|
|
|
|
|
@description.setter
|
|
|
|
|
|
def description(self, description):
|
|
|
|
|
|
check_type('description', description, str)
|
|
|
|
|
|
self._description = description
|
|
|
|
|
|
|
2025-01-31 10:12:24 -06:00
|
|
|
|
@property
|
|
|
|
|
|
def bounding_box(self) -> openmc.BoundingBox:
|
|
|
|
|
|
return self.geometry.bounding_box
|
|
|
|
|
|
|
2021-09-23 16:22:42 -05:00
|
|
|
|
@property
|
2023-04-12 14:22:17 -04:00
|
|
|
|
def is_initialized(self) -> bool:
|
2021-10-26 12:22:07 -05:00
|
|
|
|
try:
|
|
|
|
|
|
import openmc.lib
|
2021-10-25 05:15:34 -05:00
|
|
|
|
return openmc.lib.is_initialized
|
2021-10-26 12:22:07 -05:00
|
|
|
|
except ImportError:
|
2021-10-25 05:15:34 -05:00
|
|
|
|
return False
|
2021-09-23 16:22:42 -05:00
|
|
|
|
|
2022-01-10 07:39:29 -06:00
|
|
|
|
@property
|
2025-09-26 12:27:05 -04:00
|
|
|
|
@cache
|
2023-04-12 14:22:17 -04:00
|
|
|
|
def _materials_by_id(self) -> dict:
|
2022-01-10 07:39:29 -06:00
|
|
|
|
"""Dictionary mapping material ID --> material"""
|
2023-02-14 15:05:37 +01:00
|
|
|
|
if self.materials:
|
2022-01-10 07:39:29 -06:00
|
|
|
|
mats = self.materials
|
2023-02-14 15:05:37 +01:00
|
|
|
|
else:
|
|
|
|
|
|
mats = self.geometry.get_all_materials().values()
|
2022-01-10 07:39:29 -06:00
|
|
|
|
return {mat.id: mat for mat in mats}
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
2025-09-26 12:27:05 -04:00
|
|
|
|
@cache
|
2023-04-12 14:22:17 -04:00
|
|
|
|
def _cells_by_id(self) -> dict:
|
2022-01-10 07:39:29 -06:00
|
|
|
|
"""Dictionary mapping cell ID --> cell"""
|
|
|
|
|
|
cells = self.geometry.get_all_cells()
|
|
|
|
|
|
return {cell.id: cell for cell in cells.values()}
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
2025-09-26 12:27:05 -04:00
|
|
|
|
@cache
|
2024-07-18 12:40:42 -05:00
|
|
|
|
def _cells_by_name(self) -> dict[int, openmc.Cell]:
|
2022-01-10 07:39:29 -06:00
|
|
|
|
# Get the names maps, but since names are not unique, store a set for
|
|
|
|
|
|
# each name key. In this way when the user requests a change by a name,
|
|
|
|
|
|
# the change will be applied to all of the same name.
|
|
|
|
|
|
result = {}
|
|
|
|
|
|
for cell in self.geometry.get_all_cells().values():
|
|
|
|
|
|
if cell.name not in result:
|
|
|
|
|
|
result[cell.name] = set()
|
|
|
|
|
|
result[cell.name].add(cell)
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
2025-09-26 12:27:05 -04:00
|
|
|
|
@cache
|
2024-07-18 12:40:42 -05:00
|
|
|
|
def _materials_by_name(self) -> dict[int, openmc.Material]:
|
2022-01-10 07:39:29 -06:00
|
|
|
|
if self.materials is None:
|
|
|
|
|
|
mats = self.geometry.get_all_materials().values()
|
|
|
|
|
|
else:
|
|
|
|
|
|
mats = self.materials
|
|
|
|
|
|
result = {}
|
|
|
|
|
|
for mat in mats:
|
|
|
|
|
|
if mat.name not in result:
|
|
|
|
|
|
result[mat.name] = set()
|
|
|
|
|
|
result[mat.name].add(mat)
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2025-10-31 19:15:25 -05:00
|
|
|
|
# TODO: This should really get incorporated in lower-level calls to
|
|
|
|
|
|
# get_all_materials, but right now it requires information from the Model object
|
|
|
|
|
|
def _get_all_materials(self) -> dict[int, openmc.Material]:
|
|
|
|
|
|
"""Get all materials including those in DAGMC universes
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
dict
|
|
|
|
|
|
Dictionary mapping material ID to material instances
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Get all materials from the Geometry object
|
|
|
|
|
|
materials = self.geometry.get_all_materials()
|
|
|
|
|
|
|
|
|
|
|
|
# Account for materials in DAGMC universes
|
|
|
|
|
|
for cell in self.geometry.get_all_cells().values():
|
|
|
|
|
|
if isinstance(cell.fill, openmc.DAGMCUniverse):
|
|
|
|
|
|
names = cell.fill.material_names
|
|
|
|
|
|
materials.update({
|
|
|
|
|
|
mat.id: mat for mat in self.materials if mat.name in names
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return materials
|
|
|
|
|
|
|
2025-09-26 12:27:05 -04:00
|
|
|
|
def add_kinetics_parameters_tallies(self, num_groups: int | None = None):
|
|
|
|
|
|
"""Add tallies for calculating kinetics parameters using the IFP method.
|
|
|
|
|
|
|
|
|
|
|
|
This method adds tallies to the model for calculating two kinetics
|
|
|
|
|
|
parameters, the generation time and the effective delayed neutron
|
|
|
|
|
|
fraction (beta effective). After a model is run, these parameters can be
|
|
|
|
|
|
determined through the :meth:`openmc.StatePoint.ifp_results` method.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
num_groups : int, optional
|
|
|
|
|
|
Number of precursor groups to filter the delayed neutron fraction.
|
|
|
|
|
|
If None, only the total effective delayed neutron fraction is
|
|
|
|
|
|
tallied.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not any('ifp-time-numerator' in t.scores for t in self.tallies):
|
|
|
|
|
|
gen_time_tally = openmc.Tally(name='IFP time numerator')
|
|
|
|
|
|
gen_time_tally.scores = ['ifp-time-numerator']
|
|
|
|
|
|
self.tallies.append(gen_time_tally)
|
|
|
|
|
|
if not any('ifp-beta-numerator' in t.scores for t in self.tallies):
|
|
|
|
|
|
beta_tally = openmc.Tally(name='IFP beta numerator')
|
|
|
|
|
|
beta_tally.scores = ['ifp-beta-numerator']
|
|
|
|
|
|
if num_groups is not None:
|
|
|
|
|
|
beta_tally.filters = [openmc.DelayedGroupFilter(list(range(1, num_groups + 1)))]
|
|
|
|
|
|
self.tallies.append(beta_tally)
|
|
|
|
|
|
if not any('ifp-denominator' in t.scores for t in self.tallies):
|
|
|
|
|
|
denom_tally = openmc.Tally(name='IFP denominator')
|
|
|
|
|
|
denom_tally.scores = ['ifp-denominator']
|
|
|
|
|
|
self.tallies.append(denom_tally)
|
2026-06-08 13:22:32 -05:00
|
|
|
|
|
|
|
|
|
|
# TODO: This should also be incorporated into lower-level calls in
|
2026-04-28 17:10:03 -04:00
|
|
|
|
# settings.py, but it requires information about the tallies currently
|
|
|
|
|
|
# on the active Model
|
|
|
|
|
|
def _assign_fw_cadis_tally_IDs(self):
|
2026-06-08 13:22:32 -05:00
|
|
|
|
# Verify that all tallies assigned as targets on WeightWindowGenerators
|
|
|
|
|
|
# exist within model.tallies. If this is the case, convert the .targets
|
2026-04-28 17:10:03 -04:00
|
|
|
|
# attribute of each WeightWindowGenerator to a sequence of tally IDs.
|
|
|
|
|
|
if len(self.settings.weight_window_generators) == 0:
|
|
|
|
|
|
return
|
2026-06-08 13:22:32 -05:00
|
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
|
# List of valid tally IDs
|
|
|
|
|
|
reference_tally_ids = np.asarray([tal.id for tal in self.tallies])
|
2026-06-08 13:22:32 -05:00
|
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
|
for wwg in self.settings.weight_window_generators:
|
2026-06-08 13:22:32 -05:00
|
|
|
|
# Only proceeds if the "targets" attribute is an openmc.Tallies,
|
2026-04-28 17:10:03 -04:00
|
|
|
|
# which means it hasn't been checked against model.tallies.
|
|
|
|
|
|
if isinstance(wwg.targets, openmc.Tallies):
|
|
|
|
|
|
id_vec = []
|
|
|
|
|
|
for tal in wwg.targets:
|
|
|
|
|
|
# check against model tallies for equivalence
|
|
|
|
|
|
id_next = None
|
|
|
|
|
|
for reference_tal in self.tallies:
|
|
|
|
|
|
if tal == reference_tal:
|
|
|
|
|
|
id_next = reference_tal.id
|
|
|
|
|
|
break
|
2026-06-08 13:22:32 -05:00
|
|
|
|
|
2026-06-08 16:08:08 -05:00
|
|
|
|
if id_next is None:
|
2026-04-28 17:10:03 -04:00
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
f'Local FW-CADIS target tally {tal.id} not found on model.tallies!')
|
|
|
|
|
|
else:
|
|
|
|
|
|
id_vec.append(id_next)
|
|
|
|
|
|
|
|
|
|
|
|
wwg.targets = id_vec
|
|
|
|
|
|
|
|
|
|
|
|
elif isinstance(wwg.targets, np.ndarray):
|
|
|
|
|
|
invalid = wwg.targets[~np.isin(wwg.targets, reference_tally_ids)]
|
|
|
|
|
|
if len(invalid) > 0:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
f'Local FW-CADIS target tally IDs {invalid} not found on model.tallies!')
|
2025-09-26 12:27:05 -04:00
|
|
|
|
|
2021-06-24 14:56:38 +07:00
|
|
|
|
@classmethod
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def from_xml(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
geometry: PathLike = "geometry.xml",
|
|
|
|
|
|
materials: PathLike = "materials.xml",
|
|
|
|
|
|
settings: PathLike = "settings.xml",
|
|
|
|
|
|
tallies: PathLike = "tallies.xml",
|
|
|
|
|
|
plots: PathLike = "plots.xml",
|
|
|
|
|
|
) -> Model:
|
2022-12-06 13:09:04 -06:00
|
|
|
|
"""Create model from existing XML files
|
2022-11-23 22:16:34 -06:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2025-05-08 08:10:33 +02:00
|
|
|
|
geometry : PathLike
|
2022-12-06 13:09:04 -06:00
|
|
|
|
Path to geometry.xml file
|
2025-05-08 08:10:33 +02:00
|
|
|
|
materials : PathLike
|
2022-12-06 13:09:04 -06:00
|
|
|
|
Path to materials.xml file
|
2025-05-08 08:10:33 +02:00
|
|
|
|
settings : PathLike
|
2022-12-06 13:09:04 -06:00
|
|
|
|
Path to settings.xml file
|
2025-05-08 08:10:33 +02:00
|
|
|
|
tallies : PathLike
|
2022-12-06 13:09:04 -06:00
|
|
|
|
Path to tallies.xml file
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
2025-05-08 08:10:33 +02:00
|
|
|
|
plots : PathLike
|
2022-12-06 13:09:04 -06:00
|
|
|
|
Path to plots.xml file
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
2022-11-23 22:16:34 -06:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2022-12-06 13:09:04 -06:00
|
|
|
|
openmc.model.Model
|
|
|
|
|
|
Model created from XML files
|
2022-11-23 22:16:34 -06:00
|
|
|
|
|
|
|
|
|
|
"""
|
2022-12-06 13:09:04 -06:00
|
|
|
|
materials = openmc.Materials.from_xml(materials)
|
|
|
|
|
|
geometry = openmc.Geometry.from_xml(geometry, materials)
|
|
|
|
|
|
settings = openmc.Settings.from_xml(settings)
|
2025-02-17 21:11:54 -06:00
|
|
|
|
tallies = openmc.Tallies.from_xml(
|
|
|
|
|
|
tallies) if Path(tallies).exists() else None
|
2022-12-06 13:09:04 -06:00
|
|
|
|
plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None
|
|
|
|
|
|
return cls(geometry, materials, settings, tallies, plots)
|
2022-11-03 22:29:36 -05:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def from_model_xml(cls, path: PathLike = "model.xml") -> Model:
|
2022-11-03 22:29:36 -05:00
|
|
|
|
"""Create model from single XML file
|
|
|
|
|
|
|
2023-03-16 09:45:29 -05:00
|
|
|
|
.. versionadded:: 0.13.3
|
2022-12-22 18:27:42 -06:00
|
|
|
|
|
2022-11-03 22:29:36 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2025-05-08 08:10:33 +02:00
|
|
|
|
path : PathLike
|
2022-11-03 22:29:36 -05:00
|
|
|
|
Path to model.xml file
|
|
|
|
|
|
"""
|
2024-01-16 22:54:03 -08:00
|
|
|
|
parser = ET.XMLParser(huge_tree=True)
|
|
|
|
|
|
tree = ET.parse(path, parser=parser)
|
2022-11-03 22:29:36 -05:00
|
|
|
|
root = tree.getroot()
|
|
|
|
|
|
|
|
|
|
|
|
model = cls()
|
|
|
|
|
|
|
2026-07-23 16:31:42 +02:00
|
|
|
|
desc_elem = root.find('description')
|
|
|
|
|
|
if desc_elem is not None and desc_elem.text:
|
|
|
|
|
|
model.description = desc_elem.text
|
|
|
|
|
|
|
2022-12-13 23:16:57 -06:00
|
|
|
|
meshes = {}
|
2025-02-17 21:11:54 -06:00
|
|
|
|
model.settings = openmc.Settings.from_xml_element(
|
|
|
|
|
|
root.find('settings'), meshes)
|
|
|
|
|
|
model.materials = openmc.Materials.from_xml_element(
|
|
|
|
|
|
root.find('materials'))
|
|
|
|
|
|
model.geometry = openmc.Geometry.from_xml_element(
|
|
|
|
|
|
root.find('geometry'), model.materials)
|
2022-11-03 22:29:36 -05:00
|
|
|
|
|
2023-06-22 13:26:22 -05:00
|
|
|
|
if root.find('tallies') is not None:
|
2025-02-17 21:11:54 -06:00
|
|
|
|
model.tallies = openmc.Tallies.from_xml_element(
|
|
|
|
|
|
root.find('tallies'), meshes)
|
2022-11-03 22:29:36 -05:00
|
|
|
|
|
2023-06-22 13:26:22 -05:00
|
|
|
|
if root.find('plots') is not None:
|
2022-11-04 00:58:43 -05:00
|
|
|
|
model.plots = openmc.Plots.from_xml_element(root.find('plots'))
|
2022-11-03 22:29:36 -05:00
|
|
|
|
|
|
|
|
|
|
return model
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def init_lib(
|
|
|
|
|
|
self,
|
|
|
|
|
|
threads: int | None = None,
|
|
|
|
|
|
geometry_debug: bool = False,
|
|
|
|
|
|
restart_file: PathLike | None = None,
|
|
|
|
|
|
tracks: bool = False,
|
|
|
|
|
|
output: bool = True,
|
|
|
|
|
|
event_based: bool | None = None,
|
|
|
|
|
|
intracomm=None,
|
|
|
|
|
|
directory: PathLike | None = None,
|
|
|
|
|
|
):
|
2021-09-29 18:51:00 -05:00
|
|
|
|
"""Initializes the model in memory via the C API
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
2021-09-27 12:32:25 -05:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
threads : int, optional
|
2021-09-29 18:51:00 -05:00
|
|
|
|
Number of OpenMP threads. If OpenMC is compiled with OpenMP
|
|
|
|
|
|
threading enabled, the default is implementation-dependent but is
|
|
|
|
|
|
usually equal to the number of hardware threads available
|
|
|
|
|
|
(or a value set by the :envvar:`OMP_NUM_THREADS` environment
|
|
|
|
|
|
variable).
|
2021-09-27 12:32:25 -05:00
|
|
|
|
geometry_debug : bool, optional
|
|
|
|
|
|
Turn on geometry debugging during simulation. Defaults to False.
|
2025-05-08 08:10:33 +02:00
|
|
|
|
restart_file : PathLike, optional
|
2021-09-27 12:32:25 -05:00
|
|
|
|
Path to restart file to use
|
|
|
|
|
|
tracks : bool, optional
|
2022-09-09 11:40:15 +01:00
|
|
|
|
Enables the writing of particles tracks. The number of particle
|
|
|
|
|
|
tracks written to tracks.h5 is limited to 1000 unless
|
|
|
|
|
|
Settings.max_tracks is set. Defaults to False.
|
2021-09-27 12:32:25 -05:00
|
|
|
|
output : bool
|
|
|
|
|
|
Capture OpenMC output from standard out
|
2021-09-30 16:34:58 -05:00
|
|
|
|
event_based : None or bool, optional
|
|
|
|
|
|
Turns on event-based parallelism if True. If None, the value in
|
|
|
|
|
|
the Settings will be used.
|
2021-10-05 08:39:24 -05:00
|
|
|
|
intracomm : mpi4py.MPI.Intracomm or None, optional
|
2021-10-01 09:21:41 -05:00
|
|
|
|
MPI intracommunicator
|
2025-05-08 08:10:33 +02:00
|
|
|
|
directory : PathLike or None, optional
|
2025-04-22 12:17:04 -05:00
|
|
|
|
Directory to write XML files to. Defaults to None.
|
2021-09-27 12:32:25 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
2021-11-09 09:19:50 -06:00
|
|
|
|
import openmc.lib
|
2021-11-02 08:17:47 -05:00
|
|
|
|
|
2021-09-27 12:32:25 -05:00
|
|
|
|
# TODO: right now the only way to set most of the above parameters via
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# the C API are at initialization time despite use-cases existing to
|
2021-09-27 12:32:25 -05:00
|
|
|
|
# set them for individual runs. For now this functionality is exposed
|
|
|
|
|
|
# where it exists (here in init), but in the future the functionality
|
|
|
|
|
|
# should be exposed so that it can be accessed via model.run(...)
|
|
|
|
|
|
|
2021-10-01 09:21:41 -05:00
|
|
|
|
args = _process_CLI_arguments(
|
2021-09-30 11:18:35 -05:00
|
|
|
|
volume=False, geometry_debug=geometry_debug,
|
|
|
|
|
|
restart_file=restart_file, threads=threads, tracks=tracks,
|
2025-04-22 12:17:04 -05:00
|
|
|
|
event_based=event_based, path_input=directory)
|
|
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
# Args adds the openmc_exec command in the first entry; remove it
|
|
|
|
|
|
args = args[1:]
|
2021-09-23 16:22:42 -05:00
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
self.finalize_lib()
|
2021-09-23 16:22:42 -05:00
|
|
|
|
|
2021-10-01 09:21:41 -05:00
|
|
|
|
# The Model object needs to be aware of the communicator so it can
|
|
|
|
|
|
# use it in certain cases, therefore lets store the communicator
|
|
|
|
|
|
if intracomm is not None:
|
|
|
|
|
|
self._intracomm = intracomm
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._intracomm = DummyCommunicator()
|
|
|
|
|
|
|
|
|
|
|
|
if self._intracomm.rank == 0:
|
2025-04-22 12:17:04 -05:00
|
|
|
|
if directory is not None:
|
|
|
|
|
|
self.export_to_xml(directory=directory)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.export_to_xml()
|
2021-10-01 09:21:41 -05:00
|
|
|
|
self._intracomm.barrier()
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
2021-10-05 08:39:24 -05:00
|
|
|
|
# We cannot pass DummyCommunicator to openmc.lib.init so pass instead
|
|
|
|
|
|
# the user-provided intracomm which will either be None or an mpi4py
|
|
|
|
|
|
# communicator
|
|
|
|
|
|
openmc.lib.init(args=args, intracomm=intracomm, output=output)
|
2021-09-29 08:22:16 -05:00
|
|
|
|
|
2025-01-07 22:50:02 +01:00
|
|
|
|
def sync_dagmc_universes(self):
|
|
|
|
|
|
"""Synchronize all DAGMC universes in the current geometry.
|
|
|
|
|
|
|
|
|
|
|
|
This method iterates over all DAGMC universes in the geometry and
|
|
|
|
|
|
synchronizes their cells with the current material assignments. Requires
|
|
|
|
|
|
that the model has been initialized via :meth:`Model.init_lib`.
|
2026-05-20 16:43:21 -05:00
|
|
|
|
Synchronized DAGMC cells can then be edited and exported as nested
|
|
|
|
|
|
`<cell>` overrides inside each `<dagmc_universe>` element.
|
2025-01-07 22:50:02 +01:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.is_initialized:
|
|
|
|
|
|
if self.materials:
|
|
|
|
|
|
materials = self.materials
|
|
|
|
|
|
else:
|
|
|
|
|
|
materials = list(self.geometry.get_all_materials().values())
|
|
|
|
|
|
for univ in self.geometry.get_all_universes().values():
|
|
|
|
|
|
if isinstance(univ, openmc.DAGMCUniverse):
|
|
|
|
|
|
univ.sync_dagmc_cells(materials)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError("The model must be initialized before calling "
|
|
|
|
|
|
"this method")
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
def finalize_lib(self):
|
|
|
|
|
|
"""Finalize simulation and free memory allocated for the C API
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
2021-09-23 16:22:42 -05:00
|
|
|
|
|
2021-11-09 09:19:50 -06:00
|
|
|
|
import openmc.lib
|
2021-11-02 08:17:47 -05:00
|
|
|
|
|
2021-09-23 16:22:42 -05:00
|
|
|
|
openmc.lib.finalize()
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def deplete(
|
|
|
|
|
|
self,
|
|
|
|
|
|
method: str = "cecm",
|
|
|
|
|
|
final_step: bool = True,
|
|
|
|
|
|
operator_kwargs: dict | None = None,
|
|
|
|
|
|
directory: PathLike = ".",
|
|
|
|
|
|
output: bool = True,
|
|
|
|
|
|
**integrator_kwargs,
|
|
|
|
|
|
):
|
2018-02-20 16:40:18 -06:00
|
|
|
|
"""Deplete model using specified timesteps/power
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. versionchanged:: 0.13.0
|
2021-10-01 09:21:41 -05:00
|
|
|
|
The *final_step*, *operator_kwargs*, *directory*, and *output*
|
|
|
|
|
|
arguments were added.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
2018-02-20 16:40:18 -06:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2025-05-08 08:10:33 +02:00
|
|
|
|
timesteps : iterable of float or iterable of tuple
|
|
|
|
|
|
Array of timesteps. Note that values are not cumulative. The units are
|
|
|
|
|
|
specified by the `timestep_units` argument when `timesteps` is an
|
|
|
|
|
|
iterable of float. Alternatively, units can be specified for each step
|
|
|
|
|
|
by passing an iterable of (value, unit) tuples.
|
|
|
|
|
|
method : str
|
2021-09-24 16:33:47 -05:00
|
|
|
|
Integration method used for depletion (e.g., 'cecm', 'predictor').
|
|
|
|
|
|
Defaults to 'cecm'.
|
2021-09-23 16:22:42 -05:00
|
|
|
|
final_step : bool, optional
|
|
|
|
|
|
Indicate whether or not a transport solve should be run at the end
|
2021-09-24 16:33:47 -05:00
|
|
|
|
of the last timestep. Defaults to running this transport solve.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
operator_kwargs : dict
|
2022-08-12 14:37:39 -05:00
|
|
|
|
Keyword arguments passed to the depletion operator initializer
|
2021-09-29 18:51:00 -05:00
|
|
|
|
(e.g., :func:`openmc.deplete.Operator`)
|
2025-05-08 08:10:33 +02:00
|
|
|
|
directory : PathLike, optional
|
2021-09-24 16:33:47 -05:00
|
|
|
|
Directory to write XML files to. If it doesn't exist already, it
|
|
|
|
|
|
will be created. Defaults to the current working directory
|
2021-09-30 14:39:53 -05:00
|
|
|
|
output : bool
|
|
|
|
|
|
Capture OpenMC output from standard out
|
2021-09-30 11:18:35 -05:00
|
|
|
|
integrator_kwargs : dict
|
2025-05-08 08:10:33 +02:00
|
|
|
|
Remaining keyword arguments passed to the depletion integrator
|
|
|
|
|
|
(e.g., :class:`openmc.deplete.CECMIntegrator`).
|
2018-02-20 16:40:18 -06:00
|
|
|
|
|
2017-03-18 16:34:19 -04:00
|
|
|
|
"""
|
2021-09-23 16:22:42 -05:00
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
if operator_kwargs is None:
|
|
|
|
|
|
op_kwargs = {}
|
|
|
|
|
|
elif isinstance(operator_kwargs, dict):
|
|
|
|
|
|
op_kwargs = operator_kwargs
|
|
|
|
|
|
else:
|
2021-10-01 06:09:24 -05:00
|
|
|
|
raise ValueError("operator_kwargs must be a dict or None")
|
2021-09-30 11:18:35 -05:00
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# Import openmc.deplete here so the Model can be used even if the
|
|
|
|
|
|
# shared library is unavailable.
|
|
|
|
|
|
import openmc.deplete as dep
|
|
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
# Store whether or not the library was initialized when we started
|
|
|
|
|
|
started_initialized = self.is_initialized
|
|
|
|
|
|
|
2024-05-02 12:47:10 -05:00
|
|
|
|
with change_directory(directory):
|
2021-09-30 14:39:53 -05:00
|
|
|
|
with openmc.lib.quiet_dll(output):
|
2022-08-12 14:37:39 -05:00
|
|
|
|
# TODO: Support use of IndependentOperator too
|
|
|
|
|
|
depletion_operator = dep.CoupledOperator(self, **op_kwargs)
|
2021-09-30 11:18:35 -05:00
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# Tell depletion_operator.finalize NOT to clear C API memory when
|
|
|
|
|
|
# it is done
|
|
|
|
|
|
depletion_operator.cleanup_when_done = False
|
|
|
|
|
|
|
|
|
|
|
|
# Set up the integrator
|
|
|
|
|
|
check_value('method', method,
|
|
|
|
|
|
dep.integrators.integrator_by_name.keys())
|
|
|
|
|
|
integrator_class = dep.integrators.integrator_by_name[method]
|
2025-05-08 08:10:33 +02:00
|
|
|
|
integrator = integrator_class(depletion_operator, **integrator_kwargs)
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
|
|
|
|
|
# Now perform the depletion
|
2021-09-30 14:39:53 -05:00
|
|
|
|
with openmc.lib.quiet_dll(output):
|
|
|
|
|
|
integrator.integrate(final_step)
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
|
|
|
|
|
# Now make the python Materials match the C API material data
|
|
|
|
|
|
for mat_id, mat in self._materials_by_id.items():
|
|
|
|
|
|
if mat.depletable:
|
|
|
|
|
|
# Get the C data
|
|
|
|
|
|
c_mat = openmc.lib.materials[mat_id]
|
|
|
|
|
|
nuclides, densities = c_mat._get_densities()
|
|
|
|
|
|
# And now we can remove isotopes and add these ones in
|
|
|
|
|
|
mat.nuclides.clear()
|
|
|
|
|
|
for nuc, density in zip(nuclides, densities):
|
|
|
|
|
|
mat.add_nuclide(nuc, density)
|
|
|
|
|
|
mat.set_density('atom/b-cm', sum(densities))
|
2021-09-24 16:33:47 -05:00
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
# If we didnt start intialized, we should cleanup after ourselves
|
|
|
|
|
|
if not started_initialized:
|
|
|
|
|
|
depletion_operator.cleanup_when_done = True
|
|
|
|
|
|
depletion_operator.finalize()
|
|
|
|
|
|
|
2025-12-04 06:53:04 -06:00
|
|
|
|
def _link_geometry_to_filters(self):
|
|
|
|
|
|
"""Establishes a link between distribcell filters and the geometry"""
|
|
|
|
|
|
for tally in self.tallies:
|
|
|
|
|
|
for f in tally.filters:
|
|
|
|
|
|
if isinstance(f, openmc.DistribcellFilter):
|
|
|
|
|
|
f._geometry = self.geometry
|
|
|
|
|
|
|
2025-02-20 13:07:07 -06:00
|
|
|
|
def export_to_xml(self, directory: PathLike = '.', remove_surfs: bool = False,
|
|
|
|
|
|
nuclides_to_ignore: Iterable[str] | None = None):
|
2022-11-03 19:18:39 -05:00
|
|
|
|
"""Export model to separate XML files.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2025-05-08 08:10:33 +02:00
|
|
|
|
directory : PathLike
|
2022-11-03 19:18:39 -05:00
|
|
|
|
Directory to write XML files to. If it doesn't exist already, it
|
|
|
|
|
|
will be created.
|
|
|
|
|
|
remove_surfs : bool
|
|
|
|
|
|
Whether or not to remove redundant surfaces from the geometry when
|
|
|
|
|
|
exporting.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.1
|
2025-02-20 13:07:07 -06:00
|
|
|
|
nuclides_to_ignore : list of str
|
|
|
|
|
|
Nuclides to ignore when exporting to XML.
|
|
|
|
|
|
|
2022-11-03 19:18:39 -05:00
|
|
|
|
"""
|
|
|
|
|
|
# Create directory if required
|
|
|
|
|
|
d = Path(directory)
|
|
|
|
|
|
if not d.is_dir():
|
2024-03-17 15:23:24 +00:00
|
|
|
|
d.mkdir(parents=True, exist_ok=True)
|
2022-11-03 19:18:39 -05:00
|
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
|
self._assign_fw_cadis_tally_IDs()
|
2022-11-03 19:18:39 -05:00
|
|
|
|
self.settings.export_to_xml(d)
|
|
|
|
|
|
self.geometry.export_to_xml(d, remove_surfs=remove_surfs)
|
|
|
|
|
|
|
|
|
|
|
|
# If a materials collection was specified, export it. Otherwise, look
|
|
|
|
|
|
# for all materials in the geometry and use that to automatically build
|
|
|
|
|
|
# a collection.
|
|
|
|
|
|
if self.materials:
|
2025-02-20 13:07:07 -06:00
|
|
|
|
self.materials.export_to_xml(d, nuclides_to_ignore=nuclides_to_ignore)
|
2022-11-03 19:18:39 -05:00
|
|
|
|
else:
|
|
|
|
|
|
materials = openmc.Materials(self.geometry.get_all_materials()
|
|
|
|
|
|
.values())
|
2025-02-20 13:07:07 -06:00
|
|
|
|
materials.export_to_xml(d, nuclides_to_ignore=nuclides_to_ignore)
|
2022-11-03 19:18:39 -05:00
|
|
|
|
|
|
|
|
|
|
if self.tallies:
|
|
|
|
|
|
self.tallies.export_to_xml(d)
|
|
|
|
|
|
if self.plots:
|
|
|
|
|
|
self.plots.export_to_xml(d)
|
|
|
|
|
|
|
2025-12-04 06:53:04 -06:00
|
|
|
|
self._link_geometry_to_filters()
|
|
|
|
|
|
|
2025-02-20 13:07:07 -06:00
|
|
|
|
def export_to_model_xml(self, path: PathLike = 'model.xml', remove_surfs: bool = False,
|
|
|
|
|
|
nuclides_to_ignore: Iterable[str] | None = None):
|
2022-12-07 08:28:13 -06:00
|
|
|
|
"""Export model to a single XML file.
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
2022-12-22 18:27:42 -06:00
|
|
|
|
.. versionadded:: 0.13.3
|
|
|
|
|
|
|
2019-03-12 09:58:35 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2023-02-16 13:53:57 -06:00
|
|
|
|
path : str or PathLike
|
2022-12-12 21:18:48 -06:00
|
|
|
|
Location of the XML file to write (default is 'model.xml'). Can be a
|
|
|
|
|
|
directory or file path.
|
2022-04-01 07:29:36 -05:00
|
|
|
|
remove_surfs : bool
|
|
|
|
|
|
Whether or not to remove redundant surfaces from the geometry when
|
2022-04-07 13:23:12 -05:00
|
|
|
|
exporting.
|
2025-02-20 13:07:07 -06:00
|
|
|
|
nuclides_to_ignore : list of str
|
|
|
|
|
|
Nuclides to ignore when exporting to XML.
|
2019-03-12 09:58:35 -05:00
|
|
|
|
|
|
|
|
|
|
"""
|
2022-11-23 22:16:34 -06:00
|
|
|
|
xml_path = Path(path)
|
2022-12-06 12:58:32 -06:00
|
|
|
|
# if the provided path doesn't end with the XML extension, assume the
|
|
|
|
|
|
# input path is meant to be a directory. If the directory does not
|
|
|
|
|
|
# exist, create it and place a 'model.xml' file there.
|
2024-03-11 22:35:32 -05:00
|
|
|
|
if not str(xml_path).endswith('.xml'):
|
|
|
|
|
|
if not xml_path.exists():
|
2024-03-17 15:23:24 +00:00
|
|
|
|
xml_path.mkdir(parents=True, exist_ok=True)
|
2024-03-11 22:35:32 -05:00
|
|
|
|
elif not xml_path.is_dir():
|
|
|
|
|
|
raise FileExistsError(f"File exists and is not a directory: '{xml_path}'")
|
2022-12-06 12:58:32 -06:00
|
|
|
|
xml_path /= 'model.xml'
|
|
|
|
|
|
# if this is an XML file location and the file's parent directory does
|
|
|
|
|
|
# not exist, create it before continuing
|
|
|
|
|
|
elif not xml_path.parent.exists():
|
2024-03-17 15:23:24 +00:00
|
|
|
|
xml_path.parent.mkdir(parents=True, exist_ok=True)
|
2019-03-12 09:58:35 -05:00
|
|
|
|
|
2022-10-16 13:06:37 -04:00
|
|
|
|
if remove_surfs:
|
|
|
|
|
|
warnings.warn("remove_surfs kwarg will be deprecated soon, please "
|
2022-10-26 08:20:25 -04:00
|
|
|
|
"set the Geometry.merge_surfaces attribute instead.")
|
|
|
|
|
|
self.geometry.merge_surfaces = True
|
2022-11-08 21:04:15 -05:00
|
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
|
# Link FW-CADIS WeightWindowGenerator target tallies, if present
|
|
|
|
|
|
self._assign_fw_cadis_tally_IDs()
|
|
|
|
|
|
|
2022-12-07 10:10:22 -06:00
|
|
|
|
# provide a memo to track which meshes have been written
|
|
|
|
|
|
mesh_memo = set()
|
|
|
|
|
|
settings_element = self.settings.to_xml_element(mesh_memo)
|
2022-11-03 19:09:15 -05:00
|
|
|
|
geometry_element = self.geometry.to_xml_element()
|
2017-04-06 21:20:29 -05:00
|
|
|
|
|
2022-11-25 08:22:57 -06:00
|
|
|
|
xml.clean_indentation(geometry_element, level=1)
|
|
|
|
|
|
xml.clean_indentation(settings_element, level=1)
|
|
|
|
|
|
|
2017-04-06 21:20:29 -05:00
|
|
|
|
# If a materials collection was specified, export it. Otherwise, look
|
|
|
|
|
|
# for all materials in the geometry and use that to automatically build
|
|
|
|
|
|
# a collection.
|
|
|
|
|
|
if self.materials:
|
2022-11-03 19:09:15 -05:00
|
|
|
|
materials = self.materials
|
2017-04-06 21:20:29 -05:00
|
|
|
|
else:
|
|
|
|
|
|
materials = openmc.Materials(self.geometry.get_all_materials()
|
|
|
|
|
|
.values())
|
|
|
|
|
|
|
2022-11-23 22:16:34 -06:00
|
|
|
|
with open(xml_path, 'w', encoding='utf-8', errors='xmlcharrefreplace') as fh:
|
2022-11-03 19:12:18 -05:00
|
|
|
|
# write the XML header
|
|
|
|
|
|
fh.write("<?xml version='1.0' encoding='utf-8'?>\n")
|
|
|
|
|
|
fh.write("<model>\n")
|
2026-07-23 16:31:42 +02:00
|
|
|
|
if self.description:
|
|
|
|
|
|
description_element = ET.Element('description')
|
|
|
|
|
|
description_element.text = self.description
|
|
|
|
|
|
fh.write(" ")
|
|
|
|
|
|
fh.write(ET.tostring(description_element, encoding="unicode"))
|
|
|
|
|
|
fh.write("\n")
|
2022-11-03 19:09:15 -05:00
|
|
|
|
# Write the materials collection to the open XML file first.
|
|
|
|
|
|
# This will write the XML header also
|
2025-02-20 13:07:07 -06:00
|
|
|
|
materials._write_xml(fh, False, level=1,
|
|
|
|
|
|
nuclides_to_ignore=nuclides_to_ignore)
|
2022-11-03 19:09:15 -05:00
|
|
|
|
# Write remaining elements as a tree
|
2023-05-09 11:41:04 -04:00
|
|
|
|
fh.write(ET.tostring(geometry_element, encoding="unicode"))
|
|
|
|
|
|
fh.write(ET.tostring(settings_element, encoding="unicode"))
|
2022-11-03 19:09:15 -05:00
|
|
|
|
|
|
|
|
|
|
if self.tallies:
|
2022-12-07 10:10:22 -06:00
|
|
|
|
tallies_element = self.tallies.to_xml_element(mesh_memo)
|
2025-02-17 21:11:54 -06:00
|
|
|
|
xml.clean_indentation(
|
|
|
|
|
|
tallies_element, level=1, trailing_indent=self.plots)
|
2023-05-09 11:41:04 -04:00
|
|
|
|
fh.write(ET.tostring(tallies_element, encoding="unicode"))
|
2022-11-03 19:09:15 -05:00
|
|
|
|
if self.plots:
|
|
|
|
|
|
plots_element = self.plots.to_xml_element()
|
2025-02-17 21:11:54 -06:00
|
|
|
|
xml.clean_indentation(
|
|
|
|
|
|
plots_element, level=1, trailing_indent=False)
|
2023-05-09 11:41:04 -04:00
|
|
|
|
fh.write(ET.tostring(plots_element, encoding="unicode"))
|
2022-11-03 19:12:18 -05:00
|
|
|
|
fh.write("</model>\n")
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
2025-12-04 06:53:04 -06:00
|
|
|
|
self._link_geometry_to_filters()
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def import_properties(self, filename: PathLike):
|
2021-06-24 14:50:05 +07:00
|
|
|
|
"""Import physical properties
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. versionchanged:: 0.13.0
|
|
|
|
|
|
This method now updates values as loaded in memory with the C API
|
2021-09-24 16:33:47 -05:00
|
|
|
|
|
2021-06-24 14:50:05 +07:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2025-05-08 08:10:33 +02:00
|
|
|
|
filename : PathLike
|
2021-06-24 14:50:05 +07:00
|
|
|
|
Path to properties HDF5 file
|
|
|
|
|
|
|
|
|
|
|
|
See Also
|
|
|
|
|
|
--------
|
|
|
|
|
|
openmc.lib.export_properties
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
2021-11-09 09:19:50 -06:00
|
|
|
|
import openmc.lib
|
2021-11-02 08:17:47 -05:00
|
|
|
|
|
2021-06-24 14:50:05 +07:00
|
|
|
|
cells = self.geometry.get_all_cells()
|
|
|
|
|
|
materials = self.geometry.get_all_materials()
|
|
|
|
|
|
|
|
|
|
|
|
with h5py.File(filename, 'r') as fh:
|
|
|
|
|
|
cells_group = fh['geometry/cells']
|
2021-07-12 06:58:04 -05:00
|
|
|
|
|
|
|
|
|
|
# Make sure number of cells matches
|
|
|
|
|
|
n_cells = fh['geometry'].attrs['n_cells']
|
|
|
|
|
|
if n_cells != len(cells):
|
|
|
|
|
|
raise ValueError("Number of cells in properties file doesn't "
|
|
|
|
|
|
"match current model.")
|
|
|
|
|
|
|
2025-09-18 23:11:06 -05:00
|
|
|
|
# Update temperatures and densities for cells filled with materials
|
2021-06-24 14:50:05 +07:00
|
|
|
|
for name, group in cells_group.items():
|
|
|
|
|
|
cell_id = int(name.split()[1])
|
|
|
|
|
|
cell = cells[cell_id]
|
|
|
|
|
|
if cell.fill_type in ('material', 'distribmat'):
|
2023-06-22 13:26:22 -05:00
|
|
|
|
temperature = group['temperature'][()]
|
|
|
|
|
|
cell.temperature = temperature
|
2021-09-29 18:51:00 -05:00
|
|
|
|
if self.is_initialized:
|
|
|
|
|
|
lib_cell = openmc.lib.cells[cell_id]
|
2023-06-22 13:26:22 -05:00
|
|
|
|
if temperature.size > 1:
|
|
|
|
|
|
for i, T in enumerate(temperature):
|
|
|
|
|
|
lib_cell.set_temperature(T, i)
|
|
|
|
|
|
else:
|
|
|
|
|
|
lib_cell.set_temperature(temperature[0])
|
2021-06-24 14:50:05 +07:00
|
|
|
|
|
2025-09-18 23:11:06 -05:00
|
|
|
|
if group['density']:
|
|
|
|
|
|
density = group['density'][()]
|
|
|
|
|
|
if density.size > 1:
|
|
|
|
|
|
cell.density = [rho for rho in density]
|
|
|
|
|
|
else:
|
|
|
|
|
|
cell.density = density
|
|
|
|
|
|
if self.is_initialized:
|
|
|
|
|
|
lib_cell = openmc.lib.cells[cell_id]
|
|
|
|
|
|
if density.size > 1:
|
|
|
|
|
|
for i, rho in enumerate(density):
|
|
|
|
|
|
lib_cell.set_density(rho, i)
|
|
|
|
|
|
else:
|
|
|
|
|
|
lib_cell.set_density(density[0])
|
|
|
|
|
|
|
2021-07-12 06:58:04 -05:00
|
|
|
|
# Make sure number of materials matches
|
2021-06-24 14:50:05 +07:00
|
|
|
|
mats_group = fh['materials']
|
2021-07-12 06:58:04 -05:00
|
|
|
|
n_cells = mats_group.attrs['n_materials']
|
|
|
|
|
|
if n_cells != len(materials):
|
2021-09-28 08:44:31 -05:00
|
|
|
|
raise ValueError("Number of materials in properties file "
|
|
|
|
|
|
"doesn't match current model.")
|
2021-07-12 06:58:04 -05:00
|
|
|
|
|
|
|
|
|
|
# Update material densities
|
2021-06-24 14:50:05 +07:00
|
|
|
|
for name, group in mats_group.items():
|
|
|
|
|
|
mat_id = int(name.split()[1])
|
|
|
|
|
|
atom_density = group.attrs['atom_density']
|
|
|
|
|
|
materials[mat_id].set_density('atom/b-cm', atom_density)
|
2021-09-29 18:51:00 -05:00
|
|
|
|
if self.is_initialized:
|
2021-09-24 16:33:47 -05:00
|
|
|
|
C_mat = openmc.lib.materials[mat_id]
|
|
|
|
|
|
C_mat.set_density(atom_density, 'atom/b-cm')
|
2021-06-24 14:50:05 +07:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def run(
|
|
|
|
|
|
self,
|
|
|
|
|
|
particles: int | None = None,
|
|
|
|
|
|
threads: int | None = None,
|
|
|
|
|
|
geometry_debug: bool = False,
|
|
|
|
|
|
restart_file: PathLike | None = None,
|
|
|
|
|
|
tracks: bool = False,
|
|
|
|
|
|
output: bool = True,
|
|
|
|
|
|
cwd: PathLike = ".",
|
|
|
|
|
|
openmc_exec: PathLike = "openmc",
|
|
|
|
|
|
mpi_args: Iterable[str] = None,
|
|
|
|
|
|
event_based: bool | None = None,
|
|
|
|
|
|
export_model_xml: bool = True,
|
|
|
|
|
|
apply_tally_results: bool = False,
|
|
|
|
|
|
**export_kwargs,
|
|
|
|
|
|
) -> Path:
|
2023-04-08 15:34:39 +02:00
|
|
|
|
"""Run OpenMC
|
|
|
|
|
|
|
|
|
|
|
|
If the C API has been initialized, then the C API is used, otherwise,
|
|
|
|
|
|
this method creates the XML files and runs OpenMC via a system call. In
|
|
|
|
|
|
both cases this method returns the path to the last statepoint file
|
|
|
|
|
|
generated.
|
2023-09-29 20:13:19 -05:00
|
|
|
|
|
2020-04-10 21:52:43 -05:00
|
|
|
|
.. versionchanged:: 0.12
|
|
|
|
|
|
Instead of returning the final k-effective value, this function now
|
|
|
|
|
|
returns the path to the final statepoint written.
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. versionchanged:: 0.13.0
|
|
|
|
|
|
This method can utilize the C API for execution
|
2021-09-23 16:22:42 -05:00
|
|
|
|
|
2017-03-19 17:44:56 -04:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2021-09-27 12:32:25 -05:00
|
|
|
|
particles : int, optional
|
|
|
|
|
|
Number of particles to simulate per generation.
|
|
|
|
|
|
threads : int, optional
|
|
|
|
|
|
Number of OpenMP threads. If OpenMC is compiled with OpenMP
|
|
|
|
|
|
threading enabled, the default is implementation-dependent but is
|
|
|
|
|
|
usually equal to the number of hardware threads available (or a
|
|
|
|
|
|
value set by the :envvar:`OMP_NUM_THREADS` environment variable).
|
|
|
|
|
|
geometry_debug : bool, optional
|
|
|
|
|
|
Turn on geometry debugging during simulation. Defaults to False.
|
2023-02-16 13:53:57 -06:00
|
|
|
|
restart_file : str or PathLike
|
2021-09-27 12:32:25 -05:00
|
|
|
|
Path to restart file to use
|
|
|
|
|
|
tracks : bool, optional
|
2022-09-09 11:40:15 +01:00
|
|
|
|
Enables the writing of particles tracks. The number of particle
|
|
|
|
|
|
tracks written to tracks.h5 is limited to 1000 unless
|
|
|
|
|
|
Settings.max_tracks is set. Defaults to False.
|
2021-09-27 13:45:31 -05:00
|
|
|
|
output : bool, optional
|
2021-09-27 12:32:25 -05:00
|
|
|
|
Capture OpenMC output from standard out
|
2023-05-23 05:25:24 +01:00
|
|
|
|
cwd : PathLike, optional
|
2023-04-08 15:34:39 +02:00
|
|
|
|
Path to working directory to run in. Defaults to the current working
|
|
|
|
|
|
directory.
|
2021-09-27 12:32:25 -05:00
|
|
|
|
openmc_exec : str, optional
|
|
|
|
|
|
Path to OpenMC executable. Defaults to 'openmc'.
|
|
|
|
|
|
mpi_args : list of str, optional
|
2023-04-08 15:34:39 +02:00
|
|
|
|
MPI execute command and any additional MPI arguments to pass, e.g.
|
|
|
|
|
|
['mpiexec', '-n', '8'].
|
2021-09-30 16:34:58 -05:00
|
|
|
|
event_based : None or bool, optional
|
2023-04-08 15:34:39 +02:00
|
|
|
|
Turns on event-based parallelism if True. If None, the value in the
|
|
|
|
|
|
Settings will be used.
|
2023-02-28 11:31:04 -05:00
|
|
|
|
export_model_xml : bool, optional
|
2023-04-08 15:34:39 +02:00
|
|
|
|
Exports a single model.xml file rather than separate files. Defaults
|
|
|
|
|
|
to True.
|
2017-03-19 17:44:56 -04:00
|
|
|
|
|
2023-03-26 11:15:36 -05:00
|
|
|
|
.. versionadded:: 0.13.3
|
2025-01-24 16:49:58 -06:00
|
|
|
|
apply_tally_results : bool
|
|
|
|
|
|
Whether to apply results of the final statepoint file to the
|
|
|
|
|
|
model's tally objects.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
2023-04-08 15:34:39 +02:00
|
|
|
|
**export_kwargs
|
|
|
|
|
|
Keyword arguments passed to either :meth:`Model.export_to_model_xml`
|
|
|
|
|
|
or :meth:`Model.export_to_xml`.
|
2023-03-26 11:15:36 -05:00
|
|
|
|
|
2017-03-19 17:44:56 -04:00
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2020-03-04 14:43:10 +00:00
|
|
|
|
Path
|
2023-04-08 15:34:39 +02:00
|
|
|
|
Path to the last statepoint written by this run (None if no
|
|
|
|
|
|
statepoint was written)
|
2017-03-19 17:44:56 -04:00
|
|
|
|
|
|
|
|
|
|
"""
|
2017-03-18 16:34:19 -04:00
|
|
|
|
|
2021-09-23 16:22:42 -05:00
|
|
|
|
# Setting tstart here ensures we don't pick up any pre-existing
|
2022-01-11 19:06:39 -06:00
|
|
|
|
# statepoint files in the output directory -- just in case there are
|
|
|
|
|
|
# differences between the system clock and the filesystem, we get the
|
|
|
|
|
|
# time of a just-created temporary file
|
|
|
|
|
|
with NamedTemporaryFile() as fp:
|
|
|
|
|
|
tstart = Path(fp.name).stat().st_mtime
|
2020-03-04 14:43:10 +00:00
|
|
|
|
last_statepoint = None
|
2020-03-03 13:37:24 +00:00
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
# Operate in the provided working directory
|
2024-05-02 12:47:10 -05:00
|
|
|
|
with change_directory(cwd):
|
2021-09-29 18:51:00 -05:00
|
|
|
|
if self.is_initialized:
|
2021-09-28 08:44:31 -05:00
|
|
|
|
# Handle the run options as applicable
|
|
|
|
|
|
# First dont allow ones that must be set via init
|
|
|
|
|
|
for arg_name, arg, default in zip(
|
|
|
|
|
|
['threads', 'geometry_debug', 'restart_file', 'tracks'],
|
|
|
|
|
|
[threads, geometry_debug, restart_file, tracks],
|
2021-10-26 12:22:07 -05:00
|
|
|
|
[None, False, None, False]
|
|
|
|
|
|
):
|
2021-09-28 08:44:31 -05:00
|
|
|
|
if arg != default:
|
2021-09-29 18:51:00 -05:00
|
|
|
|
msg = f"{arg_name} must be set via Model.is_initialized(...)"
|
2021-09-28 08:44:31 -05:00
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
|
2021-09-30 16:34:58 -05:00
|
|
|
|
init_particles = openmc.lib.settings.particles
|
2021-09-28 08:44:31 -05:00
|
|
|
|
if particles is not None:
|
|
|
|
|
|
if isinstance(particles, Integral) and particles > 0:
|
|
|
|
|
|
openmc.lib.settings.particles = particles
|
|
|
|
|
|
|
2021-09-30 16:34:58 -05:00
|
|
|
|
init_event_based = openmc.lib.settings.event_based
|
|
|
|
|
|
if event_based is not None:
|
|
|
|
|
|
openmc.lib.settings.event_based = event_based
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# Then run using the C API
|
2021-09-30 11:18:35 -05:00
|
|
|
|
openmc.lib.run(output)
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
|
|
|
|
|
# Reset changes for the openmc.run kwargs handling
|
2021-09-30 16:34:58 -05:00
|
|
|
|
openmc.lib.settings.particles = init_particles
|
|
|
|
|
|
openmc.lib.settings.event_based = init_event_based
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Then run via the command line
|
2023-02-28 11:31:04 -05:00
|
|
|
|
if export_model_xml:
|
2023-04-08 15:34:39 +02:00
|
|
|
|
self.export_to_model_xml(**export_kwargs)
|
2023-02-28 11:31:04 -05:00
|
|
|
|
else:
|
2023-04-08 15:34:39 +02:00
|
|
|
|
self.export_to_xml(**export_kwargs)
|
2024-03-11 22:35:32 -05:00
|
|
|
|
path_input = export_kwargs.get("path", None)
|
2021-09-28 08:44:31 -05:00
|
|
|
|
openmc.run(particles, threads, geometry_debug, restart_file,
|
|
|
|
|
|
tracks, output, Path('.'), openmc_exec, mpi_args,
|
2024-03-11 22:35:32 -05:00
|
|
|
|
event_based, path_input)
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
|
|
|
|
|
# Get output directory and return the last statepoint written
|
|
|
|
|
|
if self.settings.output and 'path' in self.settings.output:
|
|
|
|
|
|
output_dir = Path(self.settings.output['path'])
|
|
|
|
|
|
else:
|
|
|
|
|
|
output_dir = Path.cwd()
|
|
|
|
|
|
for sp in output_dir.glob('statepoint.*.h5'):
|
|
|
|
|
|
mtime = sp.stat().st_mtime
|
|
|
|
|
|
if mtime >= tstart: # >= allows for poor clock resolution
|
|
|
|
|
|
tstart = mtime
|
|
|
|
|
|
last_statepoint = sp
|
2025-01-24 16:49:58 -06:00
|
|
|
|
|
|
|
|
|
|
if apply_tally_results:
|
|
|
|
|
|
self.apply_tally_results(last_statepoint)
|
|
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
return last_statepoint
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def calculate_volumes(
|
|
|
|
|
|
self,
|
|
|
|
|
|
threads: int | None = None,
|
|
|
|
|
|
output: bool = True,
|
|
|
|
|
|
cwd: PathLike = ".",
|
|
|
|
|
|
openmc_exec: PathLike = "openmc",
|
|
|
|
|
|
mpi_args: list[str] | None = None,
|
|
|
|
|
|
apply_volumes: bool = True,
|
|
|
|
|
|
export_model_xml: bool = True,
|
|
|
|
|
|
**export_kwargs,
|
|
|
|
|
|
):
|
2021-09-28 08:44:31 -05:00
|
|
|
|
"""Runs an OpenMC stochastic volume calculation and, if requested,
|
|
|
|
|
|
applies volumes to the model
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
threads : int, optional
|
|
|
|
|
|
Number of OpenMP threads. If OpenMC is compiled with OpenMP
|
|
|
|
|
|
threading enabled, the default is implementation-dependent but is
|
|
|
|
|
|
usually equal to the number of hardware threads available (or a
|
|
|
|
|
|
value set by the :envvar:`OMP_NUM_THREADS` environment variable).
|
2021-09-29 18:51:00 -05:00
|
|
|
|
This currenty only applies to the case when not using the C API.
|
2021-09-28 08:44:31 -05:00
|
|
|
|
output : bool, optional
|
|
|
|
|
|
Capture OpenMC output from standard out
|
|
|
|
|
|
openmc_exec : str, optional
|
|
|
|
|
|
Path to OpenMC executable. Defaults to 'openmc'.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
This only applies to the case when not using the C API.
|
2021-09-28 08:44:31 -05:00
|
|
|
|
mpi_args : list of str, optional
|
|
|
|
|
|
MPI execute command and any additional MPI arguments to pass,
|
|
|
|
|
|
e.g. ['mpiexec', '-n', '8'].
|
2021-09-29 18:51:00 -05:00
|
|
|
|
This only applies to the case when not using the C API.
|
2021-09-28 08:44:31 -05:00
|
|
|
|
cwd : str, optional
|
|
|
|
|
|
Path to working directory to run in. Defaults to the current
|
|
|
|
|
|
working directory.
|
|
|
|
|
|
apply_volumes : bool, optional
|
|
|
|
|
|
Whether apply the volume calculation results from this calculation
|
|
|
|
|
|
to the model. Defaults to applying the volumes.
|
2024-11-09 20:33:14 +01:00
|
|
|
|
export_model_xml : bool, optional
|
|
|
|
|
|
Exports a single model.xml file rather than separate files. Defaults
|
|
|
|
|
|
to True.
|
|
|
|
|
|
**export_kwargs
|
|
|
|
|
|
Keyword arguments passed to either :meth:`Model.export_to_model_xml`
|
|
|
|
|
|
or :meth:`Model.export_to_xml`.
|
|
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
if len(self.settings.volume_calculations) == 0:
|
2021-09-28 08:44:31 -05:00
|
|
|
|
# Then there is no volume calculation specified
|
2023-03-16 21:24:53 -07:00
|
|
|
|
raise ValueError("The Settings.volume_calculations attribute must"
|
2021-09-28 08:44:31 -05:00
|
|
|
|
" be specified before executing this method!")
|
|
|
|
|
|
|
2024-05-02 12:47:10 -05:00
|
|
|
|
with change_directory(cwd):
|
2021-09-29 18:51:00 -05:00
|
|
|
|
if self.is_initialized:
|
2021-09-28 08:44:31 -05:00
|
|
|
|
if threads is not None:
|
2021-09-29 18:51:00 -05:00
|
|
|
|
msg = "Threads must be set via Model.is_initialized(...)"
|
2021-09-28 08:44:31 -05:00
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
if mpi_args is not None:
|
|
|
|
|
|
msg = "The MPI environment must be set otherwise such as" \
|
|
|
|
|
|
"with the call to mpi4py"
|
2021-09-27 13:45:31 -05:00
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
# Compute the volumes
|
2021-09-30 11:18:35 -05:00
|
|
|
|
openmc.lib.calculate_volumes(output)
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
|
|
|
|
|
else:
|
2024-11-09 20:33:14 +01:00
|
|
|
|
if export_model_xml:
|
|
|
|
|
|
self.export_to_model_xml(**export_kwargs)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.export_to_xml(**export_kwargs)
|
|
|
|
|
|
path_input = export_kwargs.get("path", None)
|
|
|
|
|
|
openmc.calculate_volumes(
|
|
|
|
|
|
threads=threads, output=output, openmc_exec=openmc_exec,
|
|
|
|
|
|
mpi_args=mpi_args, path_input=path_input
|
|
|
|
|
|
)
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
|
|
|
|
|
# Now we apply the volumes
|
|
|
|
|
|
if apply_volumes:
|
2021-09-30 11:18:35 -05:00
|
|
|
|
# Load the results and add them to the model
|
2023-02-14 15:12:04 +01:00
|
|
|
|
for i, vol_calc in enumerate(self.settings.volume_calculations):
|
2021-10-05 08:39:24 -05:00
|
|
|
|
vol_calc.load_results(f"volume_{i + 1}.h5")
|
2021-09-28 08:44:31 -05:00
|
|
|
|
# First add them to the Python side
|
2023-11-29 12:44:14 +01:00
|
|
|
|
if vol_calc.domain_type == "material" and self.materials:
|
|
|
|
|
|
for material in self.materials:
|
|
|
|
|
|
if material.id in vol_calc.volumes:
|
|
|
|
|
|
material.add_volume_information(vol_calc)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.geometry.add_volume_information(vol_calc)
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# And now repeat for the C API
|
2021-09-30 11:18:35 -05:00
|
|
|
|
if self.is_initialized and vol_calc.domain_type == 'material':
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# Then we can do this in the C API
|
2021-09-30 11:18:35 -05:00
|
|
|
|
for domain_id in vol_calc.ids:
|
|
|
|
|
|
openmc.lib.materials[domain_id].volume = \
|
|
|
|
|
|
vol_calc.volumes[domain_id].n
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
2025-07-16 08:31:49 -05:00
|
|
|
|
|
|
|
|
|
|
def _set_plot_defaults(
|
|
|
|
|
|
self,
|
|
|
|
|
|
origin: Sequence[float] | None,
|
|
|
|
|
|
width: Sequence[float] | None,
|
|
|
|
|
|
pixels: int | Sequence[int],
|
|
|
|
|
|
basis: str
|
|
|
|
|
|
):
|
2026-07-10 17:02:14 +02:00
|
|
|
|
_check_pixels(pixels)
|
2026-07-09 23:55:32 +02:00
|
|
|
|
|
2025-07-16 08:31:49 -05:00
|
|
|
|
x, y, _ = _BASIS_INDICES[basis]
|
|
|
|
|
|
|
|
|
|
|
|
bb = self.bounding_box
|
|
|
|
|
|
# checks to see if bounding box contains -inf or inf values
|
|
|
|
|
|
if np.isinf(bb.extent[basis]).any():
|
|
|
|
|
|
if origin is None:
|
|
|
|
|
|
origin = (0, 0, 0)
|
|
|
|
|
|
if width is None:
|
|
|
|
|
|
width = (10, 10)
|
|
|
|
|
|
else:
|
|
|
|
|
|
if origin is None:
|
|
|
|
|
|
# if nan values in the bb.center they get replaced with 0.0
|
|
|
|
|
|
# this happens when the bounding_box contains inf values
|
|
|
|
|
|
with warnings.catch_warnings():
|
|
|
|
|
|
warnings.simplefilter("ignore", RuntimeWarning)
|
|
|
|
|
|
origin = np.nan_to_num(bb.center)
|
|
|
|
|
|
if width is None:
|
|
|
|
|
|
bb_width = bb.width
|
|
|
|
|
|
width = (bb_width[x], bb_width[y])
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(pixels, int):
|
|
|
|
|
|
aspect_ratio = width[0] / width[1]
|
|
|
|
|
|
pixels_y = math.sqrt(pixels / aspect_ratio)
|
|
|
|
|
|
pixels = (int(pixels / pixels_y), int(pixels_y))
|
|
|
|
|
|
|
|
|
|
|
|
return origin, width, pixels
|
|
|
|
|
|
|
|
|
|
|
|
def id_map(
|
|
|
|
|
|
self,
|
|
|
|
|
|
origin: Sequence[float] | None = None,
|
|
|
|
|
|
width: Sequence[float] | None = None,
|
|
|
|
|
|
pixels: int | Sequence[int] = 40000,
|
|
|
|
|
|
basis: str = 'xy',
|
2025-12-08 16:54:37 +01:00
|
|
|
|
color_overlaps: bool = False,
|
2025-07-16 08:31:49 -05:00
|
|
|
|
**init_kwargs
|
|
|
|
|
|
) -> np.ndarray:
|
|
|
|
|
|
"""Generate an ID map for domains based on the plot parameters
|
|
|
|
|
|
|
|
|
|
|
|
If the model is not yet initialized, it will be initialized with
|
|
|
|
|
|
openmc.lib. If the model is initialized, the model will remain
|
|
|
|
|
|
initialized after this method call exits.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.3
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
origin : Sequence[float], optional
|
|
|
|
|
|
Origin of the plot. If unspecified, this argument defaults to the
|
|
|
|
|
|
center of the bounding box if the bounding box does not contain inf
|
|
|
|
|
|
values for the provided basis, otherwise (0.0, 0.0, 0.0).
|
|
|
|
|
|
width : Sequence[float], optional
|
|
|
|
|
|
Width of the plot. If unspecified, this argument defaults to the
|
|
|
|
|
|
width of the bounding box if the bounding box does not contain inf
|
|
|
|
|
|
values for the provided basis, otherwise (10.0, 10.0).
|
|
|
|
|
|
pixels : int | Sequence[int], optional
|
|
|
|
|
|
If an iterable of ints is provided then this directly sets the
|
|
|
|
|
|
number of pixels to use in each basis direction. If a single int is
|
|
|
|
|
|
provided then this sets the total number of pixels in the plot and
|
|
|
|
|
|
the number of pixels in each basis direction is calculated from this
|
|
|
|
|
|
total and the image aspect ratio based on the width argument.
|
|
|
|
|
|
basis : {'xy', 'yz', 'xz'}, optional
|
|
|
|
|
|
Basis of the plot.
|
2025-12-08 16:54:37 +01:00
|
|
|
|
color_overlaps : bool, optional
|
|
|
|
|
|
Whether to assign unique IDs (-3) to overlapping regions. If False,
|
|
|
|
|
|
overlapping regions will be assigned the ID of the lowest-numbered
|
|
|
|
|
|
cell that occupies that region. Defaults to False.
|
2025-07-16 08:31:49 -05:00
|
|
|
|
**init_kwargs
|
|
|
|
|
|
Keyword arguments passed to :meth:`Model.init_lib`.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
id_map : numpy.ndarray
|
|
|
|
|
|
A NumPy array with shape (vertical pixels, horizontal pixels, 3) of
|
|
|
|
|
|
OpenMC property IDs with dtype int32. The last dimension of the
|
|
|
|
|
|
array contains cell IDs, cell instances, and material IDs (in that
|
|
|
|
|
|
order).
|
|
|
|
|
|
"""
|
2026-06-08 16:08:08 -05:00
|
|
|
|
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.
|
|
|
|
|
|
"""
|
2025-07-16 08:31:49 -05:00
|
|
|
|
import openmc.lib
|
|
|
|
|
|
|
2026-07-10 17:02:14 +02:00
|
|
|
|
_check_pixels(pixels)
|
|
|
|
|
|
|
2026-06-08 16:08:08 -05:00
|
|
|
|
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.")
|
2025-07-16 08:31:49 -05:00
|
|
|
|
|
2026-06-08 16:08:08 -05:00
|
|
|
|
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)
|
2025-07-16 08:31:49 -05:00
|
|
|
|
|
2025-07-27 04:42:33 +07:00
|
|
|
|
# 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'])
|
2025-07-16 08:31:49 -05:00
|
|
|
|
|
2026-06-08 16:08:08 -05:00
|
|
|
|
# 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
|
|
|
|
|
|
|
2025-07-27 04:42:33 +07:00
|
|
|
|
with openmc.lib.TemporarySession(self, **init_kwargs):
|
2026-06-08 16:08:08 -05:00
|
|
|
|
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
|
2025-07-16 08:31:49 -05:00
|
|
|
|
|
2025-01-31 10:12:24 -06:00
|
|
|
|
@add_plot_params
|
2024-09-27 00:20:40 +02:00
|
|
|
|
def plot(
|
|
|
|
|
|
self,
|
2025-01-31 10:12:24 -06:00
|
|
|
|
origin: Sequence[float] | None = None,
|
|
|
|
|
|
width: Sequence[float] | None = None,
|
|
|
|
|
|
pixels: int | Sequence[int] = 40000,
|
|
|
|
|
|
basis: str = 'xy',
|
|
|
|
|
|
color_by: str = 'cell',
|
|
|
|
|
|
colors: dict | None = None,
|
|
|
|
|
|
seed: int | None = None,
|
|
|
|
|
|
axes=None,
|
|
|
|
|
|
legend: bool = False,
|
|
|
|
|
|
axis_units: str = 'cm',
|
|
|
|
|
|
outline: bool | str = False,
|
2025-02-21 14:58:37 -06:00
|
|
|
|
show_overlaps: bool = False,
|
2025-12-17 21:44:53 +01:00
|
|
|
|
overlap_color: Sequence[int] | str = (255, 0, 0),
|
2024-09-27 00:20:40 +02:00
|
|
|
|
n_samples: int | None = None,
|
|
|
|
|
|
plane_tolerance: float = 1.,
|
2025-01-31 10:12:24 -06:00
|
|
|
|
legend_kwargs: dict | None = None,
|
2024-09-27 00:20:40 +02:00
|
|
|
|
source_kwargs: dict | None = None,
|
2025-01-31 10:12:24 -06:00
|
|
|
|
contour_kwargs: dict | None = None,
|
2024-09-27 00:20:40 +02:00
|
|
|
|
**kwargs,
|
|
|
|
|
|
):
|
2025-01-31 10:12:24 -06:00
|
|
|
|
"""Display a slice plot of the model.
|
2024-09-27 00:20:40 +02:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
|
|
"""
|
2025-01-31 10:12:24 -06:00
|
|
|
|
import matplotlib.patches as mpatches
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
2024-09-27 00:20:40 +02:00
|
|
|
|
|
|
|
|
|
|
check_type('n_samples', n_samples, int | None)
|
2026-07-09 23:55:32 +02:00
|
|
|
|
if n_samples is not None:
|
|
|
|
|
|
check_greater_than('n_samples', n_samples, 0, equality=True)
|
2025-01-31 10:12:24 -06:00
|
|
|
|
check_type('plane_tolerance', plane_tolerance, Real)
|
2026-07-09 23:55:32 +02:00
|
|
|
|
check_greater_than('plane_tolerance', plane_tolerance, 0.0)
|
|
|
|
|
|
|
2025-01-31 10:12:24 -06:00
|
|
|
|
if legend_kwargs is None:
|
|
|
|
|
|
legend_kwargs = {}
|
|
|
|
|
|
legend_kwargs.setdefault('bbox_to_anchor', (1.05, 1))
|
|
|
|
|
|
legend_kwargs.setdefault('loc', 2)
|
|
|
|
|
|
legend_kwargs.setdefault('borderaxespad', 0.0)
|
2024-09-27 00:20:40 +02:00
|
|
|
|
if source_kwargs is None:
|
|
|
|
|
|
source_kwargs = {}
|
|
|
|
|
|
source_kwargs.setdefault('marker', 'x')
|
|
|
|
|
|
|
2025-07-16 08:31:49 -05:00
|
|
|
|
# Set indices using basis and create axis labels
|
|
|
|
|
|
x, y, z = _BASIS_INDICES[basis]
|
|
|
|
|
|
xlabel, ylabel = f'{basis[0]} [{axis_units}]', f'{basis[1]} [{axis_units}]'
|
2025-01-31 10:12:24 -06:00
|
|
|
|
|
2025-07-16 08:31:49 -05:00
|
|
|
|
# Determine extents of plot
|
|
|
|
|
|
origin, width, pixels = self._set_plot_defaults(
|
|
|
|
|
|
origin, width, pixels, basis)
|
2025-01-31 10:12:24 -06:00
|
|
|
|
|
|
|
|
|
|
axis_scaling_factor = {'km': 0.00001, 'm': 0.01, 'cm': 1, 'mm': 10}
|
|
|
|
|
|
|
|
|
|
|
|
x_min = (origin[x] - 0.5*width[0]) * axis_scaling_factor[axis_units]
|
|
|
|
|
|
x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units]
|
|
|
|
|
|
y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units]
|
|
|
|
|
|
y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units]
|
|
|
|
|
|
|
2026-01-30 08:10:16 +02:00
|
|
|
|
# Determine whether any materials contains macroscopic data and if so,
|
|
|
|
|
|
# set energy mode accordingly and check that mg cross sections path is accessible
|
|
|
|
|
|
for mat in self.geometry.get_all_materials().values():
|
|
|
|
|
|
if mat._macroscopic is not None:
|
|
|
|
|
|
self.settings.energy_mode = 'multi-group'
|
|
|
|
|
|
if 'mg_cross_sections' not in openmc.config:
|
|
|
|
|
|
raise RuntimeError("'mg_cross_sections' path must be set in "
|
|
|
|
|
|
"openmc.config before plotting.")
|
|
|
|
|
|
break
|
|
|
|
|
|
|
2026-06-08 16:08:08 -05:00
|
|
|
|
# Get plot IDs from the C API
|
|
|
|
|
|
id_map, _ = self.slice_data(
|
2025-12-17 21:44:53 +01:00
|
|
|
|
origin=origin,
|
|
|
|
|
|
width=width,
|
|
|
|
|
|
pixels=pixels,
|
|
|
|
|
|
basis=basis,
|
2026-06-08 16:08:08 -05:00
|
|
|
|
show_overlaps=show_overlaps,
|
|
|
|
|
|
include_properties=False,
|
2025-12-17 21:44:53 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Generate colors if not provided
|
|
|
|
|
|
if colors is None and seed is not None:
|
|
|
|
|
|
# Use the colorize method to generate random colors
|
2025-12-05 12:17:09 -08:00
|
|
|
|
plot = openmc.SlicePlot()
|
2025-01-31 10:12:24 -06:00
|
|
|
|
plot.color_by = color_by
|
2025-12-17 21:44:53 +01:00
|
|
|
|
plot.colorize(self.geometry, seed=seed)
|
|
|
|
|
|
colors = plot.colors
|
|
|
|
|
|
|
|
|
|
|
|
# Convert ID map to RGB image
|
|
|
|
|
|
img = id_map_to_rgb(
|
2026-01-22 15:21:47 -06:00
|
|
|
|
id_map=id_map,
|
|
|
|
|
|
color_by=color_by,
|
2025-12-17 21:44:53 +01:00
|
|
|
|
colors=colors,
|
|
|
|
|
|
overlap_color=overlap_color
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Create a figure sized such that the size of the axes within
|
|
|
|
|
|
# exactly matches the number of pixels specified
|
|
|
|
|
|
if axes is None:
|
|
|
|
|
|
px = 1/plt.rcParams['figure.dpi']
|
|
|
|
|
|
fig, axes = plt.subplots()
|
|
|
|
|
|
axes.set_xlabel(xlabel)
|
|
|
|
|
|
axes.set_ylabel(ylabel)
|
|
|
|
|
|
params = fig.subplotpars
|
|
|
|
|
|
width_px = pixels[0]*px/(params.right - params.left)
|
|
|
|
|
|
height_px = pixels[1]*px/(params.top - params.bottom)
|
|
|
|
|
|
fig.set_size_inches(width_px, height_px)
|
|
|
|
|
|
|
|
|
|
|
|
if outline:
|
|
|
|
|
|
# Combine R, G, B values into a single int for contour detection
|
|
|
|
|
|
rgb = (img * 256).astype(int)
|
|
|
|
|
|
image_value = (rgb[..., 0] << 16) + \
|
|
|
|
|
|
(rgb[..., 1] << 8) + (rgb[..., 2])
|
|
|
|
|
|
|
|
|
|
|
|
# Set default arguments for contour()
|
|
|
|
|
|
if contour_kwargs is None:
|
|
|
|
|
|
contour_kwargs = {}
|
|
|
|
|
|
contour_kwargs.setdefault('colors', 'k')
|
|
|
|
|
|
contour_kwargs.setdefault('linestyles', 'solid')
|
|
|
|
|
|
contour_kwargs.setdefault('algorithm', 'serial')
|
|
|
|
|
|
|
|
|
|
|
|
axes.contour(
|
|
|
|
|
|
image_value,
|
|
|
|
|
|
origin="upper",
|
|
|
|
|
|
levels=np.unique(image_value),
|
|
|
|
|
|
extent=(x_min, x_max, y_min, y_max),
|
|
|
|
|
|
**contour_kwargs
|
|
|
|
|
|
)
|
2026-01-22 15:21:47 -06:00
|
|
|
|
|
2025-12-17 21:44:53 +01:00
|
|
|
|
# If only showing outline, set the axis limits and aspect explicitly
|
|
|
|
|
|
if outline == 'only':
|
|
|
|
|
|
axes.set_xlim(x_min, x_max)
|
|
|
|
|
|
axes.set_ylim(y_min, y_max)
|
|
|
|
|
|
axes.set_aspect('equal')
|
|
|
|
|
|
|
|
|
|
|
|
# Add legend showing which colors represent which material or cell
|
|
|
|
|
|
if legend:
|
|
|
|
|
|
if colors is None or len(colors) == 0:
|
|
|
|
|
|
raise ValueError("Must pass 'colors' dictionary if you "
|
|
|
|
|
|
"are adding a legend via legend=True.")
|
|
|
|
|
|
|
|
|
|
|
|
if color_by == "cell":
|
|
|
|
|
|
expected_key_type = openmc.Cell
|
|
|
|
|
|
else:
|
|
|
|
|
|
expected_key_type = openmc.Material
|
|
|
|
|
|
|
|
|
|
|
|
patches = []
|
|
|
|
|
|
for key, color in colors.items():
|
|
|
|
|
|
if isinstance(key, int):
|
|
|
|
|
|
raise TypeError(
|
|
|
|
|
|
"Cannot use IDs in colors dict for auto legend.")
|
|
|
|
|
|
elif not isinstance(key, expected_key_type):
|
|
|
|
|
|
raise TypeError(
|
|
|
|
|
|
"Color dict key type does not match color_by")
|
|
|
|
|
|
|
|
|
|
|
|
# this works whether we're doing cells or materials
|
|
|
|
|
|
label = key.name if key.name != '' else key.id
|
|
|
|
|
|
|
|
|
|
|
|
# matplotlib takes RGB on 0-1 scale rather than 0-255
|
|
|
|
|
|
if len(color) == 3 and not isinstance(color, str):
|
|
|
|
|
|
scaled_color = (
|
|
|
|
|
|
color[0]/255, color[1]/255, color[2]/255)
|
2025-01-31 10:12:24 -06:00
|
|
|
|
else:
|
2025-12-17 21:44:53 +01:00
|
|
|
|
scaled_color = color
|
2025-01-31 10:12:24 -06:00
|
|
|
|
|
2025-12-17 21:44:53 +01:00
|
|
|
|
key_patch = mpatches.Patch(color=scaled_color, label=label)
|
|
|
|
|
|
patches.append(key_patch)
|
2024-09-27 00:20:40 +02:00
|
|
|
|
|
2025-12-17 21:44:53 +01:00
|
|
|
|
axes.legend(handles=patches, **legend_kwargs)
|
2024-09-27 00:20:40 +02:00
|
|
|
|
|
2025-12-17 21:44:53 +01:00
|
|
|
|
# Plot image and return the axes
|
|
|
|
|
|
if outline != 'only':
|
|
|
|
|
|
axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs)
|
2025-01-31 10:12:24 -06:00
|
|
|
|
|
|
|
|
|
|
if n_samples:
|
|
|
|
|
|
# Sample external source particles
|
|
|
|
|
|
particles = self.sample_external_source(n_samples)
|
|
|
|
|
|
|
|
|
|
|
|
# Get points within tolerance of the slice plane
|
|
|
|
|
|
slice_value = origin[z]
|
2024-09-27 00:20:40 +02:00
|
|
|
|
xs = []
|
|
|
|
|
|
ys = []
|
|
|
|
|
|
tol = plane_tolerance
|
|
|
|
|
|
for particle in particles:
|
2025-01-31 10:12:24 -06:00
|
|
|
|
if (slice_value - tol < particle.r[z] < slice_value + tol):
|
2025-12-05 18:02:26 +01:00
|
|
|
|
xs.append(particle.r[x] * axis_scaling_factor[axis_units])
|
|
|
|
|
|
ys.append(particle.r[y] * axis_scaling_factor[axis_units])
|
2025-01-31 10:12:24 -06:00
|
|
|
|
axes.scatter(xs, ys, **source_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
return axes
|
2024-09-27 00:20:40 +02:00
|
|
|
|
|
|
|
|
|
|
def sample_external_source(
|
2025-07-16 08:31:49 -05:00
|
|
|
|
self,
|
|
|
|
|
|
n_samples: int = 1000,
|
|
|
|
|
|
prn_seed: int | None = None,
|
2026-03-04 15:36:43 -05:00
|
|
|
|
as_array: bool = False,
|
2025-07-16 08:31:49 -05:00
|
|
|
|
**init_kwargs
|
2026-03-04 15:36:43 -05:00
|
|
|
|
) -> openmc.ParticleList | np.ndarray:
|
2024-09-27 00:20:40 +02:00
|
|
|
|
"""Sample external source and return source particles.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
n_samples : int
|
|
|
|
|
|
Number of samples
|
|
|
|
|
|
prn_seed : int
|
|
|
|
|
|
Pseudorandom number generator (PRNG) seed; if None, one will be
|
|
|
|
|
|
generated randomly.
|
2026-03-04 15:36:43 -05:00
|
|
|
|
as_array : bool
|
|
|
|
|
|
If True, return a numpy structured array instead of a
|
|
|
|
|
|
:class:`~openmc.ParticleList`.
|
2024-09-27 00:20:40 +02:00
|
|
|
|
**init_kwargs
|
|
|
|
|
|
Keyword arguments passed to :func:`openmc.lib.init`
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2026-03-04 15:36:43 -05:00
|
|
|
|
openmc.ParticleList or numpy.ndarray
|
|
|
|
|
|
List of sampled source particles, or a structured array when
|
|
|
|
|
|
*as_array* is True.
|
2024-09-27 00:20:40 +02:00
|
|
|
|
"""
|
|
|
|
|
|
import openmc.lib
|
|
|
|
|
|
|
2025-07-27 04:42:33 +07:00
|
|
|
|
# Silence output by default. Also set arguments to start in volume
|
|
|
|
|
|
# calculation mode to avoid loading cross sections
|
|
|
|
|
|
init_kwargs.setdefault('output', False)
|
|
|
|
|
|
init_kwargs.setdefault('args', ['-c'])
|
|
|
|
|
|
|
|
|
|
|
|
with openmc.lib.TemporarySession(self, **init_kwargs):
|
2025-07-16 08:31:49 -05:00
|
|
|
|
return openmc.lib.sample_external_source(
|
2026-03-04 15:36:43 -05:00
|
|
|
|
n_samples=n_samples, prn_seed=prn_seed, as_array=as_array
|
2025-07-16 08:31:49 -05:00
|
|
|
|
)
|
2024-09-27 00:20:40 +02:00
|
|
|
|
|
2025-01-24 16:49:58 -06:00
|
|
|
|
def apply_tally_results(self, statepoint: PathLike | openmc.StatePoint):
|
|
|
|
|
|
"""Apply results from a statepoint to tally objects on the Model
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
statepoint : PathLike or openmc.StatePoint
|
|
|
|
|
|
Statepoint file used to update tally results
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.tallies.add_results(statepoint)
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def plot_geometry(
|
|
|
|
|
|
self,
|
|
|
|
|
|
output: bool = True,
|
|
|
|
|
|
cwd: PathLike = ".",
|
|
|
|
|
|
openmc_exec: PathLike = "openmc",
|
|
|
|
|
|
export_model_xml: bool = True,
|
|
|
|
|
|
**export_kwargs,
|
|
|
|
|
|
):
|
2021-09-28 08:44:31 -05:00
|
|
|
|
"""Creates plot images as specified by the Model.plots attribute
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
output : bool, optional
|
|
|
|
|
|
Capture OpenMC output from standard out
|
2025-05-08 08:10:33 +02:00
|
|
|
|
cwd : PathLike, optional
|
2021-09-28 08:44:31 -05:00
|
|
|
|
Path to working directory to run in. Defaults to the current
|
|
|
|
|
|
working directory.
|
2025-05-08 08:10:33 +02:00
|
|
|
|
openmc_exec : PathLike, optional
|
2021-09-28 08:44:31 -05:00
|
|
|
|
Path to OpenMC executable. Defaults to 'openmc'.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
This only applies to the case when not using the C API.
|
2024-11-09 20:33:14 +01:00
|
|
|
|
export_model_xml : bool, optional
|
|
|
|
|
|
Exports a single model.xml file rather than separate files. Defaults
|
|
|
|
|
|
to True.
|
|
|
|
|
|
**export_kwargs
|
|
|
|
|
|
Keyword arguments passed to either :meth:`Model.export_to_model_xml`
|
|
|
|
|
|
or :meth:`Model.export_to_xml`.
|
2021-10-05 13:42:27 -05:00
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
"""
|
2021-09-27 12:32:25 -05:00
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
if len(self.plots) == 0:
|
|
|
|
|
|
# Then there is no volume calculation specified
|
|
|
|
|
|
raise ValueError("The Model.plots attribute must be specified "
|
|
|
|
|
|
"before executing this method!")
|
2021-09-27 12:32:25 -05:00
|
|
|
|
|
2024-05-02 12:47:10 -05:00
|
|
|
|
with change_directory(cwd):
|
2021-09-30 15:15:58 -05:00
|
|
|
|
if self.is_initialized:
|
|
|
|
|
|
# Compute the volumes
|
|
|
|
|
|
openmc.lib.plot_geometry(output)
|
|
|
|
|
|
else:
|
2024-11-09 20:33:14 +01:00
|
|
|
|
if export_model_xml:
|
|
|
|
|
|
self.export_to_model_xml(**export_kwargs)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.export_to_xml(**export_kwargs)
|
|
|
|
|
|
path_input = export_kwargs.get("path", None)
|
|
|
|
|
|
openmc.plot_geometry(output=output, openmc_exec=openmc_exec,
|
|
|
|
|
|
path_input=path_input)
|
2021-09-28 08:44:31 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def _change_py_lib_attribs(
|
|
|
|
|
|
self,
|
|
|
|
|
|
names_or_ids: Iterable[str] | Iterable[int],
|
|
|
|
|
|
value: float | Iterable[float],
|
|
|
|
|
|
obj_type: str,
|
|
|
|
|
|
attrib_name: str,
|
|
|
|
|
|
density_units: str = "atom/b-cm",
|
|
|
|
|
|
):
|
2021-09-24 16:33:47 -05:00
|
|
|
|
# Method to do the same work whether it is a cell or material and
|
|
|
|
|
|
# a temperature or volume
|
2021-09-28 08:44:31 -05:00
|
|
|
|
check_type('names_or_ids', names_or_ids, Iterable, (Integral, str))
|
2021-09-24 16:33:47 -05:00
|
|
|
|
check_type('obj_type', obj_type, str)
|
|
|
|
|
|
obj_type = obj_type.lower()
|
|
|
|
|
|
check_value('obj_type', obj_type, ('material', 'cell'))
|
|
|
|
|
|
check_value('attrib_name', attrib_name,
|
|
|
|
|
|
('temperature', 'volume', 'density', 'rotation',
|
|
|
|
|
|
'translation'))
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# The C API only allows setting density units of atom/b-cm and g/cm3
|
2021-09-24 16:33:47 -05:00
|
|
|
|
check_value('density_units', density_units, ('atom/b-cm', 'g/cm3'))
|
2021-10-01 09:21:41 -05:00
|
|
|
|
# The C API has no way to set cell volume or material temperature
|
|
|
|
|
|
# so lets raise exceptions as needed
|
2021-09-24 16:33:47 -05:00
|
|
|
|
if obj_type == 'cell' and attrib_name == 'volume':
|
|
|
|
|
|
raise NotImplementedError(
|
2021-09-28 08:44:31 -05:00
|
|
|
|
'Setting a Cell volume is not supported!')
|
2021-10-01 09:21:41 -05:00
|
|
|
|
if obj_type == 'material' and attrib_name == 'temperature':
|
|
|
|
|
|
raise NotImplementedError(
|
|
|
|
|
|
'Setting a material temperature is not supported!')
|
2021-09-30 16:34:58 -05:00
|
|
|
|
|
2021-09-24 16:33:47 -05:00
|
|
|
|
# And some items just dont make sense
|
|
|
|
|
|
if obj_type == 'cell' and attrib_name == 'density':
|
|
|
|
|
|
raise ValueError('Cannot set a Cell density!')
|
|
|
|
|
|
if obj_type == 'material' and attrib_name in ('rotation',
|
|
|
|
|
|
'translation'):
|
|
|
|
|
|
raise ValueError('Cannot set a material rotation/translation!')
|
|
|
|
|
|
|
|
|
|
|
|
# Set the
|
|
|
|
|
|
if obj_type == 'cell':
|
|
|
|
|
|
by_name = self._cells_by_name
|
|
|
|
|
|
by_id = self._cells_by_id
|
2021-11-02 08:17:47 -05:00
|
|
|
|
if self.is_initialized:
|
|
|
|
|
|
obj_by_id = openmc.lib.cells
|
2021-09-24 16:33:47 -05:00
|
|
|
|
else:
|
|
|
|
|
|
by_name = self._materials_by_name
|
|
|
|
|
|
by_id = self._materials_by_id
|
2021-11-02 08:17:47 -05:00
|
|
|
|
if self.is_initialized:
|
|
|
|
|
|
obj_by_id = openmc.lib.materials
|
2021-09-24 16:33:47 -05:00
|
|
|
|
# Get the list of ids to use if converting from names and accepting
|
2021-09-23 16:22:42 -05:00
|
|
|
|
# only values that have actual ids
|
2021-09-29 18:51:00 -05:00
|
|
|
|
ids = []
|
|
|
|
|
|
for name_or_id in names_or_ids:
|
2021-09-28 08:44:31 -05:00
|
|
|
|
if isinstance(name_or_id, Integral):
|
2021-09-24 16:33:47 -05:00
|
|
|
|
if name_or_id in by_id:
|
2021-09-29 18:51:00 -05:00
|
|
|
|
ids.append(int(name_or_id))
|
2021-09-28 08:44:31 -05:00
|
|
|
|
else:
|
2021-09-29 18:51:00 -05:00
|
|
|
|
cap_obj = obj_type.capitalize()
|
|
|
|
|
|
msg = f'{cap_obj} ID {name_or_id} " \
|
|
|
|
|
|
"is not present in the model!'
|
2021-09-28 08:44:31 -05:00
|
|
|
|
raise InvalidIDError(msg)
|
2021-09-24 16:33:47 -05:00
|
|
|
|
elif isinstance(name_or_id, str):
|
|
|
|
|
|
if name_or_id in by_name:
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# Then by_name[name_or_id] is a list so we need to add all
|
|
|
|
|
|
# entries
|
|
|
|
|
|
ids.extend([obj.id for obj in by_name[name_or_id]])
|
2021-09-23 16:22:42 -05:00
|
|
|
|
else:
|
2021-09-29 18:51:00 -05:00
|
|
|
|
cap_obj = obj_type.capitalize()
|
|
|
|
|
|
msg = f'{cap_obj} {name_or_id} " \
|
|
|
|
|
|
"is not present in the model!'
|
2021-09-23 16:22:42 -05:00
|
|
|
|
raise InvalidIDError(msg)
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
# Now perform the change to both python and C API
|
2021-09-24 16:33:47 -05:00
|
|
|
|
for id_ in ids:
|
|
|
|
|
|
obj = by_id[id_]
|
2021-09-29 18:51:00 -05:00
|
|
|
|
if attrib_name == 'density':
|
2021-09-28 08:44:31 -05:00
|
|
|
|
obj.set_density(density_units, value)
|
2021-09-29 18:51:00 -05:00
|
|
|
|
else:
|
|
|
|
|
|
setattr(obj, attrib_name, value)
|
|
|
|
|
|
# Next lets keep what is in C API memory up to date as well
|
|
|
|
|
|
if self.is_initialized:
|
|
|
|
|
|
lib_obj = obj_by_id[id_]
|
2021-09-30 16:34:58 -05:00
|
|
|
|
if attrib_name == 'density':
|
2021-09-29 18:51:00 -05:00
|
|
|
|
lib_obj.set_density(value, density_units)
|
2021-10-01 09:21:41 -05:00
|
|
|
|
elif attrib_name == 'temperature':
|
2021-09-30 16:34:58 -05:00
|
|
|
|
lib_obj.set_temperature(value)
|
2021-09-29 18:51:00 -05:00
|
|
|
|
else:
|
|
|
|
|
|
setattr(lib_obj, attrib_name, value)
|
2021-09-24 16:33:47 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def rotate_cells(
|
|
|
|
|
|
self, names_or_ids: Iterable[str] | Iterable[int], vector: Iterable[float]
|
|
|
|
|
|
):
|
2021-09-23 16:22:42 -05:00
|
|
|
|
"""Rotate the identified cell(s) by the specified rotation vector.
|
|
|
|
|
|
The rotation is only applied to cells filled with a universe.
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. note:: If applying this change to a name that is not unique, then
|
2022-01-11 09:56:22 -06:00
|
|
|
|
the change will be applied to all objects of that name.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
2021-09-23 16:22:42 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2021-09-24 16:33:47 -05:00
|
|
|
|
names_or_ids : Iterable of str or int
|
2021-09-23 16:22:42 -05:00
|
|
|
|
The cell names (if str) or id (if int) that are to be translated
|
|
|
|
|
|
or rotated. This parameter can include a mix of names and ids.
|
|
|
|
|
|
vector : Iterable of float
|
|
|
|
|
|
The rotation vector of length 3 to apply. This array specifies the
|
|
|
|
|
|
angles in degrees about the x, y, and z axes, respectively.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'rotation')
|
2021-09-23 16:22:42 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def translate_cells(
|
|
|
|
|
|
self, names_or_ids: Iterable[str] | Iterable[int], vector: Iterable[float]
|
|
|
|
|
|
):
|
2021-09-23 16:22:42 -05:00
|
|
|
|
"""Translate the identified cell(s) by the specified translation vector.
|
|
|
|
|
|
The translation is only applied to cells filled with a universe.
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. note:: If applying this change to a name that is not unique, then
|
2022-01-11 09:56:22 -06:00
|
|
|
|
the change will be applied to all objects of that name.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
2021-09-23 16:22:42 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2021-09-24 16:33:47 -05:00
|
|
|
|
names_or_ids : Iterable of str or int
|
2021-09-23 16:22:42 -05:00
|
|
|
|
The cell names (if str) or id (if int) that are to be translated
|
|
|
|
|
|
or rotated. This parameter can include a mix of names and ids.
|
|
|
|
|
|
vector : Iterable of float
|
|
|
|
|
|
The translation vector of length 3 to apply. This array specifies
|
|
|
|
|
|
the x, y, and z dimensions of the translation.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2021-09-30 16:34:58 -05:00
|
|
|
|
self._change_py_lib_attribs(names_or_ids, vector, 'cell',
|
|
|
|
|
|
'translation')
|
2021-09-24 16:33:47 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def update_densities(
|
|
|
|
|
|
self,
|
|
|
|
|
|
names_or_ids: Iterable[str] | Iterable[int],
|
|
|
|
|
|
density: float,
|
|
|
|
|
|
density_units: str = "atom/b-cm",
|
|
|
|
|
|
):
|
2021-09-24 16:33:47 -05:00
|
|
|
|
"""Update the density of a given set of materials to a new value
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. note:: If applying this change to a name that is not unique, then
|
2022-01-11 09:56:22 -06:00
|
|
|
|
the change will be applied to all objects of that name.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
2021-09-24 16:33:47 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
names_or_ids : Iterable of str or int
|
|
|
|
|
|
The material names (if str) or id (if int) that are to be updated.
|
|
|
|
|
|
This parameter can include a mix of names and ids.
|
|
|
|
|
|
density : float
|
|
|
|
|
|
The density to apply in the units specified by `density_units`
|
2021-09-28 08:44:31 -05:00
|
|
|
|
density_units : {'atom/b-cm', 'g/cm3'}, optional
|
|
|
|
|
|
Units for `density`. Defaults to 'atom/b-cm'
|
2021-09-24 16:33:47 -05:00
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2021-09-30 16:34:58 -05:00
|
|
|
|
self._change_py_lib_attribs(names_or_ids, density, 'material',
|
|
|
|
|
|
'density', density_units)
|
2021-09-24 16:33:47 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def update_cell_temperatures(
|
|
|
|
|
|
self, names_or_ids: Iterable[str] | Iterable[int], temperature: float
|
|
|
|
|
|
):
|
2021-09-24 16:33:47 -05:00
|
|
|
|
"""Update the temperature of a set of cells to the given value
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. note:: If applying this change to a name that is not unique, then
|
2022-01-11 09:56:22 -06:00
|
|
|
|
the change will be applied to all objects of that name.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
2021-09-24 16:33:47 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
names_or_ids : Iterable of str or int
|
|
|
|
|
|
The cell names (if str) or id (if int) that are to be updated.
|
|
|
|
|
|
This parameter can include a mix of names and ids.
|
|
|
|
|
|
temperature : float
|
|
|
|
|
|
The temperature to apply in units of Kelvin
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
self._change_py_lib_attribs(names_or_ids, temperature, 'cell',
|
2021-09-30 16:34:58 -05:00
|
|
|
|
'temperature')
|
2021-09-24 16:33:47 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def update_material_volumes(
|
|
|
|
|
|
self, names_or_ids: Iterable[str] | Iterable[int], volume: float
|
|
|
|
|
|
):
|
2021-09-28 08:44:31 -05:00
|
|
|
|
"""Update the volume of a set of materials to the given value
|
|
|
|
|
|
|
2021-09-29 18:51:00 -05:00
|
|
|
|
.. note:: If applying this change to a name that is not unique, then
|
2022-01-11 09:56:22 -06:00
|
|
|
|
the change will be applied to all objects of that name.
|
2021-09-29 18:51:00 -05:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.0
|
|
|
|
|
|
|
2021-09-28 08:44:31 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
names_or_ids : Iterable of str or int
|
|
|
|
|
|
The material names (if str) or id (if int) that are to be updated.
|
|
|
|
|
|
This parameter can include a mix of names and ids.
|
|
|
|
|
|
volume : float
|
|
|
|
|
|
The volume to apply in units of cm^3
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2021-09-30 11:18:35 -05:00
|
|
|
|
self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume')
|
2023-10-17 13:14:55 +01:00
|
|
|
|
|
2025-01-07 22:50:02 +01:00
|
|
|
|
def differentiate_depletable_mats(self, diff_volume_method: str = None):
|
2023-10-17 13:14:55 +01:00
|
|
|
|
"""Assign distribmats for each depletable material
|
|
|
|
|
|
|
2023-10-31 16:55:19 -05:00
|
|
|
|
.. versionadded:: 0.14.0
|
2023-10-17 13:14:55 +01:00
|
|
|
|
|
2025-01-07 22:50:02 +01:00
|
|
|
|
.. versionchanged:: 0.15.1
|
|
|
|
|
|
diff_volume_method default is None, do not set volumes on the new
|
|
|
|
|
|
material ovjects. Is now a convenience method for
|
|
|
|
|
|
differentiate_mats(diff_volume_method, depletable_only=True)
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
diff_volume_method : str
|
|
|
|
|
|
Specifies how the volumes of the new materials should be found.
|
|
|
|
|
|
- None: Do not assign volumes to the new materials (Default)
|
2025-01-29 03:44:04 +08:00
|
|
|
|
- 'divide equally': Divide the original material volume equally between the new materials
|
2025-01-07 22:50:02 +01:00
|
|
|
|
- 'match cell': Set the volume of the material to the volume of the cell they fill
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.differentiate_mats(diff_volume_method, depletable_only=True)
|
|
|
|
|
|
|
|
|
|
|
|
def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bool = True):
|
|
|
|
|
|
"""Assign distribmats for each material
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
|
|
|
2023-10-17 13:14:55 +01:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
diff_volume_method : str
|
|
|
|
|
|
Specifies how the volumes of the new materials should be found.
|
2025-01-07 22:50:02 +01:00
|
|
|
|
- None: Do not assign volumes to the new materials (Default)
|
2025-01-29 03:44:04 +08:00
|
|
|
|
- 'divide equally': Divide the original material volume equally between the new materials
|
2025-01-07 22:50:02 +01:00
|
|
|
|
- 'match cell': Set the volume of the material to the volume of the cell they fill
|
|
|
|
|
|
depletable_only : bool
|
|
|
|
|
|
Default is True, only depletable materials will be differentiated. If False, all materials will be
|
|
|
|
|
|
differentiated.
|
2023-10-17 13:14:55 +01:00
|
|
|
|
"""
|
2025-01-07 22:50:02 +01:00
|
|
|
|
check_value('volume differentiation method', diff_volume_method, ("divide equally", "match cell", None))
|
|
|
|
|
|
|
2023-10-17 13:14:55 +01:00
|
|
|
|
# Count the number of instances for each cell and material
|
|
|
|
|
|
self.geometry.determine_paths(instances_only=True)
|
|
|
|
|
|
|
2025-01-29 03:44:04 +08:00
|
|
|
|
# Get list of materials
|
|
|
|
|
|
if self.materials:
|
|
|
|
|
|
materials = self.materials
|
|
|
|
|
|
else:
|
|
|
|
|
|
materials = list(self.geometry.get_all_materials().values())
|
|
|
|
|
|
|
2025-01-07 22:50:02 +01:00
|
|
|
|
# Find all or depletable_only materials which have multiple instance
|
|
|
|
|
|
distribmats = set()
|
2025-01-29 03:44:04 +08:00
|
|
|
|
for mat in materials:
|
2025-01-07 22:50:02 +01:00
|
|
|
|
# Differentiate all materials with multiple instances
|
|
|
|
|
|
diff_mat = mat.num_instances > 1
|
|
|
|
|
|
# If depletable_only is True, differentiate only depletable materials
|
|
|
|
|
|
if depletable_only:
|
|
|
|
|
|
diff_mat = diff_mat and mat.depletable
|
|
|
|
|
|
if diff_mat:
|
|
|
|
|
|
# Assign volumes to the materials according to requirements
|
|
|
|
|
|
if diff_volume_method == "divide equally":
|
|
|
|
|
|
if mat.volume is None:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
"Volume not specified for "
|
|
|
|
|
|
f"material with ID={mat.id}.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
mat.volume /= mat.num_instances
|
|
|
|
|
|
elif diff_volume_method == "match cell":
|
|
|
|
|
|
for cell in self.geometry.get_all_material_cells().values():
|
|
|
|
|
|
if cell.fill == mat:
|
2023-10-17 13:14:55 +01:00
|
|
|
|
if not cell.volume:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Volume of cell ID={cell.id} not specified. "
|
|
|
|
|
|
"Set volumes of cells prior to using "
|
2025-01-07 22:50:02 +01:00
|
|
|
|
"diff_volume_method='match cell'.")
|
|
|
|
|
|
distribmats.add(mat)
|
|
|
|
|
|
|
|
|
|
|
|
if not distribmats:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Assign distribmats to cells
|
|
|
|
|
|
for cell in self.geometry.get_all_material_cells().values():
|
|
|
|
|
|
if cell.fill in distribmats:
|
|
|
|
|
|
mat = cell.fill
|
2025-01-29 03:44:04 +08:00
|
|
|
|
|
|
|
|
|
|
# Clone materials
|
|
|
|
|
|
if cell.num_instances > 1:
|
2025-01-07 22:50:02 +01:00
|
|
|
|
cell.fill = [mat.clone() for _ in range(cell.num_instances)]
|
2025-01-29 03:44:04 +08:00
|
|
|
|
else:
|
2025-01-07 22:50:02 +01:00
|
|
|
|
cell.fill = mat.clone()
|
2025-01-29 03:44:04 +08:00
|
|
|
|
|
|
|
|
|
|
# For 'match cell', assign volumes based on the cells
|
|
|
|
|
|
if diff_volume_method == 'match cell':
|
|
|
|
|
|
if cell.fill_type == 'distribmat':
|
|
|
|
|
|
for clone_mat in cell.fill:
|
|
|
|
|
|
clone_mat.volume = cell.volume
|
|
|
|
|
|
else:
|
|
|
|
|
|
cell.fill.volume = cell.volume
|
2023-10-17 13:14:55 +01:00
|
|
|
|
|
|
|
|
|
|
if self.materials is not None:
|
|
|
|
|
|
self.materials = openmc.Materials(
|
|
|
|
|
|
self.geometry.get_all_materials().values()
|
|
|
|
|
|
)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
@staticmethod
|
2026-01-22 15:21:47 -06:00
|
|
|
|
def _auto_generate_mgxs_lib(
|
2026-06-08 16:08:08 -05:00
|
|
|
|
model: openmc.model.Model,
|
2026-01-22 15:21:47 -06:00
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
2026-06-08 13:22:32 -05:00
|
|
|
|
correction: str | None,
|
|
|
|
|
|
directory: PathLike,
|
2026-01-22 15:21:47 -06:00
|
|
|
|
) -> openmc.mgxs.Library:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Automatically generate a multi-group cross section libray from a model
|
|
|
|
|
|
with the specified group structure.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2026-07-22 00:38:20 -05:00
|
|
|
|
model : openmc.Model
|
|
|
|
|
|
The model to generate the MGXS library from.
|
2026-01-22 15:21:47 -06:00
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
|
|
|
|
|
correction : str
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
|
|
|
|
|
directory : str
|
|
|
|
|
|
Directory to run the simulation in, so as to contain XML files.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
mgxs_lib : openmc.mgxs.Library
|
|
|
|
|
|
OpenMC MGXS Library object
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Initialize MGXS library with a finished OpenMC geometry object
|
|
|
|
|
|
mgxs_lib = openmc.mgxs.Library(model.geometry)
|
|
|
|
|
|
|
|
|
|
|
|
# Pick energy group structure
|
|
|
|
|
|
mgxs_lib.energy_groups = groups
|
|
|
|
|
|
|
|
|
|
|
|
# Disable transport correction
|
|
|
|
|
|
mgxs_lib.correction = correction
|
|
|
|
|
|
|
|
|
|
|
|
# Specify needed cross sections for random ray
|
|
|
|
|
|
if correction == 'P0':
|
|
|
|
|
|
mgxs_lib.mgxs_types = [
|
|
|
|
|
|
'nu-transport', 'absorption', 'nu-fission', 'fission',
|
2026-01-23 10:23:05 -06:00
|
|
|
|
'consistent nu-scatter matrix', 'multiplicity matrix', 'chi',
|
|
|
|
|
|
'kappa-fission'
|
2026-01-22 15:21:47 -06:00
|
|
|
|
]
|
|
|
|
|
|
elif correction is None:
|
|
|
|
|
|
mgxs_lib.mgxs_types = [
|
|
|
|
|
|
'total', 'absorption', 'nu-fission', 'fission',
|
2026-01-23 10:23:05 -06:00
|
|
|
|
'consistent nu-scatter matrix', 'multiplicity matrix', 'chi',
|
|
|
|
|
|
'kappa-fission'
|
2026-01-22 15:21:47 -06:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# Specify a "material" domain type for the cross section tally filters
|
|
|
|
|
|
mgxs_lib.domain_type = "material"
|
|
|
|
|
|
|
|
|
|
|
|
# Specify the domains over which to compute multi-group cross sections
|
|
|
|
|
|
mgxs_lib.domains = model.geometry.get_all_materials().values()
|
|
|
|
|
|
|
|
|
|
|
|
# Do not compute cross sections on a nuclide-by-nuclide basis
|
|
|
|
|
|
mgxs_lib.by_nuclide = False
|
|
|
|
|
|
|
|
|
|
|
|
# Check the library - if no errors are raised, then the library is satisfactory.
|
|
|
|
|
|
mgxs_lib.check_library_for_openmc_mgxs()
|
|
|
|
|
|
|
|
|
|
|
|
# Construct all tallies needed for the multi-group cross section library
|
|
|
|
|
|
mgxs_lib.build_library()
|
|
|
|
|
|
|
|
|
|
|
|
# Create a "tallies.xml" file for the MGXS Library
|
|
|
|
|
|
mgxs_lib.add_to_tallies(model.tallies, merge=True)
|
|
|
|
|
|
|
|
|
|
|
|
# Run
|
|
|
|
|
|
statepoint_filename = model.run(cwd=directory)
|
|
|
|
|
|
|
|
|
|
|
|
# Load MGXS
|
|
|
|
|
|
with openmc.StatePoint(statepoint_filename) as sp:
|
|
|
|
|
|
mgxs_lib.load_from_statepoint(sp)
|
|
|
|
|
|
|
|
|
|
|
|
return mgxs_lib
|
|
|
|
|
|
|
2025-12-03 01:48:38 -06:00
|
|
|
|
def _create_mgxs_sources(
|
|
|
|
|
|
self,
|
|
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
|
|
|
|
|
spatial_dist: openmc.stats.Spatial,
|
|
|
|
|
|
source_energy: openmc.stats.Univariate | None = None,
|
|
|
|
|
|
) -> list[openmc.IndependentSource]:
|
|
|
|
|
|
"""Create a list of independent sources to use with MGXS generation.
|
|
|
|
|
|
|
|
|
|
|
|
Note that in all cases, a discrete source that is uniform over all
|
|
|
|
|
|
energy groups is created (strength = 0.01) to ensure that total cross
|
|
|
|
|
|
sections are generated for all energy groups. In the case that the user
|
|
|
|
|
|
has provided a source_energy distribution as an argument, an additional
|
|
|
|
|
|
source (strength = 0.99) is created using that energy distribution. If
|
|
|
|
|
|
the user has not provided a source_energy distribution, but the model
|
|
|
|
|
|
has sources defined, and all of those sources are of IndependentSource
|
|
|
|
|
|
type, then additional sources are created based on the model's existing
|
|
|
|
|
|
sources, keeping their energy distributions but replacing their
|
|
|
|
|
|
spatial/angular distributions, with their combined strength being 0.99.
|
|
|
|
|
|
If the user has not provided a source_energy distribution and no sources
|
|
|
|
|
|
are defined on the model and the run mode is 'eigenvalue', then a
|
|
|
|
|
|
default Watt spectrum source (strength = 0.99) is added.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
|
|
|
|
|
spatial_dist : openmc.stats.Spatial
|
|
|
|
|
|
Spatial distribution to use for all sources.
|
|
|
|
|
|
source_energy : openmc.stats.Univariate, optional
|
|
|
|
|
|
Energy distribution to use when generating MGXS data, replacing any
|
|
|
|
|
|
existing sources in the model.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
list[openmc.IndependentSource]
|
|
|
|
|
|
A list of independent sources to use for MGXS generation.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Make a discrete source that is uniform over the bins of the group structure
|
|
|
|
|
|
midpoints = []
|
|
|
|
|
|
strengths = []
|
|
|
|
|
|
for i in range(groups.num_groups):
|
|
|
|
|
|
bounds = groups.get_group_bounds(i+1)
|
|
|
|
|
|
midpoints.append((bounds[0] + bounds[1]) / 2.0)
|
|
|
|
|
|
strengths.append(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
uniform_energy = openmc.stats.Discrete(x=midpoints, p=strengths)
|
|
|
|
|
|
uniform_distribution = openmc.IndependentSource(spatial_dist, energy=uniform_energy, strength=0.01)
|
|
|
|
|
|
sources = [uniform_distribution]
|
|
|
|
|
|
|
|
|
|
|
|
# If the user provided an energy distribution, use that
|
|
|
|
|
|
if source_energy is not None:
|
|
|
|
|
|
user_energy = openmc.IndependentSource(
|
|
|
|
|
|
space=spatial_dist, energy=source_energy, strength=0.99)
|
|
|
|
|
|
sources.append(user_energy)
|
|
|
|
|
|
|
|
|
|
|
|
# If the user did not provide an energy distribution, create sources
|
|
|
|
|
|
# based on what is in their model, keeping the energy spectrum but
|
|
|
|
|
|
# replacing the spatial/angular distributions. We only do this if ALL
|
|
|
|
|
|
# sources are of IndependentSource type, as we can't pull the energy
|
|
|
|
|
|
# distribution from e.g. CompiledSource or FileSource types.
|
|
|
|
|
|
else:
|
|
|
|
|
|
if self.settings.source is not None:
|
|
|
|
|
|
for src in self.settings.source:
|
|
|
|
|
|
if not isinstance(src, openmc.IndependentSource):
|
|
|
|
|
|
break
|
|
|
|
|
|
else:
|
|
|
|
|
|
n_user_sources = len(self.settings.source)
|
|
|
|
|
|
for src in self.settings.source:
|
|
|
|
|
|
# Create a new IndependentSource with adjusted strength, space, and angle
|
|
|
|
|
|
user_source = openmc.IndependentSource(
|
|
|
|
|
|
space=spatial_dist,
|
|
|
|
|
|
energy=src.energy,
|
|
|
|
|
|
strength=0.99 / n_user_sources
|
|
|
|
|
|
)
|
|
|
|
|
|
sources.append(user_source)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# No user sources defined. If we are in eigenvalue mode, then use the default Watt spectrum.
|
|
|
|
|
|
if self.settings.run_mode == 'eigenvalue':
|
|
|
|
|
|
watt_energy = openmc.stats.Watt()
|
|
|
|
|
|
watt_source = openmc.IndependentSource(
|
|
|
|
|
|
space=spatial_dist, energy=watt_energy, strength=0.99)
|
|
|
|
|
|
sources.append(watt_source)
|
|
|
|
|
|
|
|
|
|
|
|
return sources
|
|
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _isothermal_infinite_media_mgxs(
|
|
|
|
|
|
material: openmc.Material,
|
|
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings: openmc.Settings,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction: str | None,
|
|
|
|
|
|
directory: PathLike,
|
|
|
|
|
|
source: openmc.IndependentSource,
|
|
|
|
|
|
temperature: float | None = None,
|
|
|
|
|
|
) -> openmc.XSdata:
|
|
|
|
|
|
"""Generate a single MGXS set for one material, where the geometry is an
|
|
|
|
|
|
infinite medium composed of that material at an isothermal temperature value.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
material : openmc.Material
|
|
|
|
|
|
The material to generate MGXS for
|
|
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings : openmc.Settings
|
|
|
|
|
|
Settings for the generation run, used verbatim except for the
|
|
|
|
|
|
fields owned by the infinite medium method.
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction : str
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
|
|
|
|
|
directory : str
|
|
|
|
|
|
Directory to run the simulation in, so as to contain XML files.
|
|
|
|
|
|
source : openmc.IndependentSource
|
|
|
|
|
|
Source to use when generating MGXS.
|
|
|
|
|
|
temperature : float, optional
|
|
|
|
|
|
The isothermal temperature value to apply to the material. If not specified,
|
|
|
|
|
|
defaults to the temperature in the material.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
data : openmc.XSdata
|
|
|
|
|
|
The material MGXS for the given temperature isotherm.
|
|
|
|
|
|
"""
|
|
|
|
|
|
model = openmc.Model()
|
|
|
|
|
|
|
|
|
|
|
|
# Set materials on the model
|
|
|
|
|
|
model.materials = [material]
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperature is not None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
model.materials[-1].temperature = temperature
|
|
|
|
|
|
|
2026-07-22 00:38:20 -05:00
|
|
|
|
# The provided settings are used verbatim, except for the fields
|
|
|
|
|
|
# owned by the infinite medium method
|
|
|
|
|
|
model.settings = copy.deepcopy(settings)
|
2026-02-09 16:54:59 -06:00
|
|
|
|
model.settings.source = source
|
|
|
|
|
|
|
|
|
|
|
|
# Geometry
|
|
|
|
|
|
box = openmc.model.RectangularPrism(
|
|
|
|
|
|
100000.0, 100000.0, boundary_type='reflective')
|
|
|
|
|
|
name = material.name
|
|
|
|
|
|
infinite_cell = openmc.Cell(name=name, fill=model.materials[-1], region=-box)
|
|
|
|
|
|
infinite_universe = openmc.Universe(name=name, cells=[infinite_cell])
|
|
|
|
|
|
model.geometry.root_universe = infinite_universe
|
|
|
|
|
|
|
|
|
|
|
|
# Generate MGXS
|
|
|
|
|
|
mgxs_lib = Model._auto_generate_mgxs_lib(
|
|
|
|
|
|
model, groups, correction, directory)
|
|
|
|
|
|
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperature is not None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
return mgxs_lib.get_xsdata(domain=material, xsdata_name=name,
|
|
|
|
|
|
temperature=temperature)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return mgxs_lib.get_xsdata(domain=material, xsdata_name=name)
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def _generate_infinite_medium_mgxs(
|
|
|
|
|
|
self,
|
|
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings: openmc.Settings,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
mgxs_path: PathLike,
|
|
|
|
|
|
correction: str | None,
|
|
|
|
|
|
directory: PathLike,
|
2025-12-03 01:48:38 -06:00
|
|
|
|
source_energy: openmc.stats.Univariate | None = None,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures: Sequence[float] | None = None,
|
|
|
|
|
|
) -> None:
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""Generate a MGXS library by running multiple OpenMC simulations, each
|
|
|
|
|
|
representing an infinite medium simulation of a single isolated
|
|
|
|
|
|
material. A discrete source is used to sample particles, with an equal
|
|
|
|
|
|
strength spread across each of the energy groups. This is a highly naive
|
|
|
|
|
|
method that ignores all spatial self shielding effects and all resonance
|
2026-02-09 16:54:59 -06:00
|
|
|
|
shielding effects between materials. If temperature data points are provided,
|
|
|
|
|
|
isothermal cross sections are generated at each temperature point for
|
|
|
|
|
|
each material to build a temperature interpolation table.
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2025-12-03 01:48:38 -06:00
|
|
|
|
Note that in all cases, a discrete source that is uniform over all
|
|
|
|
|
|
energy groups is created (strength = 0.01) to ensure that total cross
|
|
|
|
|
|
sections are generated for all energy groups. In the case that the user
|
|
|
|
|
|
has provided a source_energy distribution as an argument, an additional
|
|
|
|
|
|
source (strength = 0.99) is created using that energy distribution. If
|
|
|
|
|
|
the user has not provided a source_energy distribution, but the model
|
|
|
|
|
|
has sources defined, and all of those sources are of IndependentSource
|
|
|
|
|
|
type, then additional sources are created based on the model's existing
|
|
|
|
|
|
sources, keeping their energy distributions but replacing their
|
|
|
|
|
|
spatial/angular distributions, with their combined strength being 0.99.
|
|
|
|
|
|
If the user has not provided a source_energy distribution and no sources
|
|
|
|
|
|
are defined on the model and the run mode is 'eigenvalue', then a
|
|
|
|
|
|
default Watt spectrum source (strength = 0.99) is added.
|
|
|
|
|
|
|
2025-04-01 21:47:49 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings : openmc.Settings
|
|
|
|
|
|
Settings for the generation run(s), used verbatim except for the
|
|
|
|
|
|
fields owned by the infinite medium method.
|
2025-04-22 12:17:04 -05:00
|
|
|
|
mgxs_path : str
|
2025-04-01 21:47:49 -05:00
|
|
|
|
Filename for the MGXS HDF5 file.
|
2025-04-22 12:17:04 -05:00
|
|
|
|
correction : str
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
|
|
|
|
|
directory : str
|
|
|
|
|
|
Directory to run the simulation in, so as to contain XML files.
|
2025-12-03 01:48:38 -06:00
|
|
|
|
source_energy : openmc.stats.Univariate, optional
|
|
|
|
|
|
Energy distribution to use when generating MGXS data, replacing any
|
|
|
|
|
|
existing sources in the model.
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures : Sequence[float], optional
|
|
|
|
|
|
A list of temperatures to generate MGXS at. Each infinite material region
|
|
|
|
|
|
is isothermal at a given temperature data point for cross
|
|
|
|
|
|
section generation.
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
src = self._create_mgxs_sources(
|
|
|
|
|
|
groups,
|
|
|
|
|
|
spatial_dist=openmc.stats.Point(),
|
|
|
|
|
|
source_energy=source_energy
|
|
|
|
|
|
)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperatures is None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
mgxs_sets = []
|
|
|
|
|
|
for material in self.materials:
|
|
|
|
|
|
xs_data = Model._isothermal_infinite_media_mgxs(
|
|
|
|
|
|
material,
|
|
|
|
|
|
groups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction,
|
|
|
|
|
|
directory,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
src
|
2026-02-09 16:54:59 -06:00
|
|
|
|
)
|
|
|
|
|
|
mgxs_sets.append(xs_data)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
# Write the file to disk.
|
|
|
|
|
|
mgxs_file = openmc.MGXSLibrary(energy_groups=groups)
|
|
|
|
|
|
for mgxs_set in mgxs_sets:
|
|
|
|
|
|
mgxs_file.add_xsdata(mgxs_set)
|
|
|
|
|
|
mgxs_file.export_to_hdf5(mgxs_path)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Build a series of XSData objects, one for each isothermal temperature value.
|
|
|
|
|
|
raw_mgxs_sets = {}
|
|
|
|
|
|
for temperature in temperatures:
|
|
|
|
|
|
raw_mgxs_sets[temperature] = []
|
|
|
|
|
|
for material in self.materials:
|
|
|
|
|
|
xs_data = Model._isothermal_infinite_media_mgxs(
|
|
|
|
|
|
material,
|
|
|
|
|
|
groups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction,
|
|
|
|
|
|
directory,
|
|
|
|
|
|
src,
|
|
|
|
|
|
temperature
|
|
|
|
|
|
)
|
|
|
|
|
|
raw_mgxs_sets[temperature].append(xs_data)
|
|
|
|
|
|
|
|
|
|
|
|
# Unpack the isothermal XSData objects and build a single XSData object per material.
|
|
|
|
|
|
mgxs_sets = []
|
|
|
|
|
|
for m in range(len(self.materials)):
|
2026-02-13 10:52:10 -06:00
|
|
|
|
mgxs_sets.append(openmc.XSdata(self.materials[m].name, groups,
|
|
|
|
|
|
temperatures=temperatures))
|
2026-02-09 16:54:59 -06:00
|
|
|
|
mgxs_sets[-1].order = 0
|
|
|
|
|
|
for temperature in temperatures:
|
|
|
|
|
|
mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][m])
|
|
|
|
|
|
|
|
|
|
|
|
# Write the file to disk.
|
|
|
|
|
|
mgxs_file = openmc.MGXSLibrary(energy_groups=groups)
|
|
|
|
|
|
for mgxs_set in mgxs_sets:
|
|
|
|
|
|
mgxs_file.add_xsdata(mgxs_set)
|
|
|
|
|
|
mgxs_file.export_to_hdf5(mgxs_path)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def _create_stochastic_slab_geometry(
|
|
|
|
|
|
materials: Sequence[openmc.Material],
|
|
|
|
|
|
cell_thickness: float = 1.0,
|
|
|
|
|
|
num_repeats: int = 100,
|
|
|
|
|
|
) -> tuple[openmc.Geometry, openmc.stats.Box]:
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""Create a geometry representing a stochastic "sandwich" of materials in a
|
|
|
|
|
|
layered slab geometry. To reduce the impact of the order of materials in
|
|
|
|
|
|
the slab, the materials are applied to 'num_repeats' different randomly
|
|
|
|
|
|
positioned layers of 'cell_thickness' each.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
materials : list of openmc.Material
|
|
|
|
|
|
List of materials to assign. Each material will appear exactly num_repeats times,
|
|
|
|
|
|
then the ordering is randomly shuffled.
|
|
|
|
|
|
cell_thickness : float, optional
|
|
|
|
|
|
Thickness of each lattice cell in x (default 1.0 cm).
|
|
|
|
|
|
num_repeats : int, optional
|
|
|
|
|
|
Number of repeats for each material (default 100).
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
geometry : openmc.Geometry
|
|
|
|
|
|
The constructed geometry.
|
|
|
|
|
|
box : openmc.stats.Box
|
|
|
|
|
|
A spatial sampling distribution covering the full slab domain.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not materials:
|
|
|
|
|
|
raise ValueError("At least one material must be provided.")
|
|
|
|
|
|
|
|
|
|
|
|
num_materials = len(materials)
|
|
|
|
|
|
total_cells = num_materials * num_repeats
|
|
|
|
|
|
total_width = total_cells * cell_thickness
|
|
|
|
|
|
|
|
|
|
|
|
# Generate an infinite cell/universe for each material
|
|
|
|
|
|
universes = []
|
|
|
|
|
|
for i in range(num_materials):
|
|
|
|
|
|
cell = openmc.Cell(fill=materials[i])
|
|
|
|
|
|
universes.append(openmc.Universe(cells=[cell]))
|
|
|
|
|
|
|
|
|
|
|
|
# Make a list of randomized material idx assignments for the stochastic slab
|
|
|
|
|
|
assignments = list(range(num_materials)) * num_repeats
|
|
|
|
|
|
random.seed(42)
|
|
|
|
|
|
random.shuffle(assignments)
|
|
|
|
|
|
|
|
|
|
|
|
# Create a list of the (randomized) universe assignments to be used
|
|
|
|
|
|
# when defining the problem lattice.
|
|
|
|
|
|
lattice_entries = [universes[m] for m in assignments]
|
|
|
|
|
|
|
|
|
|
|
|
# Create the RectLattice for the 1D material variation in x.
|
|
|
|
|
|
lattice = openmc.RectLattice()
|
|
|
|
|
|
lattice.pitch = (cell_thickness, total_width, total_width)
|
|
|
|
|
|
lattice.lower_left = (0.0, 0.0, 0.0)
|
|
|
|
|
|
lattice.universes = [[lattice_entries]]
|
|
|
|
|
|
lattice.outer = universes[0]
|
|
|
|
|
|
|
|
|
|
|
|
# Define the six outer surfaces with reflective boundary conditions
|
|
|
|
|
|
rpp = openmc.model.RectangularParallelepiped(
|
|
|
|
|
|
0.0, total_width, 0.0, total_width, 0.0, total_width,
|
|
|
|
|
|
boundary_type='reflective'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Create an outer cell that fills with the lattice.
|
|
|
|
|
|
outer_cell = openmc.Cell(fill=lattice, region=-rpp)
|
|
|
|
|
|
|
|
|
|
|
|
# Build the geometry
|
|
|
|
|
|
geometry = openmc.Geometry([outer_cell])
|
|
|
|
|
|
|
|
|
|
|
|
# Define the spatial distribution that covers the full cubic domain
|
|
|
|
|
|
box = openmc.stats.Box(*outer_cell.bounding_box)
|
|
|
|
|
|
|
|
|
|
|
|
return geometry, box
|
|
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _isothermal_stochastic_slab_mgxs(
|
|
|
|
|
|
stoch_geom: openmc.Geometry,
|
|
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings: openmc.Settings,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction: str | None,
|
|
|
|
|
|
directory: PathLike,
|
|
|
|
|
|
source: openmc.IndependentSource,
|
|
|
|
|
|
temperature: float | None = None,
|
|
|
|
|
|
) -> dict[str, openmc.XSdata]:
|
|
|
|
|
|
"""Generate MGXS assuming a stochastic "sandwich" of materials in a layered
|
|
|
|
|
|
slab geometry. If a temperature is specified, all materials in the slab have
|
|
|
|
|
|
their temperatures set to be isothermal at this temperature.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
stoch_geom : openmc.Geometry
|
|
|
|
|
|
The stochastic slab geometry.
|
|
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings : openmc.Settings
|
|
|
|
|
|
Settings for the generation run, used verbatim except for the
|
|
|
|
|
|
fields owned by the stochastic slab method.
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction : str
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
|
|
|
|
|
directory : str
|
|
|
|
|
|
Directory to run the simulation in, so as to contain XML files.
|
|
|
|
|
|
source : openmc.IndependentSource
|
|
|
|
|
|
Source to use when generating MGXS.
|
|
|
|
|
|
temperature : float, optional
|
|
|
|
|
|
The isothermal temperature value to apply to the materials in the
|
|
|
|
|
|
slab. If not specified, defaults to the temperature in the materials.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
data : dict[str, openmc.XSdata]
|
|
|
|
|
|
A dictionary where the key is the name of the material and the value is the isothermal MGXS.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model = openmc.Model()
|
|
|
|
|
|
model.geometry = stoch_geom
|
|
|
|
|
|
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperature is not None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
for material in model.geometry.get_all_materials().values():
|
|
|
|
|
|
material.temperature = temperature
|
|
|
|
|
|
|
2026-07-22 00:38:20 -05:00
|
|
|
|
# The provided settings are used verbatim, except for the fields
|
|
|
|
|
|
# owned by the stochastic slab method
|
|
|
|
|
|
model.settings = copy.deepcopy(settings)
|
2026-02-09 16:54:59 -06:00
|
|
|
|
model.settings.source = source
|
|
|
|
|
|
|
|
|
|
|
|
# Generate MGXS
|
|
|
|
|
|
mgxs_lib = Model._auto_generate_mgxs_lib(
|
|
|
|
|
|
model, groups, correction, directory)
|
|
|
|
|
|
|
|
|
|
|
|
# Fetch all of the isothermal results.
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperature is not None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
return {
|
|
|
|
|
|
mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name,
|
|
|
|
|
|
temperature=temperature)
|
|
|
|
|
|
for mat in mgxs_lib.domains
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {
|
|
|
|
|
|
mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name)
|
|
|
|
|
|
for mat in mgxs_lib.domains
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def _generate_stochastic_slab_mgxs(
|
|
|
|
|
|
self,
|
|
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings: openmc.Settings,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
mgxs_path: PathLike,
|
|
|
|
|
|
correction: str | None,
|
|
|
|
|
|
directory: PathLike,
|
2025-12-03 01:48:38 -06:00
|
|
|
|
source_energy: openmc.stats.Univariate | None = None,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures: Sequence[float] | None = None,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
) -> None:
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""Generate MGXS assuming a stochastic "sandwich" of materials in a layered
|
|
|
|
|
|
slab geometry. While geometry-specific spatial shielding effects are not
|
|
|
|
|
|
captured, this method can be useful when the geometry has materials only
|
|
|
|
|
|
found far from the source region that the "material_wise" method would
|
|
|
|
|
|
not be capable of generating cross sections for. Conversely, this method
|
|
|
|
|
|
will generate cross sections for all materials in the problem regardless
|
|
|
|
|
|
of type. If this is a fixed source problem, a discrete source is used to
|
|
|
|
|
|
sample particles, with an equal strength spread across each of the
|
2026-02-09 16:54:59 -06:00
|
|
|
|
energy groups. If temperature data points are provided,
|
|
|
|
|
|
isothermal cross sections are generated at each temperature point for
|
|
|
|
|
|
the stochastic slab to build a temperature interpolation table.
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings : openmc.Settings
|
|
|
|
|
|
Settings for the generation run(s), used verbatim except for the
|
|
|
|
|
|
fields owned by the stochastic slab method.
|
2025-04-22 12:17:04 -05:00
|
|
|
|
mgxs_path : str
|
2025-04-01 21:47:49 -05:00
|
|
|
|
Filename for the MGXS HDF5 file.
|
2025-04-22 12:17:04 -05:00
|
|
|
|
correction : str
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
|
|
|
|
|
directory : str
|
|
|
|
|
|
Directory to run the simulation in, so as to contain XML files.
|
2025-12-03 01:48:38 -06:00
|
|
|
|
source_energy : openmc.stats.Univariate, optional
|
|
|
|
|
|
Energy distribution to use when generating MGXS data, replacing any
|
|
|
|
|
|
existing sources in the model. In all cases, a discrete source that
|
|
|
|
|
|
is uniform over all energy groups is created (strength = 0.01) to
|
|
|
|
|
|
ensure that total cross sections are generated for all energy
|
|
|
|
|
|
groups. In the case that the user has provided a source_energy
|
|
|
|
|
|
distribution as an argument, an additional source (strength = 0.99)
|
|
|
|
|
|
is created using that energy distribution. If the user has not
|
|
|
|
|
|
provided a source_energy distribution, but the model has sources
|
|
|
|
|
|
defined, and all of those sources are of IndependentSource type,
|
|
|
|
|
|
then additional sources are created based on the model's existing
|
|
|
|
|
|
sources, keeping their energy distributions but replacing their
|
|
|
|
|
|
spatial/angular distributions, with their combined strength being
|
|
|
|
|
|
0.99. If the user has not provided a source_energy distribution and
|
|
|
|
|
|
no sources are defined on the model and the run mode is
|
|
|
|
|
|
'eigenvalue', then a default Watt spectrum source (strength = 0.99)
|
|
|
|
|
|
is added.
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures : Sequence[float], optional
|
|
|
|
|
|
A list of temperatures to generate MGXS at. Each infinite material region
|
|
|
|
|
|
is isothermal at a given temperature data point for cross
|
|
|
|
|
|
section generation.
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Stochastic slab geometry
|
2026-02-09 16:54:59 -06:00
|
|
|
|
geo, spatial_distribution = Model._create_stochastic_slab_geometry(
|
|
|
|
|
|
self.materials)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
src = self._create_mgxs_sources(
|
2025-12-03 01:48:38 -06:00
|
|
|
|
groups,
|
|
|
|
|
|
spatial_dist=spatial_distribution,
|
|
|
|
|
|
source_energy=source_energy
|
|
|
|
|
|
)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperatures is None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
mgxs_sets = Model._isothermal_stochastic_slab_mgxs(
|
|
|
|
|
|
geo,
|
|
|
|
|
|
groups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction,
|
|
|
|
|
|
directory,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
src
|
2026-02-09 16:54:59 -06:00
|
|
|
|
).values()
|
|
|
|
|
|
|
|
|
|
|
|
# Write the file to disk.
|
|
|
|
|
|
mgxs_file = openmc.MGXSLibrary(energy_groups=groups)
|
|
|
|
|
|
for mgxs_set in mgxs_sets:
|
|
|
|
|
|
mgxs_file.add_xsdata(mgxs_set)
|
|
|
|
|
|
mgxs_file.export_to_hdf5(mgxs_path)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Build a series of XSData objects, one for each isothermal temperature value.
|
|
|
|
|
|
raw_mgxs_sets = {}
|
|
|
|
|
|
for temperature in temperatures:
|
|
|
|
|
|
raw_mgxs_sets[temperature] = Model._isothermal_stochastic_slab_mgxs(
|
|
|
|
|
|
geo,
|
|
|
|
|
|
groups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction,
|
|
|
|
|
|
directory,
|
|
|
|
|
|
src,
|
|
|
|
|
|
temperature
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Unpack the isothermal XSData objects and build a single XSData object per material.
|
|
|
|
|
|
mgxs_sets = []
|
|
|
|
|
|
for mat in self.materials:
|
2026-02-13 10:52:10 -06:00
|
|
|
|
mgxs_sets.append(openmc.XSdata(mat.name, groups, temperatures=temperatures))
|
2026-02-09 16:54:59 -06:00
|
|
|
|
mgxs_sets[-1].order = 0
|
|
|
|
|
|
for temperature in temperatures:
|
|
|
|
|
|
mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name])
|
|
|
|
|
|
|
|
|
|
|
|
# Write the file to disk.
|
|
|
|
|
|
mgxs_file = openmc.MGXSLibrary(energy_groups=groups)
|
|
|
|
|
|
for mgxs_set in mgxs_sets:
|
|
|
|
|
|
mgxs_file.add_xsdata(mgxs_set)
|
|
|
|
|
|
mgxs_file.export_to_hdf5(mgxs_path)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _isothermal_materialwise_mgxs(
|
|
|
|
|
|
input_model: openmc.Model,
|
|
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings: openmc.Settings,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction: str | None,
|
|
|
|
|
|
directory: PathLike,
|
|
|
|
|
|
temperature: float | None = None,
|
|
|
|
|
|
) -> dict[str, openmc.XSdata]:
|
|
|
|
|
|
"""Generate a material-wise MGXS library for the model by running the
|
|
|
|
|
|
original continuous energy OpenMC simulation. If a temperature is
|
|
|
|
|
|
specified, each material in the input model is set to that temperature.
|
|
|
|
|
|
Otherwise, the original material temperatures are used. If temperature
|
|
|
|
|
|
data points are provided, isothermal cross sections are generated at
|
|
|
|
|
|
each temperature point for the whole model to build a temperature
|
|
|
|
|
|
interpolation table.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
input_model : openmc.Model
|
|
|
|
|
|
The model to use when computing material-wise MGXS.
|
|
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings : openmc.Settings
|
|
|
|
|
|
Settings for the generation run, used verbatim.
|
2026-02-09 16:54:59 -06:00
|
|
|
|
correction : str
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
|
|
|
|
|
directory : str
|
|
|
|
|
|
Directory to run the simulation in, so as to contain XML files.
|
|
|
|
|
|
temperature : float, optional
|
|
|
|
|
|
The isothermal temperature value to apply to the materials in the
|
|
|
|
|
|
input model. If not specified, defaults to the temperatures in the
|
|
|
|
|
|
materials.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
data : dict[str, openmc.XSdata]
|
|
|
|
|
|
A dictionary where the key is the name of the material and the value is the isothermal MGXS.
|
|
|
|
|
|
"""
|
|
|
|
|
|
model = copy.deepcopy(input_model)
|
|
|
|
|
|
model.tallies = openmc.Tallies()
|
|
|
|
|
|
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperature is not None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
for material in model.geometry.get_all_materials().values():
|
|
|
|
|
|
material.temperature = temperature
|
|
|
|
|
|
|
2026-07-22 00:38:20 -05:00
|
|
|
|
# The provided settings are used verbatim
|
|
|
|
|
|
model.settings = copy.deepcopy(settings)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
# Generate MGXS
|
|
|
|
|
|
mgxs_lib = Model._auto_generate_mgxs_lib(
|
2026-01-22 15:21:47 -06:00
|
|
|
|
model, groups, correction, directory)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
# Fetch all of the isothermal results.
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperature is not None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
return {
|
|
|
|
|
|
mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name,
|
|
|
|
|
|
temperature=temperature)
|
|
|
|
|
|
for mat in mgxs_lib.domains
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {
|
|
|
|
|
|
mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name)
|
|
|
|
|
|
for mat in mgxs_lib.domains
|
|
|
|
|
|
}
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def _generate_material_wise_mgxs(
|
|
|
|
|
|
self,
|
|
|
|
|
|
groups: openmc.mgxs.EnergyGroups,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings: openmc.Settings,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
mgxs_path: PathLike,
|
|
|
|
|
|
correction: str | None,
|
|
|
|
|
|
directory: PathLike,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures: Sequence[float] | None = None,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
) -> None:
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""Generate a material-wise MGXS library for the model by running the
|
|
|
|
|
|
original continuous energy OpenMC simulation of the full material
|
|
|
|
|
|
geometry and source, and tally MGXS data for each material. This method
|
|
|
|
|
|
accurately conserves reaction rates totaled over the entire simulation
|
|
|
|
|
|
domain. However, when the geometry has materials only found far from the
|
|
|
|
|
|
source region, it is possible the Monte Carlo solver may not be able to
|
|
|
|
|
|
score any tallies to these material types, thus resulting in zero cross
|
|
|
|
|
|
section values for these materials. For such cases, the "stochastic
|
|
|
|
|
|
slab" method may be more appropriate.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
groups : openmc.mgxs.EnergyGroups
|
|
|
|
|
|
Energy group structure for the MGXS.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
settings : openmc.Settings
|
|
|
|
|
|
Settings for the generation run(s), used verbatim.
|
2025-05-08 08:10:33 +02:00
|
|
|
|
mgxs_path : PathLike
|
2025-04-01 21:47:49 -05:00
|
|
|
|
Filename for the MGXS HDF5 file.
|
2025-04-22 12:17:04 -05:00
|
|
|
|
correction : str
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
2025-05-08 08:10:33 +02:00
|
|
|
|
directory : PathLike
|
2025-04-22 12:17:04 -05:00
|
|
|
|
Directory to run the simulation in, so as to contain XML files.
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures : Sequence[float], optional
|
|
|
|
|
|
A list of temperatures to generate MGXS at. Each infinite material region
|
|
|
|
|
|
is isothermal at a given temperature data point for cross
|
|
|
|
|
|
section generation.
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""
|
2026-06-08 13:22:32 -05:00
|
|
|
|
if temperatures is None:
|
2026-02-09 16:54:59 -06:00
|
|
|
|
mgxs_sets = Model._isothermal_materialwise_mgxs(
|
2026-07-22 00:38:20 -05:00
|
|
|
|
self, groups, settings, correction, directory).values()
|
2026-02-09 16:54:59 -06:00
|
|
|
|
|
|
|
|
|
|
# Write the file to disk.
|
|
|
|
|
|
mgxs_file = openmc.MGXSLibrary(energy_groups=groups)
|
|
|
|
|
|
for mgxs_set in mgxs_sets:
|
|
|
|
|
|
mgxs_file.add_xsdata(mgxs_set)
|
|
|
|
|
|
mgxs_file.export_to_hdf5(mgxs_path)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Build a series of XSData objects, one for each isothermal temperature value.
|
|
|
|
|
|
raw_mgxs_sets = {}
|
|
|
|
|
|
for temperature in temperatures:
|
|
|
|
|
|
raw_mgxs_sets[temperature] = Model._isothermal_materialwise_mgxs(
|
2026-07-22 00:38:20 -05:00
|
|
|
|
self, groups, settings, correction, directory, temperature)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
# Unpack the isothermal XSData objects and build a single XSData object per material.
|
|
|
|
|
|
mgxs_sets = []
|
|
|
|
|
|
for mat in self.materials:
|
2026-02-13 10:52:10 -06:00
|
|
|
|
mgxs_sets.append(openmc.XSdata(mat.name, groups, temperatures=temperatures))
|
2026-02-09 16:54:59 -06:00
|
|
|
|
mgxs_sets[-1].order = 0
|
|
|
|
|
|
for temperature in temperatures:
|
|
|
|
|
|
mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name])
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-02-09 16:54:59 -06:00
|
|
|
|
# Write the file to disk.
|
|
|
|
|
|
mgxs_file = openmc.MGXSLibrary(energy_groups=groups)
|
|
|
|
|
|
for mgxs_set in mgxs_sets:
|
|
|
|
|
|
mgxs_file.add_xsdata(mgxs_set)
|
|
|
|
|
|
mgxs_file.export_to_hdf5(mgxs_path)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2025-05-08 08:10:33 +02:00
|
|
|
|
def convert_to_multigroup(
|
|
|
|
|
|
self,
|
|
|
|
|
|
method: str = "material_wise",
|
2026-03-12 19:47:47 +00:00
|
|
|
|
groups: str | Sequence[float] | openmc.mgxs.EnergyGroups = "CASMO-2",
|
2026-07-22 00:38:20 -05:00
|
|
|
|
nparticles: int | None = None,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
overwrite_mgxs_library: bool = False,
|
|
|
|
|
|
mgxs_path: PathLike = "mgxs.h5",
|
|
|
|
|
|
correction: str | None = None,
|
2025-12-03 01:48:38 -06:00
|
|
|
|
source_energy: openmc.stats.Univariate | None = None,
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures: Sequence[float] | None = None,
|
|
|
|
|
|
temperature_settings: dict | None = None,
|
2026-07-22 00:38:20 -05:00
|
|
|
|
**kwargs,
|
2025-05-08 08:10:33 +02:00
|
|
|
|
):
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""Convert all materials from continuous energy to multigroup.
|
|
|
|
|
|
|
|
|
|
|
|
If no MGXS data library file is found, generate one using one or more
|
|
|
|
|
|
continuous energy Monte Carlo simulations.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
method : {"material_wise", "stochastic_slab", "infinite_medium"}, optional
|
|
|
|
|
|
Method to generate the MGXS.
|
2026-03-12 19:47:47 +00:00
|
|
|
|
groups : openmc.mgxs.EnergyGroups, str, or sequence of float, optional
|
|
|
|
|
|
Energy group structure for the MGXS. Can be an
|
|
|
|
|
|
:class:`openmc.mgxs.EnergyGroups` object, a string name of a
|
|
|
|
|
|
predefined group structure from :data:`openmc.mgxs.GROUP_STRUCTURES`
|
|
|
|
|
|
(e.g., ``"CASMO-2"``), or a sequence of floats specifying energy
|
|
|
|
|
|
bin boundaries in eV (e.g., ``[0.0, 1e6]`` for a single group).
|
|
|
|
|
|
Defaults to ``"CASMO-2"``.
|
2025-11-22 23:06:17 +01:00
|
|
|
|
nparticles : int, optional
|
|
|
|
|
|
Number of particles to simulate per batch when generating MGXS.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
Defaults to 2000.
|
|
|
|
|
|
|
|
|
|
|
|
.. deprecated:: 0.15.4
|
|
|
|
|
|
Pass ``particles`` as a keyword argument instead.
|
2025-11-22 23:06:17 +01:00
|
|
|
|
overwrite_mgxs_library : bool, optional
|
|
|
|
|
|
Whether to overwrite an existing MGXS library file.
|
2025-04-01 21:47:49 -05:00
|
|
|
|
mgxs_path : str, optional
|
2025-11-22 23:06:17 +01:00
|
|
|
|
Path to the mgxs.h5 library file.
|
2025-04-01 21:47:49 -05:00
|
|
|
|
correction : str, optional
|
|
|
|
|
|
Transport correction to apply to the MGXS. Options are None and
|
|
|
|
|
|
"P0".
|
2025-12-03 01:48:38 -06:00
|
|
|
|
source_energy : openmc.stats.Univariate, optional
|
|
|
|
|
|
Energy distribution to use when generating MGXS data, replacing any
|
|
|
|
|
|
existing sources in the model. In all cases, a discrete source that
|
|
|
|
|
|
is uniform over all energy groups is created (strength = 0.01) to
|
|
|
|
|
|
ensure that total cross sections are generated for all energy
|
|
|
|
|
|
groups. In the case that the user has provided a source_energy
|
|
|
|
|
|
distribution as an argument, an additional source (strength = 0.99)
|
|
|
|
|
|
is created using that energy distribution. If the user has not
|
|
|
|
|
|
provided a source_energy distribution, but the model has sources
|
|
|
|
|
|
defined, and all of those sources are of IndependentSource type,
|
|
|
|
|
|
then additional sources are created based on the model's existing
|
|
|
|
|
|
sources, keeping their energy distributions but replacing their
|
|
|
|
|
|
spatial/angular distributions, with their combined strength being
|
|
|
|
|
|
0.99. If the user has not provided a source_energy distribution and
|
|
|
|
|
|
no sources are defined on the model and the run mode is
|
|
|
|
|
|
'eigenvalue', then a default Watt spectrum source (strength = 0.99)
|
|
|
|
|
|
is added. Note that this argument is only used when using the
|
|
|
|
|
|
"stochastic_slab" or "infinite_medium" MGXS generation methods.
|
2026-02-09 16:54:59 -06:00
|
|
|
|
temperatures : Sequence[float], optional
|
|
|
|
|
|
A list of temperatures to generate MGXS at. Each infinite material region
|
|
|
|
|
|
is isothermal at a given temperature data point for cross
|
|
|
|
|
|
section generation.
|
|
|
|
|
|
temperature_settings : dict, optional
|
|
|
|
|
|
A dictionary of temperature settings to use when generating MGXS.
|
|
|
|
|
|
Valid entries for temperature_settings are the same as the valid
|
|
|
|
|
|
entries in openmc.Settings.temperature_settings.
|
2026-07-22 00:38:20 -05:00
|
|
|
|
|
|
|
|
|
|
.. deprecated:: 0.15.4
|
|
|
|
|
|
Pass ``temperature`` as a keyword argument instead.
|
|
|
|
|
|
**kwargs
|
|
|
|
|
|
:class:`openmc.Settings` attributes used to customize the continuous
|
|
|
|
|
|
energy simulation(s) that generate the MGXS library. Only the
|
|
|
|
|
|
attributes given override the generation defaults. For example,
|
|
|
|
|
|
``model.convert_to_multigroup(particles=100_000)`` adjusts only the
|
|
|
|
|
|
particle count. The run mode cannot be overridden, as it is
|
|
|
|
|
|
determined by the generation method. The surrogate-geometry methods
|
|
|
|
|
|
also construct their own sources and always disable
|
|
|
|
|
|
``create_fission_neutrons`` so that fission is treated as capture.
|
|
|
|
|
|
A ``weight_windows_file`` is applied during ``"material_wise"``
|
|
|
|
|
|
generation and ignored with a warning by the other methods; see the
|
|
|
|
|
|
user guide for the weight window "bootstrapping" workflow this
|
|
|
|
|
|
enables. Cannot be combined with the deprecated ``nparticles`` or
|
|
|
|
|
|
``temperature_settings`` arguments.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.4
|
2025-04-01 21:47:49 -05:00
|
|
|
|
"""
|
2026-03-12 19:47:47 +00:00
|
|
|
|
if not isinstance(groups, openmc.mgxs.EnergyGroups):
|
2025-04-01 21:47:49 -05:00
|
|
|
|
groups = openmc.mgxs.EnergyGroups(groups)
|
|
|
|
|
|
|
2026-07-22 00:38:20 -05:00
|
|
|
|
check_value('method', method,
|
|
|
|
|
|
('material_wise', 'stochastic_slab', 'infinite_medium'))
|
|
|
|
|
|
|
|
|
|
|
|
# Keyword arguments are Settings attributes applied as overrides on
|
|
|
|
|
|
# the generation defaults
|
|
|
|
|
|
settings = openmc.Settings(**kwargs) if kwargs else None
|
|
|
|
|
|
|
|
|
|
|
|
# The model may reference its materials only through the geometry.
|
|
|
|
|
|
# The materials are converted in place and library-wide attributes
|
|
|
|
|
|
# like cross_sections must persist on the model afterwards, so
|
|
|
|
|
|
# populate the explicit collection if it is empty (sorted by ID for
|
|
|
|
|
|
# reproducibility, since geometry traversal order is arbitrary)
|
|
|
|
|
|
if not self.materials:
|
|
|
|
|
|
self.materials = openmc.Materials(sorted(
|
|
|
|
|
|
self.geometry.get_all_materials().values(),
|
|
|
|
|
|
key=lambda mat: mat.id))
|
|
|
|
|
|
|
|
|
|
|
|
if nparticles is not None or temperature_settings is not None:
|
|
|
|
|
|
warnings.warn(
|
|
|
|
|
|
'The "nparticles" and "temperature_settings" arguments are '
|
|
|
|
|
|
'deprecated. Pass "particles" and "temperature" as keyword '
|
|
|
|
|
|
'arguments instead.', FutureWarning)
|
|
|
|
|
|
if settings is not None:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
'The deprecated "nparticles" and "temperature_settings" '
|
|
|
|
|
|
'arguments cannot be combined with Settings keyword '
|
|
|
|
|
|
'arguments.')
|
|
|
|
|
|
|
|
|
|
|
|
# Resolve the settings for the MGXS generation run(s) in three layers,
|
|
|
|
|
|
# with later layers taking precedence: the model's own settings
|
|
|
|
|
|
# ("material_wise") or a fresh Settings object (surrogate methods), then
|
|
|
|
|
|
# the generation defaults, then the user's keyword-argument overrides.
|
|
|
|
|
|
user_settings = settings
|
|
|
|
|
|
if method == 'material_wise':
|
|
|
|
|
|
settings = copy.deepcopy(self.settings)
|
|
|
|
|
|
else:
|
|
|
|
|
|
settings = openmc.Settings()
|
|
|
|
|
|
settings.temperature = copy.deepcopy(self.settings.temperature)
|
|
|
|
|
|
|
|
|
|
|
|
settings.batches = 100 if method == 'infinite_medium' else 200
|
|
|
|
|
|
if method != 'infinite_medium':
|
|
|
|
|
|
settings.inactive = 100
|
|
|
|
|
|
settings.particles = 2000
|
|
|
|
|
|
settings.output = {'summary': True, 'tallies': False}
|
|
|
|
|
|
if nparticles is not None:
|
|
|
|
|
|
settings.particles = nparticles
|
|
|
|
|
|
if temperature_settings is not None:
|
|
|
|
|
|
settings.temperature = temperature_settings
|
|
|
|
|
|
|
|
|
|
|
|
if user_settings is not None:
|
|
|
|
|
|
# The surrogate-geometry methods construct their own sources
|
|
|
|
|
|
if method != "material_wise" and len(user_settings.source) > 0:
|
|
|
|
|
|
warnings.warn(
|
|
|
|
|
|
'The given "source" setting is ignored by the '
|
|
|
|
|
|
f'"{method}" MGXS generation method, which constructs '
|
|
|
|
|
|
'its own sources.')
|
|
|
|
|
|
# Apply the settings attributes passed by the caller
|
|
|
|
|
|
for name in kwargs:
|
|
|
|
|
|
if hasattr(type(settings), name):
|
|
|
|
|
|
setattr(settings, name, copy.deepcopy(
|
|
|
|
|
|
getattr(user_settings, name)))
|
|
|
|
|
|
|
|
|
|
|
|
# The run mode is the one attribute that cannot be detected as
|
|
|
|
|
|
# user-populated (a fresh Settings object defaults it to 'eigenvalue'),
|
|
|
|
|
|
# so it is owned by the generation method: "material_wise" always takes
|
|
|
|
|
|
# it from the model, while the surrogate-geometry methods always run in
|
|
|
|
|
|
# fixed source mode. The surrogate-geometry methods also treat fission
|
|
|
|
|
|
# as capture (nu-fission is still tallied)
|
|
|
|
|
|
if method == 'material_wise':
|
|
|
|
|
|
settings.run_mode = self.settings.run_mode
|
|
|
|
|
|
else:
|
|
|
|
|
|
settings.run_mode = 'fixed source'
|
|
|
|
|
|
settings.create_fission_neutrons = False
|
|
|
|
|
|
|
|
|
|
|
|
# A weight windows file on the generation settings is loaded and
|
|
|
|
|
|
# applied (specifying a file turns weight windows on) during the
|
|
|
|
|
|
# "material_wise" method's continuous energy simulation of the
|
|
|
|
|
|
# original geometry, allowing materials far from the source --
|
|
|
|
|
|
# which an analog simulation may struggle to reach -- to still be
|
|
|
|
|
|
# tallied, and thus obtain nonzero cross sections. The
|
|
|
|
|
|
# "stochastic_slab" and "infinite_medium" methods use simplified
|
|
|
|
|
|
# surrogate geometries for which weight windows defined over the
|
|
|
|
|
|
# original geometry are neither applicable nor needed.
|
|
|
|
|
|
if settings.weight_windows_file is not None and \
|
|
|
|
|
|
method != "material_wise":
|
|
|
|
|
|
warnings.warn(
|
|
|
|
|
|
'The "weight_windows_file" setting is only applicable to '
|
|
|
|
|
|
'the "material_wise" MGXS generation method and will be '
|
|
|
|
|
|
f'ignored for the "{method}" method.'
|
|
|
|
|
|
)
|
|
|
|
|
|
settings.weight_windows_file = None
|
|
|
|
|
|
|
2025-04-22 12:17:04 -05:00
|
|
|
|
# Do all work (including MGXS generation) in a temporary directory
|
|
|
|
|
|
# to avoid polluting the working directory with residual XML files
|
|
|
|
|
|
with TemporaryDirectory() as tmpdir:
|
|
|
|
|
|
|
|
|
|
|
|
# Determine if there are DAGMC universes in the model. If so, we need to synchronize
|
|
|
|
|
|
# the dagmc materials with cells.
|
|
|
|
|
|
# TODO: Can this be done without having to init/finalize?
|
|
|
|
|
|
for univ in self.geometry.get_all_universes().values():
|
|
|
|
|
|
if isinstance(univ, openmc.DAGMCUniverse):
|
2026-02-18 18:18:05 +01:00
|
|
|
|
# Initialize in stochastic volume mode (non-transport mode)
|
|
|
|
|
|
# This mode doesn't require
|
|
|
|
|
|
# valid transport settings like particles/batches
|
|
|
|
|
|
original_run_mode = self.settings.run_mode
|
2026-03-04 15:36:43 -05:00
|
|
|
|
self.settings.run_mode = 'volume'
|
2025-04-22 12:17:04 -05:00
|
|
|
|
self.init_lib(directory=tmpdir)
|
|
|
|
|
|
self.sync_dagmc_universes()
|
|
|
|
|
|
self.finalize_lib()
|
2026-02-18 18:18:05 +01:00
|
|
|
|
# Restore original run mode
|
|
|
|
|
|
self.settings.run_mode = original_run_mode
|
2025-04-22 12:17:04 -05:00
|
|
|
|
break
|
2025-05-08 08:10:33 +02:00
|
|
|
|
|
2026-06-30 16:38:09 +02:00
|
|
|
|
# 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]
|
2025-04-22 12:17:04 -05:00
|
|
|
|
for material in self.materials:
|
2026-06-30 16:38:09 +02:00
|
|
|
|
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}"
|
2025-04-22 12:17:04 -05:00
|
|
|
|
|
|
|
|
|
|
# If needed, generate the needed MGXS data library file
|
|
|
|
|
|
if not Path(mgxs_path).is_file() or overwrite_mgxs_library:
|
|
|
|
|
|
if method == "infinite_medium":
|
|
|
|
|
|
self._generate_infinite_medium_mgxs(
|
2026-07-22 00:38:20 -05:00
|
|
|
|
groups, settings, mgxs_path, correction, tmpdir,
|
|
|
|
|
|
source_energy, temperatures)
|
2025-04-22 12:17:04 -05:00
|
|
|
|
elif method == "material_wise":
|
|
|
|
|
|
self._generate_material_wise_mgxs(
|
2026-07-22 00:38:20 -05:00
|
|
|
|
groups, settings, mgxs_path, correction, tmpdir,
|
|
|
|
|
|
temperatures)
|
2025-04-22 12:17:04 -05:00
|
|
|
|
elif method == "stochastic_slab":
|
|
|
|
|
|
self._generate_stochastic_slab_mgxs(
|
2026-07-22 00:38:20 -05:00
|
|
|
|
groups, settings, mgxs_path, correction, tmpdir,
|
|
|
|
|
|
source_energy, temperatures)
|
2025-04-22 12:17:04 -05:00
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f'MGXS generation method "{method}" not recognized')
|
2025-04-01 21:47:49 -05:00
|
|
|
|
else:
|
2025-04-22 12:17:04 -05:00
|
|
|
|
print(f'Existing MGXS library file "{mgxs_path}" will be used')
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2025-04-22 12:17:04 -05:00
|
|
|
|
# Convert all continuous energy materials to multigroup
|
|
|
|
|
|
self.materials.cross_sections = mgxs_path
|
|
|
|
|
|
for material in self.materials:
|
|
|
|
|
|
material.set_density('macro', 1.0)
|
|
|
|
|
|
material._nuclides = []
|
|
|
|
|
|
material._sab = []
|
|
|
|
|
|
material.add_macroscopic(material.name)
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2025-04-22 12:17:04 -05:00
|
|
|
|
self.settings.energy_mode = 'multi-group'
|
2025-04-01 21:47:49 -05:00
|
|
|
|
|
2026-06-30 16:38:09 +02:00
|
|
|
|
# Restore the user's original material names.
|
|
|
|
|
|
for material, name in zip(self.materials, original_names):
|
|
|
|
|
|
material.name = name
|
|
|
|
|
|
|
2025-04-01 21:47:49 -05:00
|
|
|
|
def convert_to_random_ray(self):
|
|
|
|
|
|
"""Convert a multigroup model to use random ray.
|
|
|
|
|
|
|
|
|
|
|
|
This method determines values for the needed settings and adds them to
|
|
|
|
|
|
the settings.random_ray dictionary so as to enable random ray mode. The
|
|
|
|
|
|
settings that are populated are:
|
|
|
|
|
|
|
|
|
|
|
|
- 'ray_source' (openmc.IndependentSource): Where random ray starting
|
|
|
|
|
|
points are sampled from.
|
|
|
|
|
|
- 'distance_inactive' (float): The "dead zone" distance at the beginning
|
|
|
|
|
|
of the ray.
|
|
|
|
|
|
- 'distance_active' (float): The "active" distance of the ray
|
|
|
|
|
|
- 'particles' (int): Number of rays to simulate
|
|
|
|
|
|
|
|
|
|
|
|
The method will determine reasonable defaults for each of the above
|
|
|
|
|
|
variables based on analysis of the model's geometry. The function will
|
|
|
|
|
|
have no effect if the random ray dictionary is already defined in the
|
|
|
|
|
|
model settings.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# If the random ray dictionary is already set, don't overwrite it
|
|
|
|
|
|
if self.settings.random_ray:
|
|
|
|
|
|
warnings.warn("Random ray conversion skipped as "
|
|
|
|
|
|
"settings.random_ray dictionary is already set.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if self.settings.energy_mode != 'multi-group':
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"Random ray conversion failed: energy mode must be "
|
|
|
|
|
|
"'multi-group'. Use convert_to_multigroup() first."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Helper function for detecting infinity
|
|
|
|
|
|
def _replace_infinity(value):
|
|
|
|
|
|
if np.isinf(value):
|
|
|
|
|
|
return 1.0 if value > 0 else -1.0
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
# Get a bounding box for sampling rays. We can utilize the geometry's bounding box
|
|
|
|
|
|
# though for 2D problems we need to detect the infinities and replace them with an
|
|
|
|
|
|
# arbitrary finite value.
|
|
|
|
|
|
bounding_box = self.geometry.bounding_box
|
|
|
|
|
|
lower_left = [_replace_infinity(v) for v in bounding_box.lower_left]
|
|
|
|
|
|
upper_right = [_replace_infinity(v) for v in bounding_box.upper_right]
|
|
|
|
|
|
uniform_dist_ray = openmc.stats.Box(lower_left, upper_right)
|
|
|
|
|
|
rr_source = openmc.IndependentSource(space=uniform_dist_ray)
|
|
|
|
|
|
self.settings.random_ray['ray_source'] = rr_source
|
|
|
|
|
|
|
|
|
|
|
|
# For the dead zone and active length, a reasonable guess is the larger of either:
|
|
|
|
|
|
# 1) The maximum chord length through the geometry (as defined by its bounding box)
|
|
|
|
|
|
# 2) 30 cm
|
|
|
|
|
|
# Then, set the active length to be 5x longer than the dead zone length, for the sake of efficiency.
|
|
|
|
|
|
chord_length = np.array(upper_right) - np.array(lower_left)
|
|
|
|
|
|
max_length = max(np.linalg.norm(chord_length), 30.0)
|
|
|
|
|
|
|
|
|
|
|
|
self.settings.random_ray['distance_inactive'] = max_length
|
|
|
|
|
|
self.settings.random_ray['distance_active'] = 5 * max_length
|
|
|
|
|
|
|
|
|
|
|
|
# Take a wild guess as to how many rays are needed
|
|
|
|
|
|
self.settings.particles = 2 * int(max_length)
|
2025-10-28 11:36:03 -05:00
|
|
|
|
|
|
|
|
|
|
def keff_search(
|
|
|
|
|
|
self,
|
|
|
|
|
|
func: ModelModifier,
|
|
|
|
|
|
x0: float,
|
|
|
|
|
|
x1: float,
|
|
|
|
|
|
target: float = 1.0,
|
|
|
|
|
|
k_tol: float = 1e-4,
|
|
|
|
|
|
sigma_final: float = 3e-4,
|
|
|
|
|
|
p: float = 0.5,
|
|
|
|
|
|
q: float = 0.95,
|
|
|
|
|
|
memory: int = 4,
|
|
|
|
|
|
x_min: float | None = None,
|
|
|
|
|
|
x_max: float | None = None,
|
|
|
|
|
|
b0: int | None = None,
|
|
|
|
|
|
b_min: int = 20,
|
|
|
|
|
|
b_max: int | None = None,
|
|
|
|
|
|
maxiter: int = 50,
|
|
|
|
|
|
output: bool = False,
|
|
|
|
|
|
func_kwargs: dict[str, Any] | None = None,
|
|
|
|
|
|
run_kwargs: dict[str, Any] | None = None,
|
|
|
|
|
|
) -> SearchResult:
|
|
|
|
|
|
r"""Perform a keff search on a model parametrized by a single variable.
|
|
|
|
|
|
|
|
|
|
|
|
This method uses the GRsecant method described in a paper by `Price and
|
|
|
|
|
|
Roskoff <https://doi.org/10.1016/j.pnucene.2023.104731>`_. The GRsecant
|
|
|
|
|
|
method is a modification of the secant method that accounts for
|
|
|
|
|
|
uncertainties in the function evaluations. The method uses a weighted
|
|
|
|
|
|
linear fit of the most recent function evaluations to predict the next
|
|
|
|
|
|
point to evaluate. It also adaptively changes the number of batches to
|
|
|
|
|
|
meet the target uncertainty value at each iteration.
|
|
|
|
|
|
|
|
|
|
|
|
The target uncertainty for iteration :math:`n+1` is determined by the
|
|
|
|
|
|
following equation (following Eq. (8) in the paper):
|
|
|
|
|
|
|
|
|
|
|
|
.. math::
|
|
|
|
|
|
\sigma_{i+1} = q \sigma_\text{final} \left ( \frac{ \min \left \{
|
|
|
|
|
|
\left\lvert k_i - k_\text{target} \right\rvert : k=0,1,\dots,n
|
|
|
|
|
|
\right \} }{k_\text{tol}} \right )^p
|
|
|
|
|
|
|
|
|
|
|
|
where :math:`q` is a multiplicative factor less than 1, given as the
|
|
|
|
|
|
``sigma_factor`` parameter below.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
func : ModelModifier
|
|
|
|
|
|
Function that takes the parameter to be searched and makes a
|
|
|
|
|
|
modification to the model.
|
|
|
|
|
|
x0 : float
|
|
|
|
|
|
First guess for the parameter passed to `func`
|
|
|
|
|
|
x1 : float
|
|
|
|
|
|
Second guess for the parameter passed to `func`
|
|
|
|
|
|
target : float, optional
|
|
|
|
|
|
keff value to search for
|
|
|
|
|
|
k_tol : float, optional
|
|
|
|
|
|
Stopping criterion on the function value; the absolute value must be
|
|
|
|
|
|
within ``k_tol`` of zero to be accepted.
|
|
|
|
|
|
sigma_final : float, optional
|
|
|
|
|
|
Maximum accepted k-effective uncertainty for the stopping criterion.
|
|
|
|
|
|
p : float, optional
|
|
|
|
|
|
Exponent used in the stopping criterion.
|
|
|
|
|
|
q : float, optional
|
|
|
|
|
|
Multiplicative factor used in the stopping criterion.
|
|
|
|
|
|
memory : int, optional
|
|
|
|
|
|
Number of most-recent points used in the weighted linear fit of
|
|
|
|
|
|
``f(x) = a + b x`` to predict the next point.
|
|
|
|
|
|
x_min : float, optional
|
|
|
|
|
|
Minimum allowed value for the parameter ``x``.
|
|
|
|
|
|
x_max : float, optional
|
|
|
|
|
|
Maximum allowed value for the parameter ``x``.
|
|
|
|
|
|
b0 : int, optional
|
|
|
|
|
|
Number of active batches to use for the initial function
|
|
|
|
|
|
evaluations. If None, uses the model's current setting.
|
|
|
|
|
|
b_min : int, optional
|
|
|
|
|
|
Minimum number of active batches to use in a function evaluation.
|
|
|
|
|
|
b_max : int, optional
|
|
|
|
|
|
Maximum number of active batches to use in a function evaluation.
|
|
|
|
|
|
maxiter : int, optional
|
|
|
|
|
|
Maximum number of iterations to perform.
|
|
|
|
|
|
output : bool, optional
|
|
|
|
|
|
Whether or not to display output showing iteration progress.
|
|
|
|
|
|
func_kwargs : dict, optional
|
|
|
|
|
|
Keyword-based arguments to pass to the `func` function.
|
|
|
|
|
|
run_kwargs : dict, optional
|
|
|
|
|
|
Keyword arguments to pass to :meth:`openmc.Model.run` or
|
|
|
|
|
|
:meth:`openmc.lib.run`.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
SearchResult
|
|
|
|
|
|
Result object containing the estimated root (parameter value) and
|
|
|
|
|
|
evaluation history (parameters, means, standard deviations, and
|
|
|
|
|
|
batches), plus convergence status and termination reason.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
import openmc.lib
|
|
|
|
|
|
|
|
|
|
|
|
check_type('model modifier', func, Callable)
|
|
|
|
|
|
check_type('target', target, Real)
|
|
|
|
|
|
if memory < 2:
|
|
|
|
|
|
raise ValueError("memory must be ≥ 2")
|
|
|
|
|
|
func_kwargs = {} if func_kwargs is None else dict(func_kwargs)
|
|
|
|
|
|
run_kwargs = {} if run_kwargs is None else dict(run_kwargs)
|
|
|
|
|
|
run_kwargs.setdefault('output', False)
|
|
|
|
|
|
|
|
|
|
|
|
# Create lists to store the history of evaluations
|
|
|
|
|
|
xs: list[float] = []
|
|
|
|
|
|
fs: list[float] = []
|
|
|
|
|
|
ss: list[float] = []
|
|
|
|
|
|
gs: list[int] = []
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
|
|
|
|
|
|
# Helper function to evaluate f and store results
|
|
|
|
|
|
def eval_at(x: float, batches: int) -> tuple[float, float]:
|
|
|
|
|
|
# Modify the model with the current guess
|
|
|
|
|
|
func(x, **func_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
# Change the number of batches and run the model
|
|
|
|
|
|
batches += self.settings.inactive
|
|
|
|
|
|
if openmc.lib.is_initialized:
|
|
|
|
|
|
openmc.lib.settings.set_batches(batches)
|
|
|
|
|
|
openmc.lib.reset()
|
|
|
|
|
|
openmc.lib.run(**run_kwargs)
|
|
|
|
|
|
sp_filepath = f'statepoint.{batches}.h5'
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.settings.batches = batches
|
|
|
|
|
|
sp_filepath = self.run(**run_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
# Extract keff and its uncertainty
|
|
|
|
|
|
with openmc.StatePoint(sp_filepath) as sp:
|
|
|
|
|
|
keff = sp.keff
|
|
|
|
|
|
|
|
|
|
|
|
if output:
|
|
|
|
|
|
nonlocal count
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
print(f'Iteration {count}: {batches=}, {x=:.6g}, {keff=:.5f}')
|
|
|
|
|
|
|
|
|
|
|
|
xs.append(float(x))
|
|
|
|
|
|
fs.append(float(keff.n - target))
|
|
|
|
|
|
ss.append(float(keff.s))
|
|
|
|
|
|
gs.append(int(batches))
|
|
|
|
|
|
return fs[-1], ss[-1]
|
|
|
|
|
|
|
|
|
|
|
|
# Default b0 to current model settings if not explicitly provided
|
|
|
|
|
|
if b0 is None:
|
|
|
|
|
|
b0 = self.settings.batches - self.settings.inactive
|
|
|
|
|
|
|
|
|
|
|
|
# Perform the search (inlined GRsecant) in a temporary directory
|
|
|
|
|
|
with TemporaryDirectory() as tmpdir:
|
|
|
|
|
|
if not openmc.lib.is_initialized:
|
|
|
|
|
|
run_kwargs.setdefault('cwd', tmpdir)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- Seed with two evaluations
|
|
|
|
|
|
f0, s0 = eval_at(x0, b0)
|
|
|
|
|
|
if abs(f0) <= k_tol and s0 <= sigma_final:
|
|
|
|
|
|
return SearchResult(x0, xs, fs, ss, gs, True, "converged")
|
|
|
|
|
|
f1, s1 = eval_at(x1, b0)
|
|
|
|
|
|
if abs(f1) <= k_tol and s1 <= sigma_final:
|
|
|
|
|
|
return SearchResult(x1, xs, fs, ss, gs, True, "converged")
|
|
|
|
|
|
|
|
|
|
|
|
for _ in range(maxiter - 2):
|
|
|
|
|
|
# ------ Step 1: propose next x via GRsecant
|
|
|
|
|
|
m = min(memory, len(xs))
|
|
|
|
|
|
|
|
|
|
|
|
# Perform a curve fit on f(x) = a + bx accounting for
|
|
|
|
|
|
# uncertainties. This is equivalent to minimizing the function
|
|
|
|
|
|
# in Equation (A.14)
|
|
|
|
|
|
(a, b), _ = curve_fit(
|
|
|
|
|
|
lambda x, a, b: a + b*x,
|
|
|
|
|
|
xs[-m:], fs[-m:], sigma=ss[-m:], absolute_sigma=True
|
|
|
|
|
|
)
|
|
|
|
|
|
x_new = float(-a / b)
|
|
|
|
|
|
|
|
|
|
|
|
# Clamp x_new to the bounds if provided
|
|
|
|
|
|
if x_min is not None:
|
|
|
|
|
|
x_new = max(x_new, x_min)
|
|
|
|
|
|
if x_max is not None:
|
|
|
|
|
|
x_new = min(x_new, x_max)
|
|
|
|
|
|
|
|
|
|
|
|
# ------ Step 2: choose target σ for next run (Eq. 8 + clamp)
|
|
|
|
|
|
|
|
|
|
|
|
min_abs_f = float(np.min(np.abs(fs)))
|
|
|
|
|
|
base = q * sigma_final
|
|
|
|
|
|
ratio = min_abs_f / k_tol if k_tol > 0 else 1.0
|
|
|
|
|
|
sig = base * (ratio ** p)
|
|
|
|
|
|
sig_target = max(sig, base)
|
|
|
|
|
|
|
|
|
|
|
|
# ------ Step 3: choose generations to hit σ_target (Appendix C)
|
|
|
|
|
|
|
|
|
|
|
|
# Use at least two past points for regression
|
|
|
|
|
|
if len(gs) >= 2 and np.var(np.log(gs)) > 0.0:
|
|
|
|
|
|
# Perform a curve fit based on Eq. (C.3) to solve for ln(k).
|
|
|
|
|
|
# Note that unlike in the paper, we do not leave r as an
|
|
|
|
|
|
# undetermined parameter and choose r=0.5.
|
|
|
|
|
|
(ln_k,), _ = curve_fit(
|
|
|
|
|
|
lambda ln_b, ln_k: ln_k - 0.5*ln_b,
|
|
|
|
|
|
np.log(gs[-4:]), np.log(ss[-4:]),
|
|
|
|
|
|
)
|
|
|
|
|
|
k = float(np.exp(ln_k))
|
|
|
|
|
|
else:
|
|
|
|
|
|
k = float(ss[-1] * math.sqrt(gs[-1]))
|
|
|
|
|
|
|
|
|
|
|
|
b_new = (k / sig_target) ** 2
|
|
|
|
|
|
|
|
|
|
|
|
# Clamp and round up to integer
|
|
|
|
|
|
b_new = max(b_min, math.ceil(b_new))
|
|
|
|
|
|
if b_max is not None:
|
|
|
|
|
|
b_new = min(b_new, b_max)
|
|
|
|
|
|
|
|
|
|
|
|
# Evaluate at proposed x with batches determined above
|
|
|
|
|
|
f_new, s_new = eval_at(x_new, b_new)
|
|
|
|
|
|
|
|
|
|
|
|
# Termination based on both criteria (|f| and σ)
|
|
|
|
|
|
if abs(f_new) <= k_tol and s_new <= sigma_final:
|
|
|
|
|
|
return SearchResult(x_new, xs, fs, ss, gs, True, "converged")
|
|
|
|
|
|
|
|
|
|
|
|
return SearchResult(xs[-1], xs, fs, ss, gs, False, "maxiter")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class SearchResult:
|
|
|
|
|
|
"""Result of a GRsecant keff search.
|
|
|
|
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
|
|
|
|
|
root : float
|
|
|
|
|
|
Estimated parameter value where f(x) = 0 at termination.
|
|
|
|
|
|
parameters : list[float]
|
|
|
|
|
|
Parameter values (x) evaluated during the search, in order.
|
|
|
|
|
|
keffs : list[float]
|
|
|
|
|
|
Estimated keff values for each evaluation.
|
|
|
|
|
|
stdevs : list[float]
|
|
|
|
|
|
One-sigma uncertainties of keff for each evaluation.
|
|
|
|
|
|
batches : list[int]
|
|
|
|
|
|
Number of active batches used for each evaluation.
|
|
|
|
|
|
converged : bool
|
|
|
|
|
|
Whether both |f| <= k_tol and sigma <= sigma_final were met.
|
|
|
|
|
|
flag : str
|
|
|
|
|
|
Reason for termination (e.g., "converged", "maxiter").
|
|
|
|
|
|
"""
|
|
|
|
|
|
root: float
|
|
|
|
|
|
parameters: list[float] = field(repr=False)
|
|
|
|
|
|
means: list[float] = field(repr=False)
|
|
|
|
|
|
stdevs: list[float] = field(repr=False)
|
|
|
|
|
|
batches: list[int] = field(repr=False)
|
|
|
|
|
|
converged: bool
|
|
|
|
|
|
flag: str
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def function_calls(self) -> int:
|
|
|
|
|
|
"""Number of function evaluations performed."""
|
|
|
|
|
|
return len(self.parameters)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def total_batches(self) -> int:
|
|
|
|
|
|
"""Total number of active batches used across all evaluations."""
|
|
|
|
|
|
return sum(self.batches)
|