mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-28 06:05:58 -04:00
Merge pull request #1503 from Mikolaj-A-Kowalski/develop
Enrichment of 2-Isotope Elements
This commit is contained in:
commit
3fc25be950
6 changed files with 256 additions and 16 deletions
|
|
@ -61,6 +61,20 @@ following would add 3.2% enriched uranium to a material::
|
|||
In addition to U235 and U238, concentrations of U234 and U236 will be present
|
||||
and are determined through a correlation based on measured data.
|
||||
|
||||
It is also possible to perform enrichment of any element that is composed
|
||||
of two naturally-occurring isotopes (e.g., Li or B) in terms of atomic percent.
|
||||
To invoke this, provide the additional argument `enrichment_target` to
|
||||
:meth:`Material.add_element`. For example the following would enrich B10
|
||||
to 30ao%::
|
||||
|
||||
mat.add_element('B', 1.0, enrichment=30.0, enrichment_target='B10')
|
||||
|
||||
In order to enrich an isotope in terms of mass percent (wo%), provide the extra
|
||||
argument `enrichment_type`. For example the following would enrich Li6 to 15wo%::
|
||||
|
||||
mat.add_element('Li', 1.0, enrichment=15.0, enrichment_target='Li6',
|
||||
enrichment_type='wo')
|
||||
|
||||
Often, cross section libraries don't actually have all naturally-occurring
|
||||
isotopes for a given element. For example, in ENDF/B-VII.1, cross section
|
||||
evaluations are given for O16 and O17 but not for O18. If OpenMC is aware of
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from warnings import warn
|
|||
# pp. 293-306 (2013). The "representative isotopic abundance" values from
|
||||
# column 9 are used except where an interval is given, in which case the
|
||||
# "best measurement" is used.
|
||||
# Note that the abundances are given as atomic fractions!
|
||||
NATURAL_ABUNDANCE = {
|
||||
'H1': 0.99984426, 'H2': 0.00015574, 'He3': 0.000002,
|
||||
'He4': 0.999998, 'Li6': 0.07589, 'Li7': 0.92411,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
from xml.etree import ElementTree as ET
|
||||
|
||||
import openmc.checkvalue as cv
|
||||
from numbers import Real
|
||||
from openmc.data import NATURAL_ABUNDANCE, atomic_mass
|
||||
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ class Element(str):
|
|||
return self
|
||||
|
||||
def expand(self, percent, percent_type, enrichment=None,
|
||||
enrichment_target=None, enrichment_type=None,
|
||||
cross_sections=None):
|
||||
"""Expand natural element into its naturally-occurring isotopes.
|
||||
|
||||
|
|
@ -52,9 +54,15 @@ class Element(str):
|
|||
percent_type : {'ao', 'wo'}
|
||||
'ao' for atom percent and 'wo' for weight percent
|
||||
enrichment : float, optional
|
||||
Enrichment for U235 in weight percent. For example, input 4.95 for
|
||||
4.95 weight percent enriched U. Default is None
|
||||
(natural composition).
|
||||
Enrichment of an enrichment_taget nuclide in percent (ao or wo).
|
||||
If enrichment_taget 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
|
||||
cross_sections : str, optional
|
||||
Location of cross_sections.xml file. Default is None.
|
||||
|
||||
|
|
@ -65,16 +73,47 @@ class Element(str):
|
|||
is a tuple consisting of a nuclide string, the atom/weight percent,
|
||||
and the string 'ao' or 'wo'.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
No data is available for any of natural isotopes of the element
|
||||
ValueError
|
||||
If only some natural isotopes are available in the cross-section data
|
||||
library and the element is not O, W, or Ta
|
||||
ValueError
|
||||
If a non-naturally-occurring isotope is requested
|
||||
ValueError
|
||||
If enrichment is requested of an element with more than two
|
||||
naturally-occurring isotopes.
|
||||
ValueError
|
||||
If enrichment procedure for Uranium is used when element is not
|
||||
Uranium.
|
||||
ValueError
|
||||
Uranium enrichment is requested with enrichment_type=='ao'
|
||||
|
||||
Notes
|
||||
-----
|
||||
When the `enrichment` argument is specified, a correlation from
|
||||
`ORNL/CSD/TM-244 <https://doi.org/10.2172/5561567>`_ is used to
|
||||
calculate the weight fractions of U234, U235, U236, and U238. Namely,
|
||||
the weight fraction of U234 and U236 are taken to be 0.89% and 0.46%,
|
||||
respectively, of the U235 weight fraction. The remainder of the isotopic
|
||||
weight is assigned to U238.
|
||||
respectively, of the U235 weight fraction. The remainder of the
|
||||
isotopic weight is assigned to U238.
|
||||
|
||||
When the `enrichment` argument is specified with `enrichment_target`, a
|
||||
general enrichment procedure is used for elements composed of exactly
|
||||
two naturally-occurring isotopes. `enrichment` is interpreted as atom
|
||||
percent by default but can be controlled by the `enrichment_type`
|
||||
argument.
|
||||
|
||||
"""
|
||||
# Check input
|
||||
if enrichment_type is not None:
|
||||
cv.check_value('enrichment_type', enrichment_type, {'ao', 'wo'})
|
||||
|
||||
if enrichment is not None:
|
||||
cv.check_less_than('enrichment', enrichment, 100.0, equality=True)
|
||||
cv.check_greater_than('enrichment', enrichment, 0., equality=True)
|
||||
|
||||
# Get the nuclides present in nature
|
||||
natural_nuclides = set()
|
||||
|
|
@ -110,8 +149,8 @@ class Element(str):
|
|||
mutual_nuclides = sorted(list(mutual_nuclides))
|
||||
absent_nuclides = sorted(list(absent_nuclides))
|
||||
|
||||
# If all natural nuclides are present in the library, expand element
|
||||
# using all natural nuclides
|
||||
# If all natural nuclides are present in the library,
|
||||
# expand element using all natural nuclides
|
||||
if len(absent_nuclides) == 0:
|
||||
for nuclide in mutual_nuclides:
|
||||
abundances[nuclide] = NATURAL_ABUNDANCE[nuclide]
|
||||
|
|
@ -164,7 +203,20 @@ class Element(str):
|
|||
abundances[nuclide] = NATURAL_ABUNDANCE[nuclide]
|
||||
|
||||
# Modify mole fractions if enrichment provided
|
||||
if enrichment is not None:
|
||||
# Old treatment for Uranium
|
||||
if enrichment is not None and enrichment_target is None:
|
||||
|
||||
# Check that the element is Uranium
|
||||
if self.name != 'U':
|
||||
msg = ('Enrichment procedure for Uranium was requested, '
|
||||
'but the isotope is {} not U'.format(self))
|
||||
raise ValueError(msg)
|
||||
|
||||
# Check that enrichment_type is not 'ao'
|
||||
if enrichment_type == 'ao':
|
||||
msg = ('Enrichment procedure for Uranium requires that '
|
||||
'enrichment value is provided as wo%.')
|
||||
raise ValueError(msg)
|
||||
|
||||
# Calculate the mass fractions of isotopes
|
||||
abundances['U234'] = 0.0089 * enrichment
|
||||
|
|
@ -181,6 +233,73 @@ class Element(str):
|
|||
for nuclide in abundances.keys():
|
||||
abundances[nuclide] /= sum_abundances
|
||||
|
||||
# Modify mole fractions if enrichment provided
|
||||
# New treatment for arbitrary element
|
||||
elif enrichment is not None and enrichment_target is not None:
|
||||
|
||||
# Provide more informative error message for U235
|
||||
if enrichment_target == 'U235':
|
||||
msg = ("There is a special procedure for enrichment of U235 "
|
||||
"in U. To invoke it, the arguments 'enrichment_target'"
|
||||
"and 'enrichment_type' should be omitted. Provide "
|
||||
"a value only for 'enrichment' in weight percent.")
|
||||
raise ValueError(msg)
|
||||
|
||||
# Check if it is two-isotope mixture
|
||||
if len(abundances) != 2:
|
||||
msg = ('Element {} does not consist of two naturally-occurring '
|
||||
'isotopes. Please enter isotopic abundances manually.'
|
||||
.format(self))
|
||||
raise ValueError(msg)
|
||||
|
||||
# Check if the target nuclide is present in the mixture
|
||||
if enrichment_target not in abundances:
|
||||
msg = ('The target nuclide {} is not one of the naturally-occurring '
|
||||
'isotopes ({})'.format(enrichment_target, list(abundances)))
|
||||
raise ValueError(msg)
|
||||
|
||||
# If weight percent enrichment is requested convert to mass fractions
|
||||
if enrichment_type == 'wo':
|
||||
# Convert the atomic abundances to weight fractions
|
||||
# Compute the element atomic mass
|
||||
element_am = sum(atomic_mass(nuc)*abundances[nuc] for nuc in abundances)
|
||||
|
||||
# Convert Molar Fractions to mass fractions
|
||||
for nuclide in abundances:
|
||||
abundances[nuclide] *= atomic_mass(nuclide) / element_am
|
||||
|
||||
# Normalize to one
|
||||
sum_abundances = sum(abundances.values())
|
||||
for nuclide in abundances:
|
||||
abundances[nuclide] /= sum_abundances
|
||||
|
||||
# Enrich the mixture
|
||||
# The procedure is more generic that it needs to be. It allows
|
||||
# to enrich mixtures of more then 2 isotopes, keeping the ratios
|
||||
# of non-enriched nuclides the same as in natural composition
|
||||
|
||||
# Get fraction of non-enriched isotopes in nat. composition
|
||||
non_enriched = 1.0 - abundances[enrichment_target]
|
||||
tail_fraction = 1.0 - enrichment / 100.0
|
||||
|
||||
# Enrich all nuclides
|
||||
# Do bogus operation for enrichment target but overwrite immediatly
|
||||
# to avoid if statement in the loop
|
||||
for nuclide, fraction in abundances.items():
|
||||
abundances[nuclide] = tail_fraction * fraction / non_enriched
|
||||
abundances[enrichment_target] = enrichment / 100.0
|
||||
|
||||
# Convert back to atomic fractions if requested
|
||||
if enrichment_type == 'wo':
|
||||
# Convert the mass fractions to mole fractions
|
||||
for nuclide in abundances:
|
||||
abundances[nuclide] /= atomic_mass(nuclide)
|
||||
|
||||
# Normalize the mole fractions to one
|
||||
sum_abundances = sum(abundances.values())
|
||||
for nuclide in abundances:
|
||||
abundances[nuclide] /= sum_abundances
|
||||
|
||||
# Compute the ratio of the nuclide atomic masses to the element
|
||||
# atomic mass
|
||||
if percent_type == 'wo':
|
||||
|
|
|
|||
|
|
@ -499,7 +499,8 @@ class Material(IDManagerMixin):
|
|||
if macroscopic == self._macroscopic:
|
||||
self._macroscopic = None
|
||||
|
||||
def add_element(self, element, percent, percent_type='ao', enrichment=None):
|
||||
def add_element(self, element, percent, percent_type='ao', enrichment=None,
|
||||
enrichment_target=None, enrichment_type=None):
|
||||
"""Add a natural element to the material
|
||||
|
||||
Parameters
|
||||
|
|
@ -512,9 +513,22 @@ class Material(IDManagerMixin):
|
|||
'ao' for atom percent and 'wo' for weight percent. Defaults to atom
|
||||
percent.
|
||||
enrichment : float, optional
|
||||
Enrichment for U235 in weight percent. For example, input 4.95 for
|
||||
4.95 weight percent enriched U. Default is None
|
||||
(natural composition).
|
||||
Enrichment of an enrichment_taget nuclide in percent (ao or wo).
|
||||
If enrichment_taget 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.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -540,7 +554,7 @@ class Material(IDManagerMixin):
|
|||
'macroscopic data-set has already been added'.format(self._id)
|
||||
raise ValueError(msg)
|
||||
|
||||
if enrichment is not None:
|
||||
if enrichment is not None and enrichment_target is None:
|
||||
if not isinstance(enrichment, Real):
|
||||
msg = 'Unable to add an Element to Material ID="{}" with a ' \
|
||||
'non-floating point enrichment value "{}"'\
|
||||
|
|
@ -567,7 +581,11 @@ class Material(IDManagerMixin):
|
|||
|
||||
# Add naturally-occuring isotopes
|
||||
element = openmc.Element(element)
|
||||
for nuclide in element.expand(percent, percent_type, enrichment):
|
||||
for nuclide in element.expand(percent,
|
||||
percent_type,
|
||||
enrichment,
|
||||
enrichment_target,
|
||||
enrichment_type):
|
||||
self.add_nuclide(*nuclide)
|
||||
|
||||
def add_s_alpha_beta(self, name, fraction=1.0):
|
||||
|
|
@ -936,7 +954,7 @@ class Material(IDManagerMixin):
|
|||
Fractions of each material to be combined
|
||||
percent_type : {'ao', 'wo', 'vo'}
|
||||
Type of percentage, must be one of 'ao', 'wo', or 'vo', to signify atom
|
||||
percent (molar percent), weight percent, or volume percent,
|
||||
percent (molar percent), weight percent, or volume percent,
|
||||
optional. Defaults to 'ao'
|
||||
name : str
|
||||
The name for the new material, optional. Defaults to concatenated
|
||||
|
|
@ -1005,7 +1023,7 @@ class Material(IDManagerMixin):
|
|||
zip(materials, fracs)])
|
||||
new_mat = openmc.Material(name=name)
|
||||
|
||||
# Compute atom fractions of nuclides and add them to the new material
|
||||
# Compute atom fractions of nuclides and add them to the new material
|
||||
tot_nuclides_per_cc = np.sum([dens for dens in nuclides_per_cc.values()])
|
||||
for nuc, atom_dens in nuclides_per_cc.items():
|
||||
new_mat.add_nuclide(nuc, atom_dens/tot_nuclides_per_cc, 'ao')
|
||||
|
|
|
|||
81
tests/unit_tests/test_element.py
Normal file
81
tests/unit_tests/test_element.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import openmc
|
||||
from pytest import approx, raises
|
||||
|
||||
from openmc.data import NATURAL_ABUNDANCE, atomic_mass
|
||||
|
||||
|
||||
def test_expand_no_enrichment():
|
||||
""" Expand Li in natural compositions"""
|
||||
lithium = openmc.Element('Li')
|
||||
|
||||
# Verify the expansion into ATOMIC fraction against natural composition
|
||||
for isotope in lithium.expand(100.0, 'ao'):
|
||||
assert isotope[1] == approx(NATURAL_ABUNDANCE[isotope[0]] * 100.0)
|
||||
|
||||
# Verify the expansion into WEIGHT fraction against natural composition
|
||||
natural = {'Li6': NATURAL_ABUNDANCE['Li6'] * atomic_mass('Li6'),
|
||||
'Li7': NATURAL_ABUNDANCE['Li7'] * atomic_mass('Li7')}
|
||||
li_am = sum(natural.values())
|
||||
for key in natural:
|
||||
natural[key] /= li_am
|
||||
|
||||
for isotope in lithium.expand(100.0, 'wo'):
|
||||
assert isotope[1] == approx(natural[isotope[0]] * 100.0)
|
||||
|
||||
|
||||
def test_expand_enrichment():
|
||||
""" Expand and verify enrichment of Li """
|
||||
lithium = openmc.Element('Li')
|
||||
|
||||
# Verify the enrichment by atoms
|
||||
ref = {'Li6': 75.0, 'Li7': 25.0}
|
||||
for isotope in lithium.expand(100.0, 'ao', 25.0, 'Li7', 'ao'):
|
||||
assert isotope[1] == approx(ref[isotope[0]])
|
||||
|
||||
# Verify the enrichment by weight
|
||||
for isotope in lithium.expand(100.0, 'wo', 25.0, 'Li7', 'wo'):
|
||||
assert isotope[1] == approx(ref[isotope[0]])
|
||||
|
||||
|
||||
def test_expand_exceptions():
|
||||
""" Test that correct exceptions are raised for invalid input """
|
||||
|
||||
# 1 Isotope Element
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('Be')
|
||||
element.expand(70.0, 'ao', 4.0, 'Be9')
|
||||
|
||||
# 3 Isotope Element
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('Cr')
|
||||
element.expand(70.0, 'ao', 4.0, 'Cr52')
|
||||
|
||||
# Non-present Enrichment Target
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('H')
|
||||
element.expand(70.0, 'ao', 4.0, 'H4')
|
||||
|
||||
# Enrichment Procedure for Uranium if not Uranium
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('Li')
|
||||
element.expand(70.0, 'ao', 4.0)
|
||||
|
||||
# Missing Enrichment Target
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('Li')
|
||||
element.expand(70.0, 'ao', 4.0, enrichment_type='ao')
|
||||
|
||||
# Invalid Enrichment Type Entry
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('Li')
|
||||
element.expand(70.0, 'ao', 4.0, 'Li7', 'Grand Moff Tarkin')
|
||||
|
||||
# Trying to enrich Uranium
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('U')
|
||||
element.expand(70.0, 'ao', 4.0, 'U235', 'wo')
|
||||
|
||||
# Trying to enrich Uranium with wrong enrichment_target
|
||||
with raises(ValueError):
|
||||
element = openmc.Element('U')
|
||||
element.expand(70.0, 'ao', 4.0, enrichment_type='ao')
|
||||
|
|
@ -29,10 +29,17 @@ def test_elements():
|
|||
m = openmc.Material()
|
||||
m.add_element('Zr', 1.0)
|
||||
m.add_element('U', 1.0, enrichment=4.5)
|
||||
m.add_element('Li', 1.0, enrichment=60.0, enrichment_target='Li7')
|
||||
m.add_element('H', 1.0, enrichment=50.0, enrichment_target='H2',
|
||||
enrichment_type='wo')
|
||||
with pytest.raises(ValueError):
|
||||
m.add_element('U', 1.0, enrichment=100.0)
|
||||
with pytest.raises(ValueError):
|
||||
m.add_element('Pu', 1.0, enrichment=3.0)
|
||||
with pytest.raises(ValueError):
|
||||
m.add_element('U', 1.0, enrichment=70.0, enrichment_target='U235')
|
||||
with pytest.raises(ValueError):
|
||||
m.add_element('He', 1.0, enrichment=17.0, enrichment_target='He6')
|
||||
|
||||
def test_elements_by_name():
|
||||
"""Test adding elements by name"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue