diff --git a/openmc/config.py b/openmc/config.py
index d61fb2d31e6..874994f39ba 100644
--- a/openmc/config.py
+++ b/openmc/config.py
@@ -20,7 +20,6 @@
from typing import Any, Dict, Iterator
from openmc.data import DataLibrary
-from openmc.data.decay import _DECAY_ENERGY, _DECAY_PHOTON_ENERGY
__all__ = ["config"]
@@ -76,9 +75,6 @@ def __delitem__(self, key: str):
env_var = self._PATH_KEYS[key]
if env_var in os.environ:
del os.environ[env_var]
- if key == 'chain_file':
- _DECAY_PHOTON_ENERGY.clear()
- _DECAY_ENERGY.clear()
def __setitem__(self, key: str, value: Any):
"""Set a configuration key and its corresponding value.
@@ -102,9 +98,6 @@ def __setitem__(self, key: str, value: Any):
self._mapping[key] = stored_path
os.environ[self._PATH_KEYS[key]] = str(stored_path)
- if key == 'chain_file':
- _DECAY_PHOTON_ENERGY.clear()
- _DECAY_ENERGY.clear()
if not stored_path.exists():
warnings.warn(f"Path '{stored_path}' does not exist.", UserWarning)
@@ -168,7 +161,10 @@ def patch(self, key: str, value: Any):
"""
previous_value = self.get(key)
- self[key] = value
+ if value is not None:
+ self[key] = value
+ else:
+ del self[key]
try:
yield
finally:
diff --git a/openmc/data/data.py b/openmc/data/data.py
index 2142a5dc905..eb36ea7b59f 100644
--- a/openmc/data/data.py
+++ b/openmc/data/data.py
@@ -6,6 +6,8 @@
from math import sqrt, log
from warnings import warn
+from ..exceptions import DataError
+
# Isotopic abundances from Meija J, Coplen T B, et al, "Isotopic compositions
# of the elements 2013 (IUPAC Technical Report)", Pure. Appl. Chem. 88 (3),
# pp. 293-306 (2013). The "representative isotopic abundance" values from
@@ -363,7 +365,7 @@ def atomic_weight(element):
raise ValueError(f"No naturally-occurring isotopes for element '{element}'.")
-def half_life(isotope):
+def half_life(isotope, chain_file = None):
"""Return half-life of isotope in seconds or None if isotope is stable
Half-life values are from the `ENDF/B-VIII.0 decay sublibrary
@@ -382,6 +384,15 @@ def half_life(isotope):
Half-life of isotope in [s]
"""
+ from openmc.deplete.chain import _get_chain
+
+ try:
+ chain = _get_chain(chain_file)
+ if isotope in chain and chain[isotope].half_life is not None:
+ return chain[isotope].half_life
+ except DataError:
+ pass
+
global _HALF_LIFE
if not _HALF_LIFE:
# Load ENDF/B-VIII.0 data from JSON file
@@ -391,7 +402,7 @@ def half_life(isotope):
return _HALF_LIFE.get(isotope.lower())
-def decay_constant(isotope):
+def decay_constant(isotope, chain_file = None):
"""Return decay constant of isotope in [s^-1]
Decay constants are based on half-life values from the
@@ -415,7 +426,7 @@ def decay_constant(isotope):
openmc.data.half_life
"""
- t = half_life(isotope)
+ t = half_life(isotope, chain_file = chain_file)
return _LOG_TWO / t if t else 0.0
diff --git a/openmc/data/decay.py b/openmc/data/decay.py
index 1a11d3614fe..9c40e378482 100644
--- a/openmc/data/decay.py
+++ b/openmc/data/decay.py
@@ -569,10 +569,7 @@ def sources(self):
return merged_sources
-_DECAY_PHOTON_ENERGY = {}
-
-
-def decay_photon_energy(nuclide: str) -> Univariate | None:
+def decay_photon_energy(nuclide: str, chain_file = None) -> Univariate | None:
"""Get photon energy distribution resulting from the decay of a nuclide
This function relies on data stored in a depletion chain. Before calling it
@@ -585,6 +582,8 @@ def decay_photon_energy(nuclide: str) -> Univariate | None:
----------
nuclide : str
Name of nuclide, e.g., 'Co58'
+ chain_file : str or path-like
+ Chain file to get decay photon energy from.
Returns
-------
@@ -593,32 +592,20 @@ def decay_photon_energy(nuclide: str) -> Univariate | None:
if no photon source exists. Note that the probabilities represent
intensities, given as [Bq].
"""
- if not _DECAY_PHOTON_ENERGY:
- chain_file = openmc.config.get('chain_file')
- if chain_file is None:
+ from openmc.deplete.chain import _get_chain
+
+ chain = _get_chain(chain_file)
+ if chain is None:
raise DataError(
"A depletion chain file must be specified with "
"openmc.config['chain_file'] in order to load decay data."
)
-
- from openmc.deplete import Chain
- chain = Chain.from_xml(chain_file)
- for nuc in chain.nuclides:
- if 'photon' in nuc.sources:
- _DECAY_PHOTON_ENERGY[nuc.name] = nuc.sources['photon']
-
- # If the chain file contained no sources at all, warn the user
- if not _DECAY_PHOTON_ENERGY:
- warn(f"Chain file '{chain_file}' does not have any decay photon "
- "sources listed.")
-
- return _DECAY_PHOTON_ENERGY.get(nuclide)
+ if nuclide in chain:
+ nuclide = chain[nuclide]
+ return nuclide.sources.get('photon')
-_DECAY_ENERGY = {}
-
-
-def decay_energy(nuclide: str):
+def decay_energy(nuclide: str, chain_file = None):
"""Get decay energy value resulting from the decay of a nuclide
This function relies on data stored in a depletion chain. Before calling it
@@ -631,6 +618,8 @@ def decay_energy(nuclide: str):
----------
nuclide : str
Name of nuclide, e.g., 'H3'
+ chain_file : str or path-like
+ Chain file to get decay energy from.
Returns
-------
@@ -638,24 +627,18 @@ def decay_energy(nuclide: str):
Decay energy of nuclide in [eV]. If the nuclide is stable, a value of
0.0 is returned.
"""
- if not _DECAY_ENERGY:
- chain_file = openmc.config.get('chain_file')
- if chain_file is None:
+ from openmc.deplete.chain import _get_chain
+
+ chain = _get_chain(chain_file)
+ if chain is None:
raise DataError(
"A depletion chain file must be specified with "
"openmc.config['chain_file'] in order to load decay data."
)
+ if nuclide in chain:
+ nuclide = chain[nuclide]
+ return nuclide.decay_energy
- from openmc.deplete import Chain
- chain = Chain.from_xml(chain_file)
- for nuc in chain.nuclides:
- if nuc.decay_energy:
- _DECAY_ENERGY[nuc.name] = nuc.decay_energy
-
- # If the chain file contained no decay energy, warn the user
- if not _DECAY_ENERGY:
- warn(f"Chain file '{chain_file}' does not have any decay energy.")
-
- return _DECAY_ENERGY.get(nuclide, 0.0)
+ return 0.0
diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py
index e6d4c151273..7a5a69f91ba 100644
--- a/openmc/deplete/abc.py
+++ b/openmc/deplete/abc.py
@@ -19,7 +19,7 @@
import numpy as np
from uncertainties import ufloat
-from openmc.checkvalue import check_type, check_greater_than, PathLike
+from openmc.checkvalue import check_type, check_greater_than, check_value, PathLike
from openmc.mpi import comm
from openmc.utility_funcs import change_directory
from openmc import Material
@@ -155,6 +155,7 @@ def __init__(self, chain_file=None, fission_q=None, prev_results=None):
self.prev_res = None
else:
check_type("previous results", prev_results, Results)
+ check_value("previous results chain file", prev_results.chain, [self.chain])
self.prev_res = prev_results
@abstractmethod
diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py
index f1a23317fb9..f0892cf7940 100644
--- a/openmc/deplete/chain.py
+++ b/openmc/deplete/chain.py
@@ -22,6 +22,7 @@
from openmc.data import gnds_name, zam
from openmc.exceptions import DataError
from .nuclide import FissionYieldDistribution, Nuclide
+from ..mixin import EqualityMixin
from .._xml import get_text
import openmc.data
@@ -229,7 +230,7 @@ def replace_missing_fpy(actinide, fpy_data, decay_data):
return 'U235'
-class Chain:
+class Chain(EqualityMixin):
"""Full representation of a depletion chain.
A depletion chain can be created by using the :meth:`from_endf` method which
@@ -580,6 +581,50 @@ def export_to_xml(self, filename):
tree = ET.ElementTree(root_elem)
tree.write(str(filename), encoding='utf-8', pretty_print=True)
+
+ @classmethod
+ def from_hdf5(cls, group, fission_q=None):
+ """Reads a depletion chain XML file.
+
+ Parameters
+ ----------
+ group : str
+ The hdf5 group to read depletion chain from.
+ fission_q : dict, optional
+ Dictionary of nuclides and their fission Q values [eV].
+ If not given, values will be pulled from ``filename``
+
+ """
+ cgroup = group["depletion_chain"]
+ if fission_q is not None:
+ check_type("fission_q", fission_q, Mapping)
+ else:
+ fission_q = {}
+ if "path" in cgroup.attrs:
+ path = Path(__file__).resolve().parent.joinpath(cgroup.attrs["path"])
+ return Chain.from_xml(path, fission_q)
+ else:
+ chain = cls()
+ nuc_group = cgroup["nuclides"]
+ for name, ngroup in nuc_group.items():
+ this_q = fission_q.get(name)
+ nuc = Nuclide.from_hdf5(nuc_group, name, this_q)
+ chain.add_nuclide(nuc)
+
+ def to_hdf5(self, group):
+ cgroup = group.create_group("depletion_chain")
+ if hasattr(self, "_xml_path"):
+ path = Path(self._xml_path)
+ if openmc.config["resolve_paths"]:
+ path = path.resolve()
+ else:
+ path = path.relative_to(Path(__file__).resolve().parent, walk_up=True)
+ cgroup.attrs["path"] = str(path)
+ else:
+ nuc_group = cgroup.create_group("nuclides")
+ for nuclide in self.nuclides:
+ nuclide.to_hdf5(nuc_group)
+
def get_default_fission_yields(self):
"""Return fission yields at lowest incident neutron energy
diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py
index 95881483474..d2f0c4691f0 100644
--- a/openmc/deplete/nuclide.py
+++ b/openmc/deplete/nuclide.py
@@ -343,6 +343,125 @@ def to_xml_element(self):
self.yield_data.to_xml_element(fpy_elem)
return elem
+
+ @classmethod
+ def from_hdf5(cls, group, name, fission_q=None):
+ """Read nuclide from a hdf5 group.
+
+ Parameters
+ ----------
+ group : h5py.Group
+ hdf5 group to read nuclide data from
+ fission_q : None or float
+ User-supplied fission Q value [eV].
+ Will be read from the group if not given
+
+ Returns
+ -------
+ nuc : openmc.deplete.Nuclide
+ Instance of a nuclide
+
+ """
+ nuc = cls()
+ nuc.name = name
+
+ # Check for half-life
+ if "decay" in group[name]:
+ nuc.half_life = float(group[name]["decay"].attrs["half_life"])
+ nuc.decay_energy = float(group[name]["decay"].attrs.get("decay_energy", 0.0))
+
+ # Check for decay paths
+ for dgroup in group[name]["decay"].values():
+ d_type = dgroup.attrs["type"]
+ target = dgroup.attrs.get("target")
+ if target is not None and target.lower() == "nothing":
+ target = None
+ branching_ratio = float(dgroup.attrs.get("branching_ratio", 1.0))
+ nuc.decay_modes.append(DecayTuple(d_type, target, branching_ratio))
+
+ # Check for sources
+ if "sources" in group[name]:
+ for particle, sgroup in group[name]["sources"].items():
+ distribution = Univariate.from_hdf5(sgroup)
+ nuc.sources[particle] = distribution
+
+ # Check for reaction paths
+ if "reactions" in group[name]:
+ for rgroup in group[name]["reactions"].values():
+ r_type = rgroup.attrs["type"]
+ Q = rgroup.attrs.get("Q", 0.0)
+ branching_ratio = rgroup.attrs.get("branching_ratio", 1.0)
+
+ # If the type is not fission, get target and Q value, otherwise
+ # just set null values
+ if r_type != 'fission':
+ target = rgroup.attrs.get("target")
+ if target is not None and target.lower() == "nothing":
+ target = None
+ else:
+ target = None
+ if fission_q is not None:
+ Q = fission_q
+
+ # Append reaction
+ nuc.reactions.append(ReactionTuple(
+ r_type, target, Q, branching_ratio))
+
+ if "neutron_fission_yields" in group[name]:
+ parent = group[name]["neutron_fission_yields"].attrs.get("parent")
+ if parent is not None:
+ if "neutron_fission_yields" not in group[parent]:
+ raise ValueError(
+ "Fission product yields for {0} borrow from {1}, but {1} is"
+ " not present in the chain file or has no yields.".format(
+ name, parent
+ ))
+ nuc.yield_data = FissionYieldDistribution.from_hdf5(group[parent]["neutron_fission_yields"])
+ nuc._fpy = parent
+ else:
+ nuc.yield_data = FissionYieldDistribution.from_hdf5(group[name]["neutron_fission_yields"])
+ return nuc
+
+ def to_hdf5(self, group):
+ """Write nuclide to hdf5."""
+ group = group.create_group(self.name)
+
+ if self.half_life is not None:
+ dec_group = group.create_group("decay")
+ dec_group.attrs['half_life'] = self.half_life
+ dec_group.attrs['decay_energy'] = self.decay_energy
+
+ for i, (mode_type, daughter, br) in enumerate(self.decay_modes):
+ mgroup = dec_group.create_group(f"mode_{i:01d}")
+ mgroup.attrs["type"] = mode_type
+ if daughter:
+ mgroup.attrs["target"] = daughter
+ mgroup.attrs["branching_ratio"] = br
+
+ # Write decay sources
+ if self.sources:
+ sources_group = group.create_group("sources")
+ for particle, source in self.sources.items():
+ pgroup = sources_group.create_group(particle)
+ source.to_hdf5(pgroup)
+
+ reac_group = group.create_group("reactions")
+ for i, (rx, daughter, Q, br) in enumerate(self.reactions):
+ rgroup = reac_group.create_group(f"reaction_{i:03d}")
+ rgroup.attrs["type"] = rx
+ rgroup.attrs["Q"] = Q
+ if daughter is not None:
+ rgroup.attrs["target"] = daughter
+ if br != 1.0:
+ rgroup.attrs["branching_ratio"] = br
+
+ if self.yield_data:
+ nfy_group = group.create_group("neutron_fission_yields")
+ if hasattr(self, '_fpy'):
+ # Check for link to other nuclide data
+ nfy_group.attrs["parent"] = self._fpy
+ else:
+ self.yield_data.to_hdf5(nfy_group)
def validate(self, strict=True, quiet=False, tolerance=1e-4):
"""Search for possible inconsistencies
@@ -554,6 +673,25 @@ def to_xml_element(self, root):
product_elem.text = " ".join(map(str, yield_obj.products))
data_elem = ET.SubElement(yield_element, "data")
data_elem.text = " ".join(map(str, yield_obj.yields))
+
+ @classmethod
+ def from_hdf5(cls, group):
+ all_yields = {}
+ for egroup in group.values():
+ energy = egroup.attrs["energy"]
+ products = egroup["products"][()]
+ yields = egroup["yields"][()]
+ # Get a map of products to their corresponding yield
+ all_yields[energy] = dict(zip(products, yields))
+
+ return cls(all_yields)
+
+ def to_hdf5(self, group):
+ for energy, yield_obj in self.items():
+ egroup = group.create_group(f"{energy}eV")
+ egroup.attrs["energy"] = energy
+ egroup.create_dataset("products", data = yield_obj.products)
+ egroup.create_dataset("yields", data = yield_obj.yields)
def restrict_products(self, possible_products):
"""Return a new distribution with select products
diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py
index 7427abd7353..294ed583511 100644
--- a/openmc/deplete/results.py
+++ b/openmc/deplete/results.py
@@ -2,11 +2,13 @@
import bisect
import math
from collections.abc import Iterable
+from pathlib import Path
from warnings import warn
import h5py
import numpy as np
+from .chain import Chain
from .stepresult import StepResult, VERSION_RESULTS
import openmc.checkvalue as cv
from openmc.data import atomic_mass, AVOGADRO
@@ -69,6 +71,8 @@ def __init__(self, filename='depletion_results.h5'):
with h5py.File(str(filename), "r") as fh:
cv.check_filetype_version(fh, 'depletion results', VERSION_RESULTS[0])
+ self.chain = Chain.from_hdf5(fh)
+
# Get number of results stored
n = fh["number"][...].shape[0]
@@ -149,7 +153,7 @@ def get_activity(
# Evaluate activity for each depletion time
for i, result in enumerate(self):
times[i] = result.time[0]
- activities[i] = result.get_material(mat_id).get_activity(units, by_nuclide, volume)
+ activities[i] = result.get_material(mat_id).get_activity(units, by_nuclide, volume, chain_file = self.chain)
return times, activities
@@ -268,7 +272,7 @@ def get_decay_heat(
for i, result in enumerate(self):
times[i] = result.time[0]
decay_heat[i] = result.get_material(mat_id).get_decay_heat(
- units, by_nuclide, volume)
+ units, by_nuclide, volume, chain_file = self.chain)
return times, decay_heat
diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py
index 1a26cbe3466..1b5fccd3f63 100644
--- a/openmc/deplete/stepresult.py
+++ b/openmc/deplete/stepresult.py
@@ -12,6 +12,7 @@
import numpy as np
import openmc
+import openmc.checkvalue as cv
from openmc.mpi import comm, MPI
from openmc.checkvalue import PathLike
from .reaction_rates import ReactionRates
@@ -252,7 +253,7 @@ def export_to_hdf5(self, filename, step):
"""
# Write new file if first time step, else add to existing file
- kwargs = {'mode': "w" if step == 0 else "a"}
+ kwargs = {'mode': "a"}
if h5py.get_config().mpi and comm.size > 1:
# Write results in parallel
@@ -567,6 +568,25 @@ def save(op, x, op_results, t, source_rate, step_ind, proc_time=None,
if not Path(path).is_file():
Path(path).parent.mkdir(parents=True, exist_ok=True)
+
+ filename = str(path)
+
+ if step_ind == 0:
+ # Write new file if first time step, else add to existing file
+ kwargs = {'mode': "w"}
+
+ if h5py.get_config().mpi and comm.size > 1:
+ # Write results in parallel
+ kwargs['driver'] = 'mpio'
+ kwargs['comm'] = comm
+ with h5py.File(filename, **kwargs) as handle:
+ op.chain.to_hdf5(handle)
+ else:
+ # Only root process writes depletion chain
+ if comm.rank == 0:
+ with h5py.File(filename, **kwargs) as handle:
+ op.chain.to_hdf5(handle)
+
results.export_to_hdf5(path, step_ind)
def transfer_volumes(self, model):
diff --git a/openmc/material.py b/openmc/material.py
index c7b954b6666..812db0886eb 100644
--- a/openmc/material.py
+++ b/openmc/material.py
@@ -293,7 +293,8 @@ def get_decay_photon_energy(
self,
clip_tolerance: float = 1e-6,
units: str = 'Bq',
- volume: float | None = None
+ volume: float | None = None,
+ chain_file: str | Path | None = None,
) -> Univariate | None:
r"""Return energy distribution of decay photons from unstable nuclides.
@@ -309,6 +310,8 @@ def get_decay_photon_energy(
volume : float, optional
Volume of the material. If not passed, defaults to using the
:attr:`Material.volume` attribute.
+ chain_file : str or path-like, optional.
+ Location of chain file to get decay constants from.
Returns
-------
@@ -332,7 +335,7 @@ def get_decay_photon_energy(
dists = []
probs = []
for nuc, atoms_per_bcm in self.get_nuclide_atom_densities().items():
- source_per_atom = openmc.data.decay_photon_energy(nuc)
+ source_per_atom = openmc.data.decay_photon_energy(nuc, chain_file=chain_file)
if source_per_atom is not None and atoms_per_bcm > 0.0:
dists.append(source_per_atom)
probs.append(1e24 * atoms_per_bcm * multiplier)
@@ -1139,7 +1142,7 @@ def get_element_atom_densities(self, element: str | None = None) -> dict[str, fl
def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False,
- volume: float | None = None) -> dict[str, float] | float:
+ volume: float | None = None, chain_file: str | Path | None = None) -> dict[str, float] | float:
"""Returns the activity of the material or of each nuclide within.
.. versionadded:: 0.13.1
@@ -1156,6 +1159,8 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False,
volume : float, optional
Volume of the material. If not passed, defaults to using the
:attr:`Material.volume` attribute.
+ chain_file : str or path-like, optional.
+ Location of chain file to get decay constants from.
.. versionadded:: 0.13.3
@@ -1188,13 +1193,13 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False,
activity = {}
for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items():
- inv_seconds = openmc.data.decay_constant(nuclide)
+ inv_seconds = openmc.data.decay_constant(nuclide, chain_file = chain_file)
activity[nuclide] = inv_seconds * 1e24 * atoms_per_bcm * multiplier
return activity if by_nuclide else sum(activity.values())
def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False,
- volume: float | None = None) -> dict[str, float] | float:
+ volume: float | None = None, chain_file: str | Path | None = None) -> dict[str, float] | float:
"""Returns the decay heat of the material or for each nuclide in the
material in units of [W], [W/g], [W/kg] or [W/cm3].
@@ -1212,6 +1217,8 @@ def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False,
volume : float, optional
Volume of the material. If not passed, defaults to using the
:attr:`Material.volume` attribute.
+ chain_file : str or path-like, optional.
+ Location of chain file to get decay constants from.
.. versionadded:: 0.13.3
@@ -1237,8 +1244,8 @@ def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False,
decayheat = {}
for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items():
- decay_erg = openmc.data.decay_energy(nuclide)
- inv_seconds = openmc.data.decay_constant(nuclide)
+ decay_erg = openmc.data.decay_energy(nuclide, chain_file=chain_file)
+ inv_seconds = openmc.data.decay_constant(nuclide, chain_file=chain_file)
decay_erg *= openmc.data.JOULE_PER_EV
decayheat[nuclide] = inv_seconds * decay_erg * 1e24 * atoms_per_bcm * multiplier
diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py
index 28d5e87ef9e..47e88835fe8 100644
--- a/openmc/stats/univariate.py
+++ b/openmc/stats/univariate.py
@@ -31,6 +31,10 @@ class Univariate(EqualityMixin, ABC):
specific probability distribution.
"""
+ @abstractmethod
+ def to_hdf5(self, group):
+ return
+
@abstractmethod
def to_xml_element(self, element_name):
return ''
@@ -64,6 +68,30 @@ def from_xml_element(cls, elem):
return Legendre.from_xml_element(elem)
elif distribution == 'mixture':
return Mixture.from_xml_element(elem)
+
+ @classmethod
+ @abstractmethod
+ def from_hdf5(cls, group):
+ distribution = group.attrs["type"]
+ if distribution == 'discrete':
+ return Discrete.from_hdf5(group)
+ elif distribution == 'uniform':
+ return Uniform.from_hdf5(group)
+ elif distribution == 'powerlaw':
+ return PowerLaw.from_hdf5(group)
+ elif distribution == 'maxwell':
+ return Maxwell.from_hdf5(group)
+ elif distribution == 'watt':
+ return Watt.from_hdf5(group)
+ elif distribution == 'normal':
+ return Normal.from_hdf5(group)
+ elif distribution == 'tabular':
+ return Tabular.from_hdf5(group)
+ elif distribution == 'legendre':
+ return Legendre.from_hdf5(group)
+ elif distribution == 'mixture':
+ return Mixture.from_hdf5(group)
+
@abstractmethod
def sample(n_samples: int = 1, seed: int | None = None):
@@ -244,6 +272,17 @@ def from_xml_element(cls, elem: ET.Element):
p = params[len(params)//2:]
return cls(x, p)
+ def to_hdf5(self, group):
+ group.attrs["type"] = "discrete"
+ group.create_dataset("parameters", data=np.r_[self.x,self.p])
+
+ @classmethod
+ def from_hdf5(cls, group):
+ params = group["parameters"][()]
+ x = params[:len(params)//2]
+ p = params[len(params)//2:]
+ return cls(x, p)
+
@classmethod
def merge(
cls,
@@ -450,6 +489,14 @@ def from_xml_element(cls, elem: ET.Element):
params = get_elem_list(elem, "parameters", float)
return cls(*params)
+ def to_hdf5(self, group):
+ group.attrs["type"] = "uniform"
+ group.create_dataset("parameters", data=np.r_[self.a,self.b])
+
+ @classmethod
+ def from_hdf5(cls, group):
+ params = group["parameters"][()]
+ return cls(*params)
class PowerLaw(Univariate):
"""Distribution with power law probability over a finite interval [a,b]
@@ -558,7 +605,15 @@ def from_xml_element(cls, elem: ET.Element):
"""
params = get_elem_list(elem, "parameters", float)
return cls(*params)
-
+
+ def to_hdf5(self, group):
+ group.attrs["type"] = "powerlaw"
+ group.create_dataset("parameters", data=np.r_[self.a,self.b,self.n])
+
+ @classmethod
+ def from_hdf5(cls, group):
+ params = group["parameters"][()]
+ return cls(*params)
class Maxwell(Univariate):
r"""Maxwellian distribution in energy.
@@ -644,6 +699,14 @@ def from_xml_element(cls, elem: ET.Element):
theta = float(get_text(elem, 'parameters'))
return cls(theta)
+ def to_hdf5(self, group):
+ group.attrs["type"] = "maxwell"
+ group.attrs["theta"] = self.theta
+
+ @classmethod
+ def from_hdf5(cls, group):
+ theta = group.attrs["theta"]
+ return cls(theta)
class Watt(Univariate):
r"""Watt fission energy spectrum.
@@ -739,6 +802,14 @@ def from_xml_element(cls, elem: ET.Element):
params = get_elem_list(elem, "parameters", float)
return cls(*params)
+ def to_hdf5(self, group):
+ group.attrs["type"] = "watt"
+ group.create_dataset("parameters", data=np.r_[self.a,self.b])
+
+ @classmethod
+ def from_hdf5(cls, group):
+ params = group["parameters"][()]
+ return cls(*params)
class Normal(Univariate):
r"""Normally distributed sampling.
@@ -829,6 +900,14 @@ def from_xml_element(cls, elem: ET.Element):
params = get_elem_list(elem, "parameters", float)
return cls(*params)
+ def to_hdf5(self, group):
+ group.attrs["type"] = "normal"
+ group.create_dataset("parameters", data=np.r_[self.mean_value,self.std_dev])
+
+ @classmethod
+ def from_hdf5(cls, group):
+ params = group["parameters"][()]
+ return cls(*params)
def muir(e0: float, m_rat: float, kt: float):
"""Generate a Muir energy spectrum
@@ -1119,7 +1198,21 @@ def from_xml_element(cls, elem: ET.Element):
x = params[:m]
p = params[m:]
return cls(x, p, interpolation)
-
+
+ def to_hdf5(self, group):
+ group.attrs["type"] = "tabular"
+ group.attrs["interpolation"] = self.interpolation
+ group.create_dataset("parameters", data=np.r_[self.x,self.p])
+
+ @classmethod
+ def from_hdf5(cls, group):
+ interpolation = group.attrs["interpolation"]
+ params = group["parameters"][()]
+ m = (len(params) + 1)//2 # +1 for when len(params) is odd
+ x = params[:m]
+ p = params[m:]
+ return cls(x, p, interpolation)
+
def integral(self):
"""Return integral of distribution
@@ -1190,6 +1283,13 @@ def to_xml_element(self, element_name):
@classmethod
def from_xml_element(cls, elem):
raise NotImplementedError
+
+ def to_hdf5(self, group):
+ raise NotImplementedError
+
+ @classmethod
+ def from_hdf5(cls, group):
+ raise NotImplementedError
class Mixture(Univariate):
@@ -1323,6 +1423,23 @@ def from_xml_element(cls, elem: ET.Element):
return cls(probability, distribution)
+ def to_hdf5(self, group):
+ group.attrs["type"] = "mixture"
+ for i, (p, d) in enumerate(zip(self.probability, self.distribution)):
+ dgroup = group.create_group(f"dist_{i:d}")
+ dgroup.attrs["probability"] = p
+ d.to_hdf5(dgroup)
+
+ @classmethod
+ def from_hdf5(cls, group):
+ probability = []
+ distribution = []
+ for dgroup in group.values():
+ probability.append(dgroup.attrs["probability"])
+ distribution.append(Univariate.from_hdf5(dgroup))
+
+ return cls(probability, distribution)
+
def integral(self):
"""Return integral of the distribution
diff --git a/tests/dummy_operator.py b/tests/dummy_operator.py
index 1bb00129d04..869a5f0c8dc 100644
--- a/tests/dummy_operator.py
+++ b/tests/dummy_operator.py
@@ -115,6 +115,9 @@ def form_matrix(self, rates, _fission_yields=None):
a22 = np.sin(y_1)
return sp.csr_matrix(np.array([[a11, a12], [a21, a22]]))
+
+ def to_hdf5(self, group):
+ group.create_group("depletion_chain").create_group("nuclides")
class DummyOperator(TransportOperator):
diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5
index 5fdc656d970..4cdfff0a989 100644
Binary files a/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 differ
diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_hours.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_hours.h5
index 65bfc19a91c..adbcbc69aed 100644
Binary files a/tests/regression_tests/deplete_no_transport/test_reference_coupled_hours.h5 and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_hours.h5 differ
diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_minutes.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_minutes.h5
index 294cb589f7b..b997e46f938 100644
Binary files a/tests/regression_tests/deplete_no_transport/test_reference_coupled_minutes.h5 and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_minutes.h5 differ
diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_months.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_months.h5
index ed62e461049..6d9e532e9ad 100644
Binary files a/tests/regression_tests/deplete_no_transport/test_reference_coupled_months.h5 and b/tests/regression_tests/deplete_no_transport/test_reference_coupled_months.h5 differ
diff --git a/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 b/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5
index 9d32d89fe23..a106d61b6c4 100644
Binary files a/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 and b/tests/regression_tests/deplete_no_transport/test_reference_fission_q.h5 differ
diff --git a/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 b/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5
index 3f3b4aa2ac5..fd4d3fd86e7 100644
Binary files a/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 and b/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5
index aa09e1bcf4e..b9b80984acb 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5
index 284d5cbe4ed..ca5e34377db 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5
index 271af24103c..42870552e5f 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5
index 25365f6f3a4..9ae9400edda 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5
index d080b447093..f8e3e6c0004 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5
index 36f89e0090b..c5d1e27a746 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5
index f3b1b171ebc..7b9c0c11db5 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 differ
diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5
index b0ba997407f..e5ee6490aa0 100644
Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 differ
diff --git a/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml
index 242e2e7db5f..6370466e482 100644
--- a/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml
+++ b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml
@@ -4,469 +4,469 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
-
+
-
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
-
-
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
-
-
+
+
+
-
-
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -474,68 +474,68 @@
-
-
-
+
+
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
@@ -543,37 +543,37 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -582,194 +582,194 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
+
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
@@ -777,219 +777,219 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/tests/regression_tests/deplete_with_transport/test_reference.h5 b/tests/regression_tests/deplete_with_transport/test_reference.h5
index e8478250be6..b54bbaa534e 100644
Binary files a/tests/regression_tests/deplete_with_transport/test_reference.h5 and b/tests/regression_tests/deplete_with_transport/test_reference.h5 differ
diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py
index 242c59b5e23..ea2a8ec416a 100644
--- a/tests/unit_tests/test_config.py
+++ b/tests/unit_tests/test_config.py
@@ -94,10 +94,3 @@ def test_config_warning_nonexistent_path(tmp_path):
openmc.config['chain_file'] = bad_path
-def test_config_chain_side_effect(tmp_path):
- """Test that modifying chain_file clears decay data caches."""
- chain_file = tmp_path / "chain.xml"; chain_file.touch()
- decay._DECAY_ENERGY['U235'] = (1.0, 2.0)
- decay._DECAY_PHOTON_ENERGY['PU239'] = {}
- openmc.config['chain_file'] = chain_file
- assert not decay._DECAY_ENERGY and not decay._DECAY_PHOTON_ENERGY
diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py
index b65c704af8e..d02e30c5f6e 100644
--- a/tests/unit_tests/test_data_misc.py
+++ b/tests/unit_tests/test_data_misc.py
@@ -135,11 +135,12 @@ def test_zam():
openmc.data.zam('Am242-m1')
def test_half_life():
- assert openmc.data.half_life('H2') is None
- assert openmc.data.half_life('U235') == pytest.approx(2.22102e16)
- assert openmc.data.half_life('Am242') == pytest.approx(57672.0)
- assert openmc.data.half_life('Am242_m1') == pytest.approx(4449622000.0)
- assert openmc.data.decay_constant('H2') == 0.0
- assert openmc.data.decay_constant('U235') == pytest.approx(log(2.0)/2.22102e16)
- assert openmc.data.decay_constant('Am242') == pytest.approx(log(2.0)/57672.0)
+ with openmc.config.patch('chain_file', None):
+ assert openmc.data.half_life('H2') is None
+ assert openmc.data.half_life('U235') == pytest.approx(2.22102e16)
+ assert openmc.data.half_life('Am242') == pytest.approx(57672.0)
+ assert openmc.data.half_life('Am242_m1') == pytest.approx(4449622000.0)
+ assert openmc.data.decay_constant('H2') == 0.0
+ assert openmc.data.decay_constant('U235') == pytest.approx(log(2.0)/2.22102e16)
+ assert openmc.data.decay_constant('Am242') == pytest.approx(log(2.0)/57672.0)
assert openmc.data.decay_constant('Am242_m1') == pytest.approx(log(2.0)/4449622000.0)
diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py
index b1d2cb950eb..e28b74fb6c1 100644
--- a/tests/unit_tests/test_deplete_integrator.py
+++ b/tests/unit_tests/test_deplete_integrator.py
@@ -44,6 +44,8 @@ def test_results_save(run_in_tmpdir):
# Mock geometry
op = MagicMock()
+
+ op.chain = dummy_operator.TestChain()
# Avoid DummyOperator thinking it's doing a restart calculation
op.prev_res = None