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