diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst
index c7c65e14a1d..55289864f23 100644
--- a/docs/source/pythonapi/data.rst
+++ b/docs/source/pythonapi/data.rst
@@ -63,6 +63,7 @@ Core Functions
atomic_weight
combine_distributions
decay_constant
+ decay_photon_energy
dose_coefficients
gnd_name
half_life
diff --git a/openmc/config.py b/openmc/config.py
index 628df7edc17..087a8c27e98 100644
--- a/openmc/config.py
+++ b/openmc/config.py
@@ -4,6 +4,7 @@
import warnings
from openmc.data import DataLibrary
+from openmc.data.decay import _DECAY_PHOTON_ENERGY
__all__ = ["config"]
@@ -22,6 +23,10 @@ def __delitem__(self, key):
del os.environ['OPENMC_CROSS_SECTIONS']
elif key == 'mg_cross_sections':
del os.environ['OPENMC_MG_CROSS_SECTIONS']
+ elif key == 'chain_file':
+ del os.environ['OPENMC_CHAIN_FILE']
+ # Reset photon source data since it relies on chain file
+ _DECAY_PHOTON_ENERGY.clear()
def __setitem__(self, key, value):
if key == 'cross_sections':
@@ -34,6 +39,8 @@ def __setitem__(self, key, value):
elif key == 'chain_file':
self._set_path(key, value)
os.environ['OPENMC_CHAIN_FILE'] = str(value)
+ # Reset photon source data since it relies on chain file
+ _DECAY_PHOTON_ENERGY.clear()
else:
raise KeyError(f'Unrecognized config key: {key}')
diff --git a/openmc/data/decay.py b/openmc/data/decay.py
index 2c2505bf56f..d7ededf33dd 100644
--- a/openmc/data/decay.py
+++ b/openmc/data/decay.py
@@ -1,15 +1,19 @@
from collections.abc import Iterable
from io import StringIO
from math import log
+from pathlib import Path
import re
from warnings import warn
+from xml.etree import ElementTree as ET
import numpy as np
from uncertainties import ufloat, UFloat
+import openmc
import openmc.checkvalue as cv
+from openmc.exceptions import DataError
from openmc.mixin import EqualityMixin
-from openmc.stats import Discrete, Tabular, combine_distributions
+from openmc.stats import Discrete, Tabular, Univariate, combine_distributions
from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER
from .function import INTERPOLATION_SCHEME
from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record
@@ -570,3 +574,48 @@ def sources(self):
self._sources = merged_sources
return self._sources
+
+
+_DECAY_PHOTON_ENERGY = {}
+
+
+def decay_photon_energy(nuclide: str) -> Univariate:
+ """Get photon energy distribution resulting from the decay of a nuclide
+
+ This function relies on data stored in a depletion chain. Before calling it
+ for the first time, you need to ensure that a depletion chain has been
+ specified in openmc.config['chain_file'].
+
+ .. versionadded:: 0.14.0
+
+ Parameters
+ ----------
+ nuclide : str
+ Name of nuclide, e.g., 'Co58'
+
+ Returns
+ -------
+ openmc.stats.Univariate
+ Distribution of energies in [eV] of photons emitted from decay. Note
+ that the probabilities represent intensities, given as [decay/sec].
+ """
+ if not _DECAY_PHOTON_ENERGY:
+ chain_file = openmc.config.get('chain_file')
+ if chain_file 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)
diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py
index 217c9cac05e..ce376a18159 100644
--- a/openmc/deplete/chain.py
+++ b/openmc/deplete/chain.py
@@ -420,6 +420,8 @@ def from_endf(cls, decay_files, fpy_files, neutron_files,
# Append decay mode
nuclide.add_decay_mode(type_, target, br)
+ nuclide.sources = data.sources
+
fissionable = False
if parent in reactions:
reactions_available = set(reactions[parent].keys())
diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py
index 04e0c26c589..b84b91a7ff7 100644
--- a/openmc/deplete/nuclide.py
+++ b/openmc/deplete/nuclide.py
@@ -16,6 +16,7 @@
import numpy as np
from openmc.checkvalue import check_type
+from openmc.stats import Univariate
__all__ = [
"DecayTuple", "ReactionTuple", "Nuclide", "FissionYield",
@@ -101,6 +102,9 @@ class Nuclide:
reactions : list of openmc.deplete.ReactionTuple
Reaction information. Each element of the list is a named tuple with
attribute 'type', 'target', 'Q', and 'branching_ratio'.
+ sources : dict
+ Dictionary mapping particle type as string to energy distribution of
+ decay source represented as :class:`openmc.stats.Univariate`
yield_data : FissionYieldDistribution or None
Fission product yields at tabulated energies for this nuclide. Can be
treated as a nested dictionary ``{energy: {product: yield}}``
@@ -120,6 +124,9 @@ def __init__(self, name=None):
# Reaction paths
self.reactions = []
+ # Decay sources
+ self.sources = {}
+
# Neutron fission yields, if present
self._yield_data = None
@@ -237,6 +244,12 @@ def from_xml(cls, element, root=None, fission_q=None):
branching_ratio = float(decay_elem.get('branching_ratio'))
nuc.decay_modes.append(DecayTuple(d_type, target, branching_ratio))
+ # Check for sources
+ for src_elem in element.iter('source'):
+ particle = src_elem.get('particle')
+ distribution = Univariate.from_xml_element(src_elem)
+ nuc.sources[particle] = distribution
+
# Check for reaction paths
for reaction_elem in element.iter('reaction'):
r_type = reaction_elem.get('type')
@@ -302,6 +315,19 @@ def to_xml_element(self):
mode_elem.set('target', daughter)
mode_elem.set('branching_ratio', str(br))
+ # Write decay sources
+ if self.sources:
+ for particle, source in self.sources.items():
+ # TODO: Ugly hack to deal with the fact that
+ # 'source.to_xml_element' will return an xml.etree object
+ # whereas here lxml is being used preferentially. We should just
+ # switch to use lxml everywhere
+ import xml.etree.ElementTree as etree
+ src_elem_xmletree = source.to_xml_element('source')
+ src_elem = ET.fromstring(etree.tostring(src_elem_xmletree))
+ src_elem.set('particle', particle)
+ elem.append(src_elem)
+
elem.set('reactions', str(len(self.reactions)))
for rx, daughter, Q, br in self.reactions:
rx_elem = ET.SubElement(elem, 'reaction')
diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py
index ca1516803c2..fd9e665248d 100644
--- a/openmc/deplete/openmc_operator.py
+++ b/openmc/deplete/openmc_operator.py
@@ -334,7 +334,7 @@ def _set_number_from_results(self, mat, prev_res):
mat_id = str(mat.id)
# Get nuclide lists from geometry and depletion results
- depl_nuc = prev_res[-1].nuc_to_ind
+ depl_nuc = prev_res[-1].index_nuc
geom_nuc_densities = mat.get_nuclide_atom_densities()
# Merge lists of nuclides, with the same order for every calculation
diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py
index b2c8e77332e..d9aa52b79e4 100644
--- a/openmc/deplete/results.py
+++ b/openmc/deplete/results.py
@@ -435,7 +435,7 @@ def export_to_materials(
# results, and save them to the new depleted xml file.
for mat in mat_file:
mat_id = str(mat.id)
- if mat_id in result.mat_to_ind:
+ if mat_id in result.index_mat:
mat.volume = result.volume[mat_id]
# Change density of all nuclides in material to atom/b-cm
@@ -447,7 +447,7 @@ def export_to_materials(
# For nuclides in chain that have cross sections, replace
# density in original material with new density from results
- for nuc in result.nuc_to_ind:
+ for nuc in result.index_nuc:
if nuc not in available_cross_sections:
continue
atoms = result[0, mat_id, nuc]
diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py
index db5260c8080..e462f175420 100644
--- a/openmc/deplete/stepresult.py
+++ b/openmc/deplete/stepresult.py
@@ -6,6 +6,7 @@
from collections import OrderedDict
import copy
+import warnings
import h5py
import numpy as np
@@ -42,9 +43,9 @@ class StepResult:
The reaction rates for each substep.
volume : OrderedDict of str to float
Dictionary mapping mat id to volume.
- mat_to_ind : OrderedDict of str to int
+ index_mat : OrderedDict of str to int
A dictionary mapping mat ID as string to index.
- nuc_to_ind : OrderedDict of str to int
+ index_nuc : OrderedDict of str to int
A dictionary mapping nuclide name as string to index.
mat_to_hdf5_ind : OrderedDict of str to int
A dictionary mapping mat ID as string to global index.
@@ -67,8 +68,8 @@ def __init__(self):
self.volume = None
self.proc_time = None
- self.mat_to_ind = None
- self.nuc_to_ind = None
+ self.index_mat = None
+ self.index_nuc = None
self.mat_to_hdf5_ind = None
self.data = None
@@ -98,9 +99,9 @@ def __getitem__(self, pos):
if isinstance(mat, openmc.Material):
mat = str(mat.id)
if isinstance(mat, str):
- mat = self.mat_to_ind[mat]
+ mat = self.index_mat[mat]
if isinstance(nuc, str):
- nuc = self.nuc_to_ind[nuc]
+ nuc = self.index_nuc[nuc]
return self.data[stage, mat, nuc]
@@ -120,19 +121,19 @@ def __setitem__(self, pos, val):
"""
stage, mat, nuc = pos
if isinstance(mat, str):
- mat = self.mat_to_ind[mat]
+ mat = self.index_mat[mat]
if isinstance(nuc, str):
- nuc = self.nuc_to_ind[nuc]
+ nuc = self.index_nuc[nuc]
self.data[stage, mat, nuc] = val
@property
def n_mat(self):
- return len(self.mat_to_ind)
+ return len(self.index_mat)
@property
def n_nuc(self):
- return len(self.nuc_to_ind)
+ return len(self.index_nuc)
@property
def n_hdf5_mats(self):
@@ -160,8 +161,8 @@ def allocate(self, volume, nuc_list, burn_list, full_burn_list, stages):
"""
self.volume = copy.deepcopy(volume)
- self.nuc_to_ind = {nuc: i for i, nuc in enumerate(nuc_list)}
- self.mat_to_ind = {mat: i for i, mat in enumerate(burn_list)}
+ self.index_nuc = {nuc: i for i, nuc in enumerate(nuc_list)}
+ self.index_mat = {mat: i for i, mat in enumerate(burn_list)}
self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)}
# Create storage array
@@ -186,10 +187,10 @@ def distribute(self, local_materials, ranges):
"""
new = StepResult()
new.volume = {lm: self.volume[lm] for lm in local_materials}
- new.mat_to_ind = {mat: idx for (idx, mat) in enumerate(local_materials)}
+ new.index_mat = {mat: idx for (idx, mat) in enumerate(local_materials)}
# Direct transfer
- direct_attrs = ("time", "k", "source_rate", "nuc_to_ind",
+ direct_attrs = ("time", "k", "source_rate", "index_nuc",
"mat_to_hdf5_ind", "proc_time")
for attr in direct_attrs:
setattr(new, attr, getattr(self, attr))
@@ -198,6 +199,34 @@ def distribute(self, local_materials, ranges):
new.rates = [r[ranges] for r in self.rates]
return new
+ def get_material(self, mat_id):
+ """Return material object for given depleted composition
+
+ .. versionadded:: 0.14.0
+
+ Parameters
+ ----------
+ mat_id : str
+ Material ID as a string
+
+ Returns
+ -------
+ openmc.Material
+ Equivalent material
+ """
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', openmc.IDWarning)
+ material = openmc.Material(material_id=int(mat_id))
+ vol = self.volume[mat_id]
+ for nuc, _ in sorted(self.index_nuc.items(), key=lambda x: x[1]):
+ atoms = self[0, mat_id, nuc]
+ if atoms < 0.0:
+ continue
+ atom_per_bcm = atoms / vol * 1e-24
+ material.add_nuclide(nuc, atom_per_bcm)
+ material.volume = vol
+ return material
+
def export_to_hdf5(self, filename, step):
"""Export results to an HDF5 file
@@ -239,11 +268,11 @@ def _write_hdf5_metadata(self, handle):
"""
# Create and save the 5 dictionaries:
# quantities
- # self.mat_to_ind -> self.volume (TODO: support for changing volumes)
- # self.nuc_to_ind
+ # self.index_mat -> self.volume (TODO: support for changing volumes)
+ # self.index_nuc
# reactions
- # self.rates[0].nuc_to_ind (can be different from above, above is superset)
- # self.rates[0].react_to_ind
+ # self.rates[0].index_nuc (can be different from above, above is superset)
+ # self.rates[0].index_rx
# these are shared by every step of the simulation, and should be deduplicated.
# Store concentration mat and nuclide dictionaries (along with volumes)
@@ -252,7 +281,7 @@ def _write_hdf5_metadata(self, handle):
handle.attrs['filetype'] = np.string_('depletion results')
mat_list = sorted(self.mat_to_hdf5_ind, key=int)
- nuc_list = sorted(self.nuc_to_ind)
+ nuc_list = sorted(self.index_nuc)
rxn_list = sorted(self.rates[0].index_rx)
n_mats = self.n_hdf5_mats
@@ -272,7 +301,7 @@ def _write_hdf5_metadata(self, handle):
for nuc in nuc_list:
nuc_single_group = nuc_group.create_group(nuc)
- nuc_single_group.attrs["atom number index"] = self.nuc_to_ind[nuc]
+ nuc_single_group.attrs["atom number index"] = self.index_nuc[nuc]
if nuc in self.rates[0].index_nuc:
nuc_single_group.attrs["reaction rate index"] = self.rates[0].index_nuc[nuc]
@@ -367,13 +396,13 @@ def _to_hdf5(self, handle, index, parallel=False):
proc_time_dset.resize(proc_shape)
# If nothing to write, just return
- if len(self.mat_to_ind) == 0:
+ if len(self.index_mat) == 0:
return
# Add data
# Note, for the last step, self.n_stages = 1, even if n_stages != 1.
n_stages = self.n_stages
- inds = [self.mat_to_hdf5_ind[mat] for mat in self.mat_to_ind]
+ inds = [self.mat_to_hdf5_ind[mat] for mat in self.index_mat]
low = min(inds)
high = max(inds)
for i in range(n_stages):
@@ -427,8 +456,8 @@ def from_hdf5(cls, handle, step):
# Reconstruct dictionaries
results.volume = OrderedDict()
- results.mat_to_ind = OrderedDict()
- results.nuc_to_ind = OrderedDict()
+ results.index_mat = OrderedDict()
+ results.index_nuc = OrderedDict()
rxn_nuc_to_ind = OrderedDict()
rxn_to_ind = OrderedDict()
@@ -437,11 +466,11 @@ def from_hdf5(cls, handle, step):
ind = mat_handle.attrs["index"]
results.volume[mat] = vol
- results.mat_to_ind[mat] = ind
+ results.index_mat[mat] = ind
for nuc, nuc_handle in handle["/nuclides"].items():
ind_atom = nuc_handle.attrs["atom number index"]
- results.nuc_to_ind[nuc] = ind_atom
+ results.index_nuc[nuc] = ind_atom
if "reaction rate index" in nuc_handle.attrs:
rxn_nuc_to_ind[nuc] = nuc_handle.attrs["reaction rate index"]
@@ -452,7 +481,7 @@ def from_hdf5(cls, handle, step):
results.rates = []
# Reconstruct reactions
for i in range(results.n_stages):
- rate = ReactionRates(results.mat_to_ind, rxn_nuc_to_ind, rxn_to_ind, True)
+ rate = ReactionRates(results.index_mat, rxn_nuc_to_ind, rxn_to_ind, True)
rate[:] = handle["/reaction rates"][step, i, :, :, :]
results.rates.append(rate)
diff --git a/openmc/material.py b/openmc/material.py
index 1b2b46a73a1..16b5a3048aa 100644
--- a/openmc/material.py
+++ b/openmc/material.py
@@ -18,6 +18,7 @@
from ._xml import clean_indentation, reorder_attributes
from .mixin import IDManagerMixin
from openmc.checkvalue import PathLike
+from openmc.stats import Univariate
# Units for density supported by OpenMC
@@ -34,10 +35,10 @@ class Material(IDManagerMixin):
To create a material, one should create an instance of this class, add
nuclides or elements with :meth:`Material.add_nuclide` or
:meth:`Material.add_element`, respectively, and set the total material
- density with :meth:`Material.set_density()`. Alternatively, you can
- use :meth:`Material.add_components()` to pass a dictionary
- containing all the component information. The material can then be
- assigned to a cell using the :attr:`Cell.fill` attribute.
+ density with :meth:`Material.set_density()`. Alternatively, you can use
+ :meth:`Material.add_components()` to pass a dictionary containing all the
+ component information. The material can then be assigned to a cell using the
+ :attr:`Cell.fill` attribute.
Parameters
----------
@@ -91,6 +92,13 @@ class Material(IDManagerMixin):
fissionable_mass : float
Mass of fissionable nuclides in the material in [g]. Requires that the
:attr:`volume` attribute is set.
+ decay_photon_energy : openmc.stats.Univariate
+ Energy distribution of photons emitted from decay of unstable nuclides
+ within the material. The integral of this distribution is the total
+ intensity of the photon source in [decay/sec].
+
+ .. versionadded:: 0.14.0
+
"""
next_id = 1
@@ -255,6 +263,18 @@ def fissionable_mass(self):
/ openmc.data.AVOGADRO
return density*self.volume
+ @property
+ def decay_photon_energy(self) -> Univariate:
+ atoms = self.get_nuclide_atoms()
+ dists = []
+ probs = []
+ for nuc, num_atoms in atoms.items():
+ source_per_atom = openmc.data.decay_photon_energy(nuc)
+ if source_per_atom is not None:
+ dists.append(source_per_atom)
+ probs.append(num_atoms)
+ return openmc.data.combine_distributions(dists, probs)
+
@classmethod
def from_hdf5(cls, group: h5py.Group):
"""Create material from HDF5 group
@@ -820,7 +840,7 @@ def get_nuclides(self, element: Optional[str] = None):
for nuclide in self._nuclides:
if nuclide.name not in matching_nuclides:
matching_nuclides.append(nuclide.name)
-
+
return matching_nuclides
def get_nuclide_densities(self):
diff --git a/tests/chain_simple.xml b/tests/chain_simple.xml
index c2e50a370f3..28c5739c9fc 100644
--- a/tests/chain_simple.xml
+++ b/tests/chain_simple.xml
@@ -3,10 +3,16 @@
+
+
@@ -28,6 +34,9 @@
+
2.53000e-02
@@ -38,6 +47,9 @@
+
2.53000e-02
diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py
index f5ee4bf1dbb..384653ff7e6 100644
--- a/tests/regression_tests/deplete_no_transport/test.py
+++ b/tests/regression_tests/deplete_no_transport/test.py
@@ -161,23 +161,23 @@ def _create_operator(from_nuclides,
return op
def _assert_same_mats(res_ref, res_test):
- for mat in res_ref[0].mat_to_ind:
- assert mat in res_test[0].mat_to_ind, \
+ for mat in res_ref[0].index_mat:
+ assert mat in res_test[0].index_mat, \
"Material {} not in new results.".format(mat)
- for nuc in res_ref[0].nuc_to_ind:
- assert nuc in res_test[0].nuc_to_ind, \
+ for nuc in res_ref[0].index_nuc:
+ assert nuc in res_test[0].index_nuc, \
"Nuclide {} not in new results.".format(nuc)
- for mat in res_test[0].mat_to_ind:
- assert mat in res_ref[0].mat_to_ind, \
+ for mat in res_test[0].index_mat:
+ assert mat in res_ref[0].index_mat, \
"Material {} not in old results.".format(mat)
- for nuc in res_test[0].nuc_to_ind:
- assert nuc in res_ref[0].nuc_to_ind, \
+ for nuc in res_test[0].index_nuc:
+ assert nuc in res_ref[0].index_nuc, \
"Nuclide {} not in old results.".format(nuc)
def _assert_atoms_equal(res_ref, res_test, tol):
- for mat in res_test[0].mat_to_ind:
- for nuc in res_test[0].nuc_to_ind:
+ for mat in res_test[0].index_mat:
+ for nuc in res_test[0].index_nuc:
_, y_test = res_test.get_atoms(mat, nuc)
_, y_old = res_ref.get_atoms(mat, nuc)
diff --git a/tests/regression_tests/deplete_with_transport/test.py b/tests/regression_tests/deplete_with_transport/test.py
index 58065035f3b..9b37e43510e 100644
--- a/tests/regression_tests/deplete_with_transport/test.py
+++ b/tests/regression_tests/deplete_with_transport/test.py
@@ -81,23 +81,23 @@ def test_full(run_in_tmpdir, problem, multiproc):
res_ref = openmc.deplete.Results(path_reference)
# Assert same mats
- for mat in res_ref[0].mat_to_ind:
- assert mat in res_test[0].mat_to_ind, \
+ for mat in res_ref[0].index_mat:
+ assert mat in res_test[0].index_mat, \
"Material {} not in new results.".format(mat)
- for nuc in res_ref[0].nuc_to_ind:
- assert nuc in res_test[0].nuc_to_ind, \
+ for nuc in res_ref[0].index_nuc:
+ assert nuc in res_test[0].index_nuc, \
"Nuclide {} not in new results.".format(nuc)
- for mat in res_test[0].mat_to_ind:
- assert mat in res_ref[0].mat_to_ind, \
+ for mat in res_test[0].index_mat:
+ assert mat in res_ref[0].index_mat, \
"Material {} not in old results.".format(mat)
- for nuc in res_test[0].nuc_to_ind:
- assert nuc in res_ref[0].nuc_to_ind, \
+ for nuc in res_test[0].index_nuc:
+ assert nuc in res_ref[0].index_nuc, \
"Nuclide {} not in old results.".format(nuc)
tol = 1.0e-6
- for mat in res_test[0].mat_to_ind:
- for nuc in res_test[0].nuc_to_ind:
+ for mat in res_test[0].index_mat:
+ for nuc in res_test[0].index_nuc:
_, y_test = res_test.get_atoms(mat, nuc)
_, y_old = res_ref.get_atoms(mat, nuc)
diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py
index f0bda1bd978..8900e7eace5 100644
--- a/tests/unit_tests/test_data_decay.py
+++ b/tests/unit_tests/test_data_decay.py
@@ -1,13 +1,14 @@
#!/usr/bin/env python
-from collections.abc import Mapping
import os
from math import log
+from pathlib import Path
import numpy as np
import pytest
from uncertainties import ufloat
import openmc.data
+from openmc.exceptions import DataError
def ufloat_close(a, b):
@@ -126,3 +127,25 @@ def test_sources(ba137m, nb90):
# Nb90 decays by β+ and should emit positrons, electrons, and photons
sources = nb90.sources
assert len(set(sources.keys()) ^ {'positron', 'electron', 'photon'}) == 0
+
+
+def test_decay_photon_energy():
+ # If chain file is not set, we should get a data error
+ if 'chain_file' in openmc.config:
+ del openmc.config['chain_file']
+ with pytest.raises(DataError):
+ openmc.data.decay_photon_energy('I135')
+
+ # Set chain file to simple chain
+ openmc.config['chain_file'] = Path(__file__).parents[1] / "chain_simple.xml"
+
+ # Check strength of I135 source and presence of specific spectral line
+ src = openmc.data.decay_photon_energy('I135')
+ assert isinstance(src, openmc.stats.Discrete)
+ assert src.integral() == pytest.approx(3.920996223799345e-05)
+ assert 1260409. in src.x
+
+ # Check Xe135 source, which should be tabular
+ src = openmc.data.decay_photon_energy('Xe135')
+ assert isinstance(src, openmc.stats.Tabular)
+ assert src.integral() == pytest.approx(2.076506258964966e-05)
diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py
index 4bfdaa63e58..32c3e28098c 100644
--- a/tests/unit_tests/test_deplete_chain.py
+++ b/tests/unit_tests/test_deplete_chain.py
@@ -9,6 +9,7 @@
import numpy as np
from openmc.mpi import comm
from openmc.deplete import Chain, reaction_rates, nuclide, cram, pool
+from openmc.stats import Discrete
import pytest
from tests import cdtemp
@@ -476,6 +477,15 @@ def gnd_simple_chain():
return Chain.from_xml(chainfile)
+def test_chain_sources(gnd_simple_chain):
+ i135 = gnd_simple_chain['I135']
+ assert isinstance(i135.sources, dict)
+ assert list(i135.sources.keys()) == ['photon']
+ photon_src = i135.sources['photon']
+ assert isinstance(photon_src, Discrete)
+ assert photon_src.integral() == pytest.approx(3.920996223799345e-05)
+
+
def test_reduce(gnd_simple_chain, endf_chain):
ref_U5 = gnd_simple_chain["U235"]
ref_iodine = gnd_simple_chain["I135"]
diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py
index 63073cf57a3..e13944e0358 100644
--- a/tests/unit_tests/test_deplete_resultslist.py
+++ b/tests/unit_tests/test_deplete_resultslist.py
@@ -133,3 +133,17 @@ def test_get_steps(unit):
actual = results.get_step_where(
times[-1] * 100, time_units=unit, atol=inf, rtol=inf)
assert actual == times.size - 1
+
+
+def test_stepresult_get_material(res):
+ # Get material at first timestep
+ step_result = res[0]
+ mat1 = step_result.get_material("1")
+ assert mat1.id == 1
+ assert mat1.volume == step_result.volume["1"]
+
+ # Spot check number densities
+ densities = mat1.get_nuclide_atom_densities()
+ assert densities['Xe135'] == pytest.approx(1e-14)
+ assert densities['I135'] == pytest.approx(1e-21)
+ assert densities['U234'] == pytest.approx(1.00506e-05)
diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py
index 7b8a60629e6..58df246ddd7 100644
--- a/tests/unit_tests/test_material.py
+++ b/tests/unit_tests/test_material.py
@@ -1,8 +1,10 @@
from collections import defaultdict
+from pathlib import Path
import pytest
import openmc
+from openmc.data import decay_photon_energy
import openmc.examples
import openmc.model
import openmc.stats
@@ -541,3 +543,33 @@ def test_get_activity():
# volume is required to calculate total activity
m4.volume = 10.
assert pytest.approx(m4.get_activity(units='Bq')) == 355978108155965.94*3/2*10 # [Bq]
+
+
+def test_decay_photon_energy():
+ # Set chain file for testing
+ openmc.config['chain_file'] = Path(__file__).parents[1] / 'chain_simple.xml'
+
+ # Material representing single atom of I135 and Cs135
+ m = openmc.Material()
+ m.add_nuclide('I135', 1.0e-24)
+ m.add_nuclide('Cs135', 1.0e-24)
+ m.volume = 1.0
+
+ # Get decay photon source and make sure it's the right type
+ src = m.decay_photon_energy
+ assert isinstance(src, openmc.stats.Discrete)
+
+ # If we add Xe135 (which has a tabular distribution), the photon source
+ # should be a mixture distribution
+ m.add_nuclide('Xe135', 1.0e-24)
+ src = m.decay_photon_energy
+ assert isinstance(src, openmc.stats.Mixture)
+
+ # With a single atom of each, the intensity of the photon source should be
+ # equal to the sum of the intensities for each nuclide
+ def intensity(src):
+ return src.integral() if src is not None else 0.0
+
+ assert src.integral() == pytest.approx(sum(
+ intensity(decay_photon_energy(nuc)) for nuc in m.get_nuclides()
+ ))