Skip to content

Commit e783e01

Browse files
Add chain parameter to Material.get_activity for half-life data (#3957)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1 parent 7256d50 commit e783e01

7 files changed

Lines changed: 211 additions & 37 deletions

File tree

openmc/data/data.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1+
from __future__ import annotations
2+
13
import itertools
24
import json
35
import os
46
import re
57
from pathlib import Path
68
from math import sqrt, log
9+
from typing import TYPE_CHECKING, Literal
710
from warnings import warn
811

912
from endf.data import (ATOMIC_NUMBER, ATOMIC_SYMBOL, ELEMENT_SYMBOL,
1013
EV_PER_MEV, K_BOLTZMANN, gnds_name, zam)
1114

15+
import openmc
16+
from openmc.checkvalue import PathLike
17+
18+
if TYPE_CHECKING:
19+
from openmc.deplete import Chain
20+
1221
gnds_name.__module__ = __name__
1322
zam.__module__ = __name__
1423

@@ -296,25 +305,47 @@ def atomic_weight(element):
296305
raise ValueError(f"No naturally-occurring isotopes for element '{element}'.")
297306

298307

299-
def half_life(isotope):
308+
def half_life(
309+
isotope: str,
310+
chain_file: Literal[False] | None | PathLike | Chain = False
311+
) -> float | None:
300312
"""Return half-life of isotope in seconds or None if isotope is stable
301313
302-
Half-life values are from the `ENDF/B-VIII.0 decay sublibrary
303-
<https://www.nndc.bnl.gov/endf-b8.0/download.html>`_.
314+
By default, half-life values are from the `ENDF/B-VIII.0 decay sublibrary
315+
<https://www.nndc.bnl.gov/endf-b8.0/download.html>`_. A depletion chain can
316+
also be used as the source of half-life values.
304317
305318
.. versionadded:: 0.13.1
306319
320+
.. versionchanged:: 0.15.4
321+
Added the ``chain_file`` argument.
322+
307323
Parameters
308324
----------
309325
isotope : str
310326
Name of isotope, e.g., 'Pu239'
327+
chain_file : False, None, PathLike, or openmc.deplete.Chain, optional
328+
Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is
329+
used. If ``None``, the chain specified by
330+
``openmc.config['chain_file']`` is used when available. If a path or
331+
:class:`openmc.deplete.Chain` is given, that chain is used. For ``None``
332+
or an explicit chain, nuclides absent from the chain fall back to
333+
ENDF/B-VIII.0 data.
311334
312335
Returns
313336
-------
314-
float
315-
Half-life of isotope in [s]
337+
float or None
338+
Half-life of isotope in [s], or None if the isotope is stable
316339
317340
"""
341+
if chain_file is not False:
342+
if chain_file is not None or openmc.config.get('chain_file') is not None:
343+
# Local import avoids a circular dependency
344+
from openmc.deplete.chain import _get_chain
345+
chain = _get_chain(chain_file)
346+
if isotope in chain:
347+
return chain[isotope].half_life
348+
318349
global _HALF_LIFE
319350
if not _HALF_LIFE:
320351
# Load ENDF/B-VIII.0 data from JSON file
@@ -324,7 +355,10 @@ def half_life(isotope):
324355
return _HALF_LIFE.get(isotope.lower())
325356

326357

327-
def decay_constant(isotope):
358+
def decay_constant(
359+
isotope: str,
360+
chain_file: Literal[False] | None | PathLike | Chain = False
361+
) -> float:
328362
"""Return decay constant of isotope in [s^-1]
329363
330364
Decay constants are based on half-life values from the
@@ -333,10 +367,20 @@ def decay_constant(isotope):
333367
334368
.. versionadded:: 0.13.1
335369
370+
.. versionchanged:: 0.15.4
371+
Added the ``chain_file`` argument.
372+
336373
Parameters
337374
----------
338375
isotope : str
339376
Name of isotope, e.g., 'Pu239'
377+
chain_file : False, None, PathLike, or openmc.deplete.Chain, optional
378+
Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is
379+
used. If ``None``, the chain specified by
380+
``openmc.config['chain_file']`` is used when available. If a path or
381+
:class:`openmc.deplete.Chain` is given, that chain is used. For ``None``
382+
or an explicit chain, nuclides absent from the chain fall back to
383+
ENDF/B-VIII.0 data.
340384
341385
Returns
342386
-------
@@ -348,7 +392,7 @@ def decay_constant(isotope):
348392
openmc.data.half_life
349393
350394
"""
351-
t = half_life(isotope)
395+
t = half_life(isotope, chain_file)
352396
return _LOG_TWO / t if t else 0.0
353397

354398

@@ -496,5 +540,3 @@ def isotopes(element: str) -> list[tuple[str, float]]:
496540
result.append(kv)
497541

498542
return result
499-
500-

openmc/deplete/results.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
from __future__ import annotations
2+
13
import numbers
24
import bisect
35
import math
46
from collections.abc import Iterable
7+
from typing import Literal
58
from warnings import warn
69

710
import h5py
811
import numpy as np
912

13+
import openmc
14+
from .chain import Chain, _get_chain
1015
from .stepresult import StepResult, VERSION_RESULTS
1116
import openmc.checkvalue as cv
1217
from openmc.data import atomic_mass, AVOGADRO
@@ -103,7 +108,8 @@ def get_activity(
103108
mat: Material | str,
104109
units: str = "Bq/cm3",
105110
by_nuclide: bool = False,
106-
volume: float | None = None
111+
volume: float | None = None,
112+
chain_file: Literal[False] | None | PathLike | Chain = None
107113
) -> tuple[np.ndarray, np.ndarray | list[dict]]:
108114
"""Get activity of material over time.
109115
@@ -115,22 +121,31 @@ def get_activity(
115121
Material object or material id to evaluate
116122
units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'}
117123
Specifies the type of activity to return, options include total
118-
activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity [Bq/cm3].
124+
activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity
125+
[Bq/cm3].
119126
by_nuclide : bool
120127
Specifies if the activity should be returned for the material as a
121128
whole or per nuclide. Default is False.
122129
volume : float, optional
123130
Volume of the material. If not passed, defaults to using the
124131
:attr:`Material.volume` attribute.
132+
chain_file : False, None, PathLike, or openmc.deplete.Chain, optional
133+
Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is
134+
used. If ``None``, the chain specified by
135+
``openmc.config['chain_file']`` is used when available. If a path or
136+
:class:`openmc.deplete.Chain` is given, that chain is used. For
137+
``None`` or an explicit chain, nuclides absent from the chain fall
138+
back to ENDF/B-VIII.0 data.
139+
140+
.. versionadded:: 0.15.4
125141
126142
Returns
127143
-------
128144
times : numpy.ndarray
129145
Array of times in [s]
130146
activities : numpy.ndarray or List[dict]
131-
Array of total activities if by_nuclide = False (default)
132-
or list of dictionaries of activities by nuclide if
133-
by_nuclide = True.
147+
Array of total activities if by_nuclide = False (default) or list of
148+
dictionaries of activities by nuclide if by_nuclide = True.
134149
135150
"""
136151
if isinstance(mat, Material):
@@ -140,6 +155,13 @@ def get_activity(
140155
else:
141156
raise TypeError('mat should be of type openmc.Material or str')
142157

158+
if chain_file is not False:
159+
if chain_file is None:
160+
if openmc.config.get('chain_file') is not None:
161+
chain_file = _get_chain(None)
162+
else:
163+
chain_file = _get_chain(chain_file)
164+
143165
times = np.empty_like(self, dtype=float)
144166
if by_nuclide:
145167
activities = [None] * len(self)
@@ -149,7 +171,8 @@ def get_activity(
149171
# Evaluate activity for each depletion time
150172
for i, result in enumerate(self):
151173
times[i] = result.time[0]
152-
activities[i] = result.get_material(mat_id).get_activity(units, by_nuclide, volume)
174+
activities[i] = result.get_material(mat_id).get_activity(
175+
units, by_nuclide, volume, chain_file=chain_file)
153176

154177
return times, activities
155178

openmc/material.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import re
99
import sys
1010
import tempfile
11-
from typing import Sequence, Dict
11+
from typing import TYPE_CHECKING, Literal, Sequence, Dict
1212
import warnings
1313

1414
import lxml.etree as ET
@@ -28,6 +28,9 @@
2828
from openmc.data.function import Tabulated1D
2929
from openmc.data import mass_energy_absorption_coefficient, dose_coefficients
3030

31+
if TYPE_CHECKING:
32+
from openmc.deplete import Chain
33+
3134

3235
# Units for density supported by OpenMC
3336
DENSITY_UNITS = ('g/cm3', 'g/cc', 'kg/m3', 'atom/b-cm', 'atom/cm3', 'sum',
@@ -1385,8 +1388,13 @@ def get_element_atom_densities(self, element: str | None = None) -> dict[str, fl
13851388
return densities
13861389

13871390

1388-
def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False,
1389-
volume: float | None = None) -> dict[str, float] | float:
1391+
def get_activity(
1392+
self,
1393+
units: str = 'Bq/cm3',
1394+
by_nuclide: bool = False,
1395+
volume: float | None = None,
1396+
chain_file: Literal[False] | None | PathLike | Chain = None
1397+
) -> dict[str, float] | float:
13901398
"""Return the activity of the material or each nuclide within.
13911399
13921400
.. versionadded:: 0.13.1
@@ -1405,13 +1413,22 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False,
14051413
:attr:`Material.volume` attribute.
14061414
14071415
.. versionadded:: 0.13.3
1416+
chain_file : False, None, PathLike, or openmc.deplete.Chain, optional
1417+
Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is
1418+
used. If ``None``, the chain specified by
1419+
``openmc.config['chain_file']`` is used when available. If a path or
1420+
:class:`openmc.deplete.Chain` is given, that chain is used. For
1421+
``None`` or an explicit chain, nuclides absent from the chain fall
1422+
back to ENDF/B-VIII.0 data.
1423+
1424+
.. versionadded:: 0.15.4
14081425
14091426
Returns
14101427
-------
14111428
Union[dict, float]
1412-
If by_nuclide is True then a dictionary whose keys are nuclide
1413-
names and values are activity is returned. Otherwise the activity
1414-
of the material is returned as a float.
1429+
If by_nuclide is True then a dictionary whose keys are nuclide names
1430+
and values are activity is returned. Otherwise the activity of the
1431+
material is returned as a float.
14151432
"""
14161433

14171434
cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3', 'Ci', 'Ci/m3'})
@@ -1440,7 +1457,8 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False,
14401457

14411458
activity = {}
14421459
for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items():
1443-
inv_seconds = openmc.data.decay_constant(nuclide)
1460+
inv_seconds = openmc.data.decay_constant(
1461+
nuclide, chain_file=chain_file)
14441462
activity[nuclide] = inv_seconds * 1e24 * atoms_per_bcm * multiplier
14451463

14461464
return activity if by_nuclide else sum(activity.values())

tests/unit_tests/test_data_decay.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,16 @@ def test_decay_photon_energy():
155155
with pytest.raises(DataError):
156156
openmc.data.decay_photon_energy('I135')
157157

158-
# Set chain file to simple chain
159-
openmc.config['chain_file'] = Path(__file__).parents[1] / "chain_simple.xml"
160-
161-
# Check strength of I135 source and presence of specific spectral line
162-
src = openmc.data.decay_photon_energy('I135')
163-
assert isinstance(src, openmc.stats.Discrete)
164-
assert src.integral() == pytest.approx(3.920996223799345e-05)
165-
assert 1260409. in src.x
166-
167-
# Check Xe135 source, which should be tabular
168-
src = openmc.data.decay_photon_energy('Xe135')
169-
assert isinstance(src, openmc.stats.Tabular)
170-
assert src.integral() == pytest.approx(2.076506258964966e-05)
158+
# Temporarily Set chain file to simple chain
159+
with openmc.config.patch('chain_file', Path(__file__).parents[1] / 'chain_simple.xml'):
160+
161+
# Check strength of I135 source and presence of specific spectral line
162+
src = openmc.data.decay_photon_energy('I135')
163+
assert isinstance(src, openmc.stats.Discrete)
164+
assert src.integral() == pytest.approx(3.920996223799345e-05)
165+
assert 1260409. in src.x
166+
167+
# Check Xe135 source, which should be tabular
168+
src = openmc.data.decay_photon_energy('Xe135')
169+
assert isinstance(src, openmc.stats.Tabular)
170+
assert src.integral() == pytest.approx(2.076506258964966e-05)

tests/unit_tests/test_data_misc.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numpy as np
88
import pytest
99
import openmc.data
10+
from openmc.deplete import Chain, Nuclide
1011

1112

1213
def test_data_library(tmpdir):
@@ -134,7 +135,8 @@ def test_zam():
134135
with pytest.raises(ValueError):
135136
openmc.data.zam('Am242-m1')
136137

137-
def test_half_life():
138+
139+
def test_half_life(tmp_path):
138140
assert openmc.data.half_life('H2') is None
139141
assert openmc.data.half_life('U235') == pytest.approx(2.22102e16)
140142
assert openmc.data.half_life('Am242') == pytest.approx(57672.0)
@@ -143,3 +145,32 @@ def test_half_life():
143145
assert openmc.data.decay_constant('U235') == pytest.approx(log(2.0)/2.22102e16)
144146
assert openmc.data.decay_constant('Am242') == pytest.approx(log(2.0)/57672.0)
145147
assert openmc.data.decay_constant('Am242_m1') == pytest.approx(log(2.0)/4449622000.0)
148+
149+
# Create minimal chain with H3 and Am242 to test half-life and decay
150+
# constant retrieval from chain file
151+
chain = Chain()
152+
h3 = Nuclide("H3")
153+
h3.half_life = 1.0
154+
chain.add_nuclide(h3)
155+
am242 = Nuclide("Am242")
156+
chain.add_nuclide(am242)
157+
158+
assert openmc.data.half_life('H3', chain_file=chain) == 1.0
159+
assert openmc.data.decay_constant('H3', chain_file=chain) == pytest.approx(log(2.0))
160+
161+
# Nuclides that are present but stable in the chain should not fall back to
162+
# ENDF/B-VIII.0 data.
163+
assert openmc.data.half_life('Am242', chain_file=chain) is None
164+
assert openmc.data.decay_constant('Am242', chain_file=chain) == 0.0
165+
166+
# Nuclides missing from the chain fall back to ENDF/B-VIII.0 data.
167+
assert openmc.data.half_life('U235', chain_file=chain) == pytest.approx(2.22102e16)
168+
169+
chain_path = tmp_path / "chain.xml"
170+
chain.export_to_xml(chain_path)
171+
assert openmc.data.half_life('H3', chain_file=chain_path) == 1.0
172+
173+
endf_h3 = openmc.data.half_life('H3')
174+
with openmc.config.patch('chain_file', chain_path):
175+
assert openmc.data.half_life('H3', chain_file=None) == 1.0
176+
assert openmc.data.half_life('H3', chain_file=False) == endf_h3

0 commit comments

Comments
 (0)