From 8ba369d6cc44cbdc1fe2eaf12d043eafcd7512ce Mon Sep 17 00:00:00 2001 From: =shimwell Date: Mon, 23 Mar 2020 20:15:59 +0000 Subject: [PATCH 1/8] added elements from formula --- openmc/material.py | 96 ++++++++++++++++++++++++++++++- tests/unit_tests/test_material.py | 55 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 3952fe4e99..8d5168d512 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1,9 +1,10 @@ -from collections import OrderedDict, defaultdict +from collections import OrderedDict, defaultdict, 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 @@ -588,6 +589,99 @@ 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. Any numbers will be converted to integers (rounded down). + percent : float + Atom or weight percent + 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_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. + + """ + cv.check_type('formula', formula, str) + + # 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 not in list(openmc.data.ATOMIC_NUMBER.keys())[1:]: + 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 i in stack_top: + mat_stack[-1][i] += int(multi2 or 1) * stack_top[i] + + # 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 diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 6f51135ab2..7153fded5a 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -58,6 +58,61 @@ def test_elements_by_name(): assert a._nuclides == b._nuclides assert b._nuclides == c._nuclides +def test_adding_elements_by_formula(): + """Test adding elements from a formula""" + # testing the correct nuclides are added to the Material + m = openmc.Material() + m.add_elements_from_formula('Li4SiO4') + 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') + 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 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']: From 4e22c6a936c17f60d2c726818acf8836b76615d7 Mon Sep 17 00:00:00 2001 From: =shimwell Date: Mon, 23 Mar 2020 20:47:43 +0000 Subject: [PATCH 2/8] removed unused argument description --- openmc/material.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 8d5168d512..bdcf6adf17 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -599,8 +599,6 @@ class Material(IDManagerMixin): Formula to add, e.g., 'C2O', 'C6H12O6', or (NH4)2SO4. Note this is case sensitive, elements must start with an uppercase character. Any numbers will be converted to integers (rounded down). - percent : float - Atom or weight percent percent_type : {'ao', 'wo'}, optional 'ao' for atom percent and 'wo' for weight percent. Defaults to atom percent. From 605b3eade124c5a87cbd1d5e6e7abbdc75be0597 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 24 Mar 2020 11:34:20 +0000 Subject: [PATCH 3/8] code review imporements Co-Authored-By: Andrew Johnson --- openmc/material.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index bdcf6adf17..092389cbce 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -603,11 +603,10 @@ class Material(IDManagerMixin): 'ao' for atom percent and 'wo' for weight percent. Defaults to atom percent. enrichment : float, optional - Enrichment of an enrichment_taget nuclide in percent (ao or wo). - If enrichment_taget is not supplied then it is enrichment for U235 + 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). + 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 @@ -628,7 +627,7 @@ class Material(IDManagerMixin): for row in tokens: for token in row: if token.isalpha(): - if token not in list(openmc.data.ATOMIC_NUMBER.keys())[1:]: + if token == "n" or token not in openmc.data.ATOMIC_NUMBER: msg = 'Formula entry {} not an element symbol.' \ .format(token) raise ValueError(msg) @@ -663,8 +662,8 @@ class Material(IDManagerMixin): mat_stack.append(Counter()) if closing_bracket: stack_top = mat_stack.pop() - for i in stack_top: - mat_stack[-1][i] += int(multi2 or 1) * stack_top[i] + for symbol, value in stack_top.items(): + mat_stack[-1][symbol] += int(multi2 or 1) * value # Normalizing percentages percents = mat_stack[0].values() @@ -678,8 +677,6 @@ class Material(IDManagerMixin): 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 From 5fe6576a9cba1679c491344efa250133eafec5a5 Mon Sep 17 00:00:00 2001 From: =shimwell Date: Tue, 24 Mar 2020 11:57:17 +0000 Subject: [PATCH 4/8] added test to check decimal values fail --- tests/unit_tests/test_material.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 7153fded5a..43aeb62789 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -92,6 +92,11 @@ def test_adding_elements_by_formula(): 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): From 430b42609896f4872f50b9ea1274114a67eec407 Mon Sep 17 00:00:00 2001 From: =shimwell Date: Tue, 24 Mar 2020 12:06:56 +0000 Subject: [PATCH 5/8] added check for decimal points in formula --- openmc/material.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 092389cbce..7cc7e3db60 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -598,7 +598,7 @@ class Material(IDManagerMixin): formula : str Formula to add, e.g., 'C2O', 'C6H12O6', or (NH4)2SO4. Note this is case sensitive, elements must start with an uppercase - character. Any numbers will be converted to integers (rounded down). + character. Multiplier numbers must be integers. percent_type : {'ao', 'wo'}, optional 'ao' for atom percent and 'wo' for weight percent. Defaults to atom percent. @@ -622,6 +622,11 @@ class Material(IDManagerMixin): """ 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: From ffa0cfc03e953fdadc2e4d9fd61319a3b18c1555 Mon Sep 17 00:00:00 2001 From: =shimwell Date: Tue, 24 Mar 2020 13:33:42 +0000 Subject: [PATCH 6/8] added tests for number of elements --- tests/unit_tests/test_material.py | 34 +++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 43aeb62789..8e9ebb362d 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -3,7 +3,7 @@ import openmc.model import openmc.stats import openmc.examples import pytest - +from collections import defaultdict def test_attributes(uo2): assert uo2.name == 'UO2' @@ -60,9 +60,23 @@ def test_elements_by_name(): def test_adding_elements_by_formula(): """Test adding elements from a formula""" - # testing the correct nuclides are added to the Material + # 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} @@ -85,6 +99,22 @@ def test_adding_elements_by_formula(): # 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} From 4234fe45e71a7a3971e06c0733c253e96222bb8f Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 25 Mar 2020 14:58:02 +0000 Subject: [PATCH 7/8] Apply suggestions from code review Co-Authored-By: Paul Romano --- openmc/material.py | 4 ++-- tests/unit_tests/test_material.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 7cc7e3db60..e34123e934 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -607,9 +607,9 @@ class Material(IDManagerMixin): 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 + enrichment_target : str, optional Single nuclide name to enrich from a natural composition (e.g., 'O16') - enrichment_type: {'ao', 'wo'}, optional + 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 diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 8e9ebb362d..e366401827 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -58,7 +58,8 @@ def test_elements_by_name(): assert a._nuclides == b._nuclides assert b._nuclides == c._nuclides -def test_adding_elements_by_formula(): + +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() @@ -354,4 +355,3 @@ def test_mix_materials(): assert m3.density == pytest.approx(dens3) assert m4.density == pytest.approx(dens4) assert m5.density == pytest.approx(dens5) - From c013f6373075c64205511a182e1632bd87dea1a3 Mon Sep 17 00:00:00 2001 From: =shimwell Date: Wed, 25 Mar 2020 15:03:39 +0000 Subject: [PATCH 8/8] pep8 format applied to imports --- tests/unit_tests/test_material.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index e366401827..c47faee8cd 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -1,9 +1,12 @@ +from collections import defaultdict + +import pytest + import openmc +import openmc.examples import openmc.model import openmc.stats -import openmc.examples -import pytest -from collections import defaultdict + def test_attributes(uo2): assert uo2.name == 'UO2'