Merge branch 'develop' into pullrequestinc-part2

This commit is contained in:
Paul Romano 2020-03-25 14:48:28 -05:00
commit e7f4413415
11 changed files with 594 additions and 42 deletions

View file

@ -190,7 +190,7 @@ class TransportOperator(ABC):
Returns
-------
volume : list of float
volume : dict of str to float
Volumes corresponding to materials in burn_list
nuc_list : list of str
A list of all nuclide names. Used for sorting the simulation.

View file

@ -10,7 +10,7 @@ import math
import re
from collections import OrderedDict, defaultdict
from collections.abc import Mapping, Iterable
from numbers import Real
from numbers import Real, Integral
from warnings import warn
from openmc.checkvalue import check_type, check_greater_than
@ -500,7 +500,7 @@ class Chain:
# Gain
for _, target, branching_ratio in nuc.decay_modes:
# Allow for total annihilation for debug purposes
if target != 'Nothing':
if target is not None:
branch_val = branching_ratio * decay_constant
if branch_val != 0.0:
@ -525,17 +525,16 @@ class Chain:
matrix[i, i] -= path_rate
# Gain term; allow for total annihilation for debug purposes
if target != 'Nothing':
if r_type != 'fission':
if path_rate != 0.0:
k = self.nuclide_dict[target]
matrix[k, i] += path_rate * br
else:
for product, y in fission_yields[nuc.name].items():
yield_val = y * path_rate
if yield_val != 0.0:
k = self.nuclide_dict[product]
matrix[k, i] += yield_val
if r_type != 'fission':
if target is not None and path_rate != 0.0:
k = self.nuclide_dict[target]
matrix[k, i] += path_rate * br
else:
for product, y in fission_yields[nuc.name].items():
yield_val = y * path_rate
if yield_val != 0.0:
k = self.nuclide_dict[product]
matrix[k, i] += yield_val
# Clear set of reactions
reactions.clear()
@ -821,3 +820,168 @@ class Chain:
return stat
valid = valid and stat
return valid
def reduce(self, initial_isotopes, level=None):
"""Reduce the size of the chain by following transmutation paths
As an example, consider a simple chain with the following
isotopes and transmutation paths::
U235 (n,gamma) U236
(n,fission) (Xe135, I135, Cs135)
I135 (beta decay) Xe135 (beta decay) Cs135
Xe135 (n,gamma) Xe136
Calling ``chain.reduce(["I135"])`` will produce a depletion
chain that contains only isotopes that would originate from
I135: I135, Xe135, Cs135, and Xe136. U235 and U236 will not
be included, but multiple isotopes can be used to start
the search.
The ``level`` value controls the depth of the search.
``chain.reduce(["U235"], level=1)`` would return a chain
with all isotopes except Xe136, since it is two transmutations
removed from U235 in this case.
While targets will not be included in the new chain, the
total destruction rate and decay rate of included isotopes
will be preserved.
Parameters
----------
initial_isotopes : iterable of str
Start the search based on the contents of these isotopes
level : int, optional
Depth of transmuation path to follow. Must be greater than
or equal to zero. A value of zero returns a chain with
``initial_isotopes``. The default value of None implies
that all isotopes that appear in the transmutation paths
of the initial isotopes and their progeny should be
explored
Returns
-------
Chain
Depletion chain containing isotopes that would appear
after following up to ``level`` reactions and decay paths
"""
check_type("initial_isotopes", initial_isotopes, Iterable, str)
if level is None:
level = math.inf
else:
check_type("level", level, Integral)
check_greater_than("level", level, 0, equality=True)
all_isotopes = self._follow(set(initial_isotopes), level)
# Avoid re-sorting for fission yields
name_sort = sorted(all_isotopes)
nuclides = []
nuclide_dict = {}
reactions = set()
for idx, iso in enumerate(sorted(all_isotopes, key=openmc.data.zam)):
previous = self[iso]
new_nuclide = Nuclide(previous.name)
new_nuclide.half_life = previous.half_life
new_nuclide.decay_energy = new_nuclide.decay_energy
new_decay = []
for mode in previous.decay_modes:
if mode.target in all_isotopes:
new_decay.append(mode)
else:
new_decay.append(DecayTuple(
mode.type, None, mode.branching_ratio))
new_nuclide.decay_modes = new_decay
new_reactions = []
for rxn in previous.reactions:
if rxn.target in all_isotopes:
new_reactions.append(rxn)
reactions.add(rxn.type)
elif rxn.type == "fission":
new_yields = new_nuclide.yield_data = (
previous.yield_data.restrict_products(name_sort))
if new_yields is not None:
new_reactions.append(rxn)
reactions.add("fission")
# Maintain total destruction rates but set no target
else:
new_reactions.append(ReactionTuple(
rxn.type, None, rxn.Q, rxn.branching_ratio))
reactions.add(rxn.type)
new_nuclide.reactions = new_reactions
nuclides.append(new_nuclide)
nuclide_dict[iso] = idx
new_chain = type(self)()
new_chain.nuclides = nuclides
new_chain.nuclide_dict = nuclide_dict
# Doesn't appear that the ordering matters for the reactions,
# just the contents
new_chain.reactions = sorted(reactions)
return new_chain
def _follow(self, isotopes, level):
"""Return all isotopes present up to depth level"""
found = isotopes.copy()
remaining = set(self.nuclide_dict)
if not found.issubset(remaining):
raise IndexError(
"The following isotopes were not found in the chain: "
"{}".format(", ".join(found - remaining)))
if level == 0:
return found
remaining -= found
depth = 0
next_iso = set()
while depth < level and remaining:
# Exhaust all isotopes at this level
while isotopes:
iso = isotopes.pop()
found.add(iso)
nuclide = self[iso]
# Follow all transmutation paths for this nuclide
for rxn in nuclide.reactions + nuclide.decay_modes:
if rxn.type == "fission" or rxn.target is None:
continue
# Skip if we've already come across this isotope
elif (rxn.target in next_iso
or rxn.target in found or rxn.target in isotopes):
continue
next_iso.add(rxn.target)
if nuclide.yield_data is not None:
for product in nuclide.yield_data.products:
if (product in next_iso
or product in found or product in isotopes):
continue
next_iso.add(product)
if not next_iso:
# No additional isotopes to process, nor to update the
# current set of discovered isotopes
return found
# Prepare for next dig
depth += 1
isotopes |= next_iso
remaining -= next_iso
next_iso.clear()
# Process isotope that would have started next depth
found.update(isotopes)
return found

View file

@ -13,7 +13,7 @@ try:
except ImportError:
import xml.etree.ElementTree as ET
from numpy import empty
from numpy import empty, searchsorted
from openmc.checkvalue import check_type
@ -30,8 +30,10 @@ Parameters
----------
type : str
Type of the decay mode, e.g., 'beta-'
target : str
Nuclide resulting from decay
target : str or None
Nuclide resulting from decay. A value of ``None`` implies the
target does not exist in the currently configured depletion
chain
branching_ratio : float
Branching ratio of the decay mode
@ -53,8 +55,11 @@ Parameters
----------
type : str
Type of the reaction, e.g., 'fission'
target : str
nuclide resulting from reaction
target : str or None
Nuclide resulting from reaction. A value of ``None``
implies either no single target, e.g. from fission,
or that the target nuclide is not considered
in the current depletion chain
Q : float
Q value of the reaction in [eV]
branching_ratio : float
@ -179,6 +184,8 @@ class Nuclide:
for decay_elem in element.iter('decay'):
d_type = decay_elem.get('type')
target = decay_elem.get('target')
if target is not None and target.lower() == "nothing":
target = None
branching_ratio = float(decay_elem.get('branching_ratio'))
nuc.decay_modes.append(DecayTuple(d_type, target, branching_ratio))
@ -192,6 +199,8 @@ class Nuclide:
# just set null values
if r_type != 'fission':
target = reaction_elem.get('target')
if target is not None and target.lower() == "nothing":
target = None
else:
target = None
if fission_q is not None:
@ -226,7 +235,7 @@ class Nuclide:
for mode, daughter, br in self.decay_modes:
mode_elem = ET.SubElement(elem, 'decay')
mode_elem.set('type', mode)
mode_elem.set('target', daughter)
mode_elem.set('target', daughter or "Nothing")
mode_elem.set('branching_ratio', str(br))
elem.set('reactions', str(len(self.reactions)))
@ -234,7 +243,7 @@ class Nuclide:
rx_elem = ET.SubElement(elem, 'reaction')
rx_elem.set('type', rx)
rx_elem.set('Q', str(Q))
if rx != 'fission':
if rx != 'fission' or daughter is not None:
rx_elem.set('target', daughter)
if br != 1.0:
rx_elem.set('branching_ratio', str(br))
@ -397,9 +406,7 @@ class FissionYieldDistribution(Mapping):
for g_index, energy in enumerate(energies):
prod_map = fission_yields[energy]
for prod_ix, product in enumerate(ordered_prod):
yield_val = prod_map.get(product)
yield_matrix[g_index, prod_ix] = (
0.0 if yield_val is None else yield_val)
yield_matrix[g_index, prod_ix] = prod_map.get(product, 0.0)
self.energies = tuple(energies)
self.products = tuple(ordered_prod)
self.yield_matrix = yield_matrix
@ -435,7 +442,7 @@ class FissionYieldDistribution(Mapping):
FissionYieldDistribution
"""
all_yields = {}
for elem_index, yield_elem in enumerate(element.iter("fission_yields")):
for yield_elem in element.iter("fission_yields"):
energy = float(yield_elem.get("energy"))
products = yield_elem.find("products").text.split()
yields = map(float, yield_elem.find("data").text.split())
@ -460,6 +467,37 @@ class FissionYieldDistribution(Mapping):
data_elem = ET.SubElement(yield_element, "data")
data_elem.text = " ".join(map(str, yield_obj.yields))
def restrict_products(self, possible_products):
"""Return a new distribution with select products
Parameters
----------
possible_products : iterable of str
Candidate pool of fission products. Existing products
not contained here will not exist in the new instance
Returns
-------
FissionYieldDistribution or None
A value of None indicates no values in
``possible_products`` exist in :attr:`products`
"""
overlap = set(self.products).intersection(possible_products)
if not overlap:
return None
products = sorted(overlap)
indices = searchsorted(self.products, products)
# coerce back to dictionary to pass back to __init__
new_yields = {}
for ene, yields in zip(self.energies, self.yield_matrix.copy()):
new_yields[ene] = dict(zip(products, yields[indices]))
return type(self)(new_yields)
class FissionYield(Mapping):
"""Mapping for fission yields of a parent at a specific energy

View file

@ -10,13 +10,10 @@ densities is all done in-memory instead of through the filesystem.
import sys
import copy
from collections import OrderedDict
from itertools import chain
import os
import time
import xml.etree.ElementTree as ET
from warnings import warn
import h5py
import numpy as np
from uncertainties import ufloat
@ -113,6 +110,13 @@ class Operator(TransportOperator):
``fission_yield_mode``. Will be passed directly on to the
helper. Passing a value of None will use the defaults for
the associated helper.
reduce_chain : bool, optional
If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the
depletion chain up to ``reduce_chain_level``. Default is False.
reduce_chain_level : int, optional
Depth of the search when reducing the depletion chain. Only used
if ``reduce_chain`` evaluates to true. The default value of
``None`` implies no limit on the depth.
Attributes
----------
@ -158,7 +162,8 @@ class Operator(TransportOperator):
def __init__(self, geometry, settings, chain_file=None, prev_results=None,
diff_burnable_mats=False, energy_mode="fission-q",
fission_q=None, dilute_initial=1.0e3,
fission_yield_mode="constant", fission_yield_opts=None):
fission_yield_mode="constant", fission_yield_opts=None,
reduce_chain=False, reduce_chain_level=None):
if fission_yield_mode not in self._fission_helpers:
raise KeyError(
"fission_yield_mode must be one of {}, not {}".format(
@ -179,6 +184,16 @@ class Operator(TransportOperator):
self.geometry = geometry
self.diff_burnable_mats = diff_burnable_mats
# Reduce the chain before we create more materials
if reduce_chain:
all_isotopes = set()
for material in geometry.get_all_materials().values():
if not material.depletable:
continue
for name, _dens_percent, _dens_type in material.nuclides:
all_isotopes.add(name)
self.chain = self.chain.reduce(all_isotopes, reduce_chain_level)
# Differentiate burnable materials with multiple instances
if self.diff_burnable_mats:
self._differentiate_burnable_mats()

View file

@ -36,7 +36,7 @@ class Results:
Number of nuclides.
rates : list of ReactionRates
The reaction rates for each substep.
volume : OrderedDict of int to float
volume : OrderedDict of str to float
Dictionary mapping mat id to volume.
mat_to_ind : OrderedDict of str to int
A dictionary mapping mat ID as string to index.

View file

@ -2,7 +2,7 @@ import h5py
import numpy as np
from .results import Results, _VERSION_RESULTS
from openmc.checkvalue import check_filetype_version
from openmc.checkvalue import check_filetype_version, check_value
__all__ = ["ResultsList"]
@ -40,7 +40,7 @@ class ResultsList(list):
new.append(Results.from_hdf5(fh, i))
return new
def get_atoms(self, mat, nuc):
def get_atoms(self, mat, nuc, nuc_units="atoms", time_units="s"):
"""Get number of nuclides over time from a single material
.. note::
@ -57,15 +57,24 @@ class ResultsList(list):
Material name to evaluate
nuc : str
Nuclide name to evaluate
nuc_units : {"atoms", "atom/b-cm", "atom/cm3"}, optional
Units for the returned concentration. Default is ``"atoms"``
time_units : {"s", "min", "h", "d"}, optional
Units for the returned time array. Default is ``"s"`` to
return the value in seconds.
Returns
-------
time : numpy.ndarray
Array of times in [s]
Array of times in units of ``time_units``
concentration : numpy.ndarray
Total number of atoms for specified nuclide
Concentration of specified nuclide in units of ``nuc_units``
"""
check_value("time_units", time_units, {"s", "d", "min", "h"})
check_value("nuc_units", nuc_units,
{"atoms", "atom/b-cm", "atom/cm3"})
time = np.empty_like(self, dtype=float)
concentration = np.empty_like(self, dtype=float)
@ -74,6 +83,21 @@ class ResultsList(list):
time[i] = result.time[0]
concentration[i] = result[0, mat, nuc]
# Unit conversions
if time_units == "d":
time /= (60 * 60 * 24)
elif time_units == "h":
time /= (60 * 60)
elif time_units == "min":
time /= 60
if nuc_units != "atoms":
# Divide by volume to get density
concentration /= self[0].volume[mat]
if nuc_units == "atom/b-cm":
# 1 barn = 1e-24 cm^2
concentration *= 1e-24
return time, concentration
def get_reaction_rate(self, mat, nuc, rx):

View file

@ -1,9 +1,10 @@
from collections import OrderedDict, defaultdict, namedtuple
from collections import OrderedDict, defaultdict, namedtuple, Counter
from collections.abc import Iterable
from copy import deepcopy
from numbers import Real, Integral
from pathlib import Path
import warnings
import re
from xml.etree import ElementTree as ET
import numpy as np
@ -556,6 +557,100 @@ class Material(IDManagerMixin):
enrichment_type):
self.add_nuclide(*nuclide)
def add_elements_from_formula(self, formula, percent_type='ao', enrichment=None,
enrichment_target=None, enrichment_type=None):
"""Add a elements from a chemical formula to the material.
Parameters
----------
formula : str
Formula to add, e.g., 'C2O', 'C6H12O6', or (NH4)2SO4.
Note this is case sensitive, elements must start with an uppercase
character. Multiplier numbers must be integers.
percent_type : {'ao', 'wo'}, optional
'ao' for atom percent and 'wo' for weight percent. Defaults to atom
percent.
enrichment : float, optional
Enrichment of an enrichment_target nuclide in percent (ao or wo).
If enrichment_target is not supplied then it is enrichment for U235
in weight percent. For example, input 4.95 for 4.95 weight percent
enriched U. Default is None (natural composition).
enrichment_target : str, optional
Single nuclide name to enrich from a natural composition (e.g., 'O16')
enrichment_type : {'ao', 'wo'}, optional
'ao' for enrichment as atom percent and 'wo' for weight percent.
Default is: 'ao' for two-isotope enrichment; 'wo' for U enrichment
Notes
-----
General enrichment procedure is allowed only for elements composed of
two isotopes. If `enrichment_target` is given without `enrichment`
natural composition is added to the material.
"""
cv.check_type('formula', formula, str)
if '.' in formula:
msg = 'Non-integer multiplier values are not accepted. The ' \
'input formula {} contains a "." character.'.format(formula)
raise ValueError(msg)
# Tokenizes the formula and check validity of tokens
tokens = re.findall(r"([A-Z][a-z]*)(\d*)|(\()|(\))(\d*)", formula)
for row in tokens:
for token in row:
if token.isalpha():
if token == "n" or token not in openmc.data.ATOMIC_NUMBER:
msg = 'Formula entry {} not an element symbol.' \
.format(token)
raise ValueError(msg)
elif token not in ['(', ')', ''] and not token.isdigit():
msg = 'Formula must be made from a sequence of ' \
'element symbols, integers, and backets. ' \
'{} is not an allowable entry.'.format(token)
raise ValueError(msg)
# Checks that the number of opening and closing brackets are equal
if formula.count('(') != formula.count(')'):
msg = 'Number of opening and closing brackets is not equal ' \
'in the input formula {}.'.format(formula)
raise ValueError(msg)
# Checks that every part of the original formula has been tokenized
for row in tokens:
for token in row:
formula = formula.replace(token, '', 1)
if len(formula) != 0:
msg = 'Part of formula was not successfully parsed as an ' \
'element symbol, bracket or integer. {} was not parsed.' \
.format(formula)
raise ValueError(msg)
# Works through the tokens building a stack
mat_stack = [Counter()]
for symbol, multi1, opening_bracket, closing_bracket, multi2 in tokens:
if symbol:
mat_stack[-1][symbol] += int(multi1 or 1)
if opening_bracket:
mat_stack.append(Counter())
if closing_bracket:
stack_top = mat_stack.pop()
for symbol, value in stack_top.items():
mat_stack[-1][symbol] += int(multi2 or 1) * value
# Normalizing percentages
percents = mat_stack[0].values()
norm_percents = [float(i) / sum(percents) for i in percents]
elements = mat_stack[0].keys()
# Adds each element and percent to the material
for element, percent in zip(elements, norm_percents):
if enrichment_target is not None and element == re.sub(r'\d+$', '', enrichment_target):
self.add_element(element, percent, percent_type, enrichment,
enrichment_target, enrichment_type)
else:
self.add_element(element, percent, percent_type)
def add_s_alpha_beta(self, name, fraction=1.0):
r"""Add an :math:`S(\alpha,\beta)` table to the material

View file

@ -6,7 +6,6 @@ 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
@ -405,7 +404,8 @@ def test_fission_yield_attribute(simple_chain):
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)
cram.deplete(empty_chain, dummy_conc, None, 0.5)
def test_validate(simple_chain):
"""Test the validate method"""
@ -457,3 +457,95 @@ def test_validate_inputs():
with pytest.raises(ValueError, match="tolerance"):
c.validate(tolerance=-1)
@pytest.fixture
def gnd_simple_chain():
chainfile = Path(__file__).parents[1] / "chain_simple.xml"
return Chain.from_xml(chainfile)
def test_reduce(gnd_simple_chain):
ref_U5 = gnd_simple_chain["U235"]
ref_iodine = gnd_simple_chain["I135"]
ref_U5_yields = ref_U5.yield_data
no_depth = gnd_simple_chain.reduce(["U235", "I135"], 0)
# We should get a chain just containing U235 and I135
assert len(no_depth) == 2
assert set(no_depth.reactions) == set(gnd_simple_chain.reactions)
u5_round0 = no_depth["U235"]
assert u5_round0.n_decay_modes == ref_U5.n_decay_modes
for newmode, refmode in zip(u5_round0.decay_modes, ref_U5.decay_modes):
assert newmode.target is None
assert newmode.type == refmode.type
assert newmode.branching_ratio == refmode.branching_ratio
assert u5_round0.n_reaction_paths == ref_U5.n_reaction_paths
for newrxn, refrxn in zip(u5_round0.reactions, ref_U5.reactions):
assert newrxn.target is None
assert newrxn.type == refrxn.type
assert newrxn.Q == refrxn.Q
assert newrxn.branching_ratio == refrxn.branching_ratio
assert u5_round0.yield_data is not None
assert u5_round0.yield_data.products == ("I135",)
assert u5_round0.yield_data.yield_matrix == (
ref_U5_yields.yield_matrix[:, ref_U5_yields.products.index("I135")]
)
bareI5 = no_depth["I135"]
assert bareI5.n_decay_modes == ref_iodine.n_decay_modes
for newmode, refmode in zip(bareI5.decay_modes, ref_iodine.decay_modes):
assert newmode.target is None
assert newmode.type == refmode.type
assert newmode.branching_ratio == refmode.branching_ratio
assert bareI5.n_reaction_paths == ref_iodine.n_reaction_paths
for newrxn, refrxn in zip(bareI5.reactions, ref_iodine.reactions):
assert newrxn.target is None
assert newrxn.type == refrxn.type
assert newrxn.Q == refrxn.Q
assert newrxn.branching_ratio == refrxn.branching_ratio
follow_u5 = gnd_simple_chain.reduce(["U235"], 1)
u5_round1 = follow_u5["U235"]
assert u5_round1.decay_modes == ref_U5.decay_modes
assert u5_round1.reactions == ref_U5.reactions
assert u5_round1.yield_data is not None
assert (
u5_round1.yield_data.yield_matrix == ref_U5_yields.yield_matrix
).all()
# Per the chain_simple.xml
# I135 -> Xe135 -> Cs135
# I135 -> Xe136
# No limit on depth
iodine_chain = gnd_simple_chain.reduce(["I135"])
truncated_iodine = gnd_simple_chain.reduce(["I135"], 1)
assert len(iodine_chain) == 4
assert len(truncated_iodine) == 3
assert set(iodine_chain.nuclide_dict) == {
"I135", "Xe135", "Xe136", "Cs135"}
assert set(truncated_iodine.nuclide_dict) == {"I135", "Xe135", "Xe136"}
assert iodine_chain.reactions == ["(n,gamma)"]
assert iodine_chain["I135"].decay_modes == ref_iodine.decay_modes
assert iodine_chain["I135"].reactions == ref_iodine.reactions
for mode in truncated_iodine["Xe135"].decay_modes:
assert mode.target is None
# Test that no FissionYieldDistribution is made if there are no
# fission products
u5_noyields = gnd_simple_chain.reduce(["U235"], 0)["U235"]
assert u5_noyields.yield_data is None
# Check early termination if the eventual full chain
# is specified by using the iodine isotopes
new_iodine = gnd_simple_chain.reduce(set(iodine_chain.nuclide_dict))
assert set(iodine_chain.nuclide_dict) == set(new_iodine.nuclide_dict)
# Failure if some requested isotopes not in chain
with pytest.raises(IndexError, match=".*not found.*Xx999"):
gnd_simple_chain.reduce(["U235", "Xx999"])

View file

@ -176,6 +176,21 @@ def test_fission_yield_distribution():
with pytest.raises(TypeError):
orig_yields *= similar
# Test restriction of fission products
strict_restrict = yield_dist.restrict_products(["Xe135", "Sm149"])
with_extras = yield_dist.restrict_products(
["Xe135", "Sm149", "H1", "U235"])
assert strict_restrict.products == ("Sm149", "Xe135")
assert strict_restrict.energies == yield_dist.energies
assert with_extras.products == ("Sm149", "Xe135")
assert with_extras.energies == yield_dist.energies
for ene, new_yields in strict_restrict.items():
for product in strict_restrict.products:
assert new_yields[product] == yield_dist[ene][product]
assert with_extras[ene][product] == yield_dist[ene][product]
assert yield_dist.restrict_products(["U235"]) is None
def test_validate():

View file

@ -19,12 +19,28 @@ def test_get_atoms(res):
"""Tests evaluating single nuclide concentration."""
t, n = res.get_atoms("1", "Xe135")
t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0]
n_ref = [6.67473282e+08, 3.76986925e+14, 3.68587383e+14, 3.91338675e+14]
t_ref = np.array([0.0, 1296000.0, 2592000.0, 3888000.0])
n_ref = np.array(
[6.67473282e+08, 3.76986925e+14, 3.68587383e+14, 3.91338675e+14])
np.testing.assert_allclose(t, t_ref)
np.testing.assert_allclose(n, n_ref)
# Check alternate units
volume = res[0].volume["1"]
t_days, n_cm3 = res.get_atoms("1", "Xe135", nuc_units="atom/cm3", time_units="d")
assert t_days == pytest.approx(t_ref / (60 * 60 * 24))
assert n_cm3 == pytest.approx(n_ref / volume)
t_min, n_bcm = res.get_atoms("1", "Xe135", nuc_units="atom/b-cm", time_units="min")
assert n_bcm == pytest.approx(n_cm3 * 1e-24)
assert t_min == pytest.approx(t_ref / 60)
t_hour, _n = res.get_atoms("1", "Xe135", time_units="h")
assert t_hour == pytest.approx(t_ref / (60 * 60))
def test_get_reaction_rate(res):
"""Tests evaluating reaction rate."""

View file

@ -1,8 +1,11 @@
from collections import defaultdict
import pytest
import openmc
import openmc.examples
import openmc.model
import openmc.stats
import openmc.examples
import pytest
def test_attributes(uo2):
@ -69,6 +72,97 @@ def test_elements_by_name():
assert a._nuclides == b._nuclides
assert b._nuclides == c._nuclides
def test_add_elements_by_formula():
"""Test adding elements from a formula"""
# testing the correct nuclides and elements are added to a material
m = openmc.Material()
m.add_elements_from_formula('Li4SiO4')
# checking the ratio of elements is 4:1:4 for Li:Si:O
elem = defaultdict(float)
for nuclide, adens in m.get_nuclide_atom_densities().values():
if nuclide.startswith("Li"):
elem["Li"] += adens
if nuclide.startswith("Si"):
elem["Si"] += adens
if nuclide.startswith("O"):
elem["O"] += adens
total_number_of_atoms = 9
assert elem["Li"] == pytest.approx(4./total_number_of_atoms)
assert elem["Si"] == pytest.approx(1./total_number_of_atoms)
assert elem["O"] == pytest.approx(4/total_number_of_atoms)
# testing the correct nuclides are added to the Material
ref_dens = {'Li6': 0.033728, 'Li7': 0.410715,
'Si28': 0.102477, 'Si29': 0.0052035, 'Si30': 0.0034301,
'O16': 0.443386, 'O17': 0.000168}
nuc_dens = m.get_nuclide_atom_densities()
for nuclide in ref_dens:
assert nuc_dens[nuclide][1] == pytest.approx(ref_dens[nuclide], 1e-2)
# testing the correct nuclides are added to the Material when enriched
m = openmc.Material()
m.add_elements_from_formula('Li4SiO4',
enrichment=60.,
enrichment_target='Li6')
ref_dens = {'Li6': 0.2666, 'Li7': 0.1777,
'Si28': 0.102477, 'Si29': 0.0052035, 'Si30': 0.0034301,
'O16': 0.443386, 'O17': 0.000168}
nuc_dens = m.get_nuclide_atom_densities()
for nuclide in ref_dens:
assert nuc_dens[nuclide][1] == pytest.approx(ref_dens[nuclide], 1e-2)
# testing the use of brackets
m = openmc.Material()
m.add_elements_from_formula('Mg2(NO3)2')
# checking the ratio of elements is 2:2:6 for Mg:N:O
elem = defaultdict(float)
for nuclide, adens in m.get_nuclide_atom_densities().values():
if nuclide.startswith("Mg"):
elem["Mg"] += adens
if nuclide.startswith("N"):
elem["N"] += adens
if nuclide.startswith("O"):
elem["O"] += adens
total_number_of_atoms = 10
assert elem["Mg"] == pytest.approx(2./total_number_of_atoms)
assert elem["N"] == pytest.approx(2./total_number_of_atoms)
assert elem["O"] == pytest.approx(6/total_number_of_atoms)
# testing the correct nuclides are added when brackets are used
ref_dens = {'Mg24': 0.157902, 'Mg25': 0.02004, 'Mg26': 0.022058,
'N14': 0.199267, 'N15': 0.000732,
'O16': 0.599772, 'O17': 0.000227}
nuc_dens = m.get_nuclide_atom_densities()
for nuclide in ref_dens:
assert nuc_dens[nuclide][1] == pytest.approx(ref_dens[nuclide], 1e-2)
# testing non integer multiplier results in a value error
m = openmc.Material()
with pytest.raises(ValueError):
m.add_elements_from_formula('Li4.2SiO4')
# testing lowercase elements results in a value error
m = openmc.Material()
with pytest.raises(ValueError):
m.add_elements_from_formula('li4SiO4')
# testing lowercase elements results in a value error
m = openmc.Material()
with pytest.raises(ValueError):
m.add_elements_from_formula('Li4Sio4')
# testing incorrect character in formula results in a value error
m = openmc.Material()
with pytest.raises(ValueError):
m.add_elements_from_formula('Li4$SiO4')
# testing unequal opening and closing brackets
m = openmc.Material()
with pytest.raises(ValueError):
m.add_elements_from_formula('Fe(H2O)4(OH)2)')
def test_density():
m = openmc.Material()
for unit in ['g/cm3', 'g/cc', 'kg/m3', 'atom/b-cm', 'atom/cm3']:
@ -275,4 +369,3 @@ def test_mix_materials():
assert m3.density == pytest.approx(dens3)
assert m4.density == pytest.approx(dens4)
assert m5.density == pytest.approx(dens5)