2023-04-07 15:32:02 -04:00
|
|
|
from __future__ import annotations
|
2022-01-08 14:43:18 -06:00
|
|
|
from numbers import Real, Integral
|
2024-07-18 12:40:42 -05:00
|
|
|
from collections.abc import Iterable, Sequence
|
2025-06-25 13:44:53 -05:00
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Self
|
2023-06-09 10:47:27 -05:00
|
|
|
import warnings
|
2022-01-08 14:43:18 -06:00
|
|
|
|
2023-05-09 11:41:04 -04:00
|
|
|
import lxml.etree as ET
|
2022-01-08 14:43:18 -06:00
|
|
|
import numpy as np
|
2023-04-12 14:00:48 -04:00
|
|
|
import h5py
|
2022-01-08 14:43:18 -06:00
|
|
|
|
2023-06-09 10:47:27 -05:00
|
|
|
import openmc
|
2023-06-16 15:18:12 -05:00
|
|
|
from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh
|
2026-04-28 17:10:03 -04:00
|
|
|
from openmc.tallies import Tallies
|
2022-01-08 14:43:18 -06:00
|
|
|
import openmc.checkvalue as cv
|
2023-04-15 15:49:16 -04:00
|
|
|
from openmc.checkvalue import PathLike
|
2025-08-08 20:47:55 +03:00
|
|
|
from ._xml import get_elem_list, get_text, clean_indentation
|
2022-01-08 14:43:18 -06:00
|
|
|
from .mixin import IDManagerMixin
|
2026-02-03 01:23:24 -06:00
|
|
|
from .particle_type import ParticleType
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class WeightWindows(IDManagerMixin):
|
2022-01-08 15:19:33 -06:00
|
|
|
"""Mesh-based weight windows
|
|
|
|
|
|
|
|
|
|
This class enables you to specify weight window parameters that are used in
|
2023-04-13 17:26:26 -04:00
|
|
|
a simulation. Multiple sets of weight windows can be defined for different
|
2022-01-08 15:19:33 -06:00
|
|
|
meshes and different particles. An iterable of :class:`WeightWindows`
|
2022-01-11 08:18:47 -06:00
|
|
|
instances can be assigned to the :attr:`openmc.Settings.weight_windows`
|
2022-01-08 15:19:33 -06:00
|
|
|
attribute, which is then exported to XML.
|
|
|
|
|
|
|
|
|
|
Weight window lower/upper bounds are to be specified for each combination of
|
|
|
|
|
a mesh element and an energy bin. Thus the total number of bounds should be
|
|
|
|
|
equal to the product of the number of mesh bins and the number of energy
|
|
|
|
|
bins.
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
mesh : openmc.MeshBase
|
|
|
|
|
Mesh for the weight windows
|
|
|
|
|
lower_ww_bounds : Iterable of Real
|
|
|
|
|
A list of values for which each value is the lower bound of a weight
|
|
|
|
|
window
|
|
|
|
|
upper_ww_bounds : Iterable of Real
|
|
|
|
|
A list of values for which each value is the upper bound of a weight
|
|
|
|
|
window
|
|
|
|
|
upper_bound_ratio : float
|
|
|
|
|
Ratio of the lower to upper weight window bounds
|
2022-03-11 09:51:22 -06:00
|
|
|
energy_bounds : Iterable of Real
|
2022-01-08 14:43:18 -06:00
|
|
|
A list of values for which each successive pair constitutes a range of
|
2023-06-30 22:29:47 -05:00
|
|
|
energies in [eV] for a single bin. If no energy bins are provided, the
|
|
|
|
|
maximum and minimum energy for the data available at runtime.
|
2026-02-03 01:23:24 -06:00
|
|
|
particle_type : str or int or openmc.ParticleType
|
2022-01-08 14:43:18 -06:00
|
|
|
Particle type the weight windows apply to
|
|
|
|
|
survival_ratio : float
|
2022-01-08 15:06:47 -06:00
|
|
|
Ratio of the survival weight to the lower weight window bound for
|
2022-01-08 14:43:18 -06:00
|
|
|
rouletting
|
2022-02-01 12:17:38 -06:00
|
|
|
max_lower_bound_ratio : float
|
2022-01-26 15:22:51 -06:00
|
|
|
Maximum allowed ratio of a particle's weight to the weight window's
|
2022-03-11 11:47:58 -06:00
|
|
|
lower bound. A factor will be applied to raise the weight window to be
|
|
|
|
|
lower than the particle's weight by a factor of max_lower_bound_ratio
|
2022-02-01 12:17:38 -06:00
|
|
|
during transport if exceeded.
|
2022-01-08 14:43:18 -06:00
|
|
|
max_split : int
|
|
|
|
|
Maximum allowable number of particles when splitting
|
|
|
|
|
weight_cutoff : float
|
|
|
|
|
Threshold below which particles will be terminated
|
|
|
|
|
id : int
|
2022-03-11 11:54:56 -06:00
|
|
|
Unique identifier for the weight window settings. If not specified, an
|
2022-03-11 11:47:58 -06:00
|
|
|
identifier will automatically be assigned.
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
----------
|
|
|
|
|
id : int
|
|
|
|
|
Unique identifier for the weight window settings.
|
|
|
|
|
mesh : openmc.MeshBase
|
2022-03-11 11:47:58 -06:00
|
|
|
Mesh for the weight windows with dimension (ni, nj, nk)
|
2022-01-08 14:43:18 -06:00
|
|
|
particle_type : str
|
|
|
|
|
Particle type the weight windows apply to
|
2022-03-11 09:51:22 -06:00
|
|
|
energy_bounds : Iterable of Real
|
2022-01-08 14:43:18 -06:00
|
|
|
A list of values for which each successive pair constitutes a range of
|
|
|
|
|
energies in [eV] for a single bin
|
2022-03-11 11:47:58 -06:00
|
|
|
num_energy_bins : int
|
|
|
|
|
Number of energy bins
|
|
|
|
|
lower_ww_bounds : numpy.ndarray of float
|
|
|
|
|
An array of values for which each value is the lower bound of a weight
|
2022-03-11 15:32:39 -06:00
|
|
|
window. Shape: (ni, nj, nk, num_energy_bins) for StructuredMesh;
|
|
|
|
|
(num_elements, num_energy_bins) for UnstructuredMesh
|
2022-03-11 11:47:58 -06:00
|
|
|
upper_ww_bounds : numpy.ndarray of float
|
|
|
|
|
An array of values for which each value is the upper bound of a weight
|
2022-03-11 15:32:39 -06:00
|
|
|
window. Shape: (ni, nj, nk, num_energy_bins) for StructuredMesh;
|
|
|
|
|
(num_elements, num_energy_bins) for UnstructuredMesh
|
2022-01-08 14:43:18 -06:00
|
|
|
survival_ratio : float
|
2022-01-08 15:06:47 -06:00
|
|
|
Ratio of the survival weight to the lower weight window bound for
|
2022-01-08 14:43:18 -06:00
|
|
|
rouletting
|
2025-06-25 13:44:53 -05:00
|
|
|
max_lower_bound_ratio : float
|
2022-01-26 15:22:51 -06:00
|
|
|
Maximum allowed ratio of a particle's weight to the weight window's
|
|
|
|
|
lower bound. (Default: 1.0)
|
2022-01-08 14:43:18 -06:00
|
|
|
max_split : int
|
|
|
|
|
Maximum allowable number of particles when splitting
|
|
|
|
|
weight_cutoff : float
|
|
|
|
|
Threshold below which particles will be terminated
|
2022-01-08 15:19:33 -06:00
|
|
|
|
|
|
|
|
See Also
|
|
|
|
|
--------
|
|
|
|
|
openmc.Settings
|
|
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
"""
|
|
|
|
|
next_id = 1
|
|
|
|
|
used_ids = set()
|
|
|
|
|
|
2023-04-12 14:00:48 -04:00
|
|
|
def __init__(
|
2023-06-09 10:47:27 -05:00
|
|
|
self,
|
|
|
|
|
mesh: MeshBase,
|
2023-04-15 15:44:02 -04:00
|
|
|
lower_ww_bounds: Iterable[float],
|
2024-07-18 12:40:42 -05:00
|
|
|
upper_ww_bounds: Iterable[float] | None = None,
|
|
|
|
|
upper_bound_ratio: float | None = None,
|
|
|
|
|
energy_bounds: Iterable[Real] | None = None,
|
2026-02-03 01:23:24 -06:00
|
|
|
particle_type: str | int | openmc.ParticleType = 'neutron',
|
2025-06-25 13:44:53 -05:00
|
|
|
survival_ratio: float = 3.0,
|
2024-07-18 12:40:42 -05:00
|
|
|
max_lower_bound_ratio: float | None = None,
|
2023-04-12 14:00:48 -04:00
|
|
|
max_split: int = 10,
|
|
|
|
|
weight_cutoff: float = 1.e-38,
|
2024-07-18 12:40:42 -05:00
|
|
|
id: int | None = None
|
2023-04-12 14:00:48 -04:00
|
|
|
):
|
2022-01-08 14:43:18 -06:00
|
|
|
self.mesh = mesh
|
|
|
|
|
self.id = id
|
|
|
|
|
self.particle_type = particle_type
|
2023-06-30 22:29:47 -05:00
|
|
|
self._energy_bounds = None
|
|
|
|
|
if energy_bounds is not None:
|
|
|
|
|
self.energy_bounds = energy_bounds
|
2022-01-08 14:43:18 -06:00
|
|
|
self.lower_ww_bounds = lower_ww_bounds
|
|
|
|
|
|
|
|
|
|
if upper_ww_bounds is not None and upper_bound_ratio:
|
2022-02-02 10:42:32 +00:00
|
|
|
raise ValueError("Exactly one of upper_ww_bounds and "
|
2022-01-08 14:43:18 -06:00
|
|
|
"upper_bound_ratio must be present.")
|
|
|
|
|
|
|
|
|
|
if upper_ww_bounds is None and upper_bound_ratio is None:
|
2022-02-02 10:42:32 +00:00
|
|
|
raise ValueError("Exactly one of upper_ww_bounds and "
|
2022-01-08 14:43:18 -06:00
|
|
|
"upper_bound_ratio must be present.")
|
|
|
|
|
|
|
|
|
|
if upper_bound_ratio:
|
|
|
|
|
self.upper_ww_bounds = [
|
|
|
|
|
lb * upper_bound_ratio for lb in self.lower_ww_bounds
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if upper_ww_bounds is not None:
|
|
|
|
|
self.upper_ww_bounds = upper_ww_bounds
|
|
|
|
|
|
|
|
|
|
if len(self.lower_ww_bounds) != len(self.upper_ww_bounds):
|
2022-03-11 11:54:56 -06:00
|
|
|
raise ValueError('Size of the lower and upper weight '
|
|
|
|
|
'window bounds do not match')
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
self.survival_ratio = survival_ratio
|
2022-02-01 13:11:18 -06:00
|
|
|
|
|
|
|
|
self._max_lower_bound_ratio = None
|
|
|
|
|
if max_lower_bound_ratio is not None:
|
2022-02-01 12:17:38 -06:00
|
|
|
self.max_lower_bound_ratio = max_lower_bound_ratio
|
2022-02-01 13:11:18 -06:00
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
self.max_split = max_split
|
|
|
|
|
self.weight_cutoff = weight_cutoff
|
|
|
|
|
|
2023-04-07 15:32:02 -04:00
|
|
|
def __repr__(self) -> str:
|
2022-01-08 14:43:18 -06:00
|
|
|
string = type(self).__name__ + '\n'
|
|
|
|
|
string += '{: <16}=\t{}\n'.format('\tID', self._id)
|
2023-06-09 10:47:27 -05:00
|
|
|
string += '{: <16}=\t{}\n'.format('\tMesh', self.mesh)
|
2022-01-08 14:43:18 -06:00
|
|
|
string += '{: <16}=\t{}\n'.format('\tParticle Type', self._particle_type)
|
2022-03-11 09:51:22 -06:00
|
|
|
string += '{: <16}=\t{}\n'.format('\tEnergy Bounds', self._energy_bounds)
|
2023-07-22 15:18:46 -05:00
|
|
|
string += '{: <16}=\t{}\n'.format('\tMax lower bound ratio', self.max_lower_bound_ratio)
|
2022-01-08 14:43:18 -06:00
|
|
|
string += '{: <16}=\t{}\n'.format('\tLower WW Bounds', self._lower_ww_bounds)
|
|
|
|
|
string += '{: <16}=\t{}\n'.format('\tUpper WW Bounds', self._upper_ww_bounds)
|
|
|
|
|
string += '{: <16}=\t{}\n'.format('\tSurvival Ratio', self._survival_ratio)
|
|
|
|
|
string += '{: <16}=\t{}\n'.format('\tMax Split', self._max_split)
|
|
|
|
|
string += '{: <16}=\t{}\n'.format('\tWeight Cutoff', self._weight_cutoff)
|
|
|
|
|
return string
|
|
|
|
|
|
2023-04-12 14:00:48 -04:00
|
|
|
def __eq__(self, other: WeightWindows) -> bool:
|
2022-10-27 11:11:56 -05:00
|
|
|
# ensure that `other` is a WeightWindows object
|
|
|
|
|
if not isinstance(other, WeightWindows):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# TODO: add ability to check mesh equality
|
|
|
|
|
|
2022-10-27 11:41:12 -05:00
|
|
|
# check several attributes directly
|
2022-10-27 11:11:56 -05:00
|
|
|
attrs = ('particle_type',
|
|
|
|
|
'survival_ratio',
|
|
|
|
|
'max_lower_bound_ratio',
|
|
|
|
|
'max_split',
|
|
|
|
|
'weight_cutoff')
|
|
|
|
|
for attr in attrs:
|
|
|
|
|
if getattr(self, attr) != getattr(other, attr):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# save most expensive checks for last
|
|
|
|
|
if not np.array_equal(self.energy_bounds, other.energy_bounds):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if not np.array_equal(self.lower_ww_bounds, other.lower_ww_bounds):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if not np.array_equal(self.upper_ww_bounds, other.upper_ww_bounds):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
@property
|
2023-04-07 15:32:02 -04:00
|
|
|
def mesh(self) -> MeshBase:
|
2022-01-08 14:43:18 -06:00
|
|
|
return self._mesh
|
|
|
|
|
|
|
|
|
|
@mesh.setter
|
2023-04-12 14:00:48 -04:00
|
|
|
def mesh(self, mesh: MeshBase):
|
2022-01-08 14:43:18 -06:00
|
|
|
cv.check_type('Weight window mesh', mesh, MeshBase)
|
|
|
|
|
self._mesh = mesh
|
|
|
|
|
|
|
|
|
|
@property
|
2026-02-03 01:23:24 -06:00
|
|
|
def particle_type(self) -> ParticleType:
|
2022-01-08 14:43:18 -06:00
|
|
|
return self._particle_type
|
|
|
|
|
|
|
|
|
|
@particle_type.setter
|
2026-02-03 01:23:24 -06:00
|
|
|
def particle_type(self, pt):
|
|
|
|
|
ptype = ParticleType(pt)
|
|
|
|
|
if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}:
|
|
|
|
|
raise ValueError("Weight windows can only be applied for neutrons or photons")
|
|
|
|
|
self._particle_type = ptype
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
@property
|
2023-04-07 15:32:02 -04:00
|
|
|
def energy_bounds(self) -> Iterable[Real]:
|
2022-03-11 09:51:22 -06:00
|
|
|
return self._energy_bounds
|
2022-01-08 14:43:18 -06:00
|
|
|
|
2022-03-11 09:51:22 -06:00
|
|
|
@energy_bounds.setter
|
2023-04-15 15:44:02 -04:00
|
|
|
def energy_bounds(self, bounds: Iterable[float]):
|
2022-03-11 15:33:39 -06:00
|
|
|
cv.check_type('Energy bounds', bounds, Iterable, Real)
|
|
|
|
|
self._energy_bounds = np.asarray(bounds)
|
2022-01-08 14:43:18 -06:00
|
|
|
|
2022-03-11 11:38:04 -06:00
|
|
|
@property
|
2023-04-07 15:32:02 -04:00
|
|
|
def num_energy_bins(self) -> int:
|
2022-03-11 11:56:15 -06:00
|
|
|
if self.energy_bounds is None:
|
2023-06-30 22:29:47 -05:00
|
|
|
return 1
|
2022-03-11 11:38:04 -06:00
|
|
|
return self.energy_bounds.size - 1
|
|
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
@property
|
2023-04-10 12:56:51 -04:00
|
|
|
def lower_ww_bounds(self) -> np.ndarray:
|
2022-01-08 14:43:18 -06:00
|
|
|
return self._lower_ww_bounds
|
|
|
|
|
|
|
|
|
|
@lower_ww_bounds.setter
|
2023-04-15 15:44:02 -04:00
|
|
|
def lower_ww_bounds(self, bounds: Iterable[float]):
|
2022-03-11 11:38:04 -06:00
|
|
|
cv.check_iterable_type('Lower WW bounds',
|
|
|
|
|
bounds,
|
|
|
|
|
Real,
|
|
|
|
|
min_depth=1,
|
|
|
|
|
max_depth=4)
|
|
|
|
|
# reshape data according to mesh and energy bins
|
|
|
|
|
bounds = np.asarray(bounds)
|
|
|
|
|
if isinstance(self.mesh, UnstructuredMesh):
|
2022-10-24 13:23:15 -04:00
|
|
|
bounds = bounds.reshape(-1, self.num_energy_bins)
|
2022-03-11 11:38:04 -06:00
|
|
|
else:
|
2022-10-24 13:23:15 -04:00
|
|
|
bounds = bounds.reshape(*self.mesh.dimension, self.num_energy_bins)
|
2022-03-11 11:38:04 -06:00
|
|
|
self._lower_ww_bounds = bounds
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
@property
|
2023-04-10 12:56:51 -04:00
|
|
|
def upper_ww_bounds(self) -> np.ndarray:
|
2022-01-08 14:43:18 -06:00
|
|
|
return self._upper_ww_bounds
|
|
|
|
|
|
|
|
|
|
@upper_ww_bounds.setter
|
2023-04-15 15:44:02 -04:00
|
|
|
def upper_ww_bounds(self, bounds: Iterable[float]):
|
2022-03-11 11:38:04 -06:00
|
|
|
cv.check_iterable_type('Upper WW bounds',
|
|
|
|
|
bounds,
|
|
|
|
|
Real,
|
|
|
|
|
min_depth=1,
|
|
|
|
|
max_depth=4)
|
|
|
|
|
# reshape data according to mesh and energy bins
|
|
|
|
|
bounds = np.asarray(bounds)
|
|
|
|
|
if isinstance(self.mesh, UnstructuredMesh):
|
2022-10-24 13:23:15 -04:00
|
|
|
bounds = bounds.reshape(-1, self.num_energy_bins)
|
2022-03-11 11:38:04 -06:00
|
|
|
else:
|
2022-10-24 13:23:15 -04:00
|
|
|
bounds = bounds.reshape(*self.mesh.dimension, self.num_energy_bins)
|
2022-03-11 11:38:04 -06:00
|
|
|
self._upper_ww_bounds = bounds
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
@property
|
2023-04-07 15:32:02 -04:00
|
|
|
def survival_ratio(self) -> float:
|
2022-01-08 14:43:18 -06:00
|
|
|
return self._survival_ratio
|
|
|
|
|
|
|
|
|
|
@survival_ratio.setter
|
2023-04-12 14:00:48 -04:00
|
|
|
def survival_ratio(self, val: float):
|
2022-01-08 14:43:18 -06:00
|
|
|
cv.check_type('Survival ratio', val, Real)
|
|
|
|
|
cv.check_greater_than('Survival ratio', val, 1.0, True)
|
|
|
|
|
self._survival_ratio = val
|
|
|
|
|
|
2022-01-21 08:47:10 -06:00
|
|
|
@property
|
2023-04-07 15:32:02 -04:00
|
|
|
def max_lower_bound_ratio(self) -> float:
|
2022-01-21 08:47:10 -06:00
|
|
|
return self._max_lower_bound_ratio
|
|
|
|
|
|
|
|
|
|
@max_lower_bound_ratio.setter
|
2023-04-12 14:00:48 -04:00
|
|
|
def max_lower_bound_ratio(self, val: float):
|
2022-01-21 08:47:10 -06:00
|
|
|
cv.check_type('Maximum lower bound ratio', val, Real)
|
2023-06-09 10:47:27 -05:00
|
|
|
cv.check_greater_than('Maximum lower bound ratio', val, 1.0, equality=True)
|
2022-01-21 08:47:10 -06:00
|
|
|
self._max_lower_bound_ratio = val
|
|
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
@property
|
2023-04-07 15:32:02 -04:00
|
|
|
def max_split(self) -> int:
|
2022-01-08 14:43:18 -06:00
|
|
|
return self._max_split
|
|
|
|
|
|
|
|
|
|
@max_split.setter
|
2023-04-12 14:00:48 -04:00
|
|
|
def max_split(self, val: int):
|
2022-01-08 14:43:18 -06:00
|
|
|
cv.check_type('Max split', val, Integral)
|
|
|
|
|
self._max_split = val
|
|
|
|
|
|
|
|
|
|
@property
|
2023-04-07 15:32:02 -04:00
|
|
|
def weight_cutoff(self) -> float:
|
2022-01-08 14:43:18 -06:00
|
|
|
return self._weight_cutoff
|
|
|
|
|
|
|
|
|
|
@weight_cutoff.setter
|
2023-04-12 14:00:48 -04:00
|
|
|
def weight_cutoff(self, cutoff: float):
|
2022-01-08 14:43:18 -06:00
|
|
|
cv.check_type('Weight cutoff', cutoff, Real)
|
|
|
|
|
cv.check_greater_than('Weight cutoff', cutoff, 0.0, True)
|
|
|
|
|
self._weight_cutoff = cutoff
|
|
|
|
|
|
2023-04-07 15:32:02 -04:00
|
|
|
def to_xml_element(self) -> ET.Element:
|
2022-01-08 14:43:18 -06:00
|
|
|
"""Return an XML representation of the weight window settings
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2023-05-09 11:41:04 -04:00
|
|
|
element : lxml.etree._Element
|
2022-01-08 14:43:18 -06:00
|
|
|
XML element containing the weight window information
|
|
|
|
|
"""
|
|
|
|
|
element = ET.Element('weight_windows')
|
|
|
|
|
|
|
|
|
|
element.set('id', str(self._id))
|
|
|
|
|
|
|
|
|
|
subelement = ET.SubElement(element, 'mesh')
|
|
|
|
|
subelement.text = str(self.mesh.id)
|
|
|
|
|
|
|
|
|
|
subelement = ET.SubElement(element, 'particle_type')
|
2026-02-03 01:23:24 -06:00
|
|
|
subelement.text = str(self.particle_type)
|
2022-01-08 14:43:18 -06:00
|
|
|
|
2024-10-04 22:59:41 -05:00
|
|
|
if self.energy_bounds is not None:
|
|
|
|
|
subelement = ET.SubElement(element, 'energy_bounds')
|
|
|
|
|
subelement.text = ' '.join(str(e) for e in self.energy_bounds)
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
subelement = ET.SubElement(element, 'lower_ww_bounds')
|
2022-07-18 15:30:52 -05:00
|
|
|
subelement.text = ' '.join(str(b) for b in self.lower_ww_bounds.ravel('F'))
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
subelement = ET.SubElement(element, 'upper_ww_bounds')
|
2022-07-18 15:30:52 -05:00
|
|
|
subelement.text = ' '.join(str(b) for b in self.upper_ww_bounds.ravel('F'))
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
subelement = ET.SubElement(element, 'survival_ratio')
|
|
|
|
|
subelement.text = str(self.survival_ratio)
|
|
|
|
|
|
2022-02-01 12:17:38 -06:00
|
|
|
if self.max_lower_bound_ratio is not None:
|
|
|
|
|
subelement = ET.SubElement(element, 'max_lower_bound_ratio')
|
|
|
|
|
subelement.text = str(self.max_lower_bound_ratio)
|
2022-01-21 08:47:10 -06:00
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
subelement = ET.SubElement(element, 'max_split')
|
|
|
|
|
subelement.text = str(self.max_split)
|
|
|
|
|
|
|
|
|
|
subelement = ET.SubElement(element, 'weight_cutoff')
|
|
|
|
|
subelement.text = str(self.weight_cutoff)
|
|
|
|
|
|
|
|
|
|
return element
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-25 13:44:53 -05:00
|
|
|
def from_xml_element(cls, elem: ET.Element, meshes: dict[int, MeshBase]) -> Self:
|
2022-01-08 14:43:18 -06:00
|
|
|
"""Generate weight window settings from an XML element
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2023-05-09 11:41:04 -04:00
|
|
|
elem : lxml.etree._Element
|
2022-01-08 14:43:18 -06:00
|
|
|
XML element
|
2023-12-13 08:41:51 -06:00
|
|
|
meshes : dict
|
|
|
|
|
Dictionary mapping IDs to mesh objects
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
openmc.WeightWindows
|
|
|
|
|
Weight windows object
|
|
|
|
|
"""
|
|
|
|
|
# Get mesh for weight windows
|
|
|
|
|
mesh_id = int(get_text(elem, 'mesh'))
|
2023-12-13 08:41:51 -06:00
|
|
|
if mesh_id not in meshes:
|
|
|
|
|
raise ValueError(f'Could not locate mesh with ID "{mesh_id}"')
|
|
|
|
|
mesh = meshes[mesh_id]
|
2022-01-08 14:43:18 -06:00
|
|
|
|
|
|
|
|
# Read all other parameters
|
2025-08-08 20:47:55 +03:00
|
|
|
lower_ww_bounds = get_elem_list(elem, "lower_ww_bounds", float)
|
|
|
|
|
upper_ww_bounds = get_elem_list(elem, "upper_ww_bounds", float)
|
|
|
|
|
e_bounds = get_elem_list(elem, "energy_bounds", float)
|
2022-01-08 14:43:18 -06:00
|
|
|
particle_type = get_text(elem, 'particle_type')
|
|
|
|
|
survival_ratio = float(get_text(elem, 'survival_ratio'))
|
2022-02-01 12:17:38 -06:00
|
|
|
|
2022-10-27 14:47:16 -05:00
|
|
|
ww_shape = (len(e_bounds) - 1,) + mesh.dimension[::-1]
|
2022-10-24 16:47:34 -04:00
|
|
|
lower_ww_bounds = np.array(lower_ww_bounds).reshape(ww_shape).T
|
|
|
|
|
upper_ww_bounds = np.array(upper_ww_bounds).reshape(ww_shape).T
|
|
|
|
|
|
2022-02-01 12:17:38 -06:00
|
|
|
max_lower_bound_ratio = None
|
|
|
|
|
if get_text(elem, 'max_lower_bound_ratio'):
|
|
|
|
|
max_lower_bound_ratio = float(get_text(elem, 'max_lower_bound_ratio'))
|
|
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
max_split = int(get_text(elem, 'max_split'))
|
|
|
|
|
weight_cutoff = float(get_text(elem, 'weight_cutoff'))
|
|
|
|
|
id = int(get_text(elem, 'id'))
|
|
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
mesh=mesh,
|
|
|
|
|
lower_ww_bounds=lower_ww_bounds,
|
|
|
|
|
upper_ww_bounds=upper_ww_bounds,
|
2022-03-11 15:33:39 -06:00
|
|
|
energy_bounds=e_bounds,
|
2022-01-08 14:43:18 -06:00
|
|
|
particle_type=particle_type,
|
|
|
|
|
survival_ratio=survival_ratio,
|
2022-02-01 12:17:38 -06:00
|
|
|
max_lower_bound_ratio=max_lower_bound_ratio,
|
2022-01-08 14:43:18 -06:00
|
|
|
max_split=max_split,
|
|
|
|
|
weight_cutoff=weight_cutoff,
|
|
|
|
|
id=id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-25 13:44:53 -05:00
|
|
|
def from_hdf5(cls, group: h5py.Group, meshes: dict[int, MeshBase]) -> Self:
|
2022-01-08 14:43:18 -06:00
|
|
|
"""Create weight windows from HDF5 group
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
group : h5py.Group
|
|
|
|
|
Group in HDF5 file
|
|
|
|
|
meshes : dict
|
|
|
|
|
Dictionary mapping IDs to mesh objects
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
openmc.WeightWindows
|
|
|
|
|
A weight window object
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
id = int(group.name.split('/')[-1].lstrip('weight_windows'))
|
|
|
|
|
mesh_id = group['mesh'][()]
|
2023-06-09 10:47:27 -05:00
|
|
|
mesh = meshes[mesh_id]
|
|
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
ptype = group['particle_type'][()].decode()
|
2022-03-11 15:33:39 -06:00
|
|
|
e_bounds = group['energy_bounds'][()]
|
2023-06-09 10:47:27 -05:00
|
|
|
# weight window bounds are stored with the shape (e, k, j, i)
|
|
|
|
|
# in C++ and HDF5 -- the opposite of how they are stored here
|
|
|
|
|
shape = (e_bounds.size - 1, *mesh.dimension[::-1])
|
|
|
|
|
lower_ww_bounds = group['lower_ww_bounds'][()].reshape(shape).T
|
|
|
|
|
upper_ww_bounds = group['upper_ww_bounds'][()].reshape(shape).T
|
2022-01-08 14:43:18 -06:00
|
|
|
survival_ratio = group['survival_ratio'][()]
|
2022-02-01 12:17:38 -06:00
|
|
|
|
|
|
|
|
max_lower_bound_ratio = None
|
|
|
|
|
if group.get('max_lower_bound_ratio') is not None:
|
|
|
|
|
max_lower_bound_ratio = group['max_lower_bound_ratio'][()]
|
|
|
|
|
|
2022-01-08 14:43:18 -06:00
|
|
|
max_split = group['max_split'][()]
|
|
|
|
|
weight_cutoff = group['weight_cutoff'][()]
|
|
|
|
|
|
|
|
|
|
return cls(
|
2023-06-09 10:47:27 -05:00
|
|
|
mesh=mesh,
|
2022-01-08 14:43:18 -06:00
|
|
|
lower_ww_bounds=lower_ww_bounds,
|
|
|
|
|
upper_ww_bounds=upper_ww_bounds,
|
2022-03-11 15:33:39 -06:00
|
|
|
energy_bounds=e_bounds,
|
2022-01-08 14:43:18 -06:00
|
|
|
particle_type=ptype,
|
|
|
|
|
survival_ratio=survival_ratio,
|
2022-02-01 12:17:38 -06:00
|
|
|
max_lower_bound_ratio=max_lower_bound_ratio,
|
2022-01-08 14:43:18 -06:00
|
|
|
max_split=max_split,
|
|
|
|
|
weight_cutoff=weight_cutoff,
|
|
|
|
|
id=id
|
|
|
|
|
)
|
2022-03-02 10:12:10 -06:00
|
|
|
|
2022-03-11 09:45:16 -06:00
|
|
|
|
2025-06-25 13:44:53 -05:00
|
|
|
def wwinp_to_wws(path: PathLike) -> WeightWindowsList:
|
2022-07-29 10:08:33 -05:00
|
|
|
"""Create WeightWindows instances from a wwinp file
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.13.1
|
2022-03-02 10:12:10 -06:00
|
|
|
|
2022-03-04 14:52:55 -06:00
|
|
|
Parameters
|
|
|
|
|
----------
|
2022-03-17 22:26:26 -05:00
|
|
|
path : str or pathlib.Path
|
2022-03-11 14:33:34 -06:00
|
|
|
Path to the wwinp file
|
2022-03-02 10:12:10 -06:00
|
|
|
|
2022-03-04 14:52:55 -06:00
|
|
|
Returns
|
|
|
|
|
-------
|
2025-06-25 13:44:53 -05:00
|
|
|
WeightWindowsList
|
2022-03-04 14:52:55 -06:00
|
|
|
"""
|
2025-06-25 13:44:53 -05:00
|
|
|
warnings.warn(
|
|
|
|
|
"This function is deprecated in favor of 'WeightWindowsList.from_wwinp'",
|
|
|
|
|
FutureWarning
|
|
|
|
|
)
|
|
|
|
|
return WeightWindowsList.from_wwinp(path)
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class WeightWindowGenerator:
|
|
|
|
|
"""Class passed to setting to govern weight window generation
|
|
|
|
|
using the OpenMC executable
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
mesh : :class:`openmc.MeshBase`
|
|
|
|
|
Mesh used to represent the weight windows spatially
|
|
|
|
|
energy_bounds : Iterable of Real
|
|
|
|
|
A list of values for which each successive pair constitutes a range of
|
2023-06-30 22:29:47 -05:00
|
|
|
energies in [eV] for a single bin. If no energy bins are provided, the
|
|
|
|
|
maximum and minimum energy for the data available at runtime.
|
2026-02-03 01:23:24 -06:00
|
|
|
particle_type : str or int or openmc.ParticleType
|
2023-06-09 10:47:27 -05:00
|
|
|
Particle type the weight windows apply to
|
2025-01-27 22:54:32 -06:00
|
|
|
method : {'magic', 'fw_cadis'}
|
|
|
|
|
The weight window generation methodology applied during an update.
|
2026-04-28 17:10:03 -04:00
|
|
|
targets : :class:`openmc.Tallies` or iterable of int
|
|
|
|
|
Target tallies for local variance reduction via FW-CADIS.
|
2023-09-14 03:27:48 +01:00
|
|
|
max_realizations : int
|
|
|
|
|
The upper limit for number of tally realizations when generating weight
|
|
|
|
|
windows.
|
|
|
|
|
update_interval : int
|
|
|
|
|
The number of tally realizations between updates.
|
|
|
|
|
on_the_fly : bool
|
|
|
|
|
Whether or not to apply weight windows on the fly.
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
----------
|
|
|
|
|
mesh : openmc.MeshBase
|
|
|
|
|
Mesh used to represent the weight windows spatially
|
|
|
|
|
energy_bounds : Iterable of Real
|
|
|
|
|
A list of values for which each successive pair constitutes a range of
|
|
|
|
|
energies in [eV] for a single bin
|
2026-02-03 01:23:24 -06:00
|
|
|
particle_type : openmc.ParticleType
|
2023-06-09 10:47:27 -05:00
|
|
|
Particle type the weight windows apply to
|
2025-01-27 22:54:32 -06:00
|
|
|
method : {'magic', 'fw_cadis'}
|
|
|
|
|
The weight window generation methodology applied during an update.
|
2026-04-28 17:10:03 -04:00
|
|
|
targets : :class:`openmc.Tallies` or numpy.ndarray
|
|
|
|
|
Target tallies for local variance reduction via FW-CADIS.
|
2023-06-09 10:47:27 -05:00
|
|
|
max_realizations : int
|
|
|
|
|
The upper limit for number of tally realizations when generating weight
|
|
|
|
|
windows.
|
|
|
|
|
update_interval : int
|
2023-09-14 03:27:48 +01:00
|
|
|
The number of tally realizations between updates.
|
2023-06-09 10:47:27 -05:00
|
|
|
update_parameters : dict
|
|
|
|
|
A set of parameters related to the update.
|
|
|
|
|
on_the_fly : bool
|
2023-09-14 03:27:48 +01:00
|
|
|
Whether or not to apply weight windows on the fly.
|
2023-06-09 10:47:27 -05:00
|
|
|
"""
|
|
|
|
|
|
2026-04-28 17:10:03 -04:00
|
|
|
_WWG_PARAMS = {'value': str, 'threshold': float, 'ratio': float}
|
2023-06-09 10:47:27 -05:00
|
|
|
|
2023-09-14 03:27:48 +01:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
mesh: openmc.MeshBase,
|
2024-07-18 12:40:42 -05:00
|
|
|
energy_bounds: Sequence[float] | None = None,
|
2026-02-03 01:23:24 -06:00
|
|
|
particle_type: str | int | openmc.ParticleType = 'neutron',
|
2023-09-14 03:27:48 +01:00
|
|
|
method: str = 'magic',
|
2026-04-28 17:10:03 -04:00
|
|
|
targets: openmc.Tallies | Iterable[int] | None = None,
|
2023-09-14 03:27:48 +01:00
|
|
|
max_realizations: int = 1,
|
|
|
|
|
update_interval: int = 1,
|
|
|
|
|
on_the_fly: bool = True
|
|
|
|
|
):
|
|
|
|
|
self._update_parameters = None
|
|
|
|
|
|
2023-06-09 10:47:27 -05:00
|
|
|
self.mesh = mesh
|
2023-06-30 22:29:47 -05:00
|
|
|
self._energy_bounds = None
|
2023-06-09 10:47:27 -05:00
|
|
|
if energy_bounds is not None:
|
|
|
|
|
self.energy_bounds = energy_bounds
|
|
|
|
|
self.particle_type = particle_type
|
2023-09-14 03:27:48 +01:00
|
|
|
self.method = method
|
2026-04-28 17:10:03 -04:00
|
|
|
self.targets = targets
|
2023-09-14 03:27:48 +01:00
|
|
|
self.max_realizations = max_realizations
|
|
|
|
|
self.update_interval = update_interval
|
|
|
|
|
self.on_the_fly = on_the_fly
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
string = type(self).__name__ + '\n'
|
|
|
|
|
string += f'\t{"Mesh":<20}=\t{self.mesh.id}\n'
|
2026-02-03 01:23:24 -06:00
|
|
|
string += f'\t{"Particle:":<20}=\t{str(self.particle_type)}\n'
|
2023-06-09 10:47:27 -05:00
|
|
|
string += f'\t{"Energy Bounds:":<20}=\t{self.energy_bounds}\n'
|
|
|
|
|
string += f'\t{"Method":<20}=\t{self.method}\n'
|
|
|
|
|
string += f'\t{"Max Realizations:":<20}=\t{self.max_realizations}\n'
|
|
|
|
|
string += f'\t{"Update Interval:":<20}=\t{self.update_interval}\n'
|
|
|
|
|
string += f'\t{"On The Fly:":<20}=\t{self.on_the_fly}\n'
|
|
|
|
|
if self.update_parameters is not None:
|
|
|
|
|
string += f'\t{"Update Parameters:":<20}\n\t\t\t{self.update_parameters}\n'
|
|
|
|
|
string
|
|
|
|
|
|
|
|
|
|
return string
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def mesh(self) -> openmc.MeshBase:
|
|
|
|
|
return self._mesh
|
|
|
|
|
|
|
|
|
|
@mesh.setter
|
|
|
|
|
def mesh(self, m: openmc.MeshBase):
|
|
|
|
|
cv.check_type('mesh', m, openmc.MeshBase)
|
|
|
|
|
self._mesh = m
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def energy_bounds(self) -> Iterable[Real]:
|
|
|
|
|
return self._energy_bounds
|
|
|
|
|
|
|
|
|
|
@energy_bounds.setter
|
|
|
|
|
def energy_bounds(self, eb: Iterable[float]):
|
|
|
|
|
cv.check_type('energy bounds', eb, Iterable, Real)
|
|
|
|
|
self._energy_bounds = eb
|
|
|
|
|
|
|
|
|
|
@property
|
2026-02-03 01:23:24 -06:00
|
|
|
def particle_type(self) -> ParticleType:
|
2023-06-09 10:47:27 -05:00
|
|
|
return self._particle_type
|
|
|
|
|
|
|
|
|
|
@particle_type.setter
|
2026-02-03 01:23:24 -06:00
|
|
|
def particle_type(self, pt):
|
|
|
|
|
ptype = ParticleType(pt)
|
|
|
|
|
if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}:
|
|
|
|
|
raise ValueError("Weight windows can only be applied for neutrons or photons")
|
|
|
|
|
self._particle_type = ptype
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def method(self) -> str:
|
|
|
|
|
return self._method
|
|
|
|
|
|
|
|
|
|
@method.setter
|
|
|
|
|
def method(self, m: str):
|
|
|
|
|
cv.check_type('generation method', m, str)
|
2025-01-27 22:54:32 -06:00
|
|
|
cv.check_value('generation method', m, ('magic', 'fw_cadis'))
|
2023-06-09 10:47:27 -05:00
|
|
|
self._method = m
|
|
|
|
|
if self._update_parameters is not None:
|
|
|
|
|
try:
|
|
|
|
|
self._check_update_parameters()
|
|
|
|
|
except (TypeError, KeyError):
|
|
|
|
|
warnings.warn(f'Update parameters are invalid for the "{m}" method.')
|
2026-04-28 17:10:03 -04:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def targets(self) -> openmc.Tallies:
|
|
|
|
|
return self._targets
|
|
|
|
|
|
|
|
|
|
@targets.setter
|
|
|
|
|
def targets(self, t):
|
|
|
|
|
if t is None:
|
|
|
|
|
self._targets = t
|
|
|
|
|
else:
|
|
|
|
|
cv.check_type('Local FW-CADIS target tallies', t, Iterable)
|
|
|
|
|
cv.check_greater_than('Local FW-CADIS target tallies', len(t), 0)
|
|
|
|
|
if not isinstance(t, openmc.Tallies):
|
|
|
|
|
cv.check_iterable_type('Local FW-CADIS target tallies', t, int)
|
|
|
|
|
t = np.asarray(list(t), dtype=int)
|
|
|
|
|
self._targets = t
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def max_realizations(self) -> int:
|
|
|
|
|
return self._max_realizations
|
|
|
|
|
|
|
|
|
|
@max_realizations.setter
|
|
|
|
|
def max_realizations(self, m: int):
|
|
|
|
|
cv.check_type('max tally realizations', m, Integral)
|
|
|
|
|
cv.check_greater_than('max tally realizations', m, 0)
|
|
|
|
|
self._max_realizations = m
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def update_interval(self) -> int:
|
|
|
|
|
return self._update_interval
|
|
|
|
|
|
|
|
|
|
@update_interval.setter
|
|
|
|
|
def update_interval(self, ui: int):
|
|
|
|
|
cv.check_type('update interval', ui, Integral)
|
|
|
|
|
cv.check_greater_than('update interval', ui , 0)
|
|
|
|
|
self._update_interval = ui
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def update_parameters(self) -> dict:
|
|
|
|
|
return self._update_parameters
|
|
|
|
|
|
|
|
|
|
def _check_update_parameters(self, params: dict):
|
2025-01-27 22:54:32 -06:00
|
|
|
if self.method == 'magic' or self.method == 'fw_cadis':
|
2026-04-28 17:10:03 -04:00
|
|
|
check_params = self._WWG_PARAMS
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
for key, val in params.items():
|
|
|
|
|
if key not in check_params:
|
|
|
|
|
raise ValueError(f'Invalid param "{key}" for {self.method} '
|
|
|
|
|
'weight window generation')
|
2026-04-28 17:10:03 -04:00
|
|
|
cv.check_type(f'weight window generation param: "{key}"', val, self._WWG_PARAMS[key])
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
@update_parameters.setter
|
|
|
|
|
def update_parameters(self, params: dict):
|
|
|
|
|
self._check_update_parameters(params)
|
|
|
|
|
self._update_parameters = params
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def on_the_fly(self) -> bool:
|
|
|
|
|
return self._on_the_fly
|
|
|
|
|
|
|
|
|
|
@on_the_fly.setter
|
|
|
|
|
def on_the_fly(self, otf: bool):
|
|
|
|
|
cv.check_type('on the fly generation', otf, bool)
|
|
|
|
|
self._on_the_fly = otf
|
|
|
|
|
|
|
|
|
|
def _update_parameters_subelement(self, element: ET.Element):
|
|
|
|
|
if not self.update_parameters:
|
|
|
|
|
return
|
|
|
|
|
params_element = ET.SubElement(element, 'update_parameters')
|
|
|
|
|
for pname, value in self.update_parameters.items():
|
|
|
|
|
param_element = ET.SubElement(params_element, pname)
|
|
|
|
|
param_element.text = str(value)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _sanitize_update_parameters(cls, method: str, update_parameters: dict):
|
|
|
|
|
"""
|
|
|
|
|
Attempt to convert update parameters to their appropriate types
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
method : str
|
|
|
|
|
The update method for which these update parameters should comply
|
|
|
|
|
update_parameters : dict
|
|
|
|
|
The update parameters as-read from the XML node (keys: str, values: str)
|
|
|
|
|
"""
|
2025-01-27 22:54:32 -06:00
|
|
|
if method == 'magic' or method == 'fw_cadis':
|
2026-04-28 17:10:03 -04:00
|
|
|
check_params = cls._WWG_PARAMS
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
for param, param_type in check_params.items():
|
|
|
|
|
if param in update_parameters:
|
|
|
|
|
update_parameters[param] = param_type(update_parameters[param])
|
|
|
|
|
|
|
|
|
|
def to_xml_element(self):
|
|
|
|
|
"""Creates a 'weight_window_generator' element to be written to an XML file.
|
|
|
|
|
"""
|
|
|
|
|
element = ET.Element('weight_windows_generator')
|
|
|
|
|
|
|
|
|
|
mesh_elem = ET.SubElement(element, 'mesh')
|
|
|
|
|
mesh_elem.text = str(self.mesh.id)
|
|
|
|
|
if self.energy_bounds is not None:
|
|
|
|
|
subelement = ET.SubElement(element, 'energy_bounds')
|
|
|
|
|
subelement.text = ' '.join(str(e) for e in self.energy_bounds)
|
|
|
|
|
particle_elem = ET.SubElement(element, 'particle_type')
|
2026-02-03 01:23:24 -06:00
|
|
|
particle_elem.text = str(self.particle_type)
|
2023-06-09 10:47:27 -05:00
|
|
|
realizations_elem = ET.SubElement(element, 'max_realizations')
|
|
|
|
|
realizations_elem.text = str(self.max_realizations)
|
|
|
|
|
update_interval_elem = ET.SubElement(element, 'update_interval')
|
|
|
|
|
update_interval_elem.text = str(self.update_interval)
|
|
|
|
|
otf_elem = ET.SubElement(element, 'on_the_fly')
|
|
|
|
|
otf_elem.text = str(self.on_the_fly).lower()
|
|
|
|
|
method_elem = ET.SubElement(element, 'method')
|
|
|
|
|
method_elem.text = self.method
|
2026-04-28 17:10:03 -04:00
|
|
|
if self.targets is not None:
|
|
|
|
|
if self.method != 'fw_cadis':
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"FW-CADIS update method is required in order to use " \
|
|
|
|
|
"target tallies for WeightWindowGenerator.")
|
|
|
|
|
elif isinstance(self.targets, openmc.Tallies):
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"FW-CADIS target tallies must be checked to ensure they are " \
|
|
|
|
|
"present on model.tallies. Use model.export_to_xml() or " \
|
|
|
|
|
"model.export_to_model_xml() to link FW-CADIS target tallies.")
|
|
|
|
|
else:
|
|
|
|
|
targets_elem = ET.SubElement(element, 'targets')
|
|
|
|
|
targets_elem.text = ' '.join(str(tally_id) for tally_id in self.targets)
|
|
|
|
|
|
2023-06-09 10:47:27 -05:00
|
|
|
if self.update_parameters is not None:
|
|
|
|
|
self._update_parameters_subelement(element)
|
|
|
|
|
|
|
|
|
|
clean_indentation(element)
|
|
|
|
|
|
|
|
|
|
return element
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-25 13:44:53 -05:00
|
|
|
def from_xml_element(cls, elem: ET.Element, meshes: dict) -> Self:
|
2023-06-09 10:47:27 -05:00
|
|
|
"""
|
|
|
|
|
Create a weight window generation object from an XML element
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
elem : xml.etree.ElementTree.Element
|
|
|
|
|
XML element
|
|
|
|
|
meshes : dict
|
|
|
|
|
A dictionary with IDs as keys and openmc.MeshBase instances as values
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
openmc.WeightWindowGenerator
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
mesh_id = int(get_text(elem, 'mesh'))
|
|
|
|
|
mesh = meshes[mesh_id]
|
2026-04-28 17:10:03 -04:00
|
|
|
|
|
|
|
|
energy_bounds = get_elem_list(elem, "energy_bounds", float)
|
2023-06-09 10:47:27 -05:00
|
|
|
particle_type = get_text(elem, 'particle_type')
|
|
|
|
|
|
|
|
|
|
wwg = cls(mesh, energy_bounds, particle_type)
|
|
|
|
|
|
|
|
|
|
wwg.max_realizations = int(get_text(elem, 'max_realizations'))
|
|
|
|
|
wwg.update_interval = int(get_text(elem, 'update_interval'))
|
|
|
|
|
wwg.on_the_fly = bool(get_text(elem, 'on_the_fly'))
|
|
|
|
|
wwg.method = get_text(elem, 'method')
|
2026-04-28 17:10:03 -04:00
|
|
|
targets_elem = elem.find('targets')
|
|
|
|
|
if targets_elem is not None:
|
|
|
|
|
if wwg.method != 'fw_cadis':
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"FW-CADIS update method is required in order to use " \
|
|
|
|
|
"target tallies for WeightWindowGenerator.")
|
|
|
|
|
else:
|
|
|
|
|
wwg.targets = get_elem_list(elem, "targets")
|
2023-06-09 10:47:27 -05:00
|
|
|
|
2023-09-26 22:17:49 -05:00
|
|
|
if elem.find('update_parameters') is not None:
|
2023-06-09 10:47:27 -05:00
|
|
|
update_parameters = {}
|
|
|
|
|
params_elem = elem.find('update_parameters')
|
|
|
|
|
for entry in params_elem:
|
|
|
|
|
update_parameters[entry.tag] = entry.text
|
|
|
|
|
|
|
|
|
|
cls._sanitize_update_parameters(wwg.method, update_parameters)
|
|
|
|
|
wwg.update_parameters = update_parameters
|
|
|
|
|
|
|
|
|
|
return wwg
|
|
|
|
|
|
2025-06-25 13:44:53 -05:00
|
|
|
def hdf5_to_wws(path='weight_windows.h5') -> WeightWindowsList:
|
|
|
|
|
"""Create a WeightWindowsList from a weight windows HDF5 file
|
2023-06-09 10:47:27 -05:00
|
|
|
|
2023-10-31 16:55:19 -05:00
|
|
|
.. versionadded:: 0.14.0
|
2023-06-09 10:47:27 -05:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
path : cv.PathLike
|
|
|
|
|
Path to the weight windows hdf5 file
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
2025-06-25 13:44:53 -05:00
|
|
|
WeightWindowsList
|
|
|
|
|
"""
|
|
|
|
|
warnings.warn(
|
|
|
|
|
"This function is deprecated in favor of 'WeightWindowsList.from_hdf5'",
|
|
|
|
|
FutureWarning
|
|
|
|
|
)
|
|
|
|
|
return WeightWindowsList.from_hdf5(path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WeightWindowsList(list):
|
|
|
|
|
"""A list of WeightWindows objects.
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.3
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
iterable : iterable of openmc.WeightWindows
|
|
|
|
|
An iterable of WeightWindows objects to initialize the list with
|
|
|
|
|
|
2023-06-09 10:47:27 -05:00
|
|
|
"""
|
2025-06-25 13:44:53 -05:00
|
|
|
def __init__(self, iterable: Iterable[WeightWindows] = ()):
|
|
|
|
|
super().__init__(iterable)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_hdf5(cls, path: PathLike = 'weight_windows.h5') -> Self:
|
|
|
|
|
"""Create WeightWindowsList from a weight windows HDF5 file.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
path : PathLike
|
|
|
|
|
Path to the weight windows hdf5 file
|
2023-06-09 10:47:27 -05:00
|
|
|
|
2025-06-25 13:44:53 -05:00
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
WeightWindowsList
|
|
|
|
|
A list of WeightWindows objects read from the file
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
with h5py.File(path) as h5_file:
|
|
|
|
|
# read in all of the meshes in the mesh node
|
|
|
|
|
meshes = {}
|
|
|
|
|
for mesh_group in h5_file['meshes']:
|
|
|
|
|
mesh = MeshBase.from_hdf5(h5_file['meshes'][mesh_group])
|
|
|
|
|
meshes[mesh.id] = mesh
|
|
|
|
|
wws = [
|
|
|
|
|
WeightWindows.from_hdf5(ww, meshes)
|
|
|
|
|
for ww in h5_file['weight_windows'].values()
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return cls(wws)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_wwinp(cls, path: PathLike) -> Self:
|
|
|
|
|
"""Create WeightWindowsList from a wwinp file.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
path : PathLike
|
|
|
|
|
Path to the wwinp file
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
WeightWindowsList
|
|
|
|
|
A list of WeightWindows objects read from the file
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
with open(path) as wwinp:
|
|
|
|
|
# BLOCK 1
|
|
|
|
|
header = wwinp.readline().split(None, 4)
|
|
|
|
|
# read file type, time-dependence, number of
|
|
|
|
|
# particles, mesh type and problem identifier
|
|
|
|
|
_if, iv, ni, nr = [int(x) for x in header[:4]]
|
|
|
|
|
|
|
|
|
|
# header value checks
|
|
|
|
|
if _if != 1:
|
|
|
|
|
raise ValueError(f'Found incorrect file type, if: {_if}')
|
|
|
|
|
|
|
|
|
|
if iv > 1:
|
|
|
|
|
# read number of time bins for each particle, 'nt(1...ni)'
|
|
|
|
|
nt = np.fromstring(wwinp.readline(), sep=' ', dtype=int)
|
|
|
|
|
|
|
|
|
|
# raise error if time bins are present for now
|
|
|
|
|
raise ValueError('Time-dependent weight windows '
|
|
|
|
|
'are not yet supported')
|
|
|
|
|
else:
|
|
|
|
|
nt = ni * [1]
|
|
|
|
|
|
|
|
|
|
# read number of energy bins for each particle, 'ne(1...ni)'
|
|
|
|
|
ne = np.fromstring(wwinp.readline(), sep=' ', dtype=int)
|
|
|
|
|
|
|
|
|
|
# read coarse mesh dimensions and lower left corner
|
|
|
|
|
mesh_description = np.fromstring(wwinp.readline(), sep=' ')
|
|
|
|
|
nfx, nfy, nfz = mesh_description[:3].astype(int)
|
|
|
|
|
xyz0 = mesh_description[3:]
|
|
|
|
|
|
|
|
|
|
# read cylindrical and spherical mesh vectors if present
|
|
|
|
|
if nr == 16:
|
|
|
|
|
# read number of coarse bins
|
|
|
|
|
line_arr = np.fromstring(wwinp.readline(), sep=' ')
|
|
|
|
|
ncx, ncy, ncz = line_arr[:3].astype(int)
|
|
|
|
|
# read polar vector (x1, y1, z1)
|
|
|
|
|
xyz1 = line_arr[3:]
|
|
|
|
|
# read azimuthal vector (x2, y2, z2)
|
|
|
|
|
line_arr = np.fromstring(wwinp.readline(), sep=' ')
|
|
|
|
|
xyz2 = line_arr[:3]
|
|
|
|
|
|
|
|
|
|
# Get polar and azimuthal axes
|
|
|
|
|
polar_axis = xyz1 - xyz0
|
|
|
|
|
azimuthal_axis = xyz2 - xyz0
|
|
|
|
|
|
|
|
|
|
# Check for polar axis other than (0, 0, 1)
|
|
|
|
|
norm = np.linalg.norm(polar_axis)
|
|
|
|
|
if not np.isclose(polar_axis[2]/norm, 1.0):
|
|
|
|
|
raise NotImplementedError('Polar axis not aligned to z-axis not supported')
|
|
|
|
|
|
|
|
|
|
# Check for azimuthal axis other than (1, 0, 0)
|
|
|
|
|
norm = np.linalg.norm(azimuthal_axis)
|
|
|
|
|
if not np.isclose(azimuthal_axis[0]/norm, 1.0):
|
|
|
|
|
raise NotImplementedError('Azimuthal axis not aligned to x-axis not supported')
|
|
|
|
|
|
|
|
|
|
# read geometry type
|
|
|
|
|
nwg = int(line_arr[-1])
|
|
|
|
|
|
|
|
|
|
elif nr == 10:
|
|
|
|
|
# read rectilinear data:
|
|
|
|
|
# number of coarse mesh bins and mesh type
|
|
|
|
|
ncx, ncy, ncz, nwg = \
|
|
|
|
|
np.fromstring(wwinp.readline(), sep=' ').astype(int)
|
|
|
|
|
else:
|
|
|
|
|
raise RuntimeError(f'Invalid mesh description (nr) found: {nr}')
|
|
|
|
|
|
|
|
|
|
# read BLOCK 2 and BLOCK 3 data into a single array
|
|
|
|
|
ww_data = np.fromstring(wwinp.read(), sep=' ')
|
|
|
|
|
|
|
|
|
|
# extract mesh data from the ww_data array
|
|
|
|
|
start_idx = 0
|
|
|
|
|
|
|
|
|
|
# first values in the mesh definition arrays are the first
|
|
|
|
|
# coordinate of the grid
|
|
|
|
|
end_idx = start_idx + 1 + 3 * ncx
|
|
|
|
|
i0, i_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx]
|
|
|
|
|
start_idx = end_idx
|
|
|
|
|
|
|
|
|
|
end_idx = start_idx + 1 + 3 * ncy
|
|
|
|
|
j0, j_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx]
|
|
|
|
|
start_idx = end_idx
|
|
|
|
|
|
|
|
|
|
end_idx = start_idx + 1 + 3 * ncz
|
|
|
|
|
k0, k_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx]
|
|
|
|
|
start_idx = end_idx
|
|
|
|
|
|
|
|
|
|
# mesh consistency checks
|
|
|
|
|
if nr == 16 and nwg == 1 or nr == 10 and nwg != 1:
|
|
|
|
|
raise ValueError(f'Mesh description in header ({nr}) '
|
|
|
|
|
f'does not match the mesh type ({nwg})')
|
|
|
|
|
|
|
|
|
|
if nr == 10 and (xyz0 != (i0, j0, k0)).any():
|
|
|
|
|
raise ValueError(f'Mesh origin in the header ({xyz0}) '
|
|
|
|
|
f' does not match the origin in the mesh '
|
|
|
|
|
f' description ({i0, j0, k0})')
|
|
|
|
|
|
|
|
|
|
# create openmc mesh object
|
|
|
|
|
grids = []
|
|
|
|
|
mesh_definition = [(i0, i_vals, nfx), (j0, j_vals, nfy), (k0, k_vals, nfz)]
|
|
|
|
|
for grid0, grid_vals, n_pnts in mesh_definition:
|
|
|
|
|
# file spec checks for the mesh definition
|
|
|
|
|
if (grid_vals[2::3] != 1.0).any():
|
|
|
|
|
raise ValueError('One or more mesh ratio value, qx, '
|
|
|
|
|
'is not equal to one')
|
|
|
|
|
|
|
|
|
|
s = int(grid_vals[::3].sum())
|
|
|
|
|
if s != n_pnts:
|
|
|
|
|
raise ValueError(f'Sum of the fine bin entries, {s}, does '
|
|
|
|
|
f'not match the number of fine bins, {n_pnts}')
|
|
|
|
|
|
|
|
|
|
# extend the grid based on the next coarse bin endpoint, px
|
|
|
|
|
# and the number of fine bins in the coarse bin, sx
|
|
|
|
|
intervals = grid_vals.reshape(-1, 3)
|
|
|
|
|
coords = [grid0]
|
|
|
|
|
for sx, px, qx in intervals:
|
|
|
|
|
coords += np.linspace(coords[-1], px, int(sx + 1)).tolist()[1:]
|
|
|
|
|
|
|
|
|
|
grids.append(np.array(coords))
|
|
|
|
|
|
|
|
|
|
if nwg == 1:
|
|
|
|
|
mesh = RectilinearMesh()
|
|
|
|
|
mesh.x_grid, mesh.y_grid, mesh.z_grid = grids
|
|
|
|
|
elif nwg == 2:
|
|
|
|
|
mesh = CylindricalMesh(
|
|
|
|
|
r_grid=grids[0],
|
|
|
|
|
z_grid=grids[1],
|
|
|
|
|
phi_grid=grids[2],
|
|
|
|
|
origin = xyz0,
|
|
|
|
|
)
|
|
|
|
|
elif nwg == 3:
|
|
|
|
|
mesh = SphericalMesh(
|
|
|
|
|
r_grid=grids[0],
|
|
|
|
|
theta_grid=grids[1],
|
|
|
|
|
phi_grid=grids[2],
|
|
|
|
|
origin = xyz0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# extract weight window values from array
|
|
|
|
|
wws = cls()
|
|
|
|
|
for ne_i, nt_i, particle_type in zip(ne, nt, ('neutron', 'photon')):
|
|
|
|
|
# no information to read for this particle if
|
|
|
|
|
# either the energy bins or time bins are empty
|
|
|
|
|
if ne_i == 0 or nt_i == 0:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if iv > 1:
|
|
|
|
|
# time bins are parsed but unused for now
|
|
|
|
|
end_idx = start_idx + nt_i
|
|
|
|
|
time_bounds = ww_data[start_idx:end_idx]
|
|
|
|
|
np.insert(time_bounds, (0,), (0.0,))
|
|
|
|
|
start_idx = end_idx
|
|
|
|
|
|
|
|
|
|
# read energy boundaries
|
|
|
|
|
end_idx = start_idx + ne_i
|
|
|
|
|
energy_bounds = np.insert(ww_data[start_idx:end_idx], (0,), (0.0,))
|
|
|
|
|
# convert from MeV to eV
|
|
|
|
|
energy_bounds *= 1e6
|
|
|
|
|
start_idx = end_idx
|
|
|
|
|
|
|
|
|
|
# read weight window values
|
|
|
|
|
end_idx = start_idx + (nfx * nfy * nfz) * nt_i * ne_i
|
|
|
|
|
|
|
|
|
|
# read values and reshape according to ordering
|
|
|
|
|
# slowest to fastest: t, e, z, y, x
|
|
|
|
|
# reorder with transpose since our ordering is x, y, z, e, t
|
|
|
|
|
ww_shape = (nt_i, ne_i, nfz, nfy, nfx)
|
|
|
|
|
ww_values = ww_data[start_idx:end_idx].reshape(ww_shape).T
|
|
|
|
|
# Only use first time bin since we don't support time dependent weight
|
|
|
|
|
# windows yet.
|
|
|
|
|
ww_values = ww_values[:, :, :, :, 0]
|
|
|
|
|
start_idx = end_idx
|
|
|
|
|
|
|
|
|
|
# create a weight window object
|
|
|
|
|
ww = WeightWindows(id=None,
|
|
|
|
|
mesh=mesh,
|
|
|
|
|
lower_ww_bounds=ww_values,
|
|
|
|
|
upper_bound_ratio=5.0,
|
|
|
|
|
energy_bounds=energy_bounds,
|
|
|
|
|
particle_type=particle_type)
|
|
|
|
|
wws.append(ww)
|
|
|
|
|
|
|
|
|
|
return wws
|
|
|
|
|
|
|
|
|
|
def export_to_hdf5(self, path: PathLike = 'weight_windows.h5', **init_kwargs):
|
|
|
|
|
"""Write weight windows to an HDF5 file.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
path : PathLike
|
|
|
|
|
Path to the file to write weight windows to
|
|
|
|
|
**init_kwargs
|
|
|
|
|
Keyword arguments passed to :func:`openmc.lib.init`
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
import openmc.lib
|
|
|
|
|
cv.check_type('path', path, PathLike)
|
|
|
|
|
|
|
|
|
|
# Create a temporary model with the weight windows
|
|
|
|
|
model = openmc.Model()
|
|
|
|
|
sph = openmc.Sphere(boundary_type='vacuum')
|
|
|
|
|
cell = openmc.Cell(region=-sph)
|
|
|
|
|
model.geometry = openmc.Geometry([cell])
|
|
|
|
|
model.settings.weight_windows = self
|
|
|
|
|
model.settings.particles = 100
|
|
|
|
|
model.settings.batches = 1
|
|
|
|
|
|
|
|
|
|
# Get absolute path before moving to temporary directory
|
|
|
|
|
path = Path(path).resolve()
|
|
|
|
|
|
2025-07-01 12:43:45 -05:00
|
|
|
# Load the model with openmc.lib and then export it to an HDF5 file
|
|
|
|
|
with openmc.lib.TemporarySession(model, **init_kwargs):
|
|
|
|
|
openmc.lib.export_weight_windows(path)
|