Skip to content

Commit 58e125a

Browse files
implementation of the generalized methods to fetch particle sources from chain file
1 parent 23e8a11 commit 58e125a

6 files changed

Lines changed: 156 additions & 36 deletions

File tree

openmc/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing import Any, Dict, Iterator
2121

2222
from openmc.data import DataLibrary
23-
from openmc.data.decay import _DECAY_ENERGY, _DECAY_PHOTON_ENERGY
23+
from openmc.data.decay import _DECAY_ENERGY, _DECAY_PARTICLE_ENERGY
2424

2525
__all__ = ["config"]
2626

@@ -77,7 +77,7 @@ def __delitem__(self, key: str):
7777
if env_var in os.environ:
7878
del os.environ[env_var]
7979
if key == 'chain_file':
80-
_DECAY_PHOTON_ENERGY.clear()
80+
_DECAY_PARTICLE_ENERGY.clear()
8181
_DECAY_ENERGY.clear()
8282

8383
def __setitem__(self, key: str, value: Any):
@@ -103,7 +103,7 @@ def __setitem__(self, key: str, value: Any):
103103
os.environ[self._PATH_KEYS[key]] = str(stored_path)
104104

105105
if key == 'chain_file':
106-
_DECAY_PHOTON_ENERGY.clear()
106+
_DECAY_PARTICLE_ENERGY.clear()
107107
_DECAY_ENERGY.clear()
108108

109109
if not stored_path.exists():

openmc/data/decay.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -561,31 +561,32 @@ def sources(self):
561561
return merged_sources
562562

563563

564-
_DECAY_PHOTON_ENERGY = {}
564+
_DECAY_PARTICLE_ENERGY = {}
565565

566566

567-
def decay_photon_energy(nuclide: str) -> Univariate | None:
568-
"""Get photon energy distribution resulting from the decay of a nuclide
567+
def decay_particle_energy(nuclide: str, particle: str) -> Univariate | None:
568+
"""Get a decay particle energy distribution resulting from the decay of a nuclide
569569
570570
This function relies on data stored in a depletion chain. Before calling it
571571
for the first time, you need to ensure that a depletion chain has been
572572
specified in openmc.config['chain_file'].
573573
574-
.. versionadded:: 0.13.2
575-
576574
Parameters
577575
----------
578576
nuclide : str
579577
Name of nuclide, e.g., 'Co58'
578+
particle : str
579+
Name of the decay particle, e.g., 'photon', 'neutron'
580580
581581
Returns
582582
-------
583583
openmc.stats.Univariate or None
584-
Distribution of energies in [eV] of photons emitted from decay, or None
585-
if no photon source exists. Note that the probabilities represent
584+
Distribution of energies in [eV] of decay particles emitted from decay, or None
585+
if no decay source of that particle exists. Note that the probabilities represent
586586
intensities, given as [Bq/atom] (in other words, decay constants).
587587
"""
588-
if not _DECAY_PHOTON_ENERGY:
588+
589+
if not _DECAY_PARTICLE_ENERGY:
589590
chain_file = openmc.config.get('chain_file')
590591
if chain_file is None:
591592
raise DataError(
@@ -596,15 +597,15 @@ def decay_photon_energy(nuclide: str) -> Univariate | None:
596597
from openmc.deplete import Chain
597598
chain = Chain.from_xml(chain_file)
598599
for nuc in chain.nuclides:
599-
if 'photon' in nuc.sources:
600-
_DECAY_PHOTON_ENERGY[nuc.name] = nuc.sources['photon']
600+
for part, dist in nuc.sources.items():
601+
_DECAY_PARTICLE_ENERGY[(nuc.name, part)] = dist
601602

602603
# If the chain file contained no sources at all, warn the user
603-
if not _DECAY_PHOTON_ENERGY:
604-
warn(f"Chain file '{chain_file}' does not have any decay photon "
605-
"sources listed.")
604+
if not _DECAY_PARTICLE_ENERGY:
605+
warn(f"Chain file '{chain_file}' does not have any decay particle "
606+
"source listed.")
606607

607-
return _DECAY_PHOTON_ENERGY.get(nuclide)
608+
return _DECAY_PARTICLE_ENERGY.get((nuclide, particle))
608609

609610

610611
_DECAY_ENERGY = {}

openmc/material.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -332,20 +332,21 @@ def decay_photon_energy(self) -> Univariate | None:
332332
"version.", FutureWarning)
333333
return self.get_decay_photon_energy(0.0)
334334

335-
def get_decay_photon_energy(
335+
def get_decay_particle_energy(
336336
self,
337+
particle: str = 'photon',
337338
clip_tolerance: float = 1e-6,
338339
units: str = 'Bq',
339340
volume: float | None = None,
340341
exclude_nuclides: list[str] | None = None,
341342
include_nuclides: list[str] | None = None
342343
) -> Univariate | None:
343-
r"""Return energy distribution of decay photons from unstable nuclides.
344-
345-
.. versionadded:: 0.14.0
344+
r"""Return energy distribution of decay particles from unstable nuclides.
346345
347346
Parameters
348347
----------
348+
particle : string
349+
Particle type to return the energy distribution for.
349350
clip_tolerance : float
350351
Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions
351352
that will be discarded.
@@ -355,19 +356,21 @@ def get_decay_photon_energy(
355356
Volume of the material. If not passed, defaults to using the
356357
:attr:`Material.volume` attribute.
357358
exclude_nuclides : list of str, optional
358-
Nuclides to exclude from the photon source calculation.
359+
Nuclides to exclude from the particle source calculation.
359360
include_nuclides : list of str, optional
360-
Nuclides to include in the photon source calculation. If specified,
361+
Nuclides to include in the particle source calculation. If specified,
361362
only these nuclides are used.
362363
363364
Returns
364365
-------
365366
Univariate or None
366-
Decay photon energy distribution. The integral of this distribution is
367-
the total intensity of the photon source in the requested units.
367+
Decay particle energy distribution. The integral of this distribution is
368+
the total intensity of the particle source in the requested units.
368369
369370
"""
370371
cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'})
372+
cv.check_type('particle', particle, str)
373+
cv.check_value('particle', particle, {'alpha', 'fragment', 'neutron','photon', 'electron', 'positron','proton'})
371374

372375
if exclude_nuclides is not None and include_nuclides is not None:
373376
raise ValueError("Cannot specify both exclude_nuclides and include_nuclides")
@@ -393,12 +396,12 @@ def get_decay_photon_energy(
393396
if include_nuclides is not None and nuc not in include_nuclides:
394397
continue
395398

396-
source_per_atom = openmc.data.decay_photon_energy(nuc)
399+
source_per_atom = openmc.data.decay_particle_energy(nuc,particle)
397400
if source_per_atom is not None and atoms_per_bcm > 0.0:
398401
dists.append(source_per_atom)
399402
probs.append(1e24 * atoms_per_bcm * multiplier)
400403

401-
# If no photon sources, exit early
404+
# If no particle sources, exit early
402405
if not dists:
403406
return None
404407

@@ -414,6 +417,54 @@ def get_decay_photon_energy(
414417

415418
return combined
416419

420+
def get_decay_photon_energy(
421+
self,
422+
clip_tolerance: float = 1e-6,
423+
units: str = 'Bq',
424+
volume: float | None = None,
425+
exclude_nuclides: list[str] | None = None,
426+
include_nuclides: list[str] | None = None
427+
) -> Univariate | None:
428+
r"""Return energy distribution of decay photons from unstable nuclides.
429+
430+
This is a convenience wrapper around :meth:`Material.get_decay_particle_energy`
431+
with ``particle='photon'``.
432+
433+
.. versionadded:: 0.14.0
434+
435+
Parameters
436+
----------
437+
clip_tolerance : float
438+
Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions
439+
that will be discarded.
440+
units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'}
441+
Specifies the units on the integral of the distribution.
442+
volume : float, optional
443+
Volume of the material. If not passed, defaults to using the
444+
:attr:`Material.volume` attribute.
445+
exclude_nuclides : list of str, optional
446+
Nuclides to exclude from the photon source calculation.
447+
include_nuclides : list of str, optional
448+
Nuclides to include in the photon source calculation. If specified,
449+
only these nuclides are used.
450+
451+
Returns
452+
-------
453+
Univariate or None
454+
Decay photon energy distribution. The integral of this distribution is
455+
the total intensity of the photon source in the requested units.
456+
457+
"""
458+
459+
return self.get_decay_particle_energy(
460+
particle='photon',
461+
clip_tolerance=clip_tolerance,
462+
units=units,
463+
volume=volume,
464+
exclude_nuclides=exclude_nuclides,
465+
include_nuclides=include_nuclides,
466+
)
467+
417468
def get_photon_contact_dose_rate(
418469
self,
419470
dose_quantity: str = "absorbed-air",
@@ -540,7 +591,7 @@ def get_photon_contact_dose_rate(
540591
* sv_per_psv * 1e24)
541592

542593
for nuc, nuc_atoms_per_bcm in self.get_nuclide_atom_densities().items():
543-
photon_source_per_atom = openmc.data.decay_photon_energy(nuc)
594+
photon_source_per_atom = openmc.data.decay_particle_energy(nuc,'photon')
544595

545596
# nuclides with no contribution
546597
if photon_source_per_atom is None or nuc_atoms_per_bcm <= 0.0:

tests/unit_tests/test_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,6 @@ def test_config_chain_side_effect(tmp_path):
9999
"""Test that modifying chain_file clears decay data caches."""
100100
chain_file = tmp_path / "chain.xml"; chain_file.touch()
101101
decay._DECAY_ENERGY['U235'] = (1.0, 2.0)
102-
decay._DECAY_PHOTON_ENERGY['PU239'] = {}
102+
decay._DECAY_PARTICLE_ENERGY['PU239','photon'] = {}
103103
openmc.config['chain_file'] = chain_file
104-
assert not decay._DECAY_ENERGY and not decay._DECAY_PHOTON_ENERGY
104+
assert not decay._DECAY_ENERGY and not decay._DECAY_PARTICLE_ENERGY

tests/unit_tests/test_data_decay.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,38 @@ def test_decay_photon_energy():
131131
if 'chain_file' in openmc.config:
132132
del openmc.config['chain_file']
133133
with pytest.raises(DataError):
134-
openmc.data.decay_photon_energy('I135')
134+
openmc.data.decay_particle_energy('I135','photon')
135135

136136
# Set chain file to simple chain
137137
openmc.config['chain_file'] = Path(__file__).parents[1] / "chain_simple.xml"
138138

139139
# Check strength of I135 source and presence of specific spectral line
140-
src = openmc.data.decay_photon_energy('I135')
140+
src = openmc.data.decay_particle_energy('I135','photon')
141141
assert isinstance(src, openmc.stats.Discrete)
142142
assert src.integral() == pytest.approx(3.920996223799345e-05)
143143
assert 1260409. in src.x
144144

145145
# Check Xe135 source, which should be tabular
146-
src = openmc.data.decay_photon_energy('Xe135')
146+
src = openmc.data.decay_particle_energy('Xe135','photon')
147147
assert isinstance(src, openmc.stats.Tabular)
148148
assert src.integral() == pytest.approx(2.076506258964966e-05)
149+
150+
151+
def test_decay_particle_energy():
152+
# If chain file is not set, we should get a data error
153+
if 'chain_file' in openmc.config:
154+
del openmc.config['chain_file']
155+
with pytest.raises(DataError):
156+
openmc.data.decay_particle_energy('Fe55', 'electron')
157+
158+
# Set chain file to Ni chain and check electron/positron sources
159+
with openmc.config.patch('chain_file', Path(__file__).parents[1] / "chain_ni.xml"):
160+
src = openmc.data.decay_particle_energy('Fe55', 'electron')
161+
assert isinstance(src, openmc.stats.Discrete)
162+
assert src.integral() == pytest.approx(4.225437849490689e-08)
163+
assert 6377.571 in src.x
164+
165+
src = openmc.data.decay_particle_energy('Fe55', 'positron')
166+
assert isinstance(src, openmc.stats.Discrete)
167+
assert src.integral() == pytest.approx(8.004558990612365e-09)
168+
assert 231210.0 in src.x

tests/unit_tests/test_material.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import numpy as np
77

88
import openmc
9-
from openmc.data import decay_photon_energy
9+
from openmc.data import decay_particle_energy
1010
from openmc.deplete import Chain
1111
import openmc.examples
1212
import openmc.model
@@ -699,13 +699,13 @@ def intensity(src):
699699
return src.integral() if src is not None else 0.0
700700

701701
assert src.integral() == pytest.approx(sum(
702-
intensity(decay_photon_energy(nuc)) for nuc in m.get_nuclides()
702+
intensity(decay_particle_energy(nuc,"photon")) for nuc in m.get_nuclides()
703703
), rel=1e-3)
704704

705705
# When the clipping threshold is zero, the intensities should match exactly
706706
src = m.get_decay_photon_energy(0.0)
707707
assert src.integral() == pytest.approx(sum(
708-
intensity(decay_photon_energy(nuc)) for nuc in m.get_nuclides()
708+
intensity(decay_particle_energy(nuc,"photon")) for nuc in m.get_nuclides()
709709
))
710710

711711
# A material with no unstable nuclides should have no decay photon source
@@ -715,6 +715,54 @@ def intensity(src):
715715
assert stable.get_decay_photon_energy() is None
716716

717717

718+
def test_decay_particle_energy():
719+
with openmc.config.patch('chain_file', Path(__file__).parents[1] / 'chain_ni.xml'):
720+
# Material representing single atom of Fe55 and Co58
721+
m = openmc.Material()
722+
m.add_nuclide('Fe55', 1.0e-24)
723+
m.add_nuclide('Co58', 1.0e-24)
724+
m.volume = 1.0
725+
726+
# Get decay positron source and make sure it's the right type
727+
src = m.get_decay_particle_energy('positron')
728+
assert isinstance(src, openmc.stats.Discrete)
729+
730+
# Make sure units/volume work as expected
731+
src_v2 = m.get_decay_particle_energy('positron', volume=2.0)
732+
assert src.p * 2.0 == pytest.approx(src_v2.p)
733+
src_per_cm3 = m.get_decay_particle_energy('positron', units='Bq/cm3',
734+
volume=100.0)
735+
assert (src.p == src_per_cm3.p).all()
736+
src_per_bqg = m.get_decay_particle_energy('positron', units='Bq/g')
737+
src_per_bqkg = m.get_decay_particle_energy('positron', units='Bq/kg')
738+
assert pytest.approx(src_per_bqg.integral()) == src_per_bqkg.integral() / 1000.
739+
src_per_bqm3 = m.get_decay_particle_energy('positron', units='Bq/m3')
740+
assert pytest.approx(src_per_bqm3.integral()) == src_per_cm3.integral() * 1e6
741+
742+
# With a single atom of each, the intensity of the positron source
743+
# should be equal to the sum of the intensities for each nuclide
744+
def intensity(src):
745+
return src.integral() if src is not None else 0.0
746+
747+
assert src.integral() == pytest.approx(sum(
748+
intensity(decay_particle_energy(nuc, 'positron'))
749+
for nuc in m.get_nuclides()
750+
), rel=1e-3)
751+
752+
# When the clipping threshold is zero, the intensities should match exactly
753+
src = m.get_decay_particle_energy('positron', 0.0)
754+
assert src.integral() == pytest.approx(sum(
755+
intensity(decay_particle_energy(nuc, 'positron'))
756+
for nuc in m.get_nuclides()
757+
))
758+
759+
# A material with no unstable nuclides should have no decay positron source
760+
stable = openmc.Material()
761+
stable.add_nuclide('Gd156', 1.0)
762+
stable.volume = 1.0
763+
assert stable.get_decay_particle_energy('positron') is None
764+
765+
718766
def test_avoid_subnormal(run_in_tmpdir):
719767
# Write a materials.xml with a material that has a nuclide density that is
720768
# represented as a subnormal floating point value

0 commit comments

Comments
 (0)