Merge pull request #1 from davidjohnlong/develop

Update fixed source development branch
This commit is contained in:
davidjohnlong 2020-03-09 10:13:45 +00:00 committed by GitHub
commit 68841672a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 445 additions and 119 deletions

View file

@ -165,7 +165,6 @@ endif()
add_subdirectory(vendor/xtl)
set(xtl_DIR ${CMAKE_CURRENT_BINARY_DIR}/vendor/xtl)
add_subdirectory(vendor/xtensor)
target_link_libraries(xtensor INTERFACE xtl)
#===============================================================================
# GSL header-only library

View file

@ -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
@ -141,6 +155,33 @@ attribute, e.g.,
:attr:`Material.temperature` or :attr:`Cell.temperature`
attributes, respectively.
-----------------
Material Mixtures
-----------------
In OpenMC it is possible to mix any number of materials to create a new material
with the correct nuclide composition and density. The
:meth:`Material.mix_materials` method takes a list of materials and
a list of their mixing fractions. Mixing fractions can be provided as atomic
fractions, weight fractions, or volume fractions. The fraction type
can be specified by passing 'ao', 'wo', or 'vo' as the third argument, respectively.
For example, assuming the required materials have already been defined, a MOX
material with 3% plutonium oxide by weight could be created using the following:
::
mox = openmc.Material.mix_materials([uo2, puo2], [0.97, 0.03], 'wo')
It should be noted that, if mixing fractions are specifed as atomic or weight
fractions, the supplied fractions should sum to one. If the fractions are specified
as volume fractions, and the sum of the fractions is less than one, then the remaining
fraction is set as void material.
.. warning:: Materials with :math:`S(\alpha,\beta)` thermal scattering data
cannot be used in :meth:`Material.mix_materials`. However, thermal
scattering data can be added to a material created by
:meth:`Material.mix_materials`.
--------------------
Material Collections
--------------------

File diff suppressed because one or more lines are too long

View file

@ -86,11 +86,15 @@ void count_cell_instances(int32_t univ_indx);
//! Recursively search through universes and count universe instances.
//! \param search_univ The index of the universe to begin searching from.
//! \param target_univ_id The ID of the universe to be counted.
//! \param univ_count_memo Memoized counts that make this function faster for
//! large systems. The first call to this function for each target_univ_id
//! should start with an empty memo.
//! \return The number of instances of target_univ_id in the geometry tree under
//! search_univ.
//==============================================================================
int count_universe_instances(int32_t search_univ, int32_t target_univ_id);
int count_universe_instances(int32_t search_univ, int32_t target_univ_id,
std::unordered_map<int32_t, int32_t>& univ_count_memo);
//==============================================================================
//! Build a character array representing the path to a distribcell instance.

View file

@ -77,7 +77,8 @@ public:
{offsets_.resize(n_maps * universes_.size(), C_NONE);}
//! Populate the distribcell offset tables.
int32_t fill_offset_table(int32_t offset, int32_t target_univ_id, int map);
int32_t fill_offset_table(int32_t offset, int32_t target_univ_id, int map,
std::unordered_map<int32_t, int32_t>& univ_count_memo);
//! \brief Check lattice indices.
//! \param i_xyz[3] The indices for a lattice tile.

View file

@ -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,

View file

@ -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':

View file

@ -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')

View file

@ -385,8 +385,10 @@ prepare_distribcell()
}
// Fill the cell and lattice offset tables.
#pragma omp parallel for
for (int map = 0; map < target_univ_ids.size(); map++) {
auto target_univ_id = target_univ_ids[map];
std::unordered_map<int32_t, int32_t> univ_count_memo;
for (const auto& univ : model::universes) {
int32_t offset = 0;
for (int32_t cell_indx : univ->cells_) {
@ -395,11 +397,13 @@ prepare_distribcell()
if (c.type_ == Fill::UNIVERSE) {
c.offset_[map] = offset;
int32_t search_univ = c.fill_;
offset += count_universe_instances(search_univ, target_univ_id);
offset += count_universe_instances(search_univ, target_univ_id,
univ_count_memo);
} else if (c.type_ == Fill::LATTICE) {
Lattice& lat = *model::lattices[c.fill_];
offset = lat.fill_offset_table(offset, target_univ_id, map);
offset = lat.fill_offset_table(offset, target_univ_id, map,
univ_count_memo);
}
}
}
@ -411,7 +415,6 @@ prepare_distribcell()
void
count_cell_instances(int32_t univ_indx)
{
const auto univ_counts = model::universe_cell_counts.find(univ_indx);
if (univ_counts != model::universe_cell_counts.end()) {
for (const auto& it : univ_counts->second) {
@ -442,30 +445,42 @@ count_cell_instances(int32_t univ_indx)
//==============================================================================
int
count_universe_instances(int32_t search_univ, int32_t target_univ_id)
count_universe_instances(int32_t search_univ, int32_t target_univ_id,
std::unordered_map<int32_t, int32_t>& univ_count_memo)
{
// If this is the target, it can't contain itself.
// If this is the target, it can't contain itself.
if (model::universes[search_univ]->id_ == target_univ_id) {
return 1;
}
// If we have already counted the number of instances, reuse that value.
auto search = univ_count_memo.find(search_univ);
if (search != univ_count_memo.end()) {
return search->second;
}
int count {0};
for (int32_t cell_indx : model::universes[search_univ]->cells_) {
Cell& c = *model::cells[cell_indx];
if (c.type_ == Fill::UNIVERSE) {
int32_t next_univ = c.fill_;
count += count_universe_instances(next_univ, target_univ_id);
count += count_universe_instances(next_univ, target_univ_id,
univ_count_memo);
} else if (c.type_ == Fill::LATTICE) {
Lattice& lat = *model::lattices[c.fill_];
for (auto it = lat.begin(); it != lat.end(); ++it) {
int32_t next_univ = *it;
count += count_universe_instances(next_univ, target_univ_id);
count += count_universe_instances(next_univ, target_univ_id,
univ_count_memo);
}
}
}
// Remember the number of instances in this universe.
univ_count_memo[search_univ] = count;
return count;
}

View file

@ -93,11 +93,12 @@ Lattice::adjust_indices()
//==============================================================================
int32_t
Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, int map)
Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, int map,
std::unordered_map<int32_t, int32_t>& univ_count_memo)
{
for (LatticeIter it = begin(); it != end(); ++it) {
offsets_[map * universes_.size() + it.indx_] = offset;
offset += count_universe_instances(*it, target_univ_id);
offset += count_universe_instances(*it, target_univ_id, univ_count_memo);
}
return offset;
}

View 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')

View file

@ -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"""

2
vendor/xtensor vendored

@ -1 +1 @@
Subproject commit ef091807f7ed0e5ba7e251a6c46f4af7bba79e2e
Subproject commit 31acec1e90bbea6d4bc17af0710a123bd5da6689

2
vendor/xtl vendored

@ -1 +1 @@
Subproject commit f5d13e6c4f856becc178939365fcdcf9a657ffb5
Subproject commit 0024346605bd92bcc4009caad7f4be88687e063a