diff --git a/docs/source/usersguide/input.rst b/docs/source/usersguide/input.rst index 78f051145a..0244b75fc3 100644 --- a/docs/source/usersguide/input.rst +++ b/docs/source/usersguide/input.rst @@ -1151,14 +1151,17 @@ Each ``material`` element can have the following attributes or sub-elements: The "sum" unit indicates that the density should be calculated as the sum of the atom fractions for each nuclide in the material. This should not be used in conjunction with weight percents. The "macro" unit is used with - a ``macroscopic`` to indicate that the density is already included in the - library and thus not needed here. However, if a value is provided for the - ``value``, then this is treated as a number density multiplier on the - macroscopic cross sections in the multi-group data. This can be used, + a ``macroscopic`` quantity to indicate that the density is already included + in the library and thus not needed here. However, if a value is provided + for the ``value``, then this is treated as a number density multiplier on + the macroscopic cross sections in the multi-group data. This can be used, for example, when perturbing the density slightly. *Default*: None + .. note:: A ``macroscopic`` quantity can not be used in conjunction with a + ``nuclide``, ``element``, or ``sab`` quantity. + :nuclide: An element with attributes/sub-elements called ``name``, ``xs``, and ``ao`` or ``wo``. The ``name`` attribute is the name of the cross-section for a diff --git a/openmc/macroscopic.py b/openmc/macroscopic.py new file mode 100644 index 0000000000..094fa50421 --- /dev/null +++ b/openmc/macroscopic.py @@ -0,0 +1,85 @@ +from numbers import Integral +import sys + +from openmc.checkvalue import check_type + +if sys.version_info[0] >= 3: + basestring = str + + +class Macroscopic(object): + """A nuclide that can be used in a material. + + Parameters + ---------- + name : str + Name of the macroscopic data, e.g. UO2 + xs : str + Cross section identifier, e.g. 71c + + Attributes + ---------- + name : str + Name of the nuclide, e.g. UO2 + xs : str + Cross section identifier, e.g. 71c + + """ + + def __init__(self, name='', xs=None): + # Initialize class attributes + self._name = '' + self._xs = None + + # Set the Material class attributes + self.name = name + + if xs is not None: + self.xs = xs + + def __eq__(self, other): + if isinstance(other, Macroscopic): + if self._name != other._name: + return False + elif self._xs != other._xs: + return False + else: + return True + elif isinstance(other, basestring) and other == self.name: + return True + else: + return False + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self._name, self._xs)) + + def __repr__(self): + string = 'Nuclide - {0}\n'.format(self._name) + string += '{0: <16}{1}{2}\n'.format('\tXS', '=\t', self._xs) + return string + + @property + def name(self): + return self._name + + @property + def xs(self): + return self._xs + + @name.setter + def name(self, name): + check_type('name', name, basestring) + self._name = name + + @xs.setter + def xs(self, xs): + check_type('cross-section identifier', xs, basestring) + self._xs = xs + + def __repr__(self): + string = 'Macroscopic - {0}\n'.format(self._name) + string += '{0: <16}{1}{2}\n'.format('\tXS', '=\t', self.xs) + return string diff --git a/openmc/material.py b/openmc/material.py index 292fc82ca7..27e7c64ff8 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -26,14 +26,15 @@ def reset_auto_material_id(): # Units for density supported by OpenMC -DENSITY_UNITS = ['g/cm3', 'g/cc', 'kg/cm3', 'atom/b-cm', 'atom/cm3', 'sum'] +DENSITY_UNITS = ['g/cm3', 'g/cc', 'kg/cm3', 'atom/b-cm', 'atom/cm3', 'sum', + 'macro'] # Constant for density when not needed NO_DENSITY = 99999. class Material(object): - """A material composed of a collection of nuclides/elements that can be + """A material composed of a collection of nuclides/elements that can be assigned to a region of space. Parameters @@ -53,7 +54,8 @@ class Material(object): 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'. + 'atom/b-cm', 'atom/cm3', 'sum', or 'macro' (the latter only applies + if in multi-group mode). """ @@ -69,6 +71,10 @@ class Material(object): # Values - tuple (nuclide, percent, percent type) self._nuclides = OrderedDict() + # The single instance of Macroscopic data present in this material + # (only one is allowed, hence this is different than _nuclides, etc) + self._macroscopic = None + # An ordered dictionary of Elements (order affects OpenMC results) # Keys - Element names # Values - tuple (element, percent, percent type) @@ -105,6 +111,10 @@ class Material(object): string += '{0: <16}'.format('\t{0}'.format(nuclide)) string += '=\t{0: <12} [{1}]\n'.format(percent, percent_type) + if self._macroscopic is not None: + string += '{0: <16}\n'.format('\tMacroscopic Data') + string += '{0: <16}'.format('\t{0}'.format(self._macroscopic)) + string += '{0: <16}\n'.format('\tElements') for element in self._elements: @@ -126,6 +136,7 @@ class Material(object): clone._density = self._density clone._density_units = self._density_units clone._nuclides = deepcopy(self._nuclides, memo) + clone._macroscopic = self._macroscopic clone._elements = deepcopy(self._elements, memo) clone._sab = deepcopy(self._sab, memo) clone._convert_to_distrib_comps = self._convert_to_distrib_comps @@ -256,6 +267,11 @@ class Material(object): """ + if self._macroscopic is not None: + msg = 'Unable to add a Nuclide to Material ID="{0}" as a ' \ + 'macroscopic data-set has already been added'.format(self._id) + raise ValueError(msg) + 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) @@ -299,6 +315,66 @@ class Material(object): if nuclide._name in self._nuclides: del self._nuclides[nuclide._name] + def add_macroscopic(self, macroscopic): + """Add a macroscopic to the material + + Parameters + ---------- + macroscopic : str or openmc.macroscopic.Macroscopic + Macroscopic to add + + """ + + # Ensureno nuclides, elements, or sab are added since these would be + # incompatible with macroscopics + if (not. self._nuclides) and (not self._elements) and (not self._sab): + msg = 'Unable to add a Macroscopic data set to Material ID="{0}" ' \ + 'with a macroscopic value "{1}" as an incompatible data ' \ + 'member (i.e., nuclide, element, or S(a,b) table) ' \ + 'has already been added'.format(self._id, macroscopic) + raise ValueError(msg) + + + + if not isinstance(macroscopic, (openmc.Macroscopic, str)): + msg = 'Unable to add a Macroscopic to Material ID="{0}" with a ' \ + 'non-Macroscopic value "{1}"'.format(self._id, macroscopic) + raise ValueError(msg) + + if isinstance(macroscopic, openmc.Macroscopic): + # Copy this Macroscopic to separate it from the Macroscopic in + # other Materials + macroscopic = deepcopy(macroscopic) + else: + macroscopic = openmc.Macroscopic(macroscopic) + + if self._macroscopic is not None: + self._macroscopic = macroscopic + else: + msg = 'Unable to add a Macroscopic to Material ID="{0}", ' \ + 'Only One Macroscopic allowed per ' \ + 'Material!'.format(self._id, macroscopic) + raise ValueError(msg) + + def remove_macroscopic(self, macroscopic): + """Remove a macroscopic from the material + + Parameters + ---------- + macroscopic : openmc.macroscopic.Macroscopic + Macroscopic to remove + + """ + + if not isinstance(macroscopic, openmc.Macroscopic): + msg = 'Unable to remove a Macroscopic "{0}" in Material ID="{1}" ' \ + 'since it is not a Macroscopic'.format(self._id, macroscopic) + raise ValueError(msg) + + # If the Material contains the Macroscopic, delete it + if macroscopic._name == self._macroscopic.name: + self._macroscopic = None + def add_element(self, element, percent, percent_type='ao'): """Add a natural element to the material @@ -313,6 +389,11 @@ class Material(object): """ + if self._macroscopic is not None: + msg = 'Unable to add an Element to Material ID="{0}" as a ' \ + 'macroscopic data-set has already been added'.format(self._id) + raise ValueError(msg) + 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) @@ -359,6 +440,11 @@ class Material(object): """ + if self._macroscopic is not None: + msg = 'Unable to add an S(a,b) table to Material ID="{0}" as a ' \ + 'macroscopic data-set has already been added'.format(self._id) + raise ValueError(msg) + 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) @@ -415,6 +501,15 @@ class Material(object): return xml_element + def _get_macroscopic_xml(self, macroscopic, distrib=False): + xml_element = ET.Element("macroscopic") + xml_element.set("name", macroscopic[0]._name) + + if macroscopic[0].xs is not None: + xml_element.set("xs", macroscopic[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)) @@ -470,14 +565,19 @@ class Material(object): 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) + if self._macroscopic is None: + # 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: + # Create element XML subelements + subelements = self._get_elements_xml(self._elements) + for subelement in subelements: + element.append(subelement) + else: + # Create macroscopic XML subelements + subelement = self._get_macroscopic_xml(self, self._macroscopic) element.append(subelement) else: @@ -504,14 +604,21 @@ class Material(object): 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) + if self._macroscopic is None: + # 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: + # Create element XML subelements + subelements = self._get_elements_xml(self._elements, distrib=True) + for subelement_ele in subelements: + subelement.append(subelement_ele) + else: + # Create macroscopic XML subelements + subelement_ele = self._get_macroscopic_xml(self, + self._macroscopic, + distrib=True) subelement.append(subelement_ele) if len(self._sab) > 0: diff --git a/openmc/settings.py b/openmc/settings.py index 519b5c7cf2..2e09e70f44 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -67,11 +67,17 @@ class SettingsFile(object): cross_sections : str Indicates the path to an XML cross section listing file (usually named cross_sections.xml). If it is not set, the :envvar:`CROSS_SECTIONS` - environment variable will be used to find the path to the XML cross - section listing. + environment variable will be used for continuous-energy calculations + and :envvar:`MG_CROSS_SECTIONS` will be used for multi-group + calculations to find the path to the XML cross section file. energy_grid : str Set the method used to search energy grids. Acceptable values are 'nuclide', 'logarithm', and 'material-union'. + energy_mode : str + Set whether the calculation should be continuous-energy or multi-group. + Acceptable values are 'continuous-energy' or 'multi-group' + max_order : int + Maximum scattering order to apply globally when in multi-grup mode. ptables : bool Determine whether probability tables are used. run_cmfd : bool @@ -129,6 +135,10 @@ class SettingsFile(object): self._particles = None self._keff_trigger = None + # Energy mode subelement + self._energy_mode = None + self._max_order = None + # Source subelement self._source_subelement = None self._source_file = None @@ -219,6 +229,14 @@ class SettingsFile(object): def keff_trigger(self): return self._keff_trigger + @property + def energy_mode(self): + return self._energy_mode + + @property + def max_order(self): + return self._max_order + @property def source_file(self): return self._source_file @@ -452,6 +470,18 @@ class SettingsFile(object): self._keff_trigger = keff_trigger + @energy_mode.setter + def energy_mode(self, energy_mode): + check_value('energy mode', energy_mode, + ['continuous-energy', 'multi-group']) + self._energy_mode = energy_mode + + @max_order.setter + def max_order(self, max_order): + check_type('maximum scattering order', max_order, Integral) + check_greater_than('maximum scattering order', max_order, 0) + self._max_order = max_order + @source_file.setter def source_file(self, source_file): check_type('source file', source_file, basestring) @@ -917,6 +947,16 @@ class SettingsFile(object): subelement = ET.SubElement(element, key) subelement.text = str(self._keff_trigger[key]).lower() + def _create_energy_mode_subelement(self): + if self._energy_mode is not None: + element = ET.SubElement(self._settings_file, "energy_mode") + element.text = str(self._energy_mode) + + def _create_max_order_subelement(self): + if self._max_order is not None: + element = ET.SubElement(self._settings_file, "max_order") + element.text = str(self._max_order) + def _create_source_subelement(self): self._create_source_space_subelement() self._create_source_energy_subelement() @@ -1186,6 +1226,8 @@ class SettingsFile(object): self._source_element = None self._create_eigenvalue_subelement() + self._create_energy_mode_subelement() + self._create_max_order_subelement() self._create_source_subelement() self._create_output_subelement() self._create_statepoint_subelement() diff --git a/openmc/summary.py b/openmc/summary.py index 4b1088e827..f7077efab5 100644 --- a/openmc/summary.py +++ b/openmc/summary.py @@ -58,6 +58,9 @@ class Summary(object): # Read date and time self.date_and_time = self._f['date_and_time'][...] + # Read if continuous-energy or multi-group + self.run_CE = bool(self._f['run_CE'].value) + self.n_batches = self._f['n_batches'].value self.n_particles = self._f['n_particles'].value self.n_active = self._f['n_active'].value diff --git a/src/state_point.F90 b/src/state_point.F90 index 7a414f4574..883df4932a 100644 --- a/src/state_point.F90 +++ b/src/state_point.F90 @@ -93,6 +93,11 @@ contains call write_dataset(file_id, "seed", seed) ! Write run information + if (run_CE) then + call write_dataset(file_id, "run_CE", 1) + else + call write_dataset(file_id, "run_CE", 0) + end if select case(run_mode) case (MODE_FIXEDSOURCE) call write_dataset(file_id, "run_mode", "fixed source") @@ -695,6 +700,7 @@ contains integer(HID_T) :: tally_group real(8) :: real_array(3) logical :: source_present + integer :: sp_run_CE character(MAX_WORD_LEN) :: word type(TallyObject), pointer :: tally @@ -719,6 +725,19 @@ contains ! Read and overwrite random number seed call read_dataset(file_id, "seed", seed) + ! It is not impossible for a state point to be generated from a CE run but + ! to be loaded in to an MG run (or vice versa), check to prevent that. + call read_dataset(file_id, "run_CE", sp_run_CE) + if (sp_run_CE == 0) then + if (run_CE) & + call fatal_error("State point file is from multi-group run but & + & current run is continous-energy!") + else if (sp_run_CE == 1) then + if (.not. run_CE) & + call fatal_error("State point file is from continuous-energy run but & + & current run is multi-group!") + end if + ! Read and overwrite run information except number of batches call read_dataset(file_id, "run_mode", word) select case(word) diff --git a/src/summary.F90 b/src/summary.F90 index 078099dbd0..cf6c39e8ff 100644 --- a/src/summary.F90 +++ b/src/summary.F90 @@ -38,6 +38,12 @@ contains ! Write header information call write_header(file_id) + if (run_CE) then + call write_dataset(file_id, "run_CE", 1) + else + call write_dataset(file_id, "run_CE", 0) + end if + ! Write number of particles call write_dataset(file_id, "n_particles", n_particles) call write_dataset(file_id, "n_batches", n_batches)