1from collections import OrderedDict
2import os
3import re
4from xml.etree import ElementTree as ET
5
6import openmc.checkvalue as cv
7from openmc.data import NATURAL_ABUNDANCE, atomic_mass, \
8    isotopes as natural_isotopes
9
10
11class Element(str):
12    """A natural element that auto-expands to add the isotopes of an element to
13    a material in their natural abundance. Internally, the OpenMC Python API
14    expands the natural element into isotopes only when the materials.xml file
15    is created.
16
17    Parameters
18    ----------
19    name : str
20        Chemical symbol of the element, e.g. Pu
21
22    Attributes
23    ----------
24    name : str
25        Chemical symbol of the element, e.g. Pu
26
27    """
28
29    def __new__(cls, name):
30        cv.check_type('element name', name, str)
31        cv.check_length('element name', name, 1, 2)
32        return super().__new__(cls, name)
33
34    @property
35    def name(self):
36        return self
37
38    def expand(self, percent, percent_type, enrichment=None,
39               enrichment_target=None, enrichment_type=None,
40               cross_sections=None):
41        """Expand natural element into its naturally-occurring isotopes.
42
43        An optional cross_sections argument or the :envvar:`OPENMC_CROSS_SECTIONS`
44        environment variable is used to specify a cross_sections.xml file.
45        If the cross_sections.xml file is found, the element is expanded only
46        into the isotopes/nuclides present in cross_sections.xml. If no
47        cross_sections.xml file is found, the element is expanded based on its
48        naturally occurring isotopes.
49
50        Parameters
51        ----------
52        percent : float
53            Atom or weight percent
54        percent_type : {'ao', 'wo'}
55            'ao' for atom percent and 'wo' for weight percent
56        enrichment : float, optional
57            Enrichment of an enrichment_taget nuclide in percent (ao or wo).
58            If enrichment_taget is not supplied then it is enrichment for U235
59            in weight percent. For example, input 4.95 for 4.95 weight percent
60            enriched U. Default is None (natural composition).
61        enrichment_target: str, optional
62            Single nuclide name to enrich from a natural composition (e.g., 'O16')
63
64            .. versionadded:: 0.12
65        enrichment_type: {'ao', 'wo'}, optional
66            'ao' for enrichment as atom percent and 'wo' for weight percent.
67            Default is: 'ao' for two-isotope enrichment; 'wo' for U enrichment
68
69            .. versionadded:: 0.12
70        cross_sections : str, optional
71            Location of cross_sections.xml file. Default is None.
72
73        Returns
74        -------
75        isotopes : list
76            Naturally-occurring isotopes of the element. Each item of the list
77            is a tuple consisting of a nuclide string, the atom/weight percent,
78            and the string 'ao' or 'wo'.
79
80        Raises
81        ------
82        ValueError
83            No data is available for any of natural isotopes of the element
84        ValueError
85            If only some natural isotopes are available in the cross-section data
86            library and the element is not O, W, or Ta
87        ValueError
88            If a non-naturally-occurring isotope is requested
89        ValueError
90            If enrichment is requested of an element with more than two
91            naturally-occurring isotopes.
92        ValueError
93            If enrichment procedure for Uranium is used when element is not
94            Uranium.
95        ValueError
96            Uranium enrichment is requested with enrichment_type=='ao'
97
98        Notes
99        -----
100        When the `enrichment` argument is specified, a correlation from
101        `ORNL/CSD/TM-244 <https://doi.org/10.2172/5561567>`_ is used to
102        calculate the weight fractions of U234, U235, U236, and U238. Namely,
103        the weight fraction of U234 and U236 are taken to be 0.89% and 0.46%,
104        respectively, of the U235 weight fraction. The remainder of the
105        isotopic weight is assigned to U238.
106
107        When the `enrichment` argument is specified with `enrichment_target`, a
108        general enrichment procedure is used for elements composed of exactly
109        two naturally-occurring isotopes. `enrichment` is interpreted as atom
110        percent by default but can be controlled by the `enrichment_type`
111        argument.
112
113        """
114        # Check input
115        if enrichment_type is not None:
116            cv.check_value('enrichment_type', enrichment_type, {'ao', 'wo'})
117
118        if enrichment is not None:
119            cv.check_less_than('enrichment', enrichment, 100.0, equality=True)
120            cv.check_greater_than('enrichment', enrichment, 0., equality=True)
121
122        # Get the nuclides present in nature
123        natural_nuclides = {name for name, abundance in natural_isotopes(self)}
124
125        # Create dict to store the expanded nuclides and abundances
126        abundances = OrderedDict()
127
128        # If cross_sections is None, get the cross sections from the
129        # OPENMC_CROSS_SECTIONS environment variable
130        if cross_sections is None:
131            cross_sections = os.environ.get('OPENMC_CROSS_SECTIONS')
132
133        # If a cross_sections library is present, check natural nuclides
134        # against the nuclides in the library
135        if cross_sections is not None:
136            library_nuclides = set()
137            tree = ET.parse(cross_sections)
138            root = tree.getroot()
139            for child in root.findall('library'):
140                nuclide = child.attrib['materials']
141                if re.match(r'{}\d+'.format(self), nuclide) and \
142                   '_m' not in nuclide:
143                    library_nuclides.add(nuclide)
144
145            # Get a set of the mutual and absent nuclides. Convert to lists
146            # and sort to avoid different ordering between Python 2 and 3.
147            mutual_nuclides = natural_nuclides.intersection(library_nuclides)
148            absent_nuclides = natural_nuclides.difference(mutual_nuclides)
149            mutual_nuclides = sorted(list(mutual_nuclides))
150            absent_nuclides = sorted(list(absent_nuclides))
151
152            # If all naturally ocurring isotopes are present in the library,
153            # add them based on their abundance
154            if len(absent_nuclides) == 0:
155                for nuclide in mutual_nuclides:
156                    abundances[nuclide] = NATURAL_ABUNDANCE[nuclide]
157
158            # If some naturally occurring isotopes are not present in the
159            # library, check if the "natural" nuclide (e.g., C0) is present. If
160            # so, set the abundance to 1 for this nuclide.
161            elif (self + '0') in library_nuclides:
162                abundances[self + '0'] = 1.0
163
164            elif len(mutual_nuclides) == 0:
165                msg = ('Unable to expand element {} because the cross '
166                       'section library provided does not contain any of '
167                       'the natural isotopes for that element.'
168                       .format(self))
169                raise ValueError(msg)
170
171            # If some naturally occurring isotopes are in the library, add them.
172            # For the absent nuclides, add them based on our knowledge of the
173            # common cross section libraries (ENDF, JEFF, and JENDL)
174            else:
175                # Add the mutual isotopes
176                for nuclide in mutual_nuclides:
177                    abundances[nuclide] = NATURAL_ABUNDANCE[nuclide]
178
179                # Adjust the abundances for the absent nuclides
180                for nuclide in absent_nuclides:
181                    if nuclide in ['O17', 'O18'] and 'O16' in mutual_nuclides:
182                        abundances['O16'] += NATURAL_ABUNDANCE[nuclide]
183                    elif nuclide == 'Ta180' and 'Ta181' in mutual_nuclides:
184                        abundances['Ta181'] += NATURAL_ABUNDANCE[nuclide]
185                    elif nuclide == 'W180' and 'W182' in mutual_nuclides:
186                        abundances['W182'] += NATURAL_ABUNDANCE[nuclide]
187                    else:
188                        msg = 'Unsure how to partition natural abundance of ' \
189                              'isotope {0} into other natural isotopes of ' \
190                              'this element that are present in the cross ' \
191                              'section library provided. Consider adding ' \
192                              'the isotopes of this element individually.'
193                        raise ValueError(msg)
194
195        # If a cross_section library is not present, expand the element into
196        # its natural nuclides
197        else:
198            for nuclide in natural_nuclides:
199                abundances[nuclide] = NATURAL_ABUNDANCE[nuclide]
200
201        # Modify mole fractions if enrichment provided
202        # Old treatment for Uranium
203        if enrichment is not None and enrichment_target is None:
204
205            # Check that the element is Uranium
206            if self.name != 'U':
207                msg = ('Enrichment procedure for Uranium was requested, '
208                       'but the isotope is {} not U'.format(self))
209                raise ValueError(msg)
210
211            # Check that enrichment_type is not 'ao'
212            if enrichment_type == 'ao':
213                msg = ('Enrichment procedure for Uranium requires that '
214                       'enrichment value is provided as wo%.')
215                raise ValueError(msg)
216
217            # Calculate the mass fractions of isotopes
218            abundances['U234'] = 0.0089 * enrichment
219            abundances['U235'] = enrichment
220            abundances['U236'] = 0.0046 * enrichment
221            abundances['U238'] = 100.0 - 1.0135 * enrichment
222
223            # Convert the mass fractions to mole fractions
224            for nuclide in abundances.keys():
225                abundances[nuclide] /= atomic_mass(nuclide)
226
227            # Normalize the mole fractions to one
228            sum_abundances = sum(abundances.values())
229            for nuclide in abundances.keys():
230                abundances[nuclide] /= sum_abundances
231
232        # Modify mole fractions if enrichment provided
233        # New treatment for arbitrary element
234        elif enrichment is not None and enrichment_target is not None:
235
236            # Provide more informative error message for U235
237            if enrichment_target == 'U235':
238                msg = ("There is a special procedure for enrichment of U235 "
239                       "in U. To invoke it, the arguments 'enrichment_target'"
240                       "and 'enrichment_type' should be omitted. Provide "
241                       "a value only for 'enrichment' in weight percent.")
242                raise ValueError(msg)
243
244            # Check if it is two-isotope mixture
245            if len(abundances) != 2:
246                msg = ('Element {} does not consist of two naturally-occurring '
247                       'isotopes. Please enter isotopic abundances manually.'
248                       .format(self))
249                raise ValueError(msg)
250
251            # Check if the target nuclide is present in the mixture
252            if enrichment_target not in abundances:
253                msg = ('The target nuclide {} is not one of the naturally-occurring '
254                       'isotopes ({})'.format(enrichment_target, list(abundances)))
255                raise ValueError(msg)
256
257            # If weight percent enrichment is requested convert to mass fractions
258            if enrichment_type == 'wo':
259                # Convert the atomic abundances to weight fractions
260                # Compute the element atomic mass
261                element_am = sum(atomic_mass(nuc)*abundances[nuc] for nuc in abundances)
262
263                # Convert Molar Fractions to mass fractions
264                for nuclide in abundances:
265                    abundances[nuclide] *= atomic_mass(nuclide) / element_am
266
267                # Normalize to one
268                sum_abundances = sum(abundances.values())
269                for nuclide in abundances:
270                    abundances[nuclide] /= sum_abundances
271
272            # Enrich the mixture
273            # The procedure is more generic that it needs to be. It allows
274            # to enrich mixtures of more then 2 isotopes, keeping the ratios
275            # of non-enriched nuclides the same as in natural composition
276
277            # Get fraction of non-enriched isotopes in nat. composition
278            non_enriched = 1.0 - abundances[enrichment_target]
279            tail_fraction = 1.0 - enrichment / 100.0
280
281            # Enrich all nuclides
282            # Do bogus operation for enrichment target but overwrite immediatly
283            # to avoid if statement in the loop
284            for nuclide, fraction in abundances.items():
285                abundances[nuclide] = tail_fraction * fraction / non_enriched
286            abundances[enrichment_target] = enrichment / 100.0
287
288            # Convert back to atomic fractions if requested
289            if enrichment_type == 'wo':
290                # Convert the mass fractions to mole fractions
291                for nuclide in abundances:
292                    abundances[nuclide] /= atomic_mass(nuclide)
293
294                # Normalize the mole fractions to one
295                sum_abundances = sum(abundances.values())
296                for nuclide in abundances:
297                    abundances[nuclide] /= sum_abundances
298
299        # Compute the ratio of the nuclide atomic masses to the element
300        # atomic mass
301        if percent_type == 'wo':
302
303            # Compute the element atomic mass
304            element_am = 0.
305            for nuclide in abundances.keys():
306                element_am += atomic_mass(nuclide) * abundances[nuclide]
307
308            # Convert the molar fractions to mass fractions
309            for nuclide in abundances.keys():
310                abundances[nuclide] *= atomic_mass(nuclide) / element_am
311
312            # Normalize the mass fractions to one
313            sum_abundances = sum(abundances.values())
314            for nuclide in abundances.keys():
315                abundances[nuclide] /= sum_abundances
316
317        # Create a list of the isotopes in this element
318        isotopes = []
319        for nuclide, abundance in abundances.items():
320            isotopes.append((nuclide, percent * abundance, percent_type))
321
322        return isotopes
323