mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-26 13:15:39 -04:00
590 lines
19 KiB
Python
590 lines
19 KiB
Python
from collections import Iterable
|
|
from copy import deepcopy
|
|
from numbers import Real, Integral
|
|
import warnings
|
|
from xml.etree import ElementTree as ET
|
|
import sys
|
|
if sys.version_info[0] >= 3:
|
|
basestring = str
|
|
|
|
import openmc
|
|
from openmc.checkvalue import check_type, check_value, check_greater_than
|
|
from openmc.clean_xml import *
|
|
|
|
|
|
# A list of all IDs for all Materials created
|
|
MATERIAL_IDS = []
|
|
|
|
# A static variable for auto-generated Material IDs
|
|
AUTO_MATERIAL_ID = 10000
|
|
|
|
|
|
def reset_auto_material_id():
|
|
global AUTO_MATERIAL_ID, MATERIAL_IDS
|
|
AUTO_MATERIAL_ID = 10000
|
|
MATERIAL_IDS = []
|
|
|
|
|
|
# Units for density supported by OpenMC
|
|
DENSITY_UNITS = ['g/cm3', 'g/cc', 'kg/cm3', 'atom/b-cm', 'atom/cm3', 'sum']
|
|
|
|
# Constant for density when not needed
|
|
NO_DENSITY = 99999.
|
|
|
|
|
|
class Material(object):
|
|
"""A material composed of a collection of nuclides/elements that can be assigned
|
|
to a region of space.
|
|
|
|
Parameters
|
|
----------
|
|
material_id : int, optional
|
|
Unique identifier for the material. If not specified, an identifier will
|
|
automatically be assigned.
|
|
name : str, optional
|
|
Name of the material. If not specified, the name will be the empty
|
|
string.
|
|
|
|
Attributes
|
|
----------
|
|
id : int
|
|
Unique identifier for the material
|
|
density : float
|
|
Density of the material (units defined separately)
|
|
density_units : str
|
|
Units used for `density`. Can be one of 'g/cm3', 'g/cc', 'kg/cm3',
|
|
'atom/b-cm', 'atom/cm3', or 'sum'.
|
|
|
|
"""
|
|
|
|
def __init__(self, material_id=None, name=''):
|
|
# Initialize class attributes
|
|
self.id = material_id
|
|
self.name = name
|
|
self._density = None
|
|
self._density_units = ''
|
|
|
|
# A dictionary of Nuclides
|
|
# Keys - Nuclide names
|
|
# Values - tuple (nuclide, percent, percent type)
|
|
self._nuclides = {}
|
|
|
|
# A dictionary of Elements
|
|
# Keys - Element names
|
|
# Values - tuple (element, percent, percent type)
|
|
self._elements = {}
|
|
|
|
# If specified, a list of tuples of (table name, xs identifier)
|
|
self._sab = []
|
|
|
|
# If true, the material will be initialized as distributed
|
|
self._convert_to_distrib_comps = False
|
|
|
|
# If specified, this file will be used instead of composition values
|
|
self._distrib_otf_file = None
|
|
|
|
@property
|
|
def id(self):
|
|
return self._id
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@property
|
|
def density(self):
|
|
return self._density
|
|
|
|
@property
|
|
def density_units(self):
|
|
return self._density_units
|
|
|
|
@property
|
|
def convert_to_distrib_comps(self):
|
|
return self._convert_to_distrib_comps
|
|
|
|
@property
|
|
def distrib_otf_file(self):
|
|
return self._distrib_otf_file
|
|
|
|
@id.setter
|
|
def id(self, material_id):
|
|
global AUTO_MATERIAL_ID, MATERIAL_IDS
|
|
|
|
# If the Material already has an ID, remove it from global list
|
|
if hasattr(self, '_id') and self._id is not None:
|
|
MATERIAL_IDS.remove(self._id)
|
|
|
|
if material_id is None:
|
|
self._id = AUTO_MATERIAL_ID
|
|
MATERIAL_IDS.append(AUTO_MATERIAL_ID)
|
|
AUTO_MATERIAL_ID += 1
|
|
else:
|
|
check_type('material ID', material_id, Integral)
|
|
if material_id in MATERIAL_IDS:
|
|
msg = 'Unable to set Material ID to "{0}" since a Material with ' \
|
|
'this ID was already initialized'.format(material_id)
|
|
raise ValueError(msg)
|
|
check_greater_than('material ID', material_id, 0)
|
|
|
|
self._id = material_id
|
|
MATERIAL_IDS.append(material_id)
|
|
|
|
@name.setter
|
|
def name(self, name):
|
|
check_type('name for Material ID="{0}"'.format(self._id),
|
|
name, basestring)
|
|
self._name = name
|
|
|
|
def set_density(self, units, density=NO_DENSITY):
|
|
"""Set the density of the material
|
|
|
|
Parameters
|
|
----------
|
|
units : str
|
|
Physical units of density
|
|
density : float, optional
|
|
Value of the density. Must be specified unless units is given as
|
|
'sum'.
|
|
|
|
"""
|
|
|
|
check_type('the density for Material ID="{0}"'.format(self._id),
|
|
density, Real)
|
|
check_value('density units', units, DENSITY_UNITS)
|
|
|
|
if density == NO_DENSITY and units is not 'sum':
|
|
msg = 'Unable to set the density Material ID="{0}" ' \
|
|
'because a density must be set when not using ' \
|
|
'sum unit'.format(self._id)
|
|
raise ValueError(msg)
|
|
|
|
self._density = density
|
|
self._density_units = units
|
|
|
|
@distrib_otf_file.setter
|
|
def distrib_otf_file(self, filename):
|
|
# TODO: remove this when distributed materials are merged
|
|
warnings.warn('This feature is not yet implemented in a release '
|
|
'version of openmc')
|
|
|
|
if not isinstance(filename, basestring) and filename is not None:
|
|
msg = 'Unable to add OTF material file to Material ID="{0}" with a ' \
|
|
'non-string name "{1}"'.format(self._id, filename)
|
|
raise ValueError(msg)
|
|
|
|
self._distrib_otf_file = filename
|
|
|
|
@convert_to_distrib_comps.setter
|
|
def convert_to_distrib_comps(self):
|
|
# TODO: remove this when distributed materials are merged
|
|
warnings.warn('This feature is not yet implemented in a release '
|
|
'version of openmc')
|
|
|
|
self._convert_to_distrib_comps = True
|
|
|
|
def add_nuclide(self, nuclide, percent, percent_type='ao'):
|
|
"""Add a nuclide to the material
|
|
|
|
Parameters
|
|
----------
|
|
nuclide : str or openmc.nuclide.Nuclide
|
|
Nuclide to add
|
|
percent : float
|
|
Atom or weight percent
|
|
percent_type : str
|
|
'ao' for atom percent and 'wo' for weight percent
|
|
|
|
"""
|
|
|
|
if not isinstance(nuclide, (openmc.Nuclide, str)):
|
|
msg = 'Unable to add a Nuclide to Material ID="{0}" with a ' \
|
|
'non-Nuclide value "{1}"'.format(self._id, nuclide)
|
|
raise ValueError(msg)
|
|
|
|
elif not isinstance(percent, Real):
|
|
msg = 'Unable to add a Nuclide to Material ID="{0}" with a ' \
|
|
'non-floating point value "{1}"'.format(self._id, percent)
|
|
raise ValueError(msg)
|
|
|
|
elif percent_type not in ['ao', 'wo', 'at/g-cm']:
|
|
msg = 'Unable to add a Nuclide to Material ID="{0}" with a ' \
|
|
'percent type "{1}"'.format(self._id, percent_type)
|
|
raise ValueError(msg)
|
|
|
|
if isinstance(nuclide, openmc.Nuclide):
|
|
# Copy this Nuclide to separate it from the Nuclide in
|
|
# other Materials
|
|
nuclide = deepcopy(nuclide)
|
|
else:
|
|
nuclide = openmc.Nuclide(nuclide)
|
|
|
|
self._nuclides[nuclide._name] = (nuclide, percent, percent_type)
|
|
|
|
def remove_nuclide(self, nuclide):
|
|
"""Remove a nuclide from the material
|
|
|
|
Parameters
|
|
----------
|
|
nuclide : openmc.nuclide.Nuclide
|
|
Nuclide to remove
|
|
|
|
"""
|
|
|
|
if not isinstance(nuclide, openmc.Nuclide):
|
|
msg = 'Unable to remove a Nuclide "{0}" in Material ID="{1}" ' \
|
|
'since it is not a Nuclide'.format(self._id, nuclide)
|
|
raise ValueError(msg)
|
|
|
|
# If the Material contains the Nuclide, delete it
|
|
if nuclide._name in self._nuclides:
|
|
del self._nuclides[nuclide._name]
|
|
|
|
def add_element(self, element, percent, percent_type='ao'):
|
|
"""Add a natural element to the material
|
|
|
|
Parameters
|
|
----------
|
|
element : openmc.element.Element
|
|
Element to add
|
|
percent : float
|
|
Atom or weight percent
|
|
percent_type : str
|
|
'ao' for atom percent and 'wo' for weight percent
|
|
|
|
"""
|
|
|
|
if not isinstance(element, openmc.Element):
|
|
msg = 'Unable to add an Element to Material ID="{0}" with a ' \
|
|
'non-Element value "{1}"'.format(self._id, element)
|
|
raise ValueError(msg)
|
|
|
|
if not isinstance(percent, Real):
|
|
msg = 'Unable to add an Element to Material ID="{0}" with a ' \
|
|
'non-floating point value "{1}"'.format(self._id, percent)
|
|
raise ValueError(msg)
|
|
|
|
if percent_type not in ['ao', 'wo']:
|
|
msg = 'Unable to add an Element to Material ID="{0}" with a ' \
|
|
'percent type "{1}"'.format(self._id, percent_type)
|
|
raise ValueError(msg)
|
|
|
|
# Copy this Element to separate it from same Element in other Materials
|
|
element = deepcopy(element)
|
|
|
|
self._elements[element._name] = (element, percent, percent_type)
|
|
|
|
def remove_element(self, element):
|
|
"""Remove a natural element from the material
|
|
|
|
Parameters
|
|
----------
|
|
element : openmc.element.Element
|
|
Element to remove
|
|
|
|
"""
|
|
|
|
# If the Material contains the Element, delete it
|
|
if element._name in self._elements:
|
|
del self._elements[element._name]
|
|
|
|
def add_s_alpha_beta(self, name, xs):
|
|
r"""Add an :math:`S(\alpha,\beta)` table to the material
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
Name of the :math:`S(\alpha,\beta)` table
|
|
xs : str
|
|
Cross section identifier, e.g. '71t'
|
|
|
|
"""
|
|
|
|
if not isinstance(name, basestring):
|
|
msg = 'Unable to add an S(a,b) table to Material ID="{0}" with a ' \
|
|
'non-string table name "{1}"'.format(self._id, name)
|
|
raise ValueError(msg)
|
|
|
|
if not isinstance(xs, basestring):
|
|
msg = 'Unable to add an S(a,b) table to Material ID="{0}" with a ' \
|
|
'non-string cross-section identifier "{1}"'.format(self._id, xs)
|
|
raise ValueError(msg)
|
|
|
|
self._sab.append((name, xs))
|
|
|
|
def get_all_nuclides(self):
|
|
"""Returns all nuclides in the material
|
|
|
|
Returns
|
|
-------
|
|
nuclides : dict
|
|
Dictionary whose keys are nuclide names and values are 2-tuples of
|
|
(nuclide, density)
|
|
|
|
"""
|
|
|
|
nuclides = {}
|
|
|
|
for nuclide_name, nuclide_tuple in self._nuclides.items():
|
|
nuclide = nuclide_tuple[0]
|
|
density = nuclide_tuple[1]
|
|
nuclides[nuclide._name] = (nuclide, density)
|
|
|
|
return nuclides
|
|
|
|
def __repr__(self):
|
|
string = 'Material\n'
|
|
string += '{0: <16}{1}{2}\n'.format('\tID', '=\t', self._id)
|
|
string += '{0: <16}{1}{2}\n'.format('\tName', '=\t', self._name)
|
|
|
|
string += '{0: <16}{1}{2}'.format('\tDensity', '=\t', self._density)
|
|
string += ' [{0}]\n'.format(self._density_units)
|
|
|
|
string += '{0: <16}\n'.format('\tS(a,b) Tables')
|
|
|
|
for sab in self._sab:
|
|
string += '{0: <16}{1}[{2}{3}]\n'.format('\tS(a,b)', '=\t',
|
|
sab[0], sab[1])
|
|
|
|
string += '{0: <16}\n'.format('\tNuclides')
|
|
|
|
for nuclide in self._nuclides:
|
|
percent = self._nuclides[nuclide][1]
|
|
percent_type = self._nuclides[nuclide][2]
|
|
string += '{0: <16}'.format('\t{0}'.format(nuclide))
|
|
string += '=\t{0: <12} [{1}]\n'.format(percent, percent_type)
|
|
|
|
string += '{0: <16}\n'.format('\tElements')
|
|
|
|
for element in self._elements:
|
|
percent = self._nuclides[element][1]
|
|
percent_type = self._nuclides[element][2]
|
|
string += '{0: >16}'.format('\t{0}'.format(element))
|
|
string += '=\t{0: <12} [{1}]\n'.format(percent, percent_type)
|
|
|
|
return string
|
|
|
|
def _get_nuclide_xml(self, nuclide, distrib=False):
|
|
xml_element = ET.Element("nuclide")
|
|
xml_element.set("name", nuclide[0]._name)
|
|
|
|
if not distrib:
|
|
if nuclide[2] is 'ao':
|
|
xml_element.set("ao", str(nuclide[1]))
|
|
else:
|
|
xml_element.set("wo", str(nuclide[1]))
|
|
|
|
if nuclide[0]._xs is not None:
|
|
xml_element.set("xs", nuclide[0]._xs)
|
|
|
|
return xml_element
|
|
|
|
def _get_element_xml(self, element, distrib=False):
|
|
xml_element = ET.Element("element")
|
|
xml_element.set("name", str(element[0]._name))
|
|
|
|
if not distrib:
|
|
if element[2] is 'ao':
|
|
xml_element.set("ao", str(element[1]))
|
|
else:
|
|
xml_element.set("wo", str(element[1]))
|
|
|
|
return xml_element
|
|
|
|
def _get_nuclides_xml(self, nuclides, distrib=False):
|
|
xml_elements = []
|
|
|
|
for nuclide in nuclides.values():
|
|
xml_elements.append(self._get_nuclide_xml(nuclide, distrib))
|
|
|
|
return xml_elements
|
|
|
|
def _get_elements_xml(self, elements, distrib=False):
|
|
xml_elements = []
|
|
|
|
for element in elements.values():
|
|
xml_elements.append(self._get_element_xml(element, distrib))
|
|
|
|
return xml_elements
|
|
|
|
def get_material_xml(self):
|
|
"""Return XML representation of the material
|
|
|
|
Returns
|
|
-------
|
|
element : xml.etree.ElementTree.Element
|
|
XML element containing material data
|
|
|
|
"""
|
|
|
|
# Create Material XML element
|
|
element = ET.Element("material")
|
|
element.set("id", str(self._id))
|
|
|
|
if len(self._name) > 0:
|
|
element.set("name", str(self._name))
|
|
|
|
# Create density XML subelement
|
|
subelement = ET.SubElement(element, "density")
|
|
if self._density_units is not 'sum':
|
|
subelement.set("value", str(self._density))
|
|
subelement.set("units", self._density_units)
|
|
|
|
if not self._convert_to_distrib_comps:
|
|
# Create nuclide XML subelements
|
|
subelements = self._get_nuclides_xml(self._nuclides)
|
|
for subelement in subelements:
|
|
element.append(subelement)
|
|
|
|
# Create element XML subelements
|
|
subelements = self._get_elements_xml(self._elements)
|
|
for subelement in subelements:
|
|
element.append(subelement)
|
|
|
|
else:
|
|
subelement = ET.SubElement(element, "compositions")
|
|
|
|
comps = []
|
|
allnucs = self._nuclides.values() + self._elements.values()
|
|
dist_per_type = allnucs[0][2]
|
|
for nuc, per, typ in allnucs:
|
|
if not typ == dist_per_type:
|
|
msg = 'All nuclides and elements in a distributed ' \
|
|
'material must have the same type, either ao or wo'
|
|
raise ValueError(msg)
|
|
comps.append(per)
|
|
|
|
if self._distrib_otf_file is None:
|
|
# Create values and units subelements
|
|
subsubelement = ET.SubElement(subelement, "values")
|
|
subsubelement.text = ' '.join([str(c) for c in comps])
|
|
subsubelement = ET.SubElement(subelement, "units")
|
|
subsubelement.text = dist_per_type
|
|
else:
|
|
# Specify the materials file
|
|
subsubelement = ET.SubElement(subelement, "otf_file_path")
|
|
subsubelement.text = self._distrib_otf_file
|
|
|
|
# Create nuclide XML subelements
|
|
subelements = self.get_nuclides_xml(self._nuclides, distrib=True)
|
|
for subelement_nuc in subelements:
|
|
subelement.append(subelement_nuc)
|
|
|
|
# Create element XML subelements
|
|
subelements = self._get_elements_xml(self._elements, distrib=True)
|
|
for subelement_ele in subelements:
|
|
subelement.append(subelement_ele)
|
|
|
|
if len(self._sab) > 0:
|
|
for sab in self._sab:
|
|
subelement = ET.SubElement(element, "sab")
|
|
subelement.set("name", sab[0])
|
|
subelement.set("xs", sab[1])
|
|
|
|
return element
|
|
|
|
|
|
class MaterialsFile(object):
|
|
"""Materials file used for an OpenMC simulation. Corresponds directly to the
|
|
materials.xml input file.
|
|
|
|
Attributes
|
|
----------
|
|
default_xs : str
|
|
The default cross section identifier applied to a nuclide when none is
|
|
specified
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
|
# Initialize MaterialsFile class attributes
|
|
self._materials = []
|
|
self._default_xs = None
|
|
self._materials_file = ET.Element("materials")
|
|
|
|
@property
|
|
def default_xs(self):
|
|
return self._default_xs
|
|
|
|
@default_xs.setter
|
|
def default_xs(self, xs):
|
|
check_type('default xs', xs, basestring)
|
|
self._default_xs = xs
|
|
|
|
def add_material(self, material):
|
|
"""Add a material to the file.
|
|
|
|
Parameters
|
|
----------
|
|
material : Material
|
|
Material to add
|
|
|
|
"""
|
|
|
|
if not isinstance(material, Material):
|
|
msg = 'Unable to add a non-Material "{0}" to the ' \
|
|
'MaterialsFile'.format(material)
|
|
raise ValueError(msg)
|
|
|
|
self._materials.append(material)
|
|
|
|
def add_materials(self, materials):
|
|
"""Add multiple materials to the file.
|
|
|
|
Parameters
|
|
----------
|
|
materials : tuple or list of Material
|
|
Materials to add
|
|
|
|
"""
|
|
|
|
if not isinstance(materials, Iterable):
|
|
msg = 'Unable to create OpenMC materials.xml file from "{0}" which ' \
|
|
'is not iterable'.format(materials)
|
|
raise ValueError(msg)
|
|
|
|
for material in materials:
|
|
self.add_material(material)
|
|
|
|
def remove_material(self, material):
|
|
"""Remove a material from the file
|
|
|
|
Parameters
|
|
----------
|
|
material : Material
|
|
Material to remove
|
|
|
|
"""
|
|
|
|
if not isinstance(material, Material):
|
|
msg = 'Unable to remove a non-Material "{0}" from the ' \
|
|
'MaterialsFile'.format(material)
|
|
raise ValueError(msg)
|
|
|
|
self._materials.remove(material)
|
|
|
|
def _create_material_subelements(self):
|
|
subelement = ET.SubElement(self._materials_file, "default_xs")
|
|
|
|
if self._default_xs is not None:
|
|
subelement.text = self._default_xs
|
|
|
|
for material in self._materials:
|
|
xml_element = material.get_material_xml()
|
|
self._materials_file.append(xml_element)
|
|
|
|
def export_to_xml(self):
|
|
"""Create a materials.xml file that can be used for a simulation.
|
|
|
|
"""
|
|
|
|
self._create_material_subelements()
|
|
|
|
# Clean the indentation in the file to be user-readable
|
|
sort_xml_elements(self._materials_file)
|
|
clean_xml_indentation(self._materials_file)
|
|
|
|
# Write the XML Tree to the materials.xml file
|
|
tree = ET.ElementTree(self._materials_file)
|
|
tree.write("materials.xml", xml_declaration=True,
|
|
encoding='utf-8', method="xml")
|