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