mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-27 21:55:41 -04:00
Merge c481c1e8b8 into d3bc1669d3
This commit is contained in:
commit
0543dca247
4 changed files with 2042 additions and 4 deletions
|
|
@ -1298,6 +1298,54 @@ class Chain:
|
|||
|
||||
return new_chain
|
||||
|
||||
def get_decay_daughters(self, nuclide, max_half_life=None):
|
||||
"""Return all nuclides reachable via decay from a given nuclide.
|
||||
|
||||
Unlike :meth:`reduce`, this follows only decay paths (not
|
||||
reactions such as neutron capture) and returns a set of nuclide
|
||||
names rather than a new :class:`Chain`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nuclide : str
|
||||
Name of the parent nuclide.
|
||||
max_half_life : float, optional
|
||||
Maximum daughter half-life in seconds. Stops descending a
|
||||
branch when a daughter's half-life exceeds this value. If
|
||||
None, all radioactive daughters are returned regardless of
|
||||
half-life.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set of str
|
||||
Names of all decay daughters reachable from the parent.
|
||||
|
||||
"""
|
||||
daughters = set()
|
||||
stack = [nuclide]
|
||||
visited = set()
|
||||
while stack:
|
||||
name = stack.pop()
|
||||
if name in visited:
|
||||
continue
|
||||
visited.add(name)
|
||||
if name not in self.nuclide_dict:
|
||||
continue
|
||||
for mode in self[name].decay_modes:
|
||||
target = mode.target
|
||||
if target is None or target in visited:
|
||||
continue
|
||||
if target not in self.nuclide_dict:
|
||||
continue
|
||||
target_nuc = self[target]
|
||||
if target_nuc.half_life is None or target_nuc.half_life <= 0:
|
||||
continue
|
||||
if max_half_life is not None and target_nuc.half_life >= max_half_life:
|
||||
continue
|
||||
daughters.add(target)
|
||||
stack.append(target)
|
||||
return daughters
|
||||
|
||||
def _follow(self, isotopes, level):
|
||||
"""Return all isotopes present up to depth level"""
|
||||
found = isotopes.copy()
|
||||
|
|
|
|||
|
|
@ -1627,6 +1627,7 @@ class Material(IDManagerMixin):
|
|||
limits: str | dict[str, float] = 'Fetter',
|
||||
metal: bool = False,
|
||||
by_nuclide: bool = False,
|
||||
chain=None,
|
||||
) -> float | dict[str, float]:
|
||||
"""Return the waste disposal rating for the material.
|
||||
|
||||
|
|
@ -1660,6 +1661,37 @@ class Material(IDManagerMixin):
|
|||
short-lived radionuclides
|
||||
- 'NRC_short_C': Uses the 10 CFR 61.55 class C limits for
|
||||
short-lived radionuclides
|
||||
The following options use clearance values from the German
|
||||
Strahlenschutzverordnung (StrlSchV) 2018, Anlage 4, Tabelle 1.
|
||||
These limits are in [Bq/g] and the material activity will be
|
||||
compared using the same units.
|
||||
|
||||
- 'StrlSchV_unrestricted': Unrestricted clearance of solid and
|
||||
liquid substances (column 3)
|
||||
- 'StrlSchV_metal_recycling': Metal scrap recycling (column 14)
|
||||
- 'StrlSchV_landfill_100': Landfill disposal for facilities
|
||||
receiving ≤100 Mg/a (column 8)
|
||||
- 'StrlSchV_landfill_1000': Landfill disposal for facilities
|
||||
receiving ≤1000 Mg/a (column 10)
|
||||
- 'StrlSchV_incineration_100': Incineration for facilities
|
||||
receiving ≤100 Mg/a (column 9)
|
||||
- 'StrlSchV_incineration_1000': Incineration for facilities
|
||||
receiving ≤1000 Mg/a (column 11)
|
||||
- 'StrlSchV_soil': Soil surface clearance (column 7)
|
||||
- 'StrlSchV_rubble': Building rubble clearance for >1000 Mg/a
|
||||
(column 6)
|
||||
|
||||
.. note::
|
||||
Some nuclides in Anlage 4 are listed with a '+' suffix,
|
||||
indicating that their clearance value includes daughter
|
||||
nuclides in secular equilibrium. When a depletion chain is
|
||||
provided via the ``chain`` parameter, daughters in secular
|
||||
equilibrium are automatically excluded from the
|
||||
sum-of-fractions to avoid double-counting. Without a
|
||||
chain, all nuclides with clearance entries are counted
|
||||
individually, which may overestimate the rating for some
|
||||
decay chains.
|
||||
|
||||
metal : bool, optional
|
||||
Whether or not the material is in metal form (only applicable for
|
||||
NRC based limits)
|
||||
|
|
@ -1669,6 +1701,11 @@ class Material(IDManagerMixin):
|
|||
nuclide names and the values are the waste disposal ratings for each
|
||||
nuclide. If False, a single float value is returned that represents
|
||||
the overall waste disposal rating for the material.
|
||||
chain : openmc.deplete.Chain, optional
|
||||
Depletion chain used to identify daughter nuclides in secular
|
||||
equilibrium for StrlSchV limits. When provided, daughters
|
||||
covered by a parent's '+' clearance value are excluded from
|
||||
the sum-of-fractions to avoid double-counting.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -1681,7 +1718,7 @@ class Material(IDManagerMixin):
|
|||
Material.waste_classification()
|
||||
|
||||
"""
|
||||
return waste._waste_disposal_rating(self, limits, metal, by_nuclide)
|
||||
return waste._waste_disposal_rating(self, limits, metal, by_nuclide, chain)
|
||||
|
||||
def clone(self, memo: dict | None = None) -> Material:
|
||||
"""Create a copy of this material with a new unique ID.
|
||||
|
|
|
|||
1748
openmc/waste.py
1748
openmc/waste.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,8 @@
|
|||
import random
|
||||
|
||||
import openmc
|
||||
from openmc.deplete import Chain
|
||||
from openmc.deplete.nuclide import Nuclide, DecayTuple
|
||||
import pytest
|
||||
|
||||
|
||||
|
|
@ -100,3 +102,212 @@ def test_waste_disposal_rating():
|
|||
wdr = mat.waste_disposal_rating(limits={'K40': 4*ci_m3}, by_nuclide=True)
|
||||
assert isinstance(wdr, dict)
|
||||
assert wdr['K40'] == pytest.approx(1/4)
|
||||
|
||||
|
||||
def test_strlschv_unrestricted():
|
||||
"""Test German StrlSchV unrestricted clearance rating"""
|
||||
# Co-60 unrestricted clearance limit is 0.1 Bq/g.
|
||||
# Create a material with Co60 and stable Co59 (which should not
|
||||
# contribute to the waste disposal rating).
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Co59', 1e-10)
|
||||
mat.add_nuclide('Co60', 1e-12)
|
||||
bq_g = mat.get_activity('Bq/g', by_nuclide=True)['Co60']
|
||||
|
||||
# Rating should equal activity / limit (Co59 is stable, no contribution)
|
||||
expected = bq_g / 0.1
|
||||
rating = mat.waste_disposal_rating(limits='StrlSchV_unrestricted')
|
||||
assert rating == pytest.approx(expected, rel=1e-3)
|
||||
|
||||
# by_nuclide should return dict with Co60 entry
|
||||
wdr = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted', by_nuclide=True
|
||||
)
|
||||
assert isinstance(wdr, dict)
|
||||
assert 'Co60' in wdr
|
||||
assert wdr['Co60'] == pytest.approx(expected, rel=1e-3)
|
||||
|
||||
|
||||
def test_strlschv_metal_recycling():
|
||||
"""Test German StrlSchV metal scrap recycling clearance rating"""
|
||||
# Co-60 metal recycling limit is 0.6 Bq/g, which is less restrictive
|
||||
# than the unrestricted clearance limit of 0.1 Bq/g.
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Co60', 1e-12)
|
||||
|
||||
unrestricted = mat.waste_disposal_rating(limits='StrlSchV_unrestricted')
|
||||
metal = mat.waste_disposal_rating(limits='StrlSchV_metal_recycling')
|
||||
|
||||
# Metal recycling limit is 6x higher, so rating should be 6x lower
|
||||
assert unrestricted == pytest.approx(6.0 * metal, rel=1e-3)
|
||||
|
||||
|
||||
def test_strlschv_sum_rule():
|
||||
"""Test that the sum-of-fractions rule works for StrlSchV limits"""
|
||||
# Create material with two nuclides, each at half their unrestricted limit
|
||||
# Co-60 limit = 0.1 Bq/g, Cs-137 limit = 0.1 Bq/g
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Co60', 1e-12)
|
||||
mat.add_nuclide('Cs137', 1e-12)
|
||||
|
||||
wdr = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted', by_nuclide=True
|
||||
)
|
||||
assert 'Co60' in wdr
|
||||
assert 'Cs137' in wdr
|
||||
|
||||
# Total rating should be the sum of individual contributions
|
||||
total = mat.waste_disposal_rating(limits='StrlSchV_unrestricted')
|
||||
assert total == pytest.approx(wdr['Co60'] + wdr['Cs137'], rel=1e-6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pathway", [
|
||||
'StrlSchV_unrestricted',
|
||||
'StrlSchV_metal_recycling',
|
||||
'StrlSchV_landfill_100',
|
||||
'StrlSchV_landfill_1000',
|
||||
'StrlSchV_incineration_100',
|
||||
'StrlSchV_incineration_1000',
|
||||
'StrlSchV_soil',
|
||||
'StrlSchV_rubble',
|
||||
])
|
||||
def test_strlschv_all_pathways(pathway):
|
||||
"""Test that all StrlSchV pathways return valid ratings"""
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Co60', 1e-12)
|
||||
|
||||
rating = mat.waste_disposal_rating(limits=pathway)
|
||||
assert isinstance(rating, float)
|
||||
assert rating > 0.0
|
||||
|
||||
wdr = mat.waste_disposal_rating(limits=pathway, by_nuclide=True)
|
||||
assert isinstance(wdr, dict)
|
||||
assert rating == pytest.approx(sum(wdr.values()), rel=1e-6)
|
||||
|
||||
|
||||
def _make_test_chain():
|
||||
"""Build a minimal chain with Cs137 -> Ba137_m1 and Sr90 -> Y90."""
|
||||
chain = Chain()
|
||||
|
||||
cs137 = Nuclide("Cs137")
|
||||
cs137.half_life = 9.49e8 # ~30 years in seconds
|
||||
cs137.add_decay_mode("beta-", "Ba137_m1", 0.947)
|
||||
cs137.add_decay_mode("beta-", "Ba137", 0.053)
|
||||
|
||||
ba137_m1 = Nuclide("Ba137_m1")
|
||||
ba137_m1.half_life = 153.12 # seconds
|
||||
ba137_m1.add_decay_mode("IT", "Ba137", 1.0)
|
||||
|
||||
ba137 = Nuclide("Ba137")
|
||||
ba137.half_life = None # stable
|
||||
|
||||
sr90 = Nuclide("Sr90")
|
||||
sr90.half_life = 9.09e8 # ~28.8 years in seconds
|
||||
sr90.add_decay_mode("beta-", "Y90", 1.0)
|
||||
|
||||
y90 = Nuclide("Y90")
|
||||
y90.half_life = 2.304e5 # ~2.67 days in seconds
|
||||
y90.add_decay_mode("beta-", "Zr90", 1.0)
|
||||
|
||||
zr90 = Nuclide("Zr90")
|
||||
zr90.half_life = None # stable
|
||||
|
||||
co60 = Nuclide("Co60")
|
||||
co60.half_life = 1.66e8 # ~5.27 years
|
||||
co60.add_decay_mode("beta-", "Ni60", 1.0)
|
||||
|
||||
ni60 = Nuclide("Ni60")
|
||||
ni60.half_life = None # stable
|
||||
|
||||
chain.nuclides = [cs137, ba137_m1, ba137, sr90, y90, zr90, co60, ni60]
|
||||
chain.nuclide_dict = {n.name: i for i, n in enumerate(chain.nuclides)}
|
||||
return chain
|
||||
|
||||
|
||||
def test_strlschv_chain_excludes_daughters():
|
||||
"""Test that providing a chain excludes '+' daughters from the rating."""
|
||||
chain = _make_test_chain()
|
||||
|
||||
# Sr-90 is a '+' parent. Y-90 is its daughter in secular equilibrium.
|
||||
# Both have entries in the unrestricted clearance table (Sr90 at 1 Bq/g,
|
||||
# Y90 at 1000 Bq/g). Without a chain, both contribute to the sum.
|
||||
# With a chain, Y-90 should be excluded.
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Sr90', 1e-12)
|
||||
mat.add_nuclide('Y90', 1e-12)
|
||||
|
||||
rating_no_chain = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted'
|
||||
)
|
||||
rating_with_chain = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted', chain=chain
|
||||
)
|
||||
|
||||
# With chain, Y90 should be excluded so rating should be lower
|
||||
assert rating_with_chain < rating_no_chain
|
||||
|
||||
# by_nuclide with chain should not contain Y90
|
||||
wdr = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted', by_nuclide=True, chain=chain
|
||||
)
|
||||
assert 'Sr90' in wdr
|
||||
assert 'Y90' not in wdr
|
||||
|
||||
|
||||
def test_strlschv_chain_excludes_sr90_daughter():
|
||||
"""Test that Y90 is excluded as a daughter of Sr90+."""
|
||||
chain = _make_test_chain()
|
||||
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Sr90', 1e-12)
|
||||
mat.add_nuclide('Y90', 1e-12)
|
||||
|
||||
wdr_no_chain = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted', by_nuclide=True
|
||||
)
|
||||
wdr_with_chain = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted', by_nuclide=True, chain=chain
|
||||
)
|
||||
|
||||
# Without chain, both Sr90 and Y90 contribute
|
||||
assert 'Sr90' in wdr_no_chain
|
||||
assert 'Y90' in wdr_no_chain
|
||||
|
||||
# With chain, Y90 is excluded as daughter of Sr90+
|
||||
assert 'Sr90' in wdr_with_chain
|
||||
assert 'Y90' not in wdr_with_chain
|
||||
|
||||
|
||||
def test_strlschv_chain_no_effect_on_non_plus():
|
||||
"""Test that the chain does not exclude non-'+' nuclides."""
|
||||
chain = _make_test_chain()
|
||||
|
||||
# Co-60 is NOT a '+' parent (it has a non-'+' entry in the table).
|
||||
# Its daughter Ni-60 is stable and has no clearance entry.
|
||||
# The chain should not affect the Co-60 rating.
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Co60', 1e-12)
|
||||
|
||||
rating_no_chain = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted'
|
||||
)
|
||||
rating_with_chain = mat.waste_disposal_rating(
|
||||
limits='StrlSchV_unrestricted', chain=chain
|
||||
)
|
||||
|
||||
assert rating_no_chain == pytest.approx(rating_with_chain, rel=1e-6)
|
||||
|
||||
|
||||
def test_strlschv_chain_no_effect_on_nrc():
|
||||
"""Test that the chain parameter has no effect on NRC/Fetter limits."""
|
||||
chain = _make_test_chain()
|
||||
|
||||
mat = openmc.Material()
|
||||
mat.add_nuclide('Cs137', 1e-10)
|
||||
|
||||
rating_no_chain = mat.waste_disposal_rating(limits='Fetter')
|
||||
rating_with_chain = mat.waste_disposal_rating(
|
||||
limits='Fetter', chain=chain
|
||||
)
|
||||
|
||||
assert rating_no_chain == pytest.approx(rating_with_chain, rel=1e-6)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue