mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-27 13:45:36 -04:00
459 lines
15 KiB
Python
459 lines
15 KiB
Python
"""Tests for openmc.deplete.Chain class."""
|
|
|
|
from collections.abc import Mapping
|
|
import os
|
|
from pathlib import Path
|
|
from itertools import product
|
|
|
|
import numpy as np
|
|
from openmc.data import zam, ATOMIC_SYMBOL
|
|
from openmc.deplete import comm, Chain, reaction_rates, nuclide, cram
|
|
import pytest
|
|
|
|
from tests import cdtemp
|
|
|
|
_TEST_CHAIN = """\
|
|
<depletion_chain>
|
|
<nuclide name="A" half_life="23652.0" decay_modes="2" decay_energy="0.0" reactions="1">
|
|
<decay type="beta1" target="B" branching_ratio="0.6"/>
|
|
<decay type="beta2" target="C" branching_ratio="0.4"/>
|
|
<reaction type="(n,gamma)" Q="0.0" target="C"/>
|
|
</nuclide>
|
|
<nuclide name="B" half_life="32904.0" decay_modes="1" decay_energy="0.0" reactions="1">
|
|
<decay type="beta" target="A" branching_ratio="1.0"/>
|
|
<reaction type="(n,gamma)" Q="0.0" target="C"/>
|
|
</nuclide>
|
|
<nuclide name="C" reactions="3">
|
|
<reaction type="fission" Q="200000000.0"/>
|
|
<reaction type="(n,gamma)" Q="0.0" target="A" branching_ratio="0.7"/>
|
|
<reaction type="(n,gamma)" Q="0.0" target="B" branching_ratio="0.3"/>
|
|
<neutron_fission_yields>
|
|
<energies>0.0253</energies>
|
|
<fission_yields energy="0.0253">
|
|
<products>A B</products>
|
|
<data>0.0292737 0.002566345</data>
|
|
</fission_yields>
|
|
</neutron_fission_yields>
|
|
</nuclide>
|
|
</depletion_chain>
|
|
"""
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
def simple_chain():
|
|
with cdtemp():
|
|
with open('chain_test.xml', 'w') as fh:
|
|
fh.write(_TEST_CHAIN)
|
|
yield Chain.from_xml('chain_test.xml')
|
|
|
|
|
|
def test_init():
|
|
"""Test depletion chain initialization."""
|
|
chain = Chain()
|
|
|
|
assert isinstance(chain.nuclides, list)
|
|
assert isinstance(chain.nuclide_dict, Mapping)
|
|
|
|
|
|
def test_len():
|
|
"""Test depletion chain length."""
|
|
chain = Chain()
|
|
chain.nuclides = ["NucA", "NucB", "NucC"]
|
|
|
|
assert len(chain) == 3
|
|
|
|
|
|
def test_from_endf():
|
|
"""Test depletion chain building from ENDF files"""
|
|
endf_data = Path(os.environ['OPENMC_ENDF_DATA'])
|
|
decay_data = (endf_data / 'decay').glob('*.endf')
|
|
fpy_data = (endf_data / 'nfy').glob('*.endf')
|
|
neutron_data = (endf_data / 'neutrons').glob('*.endf')
|
|
chain = Chain.from_endf(decay_data, fpy_data, neutron_data)
|
|
|
|
assert len(chain) == len(chain.nuclides) == len(chain.nuclide_dict) == 3820
|
|
for nuc in chain.nuclides:
|
|
assert nuc == chain[nuc.name]
|
|
|
|
|
|
def test_from_xml(simple_chain):
|
|
"""Read chain_test.xml and ensure all values are correct."""
|
|
# Unfortunately, this routine touches a lot of the code, but most of
|
|
# the components external to depletion_chain.py are simple storage
|
|
# types.
|
|
|
|
chain = simple_chain
|
|
|
|
# Basic checks
|
|
assert len(chain) == 3
|
|
|
|
# A tests
|
|
nuc = chain["A"]
|
|
|
|
assert nuc.name == "A"
|
|
assert nuc.half_life == 2.36520E+04
|
|
assert nuc.n_decay_modes == 2
|
|
modes = nuc.decay_modes
|
|
assert [m.target for m in modes] == ["B", "C"]
|
|
assert [m.type for m in modes] == ["beta1", "beta2"]
|
|
assert [m.branching_ratio for m in modes] == [0.6, 0.4]
|
|
assert nuc.n_reaction_paths == 1
|
|
assert [r.target for r in nuc.reactions] == ["C"]
|
|
assert [r.type for r in nuc.reactions] == ["(n,gamma)"]
|
|
assert [r.branching_ratio for r in nuc.reactions] == [1.0]
|
|
|
|
# B tests
|
|
nuc = chain["B"]
|
|
|
|
assert nuc.name == "B"
|
|
assert nuc.half_life == 3.29040E+04
|
|
assert nuc.n_decay_modes == 1
|
|
modes = nuc.decay_modes
|
|
assert [m.target for m in modes] == ["A"]
|
|
assert [m.type for m in modes] == ["beta"]
|
|
assert [m.branching_ratio for m in modes] == [1.0]
|
|
assert nuc.n_reaction_paths == 1
|
|
assert [r.target for r in nuc.reactions] == ["C"]
|
|
assert [r.type for r in nuc.reactions] == ["(n,gamma)"]
|
|
assert [r.branching_ratio for r in nuc.reactions] == [1.0]
|
|
|
|
# C tests
|
|
nuc = chain["C"]
|
|
|
|
assert nuc.name == "C"
|
|
assert nuc.n_decay_modes == 0
|
|
assert nuc.n_reaction_paths == 3
|
|
assert [r.target for r in nuc.reactions] == [None, "A", "B"]
|
|
assert [r.type for r in nuc.reactions] == ["fission", "(n,gamma)", "(n,gamma)"]
|
|
assert [r.branching_ratio for r in nuc.reactions] == [1.0, 0.7, 0.3]
|
|
|
|
# Yield tests
|
|
assert nuc.yield_energies == (0.0253,)
|
|
assert list(nuc.yield_data) == [0.0253]
|
|
assert nuc.yield_data[0.0253].products == ("A", "B")
|
|
assert (nuc.yield_data[0.0253].yields == [0.0292737, 0.002566345]).all()
|
|
|
|
|
|
def test_export_to_xml(run_in_tmpdir):
|
|
"""Test writing a depletion chain to XML."""
|
|
|
|
# Prevent different MPI ranks from conflicting
|
|
filename = 'test{}.xml'.format(comm.rank)
|
|
|
|
A = nuclide.Nuclide("A")
|
|
A.half_life = 2.36520e4
|
|
A.decay_modes = [
|
|
nuclide.DecayTuple("beta1", "B", 0.6),
|
|
nuclide.DecayTuple("beta2", "C", 0.4)
|
|
]
|
|
A.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)]
|
|
|
|
B = nuclide.Nuclide("B")
|
|
B.half_life = 3.29040e4
|
|
B.decay_modes = [nuclide.DecayTuple("beta", "A", 1.0)]
|
|
B.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)]
|
|
|
|
C = nuclide.Nuclide("C")
|
|
C.reactions = [
|
|
nuclide.ReactionTuple("fission", None, 2.0e8, 1.0),
|
|
nuclide.ReactionTuple("(n,gamma)", "A", 0.0, 0.7),
|
|
nuclide.ReactionTuple("(n,gamma)", "B", 0.0, 0.3)
|
|
]
|
|
C.yield_data = nuclide.FissionYieldDistribution({
|
|
0.0253: {"A": 0.0292737, "B": 0.002566345}})
|
|
|
|
chain = Chain()
|
|
chain.nuclides = [A, B, C]
|
|
chain.export_to_xml(filename)
|
|
|
|
chain_xml = open(filename, 'r').read()
|
|
assert _TEST_CHAIN == chain_xml
|
|
|
|
|
|
def test_form_matrix(simple_chain):
|
|
""" Using chain_test, and a dummy reaction rate, compute the matrix. """
|
|
# Relies on test_from_xml passing.
|
|
|
|
chain = simple_chain
|
|
|
|
mats = ["10000", "10001"]
|
|
nuclides = ["A", "B", "C"]
|
|
|
|
react = reaction_rates.ReactionRates(mats, nuclides, chain.reactions)
|
|
|
|
react.set("10000", "C", "fission", 1.0)
|
|
react.set("10000", "A", "(n,gamma)", 2.0)
|
|
react.set("10000", "B", "(n,gamma)", 3.0)
|
|
react.set("10000", "C", "(n,gamma)", 4.0)
|
|
|
|
mat = chain.form_matrix(react[0, :, :])
|
|
# Loss A, decay, (n, gamma)
|
|
mat00 = -np.log(2) / 2.36520E+04 - 2
|
|
# A -> B, decay, 0.6 branching ratio
|
|
mat10 = np.log(2) / 2.36520E+04 * 0.6
|
|
# A -> C, decay, 0.4 branching ratio + (n,gamma)
|
|
mat20 = np.log(2) / 2.36520E+04 * 0.4 + 2
|
|
|
|
# B -> A, decay, 1.0 branching ratio
|
|
mat01 = np.log(2)/3.29040E+04
|
|
# Loss B, decay, (n, gamma)
|
|
mat11 = -np.log(2)/3.29040E+04 - 3
|
|
# B -> C, (n, gamma)
|
|
mat21 = 3
|
|
|
|
# C -> A fission, (n, gamma)
|
|
mat02 = 0.0292737 * 1.0 + 4.0 * 0.7
|
|
# C -> B fission, (n, gamma)
|
|
mat12 = 0.002566345 * 1.0 + 4.0 * 0.3
|
|
# Loss C, fission, (n, gamma)
|
|
mat22 = -1.0 - 4.0
|
|
|
|
assert mat[0, 0] == mat00
|
|
assert mat[1, 0] == mat10
|
|
assert mat[2, 0] == mat20
|
|
assert mat[0, 1] == mat01
|
|
assert mat[1, 1] == mat11
|
|
assert mat[2, 1] == mat21
|
|
assert mat[0, 2] == mat02
|
|
assert mat[1, 2] == mat12
|
|
assert mat[2, 2] == mat22
|
|
|
|
# Pass equivalent fission yields directly
|
|
# Ensure identical matrix is formed
|
|
f_yields = {"C": {"A": 0.0292737, "B": 0.002566345}}
|
|
new_mat = chain.form_matrix(react[0], f_yields)
|
|
for r, c in product(range(3), range(3)):
|
|
assert new_mat[r, c] == mat[r, c]
|
|
|
|
|
|
def test_getitem():
|
|
""" Test nuc_by_ind converter function. """
|
|
chain = Chain()
|
|
chain.nuclides = ["NucA", "NucB", "NucC"]
|
|
chain.nuclide_dict = {nuc: chain.nuclides.index(nuc)
|
|
for nuc in chain.nuclides}
|
|
|
|
assert "NucA" == chain["NucA"]
|
|
assert "NucB" == chain["NucB"]
|
|
assert "NucC" == chain["NucC"]
|
|
|
|
|
|
def test_set_fiss_q():
|
|
"""Make sure new fission q values can be set on the chain"""
|
|
new_q = {"U235": 2.0E8, "U238": 2.0E8, "U234": 5.0E7}
|
|
chain_file = Path(__file__).parents[1] / "chain_simple.xml"
|
|
mod_chain = Chain.from_xml(chain_file, new_q)
|
|
for name, q in new_q.items():
|
|
chain_nuc = mod_chain[name]
|
|
for rx in chain_nuc.reactions:
|
|
if rx.type == 'fission':
|
|
assert rx.Q == q
|
|
|
|
|
|
def test_get_set_chain_br(simple_chain):
|
|
"""Test minor modifications to capture branch ratios"""
|
|
expected = {"C": {"A": 0.7, "B": 0.3}}
|
|
assert simple_chain.get_branch_ratios() == expected
|
|
|
|
# safely modify
|
|
new_chain = Chain.from_xml("chain_test.xml")
|
|
new_br = {"C": {"A": 0.5, "B": 0.5}, "A": {"C": 0.99, "B": 0.01}}
|
|
new_chain.set_branch_ratios(new_br)
|
|
assert new_chain.get_branch_ratios() == new_br
|
|
|
|
# write, re-read
|
|
new_chain.export_to_xml("chain_mod.xml")
|
|
assert Chain.from_xml("chain_mod.xml").get_branch_ratios() == new_br
|
|
|
|
# Test non-strict [warn, not error] setting
|
|
bad_br = {"B": {"X": 0.6, "A": 0.4}, "X": {"A": 0.5, "C": 0.5}}
|
|
bad_br.update(new_br)
|
|
new_chain.set_branch_ratios(bad_br, strict=False)
|
|
assert new_chain.get_branch_ratios() == new_br
|
|
|
|
# Ensure capture reactions are removed
|
|
rem_br = {"A": {"C": 1.0}}
|
|
new_chain.set_branch_ratios(rem_br)
|
|
# A is not in returned dict because there is no branch
|
|
assert "A" not in new_chain.get_branch_ratios()
|
|
|
|
|
|
def test_capture_branch_infer_ground():
|
|
"""Ensure the ground state is infered if not given"""
|
|
# Make up a metastable capture transition:
|
|
infer_br = {"Xe135": {"Xe136_m1": 0.5}}
|
|
set_br = {"Xe135": {"Xe136": 0.5, "Xe136_m1": 0.5}}
|
|
|
|
chain_file = Path(__file__).parents[1] / "chain_simple.xml"
|
|
chain = Chain.from_xml(chain_file)
|
|
|
|
# Create nuclide to be added into the chain
|
|
xe136m = nuclide.Nuclide("Xe136_m1")
|
|
|
|
chain.nuclides.append(xe136m)
|
|
chain.nuclide_dict[xe136m.name] = len(chain.nuclides) - 1
|
|
|
|
chain.set_branch_ratios(infer_br, "(n,gamma)")
|
|
|
|
assert chain.get_branch_ratios("(n,gamma)") == set_br
|
|
|
|
|
|
def test_capture_branch_no_rxn():
|
|
"""Ensure capture reactions that don't exist aren't created"""
|
|
u4br = {"U234": {"U235": 0.5, "U235_m1": 0.5}}
|
|
|
|
chain_file = Path(__file__).parents[1] / "chain_simple.xml"
|
|
chain = Chain.from_xml(chain_file)
|
|
|
|
u5m = nuclide.Nuclide("U235_m1")
|
|
|
|
chain.nuclides.append(u5m)
|
|
chain.nuclide_dict[u5m.name] = len(chain.nuclides) - 1
|
|
|
|
with pytest.raises(AttributeError, match="U234"):
|
|
chain.set_branch_ratios(u4br)
|
|
|
|
|
|
def test_capture_branch_failures(simple_chain):
|
|
"""Test failure modes for setting capture branch ratios"""
|
|
|
|
# Parent isotope not present
|
|
br = {"X": {"A": 0.6, "B": 0.7}}
|
|
with pytest.raises(KeyError, match="X"):
|
|
simple_chain.set_branch_ratios(br)
|
|
|
|
# Product isotope not present
|
|
br = {"C": {"X": 0.4, "A": 0.2, "B": 0.4}}
|
|
with pytest.raises(KeyError, match="X"):
|
|
simple_chain.set_branch_ratios(br)
|
|
|
|
# Sum of ratios > 1.0
|
|
br = {"C": {"A": 1.0, "B": 1.0}}
|
|
with pytest.raises(ValueError, match=r"Sum of \(n,gamma\).*for C"):
|
|
simple_chain.set_branch_ratios(br, "(n,gamma)")
|
|
|
|
|
|
def test_set_alpha_branches():
|
|
"""Test setting of alpha reaction branching ratios"""
|
|
# Build a mock chain
|
|
chain = Chain()
|
|
|
|
parent = nuclide.Nuclide()
|
|
parent.name = "A"
|
|
|
|
he4 = nuclide.Nuclide()
|
|
he4.name = "He4"
|
|
|
|
ground_tgt = nuclide.Nuclide()
|
|
ground_tgt.name = "B"
|
|
|
|
meta_tgt = nuclide.Nuclide()
|
|
meta_tgt.name = "B_m1"
|
|
|
|
for ix, nuc in enumerate((parent, ground_tgt, meta_tgt, he4)):
|
|
chain.nuclides.append(nuc)
|
|
chain.nuclide_dict[nuc.name] = ix
|
|
|
|
# add reactions to parent
|
|
parent.reactions.append(nuclide.ReactionTuple(
|
|
"(n,a)", ground_tgt.name, 1.0, 0.6))
|
|
parent.reactions.append(nuclide.ReactionTuple(
|
|
"(n,a)", meta_tgt.name, 1.0, 0.4))
|
|
parent.reactions.append(nuclide.ReactionTuple(
|
|
"(n,a)", he4.name, 1.0, 1.0))
|
|
|
|
expected_ref = {"A": {"B": 0.6, "B_m1": 0.4}}
|
|
|
|
assert chain.get_branch_ratios("(n,a)") == expected_ref
|
|
|
|
# alter and check again
|
|
|
|
altered = {"A": {"B": 0.5, "B_m1": 0.5}}
|
|
|
|
chain.set_branch_ratios(altered, "(n,a)")
|
|
assert chain.get_branch_ratios("(n,a)") == altered
|
|
|
|
# make sure that alpha particle still produced
|
|
for r in parent.reactions:
|
|
if r.target == he4.name:
|
|
break
|
|
else:
|
|
raise ValueError("Helium has been removed and should not have been")
|
|
|
|
|
|
def test_simple_fission_yields(simple_chain):
|
|
"""Check the default fission yields that can be used to form the matrix
|
|
"""
|
|
fission_yields = simple_chain.get_default_fission_yields()
|
|
assert fission_yields == {"C": {"A": 0.0292737, "B": 0.002566345}}
|
|
|
|
|
|
def test_fission_yield_attribute(simple_chain):
|
|
"""Test the fission_yields property"""
|
|
thermal_yields = simple_chain.get_default_fission_yields()
|
|
# generate default with property
|
|
assert simple_chain.fission_yields[0] == thermal_yields
|
|
empty_chain = Chain()
|
|
empty_chain.fission_yields = thermal_yields
|
|
assert empty_chain.fission_yields[0] == thermal_yields
|
|
empty_chain.fission_yields = [thermal_yields] * 2
|
|
assert empty_chain.fission_yields[0] == thermal_yields
|
|
assert empty_chain.fission_yields[1] == thermal_yields
|
|
|
|
# test failure with deplete function
|
|
# number fission yields != number of materials
|
|
dummy_conc = [[1, 2]] * (len(empty_chain.fission_yields) + 1)
|
|
with pytest.raises(
|
|
ValueError, match="fission yield.*not equal.*compositions"):
|
|
cram.deplete(empty_chain, dummy_conc, None, 0.5)
|
|
|
|
def test_validate(simple_chain):
|
|
"""Test the validate method"""
|
|
|
|
# current chain is invalid
|
|
# fission yields do not sum to 2.0
|
|
with pytest.raises(ValueError, match="Nuclide C.*fission yields"):
|
|
simple_chain.validate(strict=True, tolerance=0.0)
|
|
|
|
with pytest.warns(UserWarning) as record:
|
|
assert not simple_chain.validate(strict=False, quiet=False, tolerance=0.0)
|
|
assert not simple_chain.validate(strict=False, quiet=True, tolerance=0.0)
|
|
assert len(record) == 1
|
|
assert "Nuclide C" in record[0].message.args[0]
|
|
|
|
# Fix fission yields but keep to restore later
|
|
old_yields = simple_chain["C"].yield_data
|
|
simple_chain["C"].yield_data = {0.0253: {"A": 1.4, "B": 0.6}}
|
|
|
|
assert simple_chain.validate(strict=True, tolerance=0.0)
|
|
with pytest.warns(None) as record:
|
|
assert simple_chain.validate(strict=False, quiet=False, tolerance=0.0)
|
|
assert len(record) == 0
|
|
|
|
# Mess up "earlier" nuclide's reactions
|
|
decay_mode = simple_chain["A"].decay_modes.pop()
|
|
|
|
with pytest.raises(ValueError, match="Nuclide A.*decay mode"):
|
|
simple_chain.validate(strict=True, tolerance=0.0)
|
|
|
|
# restore old fission yields
|
|
simple_chain["C"].yield_data = old_yields
|
|
|
|
with pytest.warns(UserWarning) as record:
|
|
assert not simple_chain.validate(strict=False, quiet=False, tolerance=0.0)
|
|
assert len(record) == 2
|
|
assert "Nuclide A" in record[0].message.args[0]
|
|
assert "Nuclide C" in record[1].message.args[0]
|
|
|
|
# restore decay modes
|
|
simple_chain["A"].decay_modes.append(decay_mode)
|
|
|
|
|
|
def test_validate_inputs():
|
|
c = Chain()
|
|
|
|
with pytest.raises(TypeError, match="tolerance"):
|
|
c.validate(tolerance=None)
|
|
|
|
with pytest.raises(ValueError, match="tolerance"):
|
|
c.validate(tolerance=-1)
|