Merge pull request #2291 from pshriwise/model-xml

Support for a single `model.xml` file
This commit is contained in:
Paul Romano 2022-12-24 13:33:27 -06:00 committed by GitHub
commit 3f8f8f6701
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1284 additions and 372 deletions

View file

@ -1,22 +1,40 @@
def clean_indentation(element, level=0, spaces_per_level=2):
"""
copy and paste from https://effbot.org/zone/element-lib.htm#prettyprint
it basically walks your tree and adds spaces and newlines so the tree is
printed in a nice way
def clean_indentation(element, level=0, spaces_per_level=2, trailing_indent=True):
"""Set indentation of XML element and its sub-elements.
Copied and pasted from https://effbot.org/zone/element-lib.htm#prettyprint.
It walks your tree and adds spaces and newlines so the tree is
printed in a nice way.
Parameters
----------
level : int
Indentation level for the element passed in (default 0)
spaces_per_level : int
Number of spaces per indentation level (default 2)
trailing_indent : bool
Whether or not to add indentation after closing the element
"""
i = "\n" + level*spaces_per_level*" "
# ensure there's always some tail for the element passed in
if not element.tail:
element.tail = ""
if len(element):
if not element.text or not element.text.strip():
element.text = i + spaces_per_level*" "
if not element.tail or not element.tail.strip():
if trailing_indent and (not element.tail or not element.tail.strip()):
element.tail = i
for sub_element in element:
# `trailing_indent` is intentionally not forwarded to the recursive
# call. Any child element of the topmost element should add
# indentation at the end to ensure its parent's indentation is
# correct.
clean_indentation(sub_element, level+1, spaces_per_level)
if not sub_element.tail or not sub_element.tail.strip():
sub_element.tail = i
else:
if level and (not element.tail or not element.tail.strip()):
if trailing_indent and level and (not element.tail or not element.tail.strip()):
element.tail = i

View file

@ -650,7 +650,7 @@ class Cell(IDManagerMixin):
surfaces : dict
Dictionary mapping surface IDs to :class:`openmc.Surface` instances
materials : dict
Dictionary mapping material IDs to :class:`openmc.Material`
Dictionary mapping material ID strings to :class:`openmc.Material`
instances (defined in :math:`openmc.Geometry.from_xml`)
get_universe : function
Function returning universe (defined in

View file

@ -9,7 +9,7 @@ from .plots import _get_plot_image
def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None,
plot=False, restart_file=None, threads=None,
tracks=False, event_based=None,
openmc_exec='openmc', mpi_args=None):
openmc_exec='openmc', mpi_args=None, path_input=None):
"""Converts user-readable flags in to command-line arguments to be run with
the OpenMC executable via subprocess.
@ -42,6 +42,9 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None,
mpi_args : list of str, optional
MPI execute command and any additional MPI arguments to pass,
e.g. ['mpiexec', '-n', '8'].
path_input : str or Pathlike
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.
.. versionadded:: 0.13.0
@ -82,6 +85,9 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None,
if mpi_args is not None:
args = mpi_args + args
if path_input is not None:
args += [path_input]
return args
@ -118,7 +124,7 @@ def _run(args, output, cwd):
raise RuntimeError(error_msg)
def plot_geometry(output=True, openmc_exec='openmc', cwd='.'):
def plot_geometry(output=True, openmc_exec='openmc', cwd='.', path_input=None):
"""Run OpenMC in plotting mode
Parameters
@ -129,6 +135,9 @@ def plot_geometry(output=True, openmc_exec='openmc', cwd='.'):
Path to OpenMC executable
cwd : str, optional
Path to working directory to run in
path_input : str
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.
Raises
------
@ -136,10 +145,13 @@ def plot_geometry(output=True, openmc_exec='openmc', cwd='.'):
If the `openmc` executable returns a non-zero status
"""
_run([openmc_exec, '-p'], output, cwd)
args = [openmc_exec, '-p']
if path_input is not None:
args += [path_input]
_run(args, output, cwd)
def plot_inline(plots, openmc_exec='openmc', cwd='.'):
def plot_inline(plots, openmc_exec='openmc', cwd='.', path_input=None):
"""Display plots inline in a Jupyter notebook.
.. versionchanged:: 0.13.0
@ -155,6 +167,9 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.'):
Path to OpenMC executable
cwd : str, optional
Path to working directory to run in
path_input : str
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.
Raises
------
@ -171,7 +186,7 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.'):
openmc.Plots(plots).export_to_xml(cwd)
# Run OpenMC in geometry plotting mode
plot_geometry(False, openmc_exec, cwd)
plot_geometry(False, openmc_exec, cwd, path_input)
if plots is not None:
images = [_get_plot_image(p, cwd) for p in plots]
@ -179,7 +194,8 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.'):
def calculate_volumes(threads=None, output=True, cwd='.',
openmc_exec='openmc', mpi_args=None):
openmc_exec='openmc', mpi_args=None,
path_input=None):
"""Run stochastic volume calculations in OpenMC.
This function runs OpenMC in stochastic volume calculation mode. To specify
@ -210,6 +226,10 @@ def calculate_volumes(threads=None, output=True, cwd='.',
cwd : str, optional
Path to working directory to run in. Defaults to the current working
directory.
path_input : str or Pathlike
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.
Raises
------
@ -223,14 +243,16 @@ def calculate_volumes(threads=None, output=True, cwd='.',
"""
args = _process_CLI_arguments(volume=True, threads=threads,
openmc_exec=openmc_exec, mpi_args=mpi_args)
openmc_exec=openmc_exec, mpi_args=mpi_args,
path_input=path_input)
_run(args, output, cwd)
def run(particles=None, threads=None, geometry_debug=False,
restart_file=None, tracks=False, output=True, cwd='.',
openmc_exec='openmc', mpi_args=None, event_based=False):
openmc_exec='openmc', mpi_args=None, event_based=False,
path_input=None):
"""Run an OpenMC simulation.
Parameters
@ -239,17 +261,17 @@ def run(particles=None, threads=None, geometry_debug=False,
Number of particles to simulate per generation.
threads : int, optional
Number of OpenMP threads. If OpenMC is compiled with OpenMP threading
enabled, the default is implementation-dependent but is usually equal
to the number of hardware threads available (or a value set by the
enabled, the default is implementation-dependent but is usually equal to
the number of hardware threads available (or a value set by the
:envvar:`OMP_NUM_THREADS` environment variable).
geometry_debug : bool, optional
Turn on geometry debugging during simulation. Defaults to False.
restart_file : str, optional
Path to restart file to use
tracks : bool, optional
Enables the writing of particles tracks. The number of particle
tracks written to tracks.h5 is limited to 1000 unless
Settings.max_tracks is set. Defaults to False.
Enables the writing of particles tracks. The number of particle tracks
written to tracks.h5 is limited to 1000 unless Settings.max_tracks is
set. Defaults to False.
output : bool
Capture OpenMC output from standard out
cwd : str, optional
@ -258,13 +280,17 @@ def run(particles=None, threads=None, geometry_debug=False,
openmc_exec : str, optional
Path to OpenMC executable. Defaults to 'openmc'.
mpi_args : list of str, optional
MPI execute command and any additional MPI arguments to pass,
e.g. ['mpiexec', '-n', '8'].
MPI execute command and any additional MPI arguments to pass, e.g.
['mpiexec', '-n', '8'].
event_based : bool, optional
Turns on event-based parallelism, instead of default history-based
.. versionadded:: 0.12
path_input : str or Pathlike
Path to a single XML file or a directory containing XML files for the
OpenMC executable to read.
Raises
------
RuntimeError
@ -275,6 +301,7 @@ def run(particles=None, threads=None, geometry_debug=False,
args = _process_CLI_arguments(
volume=False, geometry_debug=geometry_debug, particles=particles,
restart_file=restart_file, threads=threads, tracks=tracks,
event_based=event_based, openmc_exec=openmc_exec, mpi_args=mpi_args)
event_based=event_based, openmc_exec=openmc_exec, mpi_args=mpi_args,
path_input=path_input)
_run(args, output, cwd)

View file

@ -103,6 +103,39 @@ class Geometry:
if universe.id in volume_calc.volumes:
universe.add_volume_information(volume_calc)
def to_xml_element(self, remove_surfs=False):
"""Creates a 'geometry' element to be written to an XML file.
Parameters
----------
remove_surfs : bool
Whether or not to remove redundant surfaces from the geometry when
exporting
"""
# Find and remove redundant surfaces from the geometry
if remove_surfs:
warnings.warn("remove_surfs kwarg will be deprecated soon, please "
"set the Geometry.merge_surfaces attribute instead.")
self.merge_surfaces = True
if self.merge_surfaces:
self.remove_redundant_surfaces()
# Create XML representation
element = ET.Element("geometry")
self.root_universe.create_xml_subelement(element, memo=set())
# Sort the elements in the file
element[:] = sorted(element, key=lambda x: (
x.tag, int(x.get('id'))))
# Clean the indentation in the file to be user-readable
xml.clean_indentation(element)
xml.reorder_attributes(element) # TODO: Remove when support is Python 3.8+
return element
def export_to_xml(self, path='geometry.xml', remove_surfs=False):
"""Export geometry to an XML file.
@ -117,25 +150,7 @@ class Geometry:
.. versionadded:: 0.12
"""
# Find and remove redundant surfaces from the geometry
if remove_surfs:
warnings.warn("remove_surfs kwarg will be deprecated soon, please "
"set the Geometry.merge_surfaces attribute instead.")
self.merge_surfaces = True
if self.merge_surfaces:
self.remove_redundant_surfaces()
# Create XML representation
root_element = ET.Element("geometry")
self.root_universe.create_xml_subelement(root_element, memo=set())
# Sort the elements in the file
root_element[:] = sorted(root_element, key=lambda x: (
x.tag, int(x.get('id'))))
# Clean the indentation in the file to be user-readable
xml.clean_indentation(root_element)
root_element = self.to_xml_element(remove_surfs)
# Check if path is a directory
p = Path(path)
@ -143,10 +158,101 @@ class Geometry:
p /= 'geometry.xml'
# Write the XML Tree to the geometry.xml file
xml.reorder_attributes(root_element) # TODO: Remove when support is Python 3.8+
tree = ET.ElementTree(root_element)
tree.write(str(p), xml_declaration=True, encoding='utf-8')
@classmethod
def from_xml_element(cls, elem, materials=None):
"""Generate geometry from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
materials : openmc.Materials or None
Materials used to assign to cells. If None, an attempt is made to
generate it from the materials.xml file.
Returns
-------
openmc.Geometry
Geometry object
"""
mats = dict()
if materials is not None:
mats.update({str(m.id): m for m in materials})
mats['void'] = None
# Helper function for keeping a cache of Universe instances
universes = {}
def get_universe(univ_id):
if univ_id not in universes:
univ = openmc.Universe(univ_id)
universes[univ_id] = univ
return universes[univ_id]
# Get surfaces
surfaces = {}
periodic = {}
for surface in elem.findall('surface'):
s = openmc.Surface.from_xml_element(surface)
surfaces[s.id] = s
# Check for periodic surface
other_id = xml.get_text(surface, 'periodic_surface_id')
if other_id is not None:
periodic[s.id] = int(other_id)
# Apply periodic surfaces
for s1, s2 in periodic.items():
surfaces[s1].periodic_surface = surfaces[s2]
# Add any DAGMC universes
for e in elem.findall('dagmc_universe'):
dag_univ = openmc.DAGMCUniverse.from_xml_element(e)
universes[dag_univ.id] = dag_univ
# Dictionary that maps each universe to a list of cells/lattices that
# contain it (needed to determine which universe is the elem)
child_of = defaultdict(list)
for e in elem.findall('lattice'):
lat = openmc.RectLattice.from_xml_element(e, get_universe)
universes[lat.id] = lat
if lat.outer is not None:
child_of[lat.outer].append(lat)
for u in lat.universes.ravel():
child_of[u].append(lat)
for e in elem.findall('hex_lattice'):
lat = openmc.HexLattice.from_xml_element(e, get_universe)
universes[lat.id] = lat
if lat.outer is not None:
child_of[lat.outer].append(lat)
if lat.ndim == 2:
for ring in lat.universes:
for u in ring:
child_of[u].append(lat)
else:
for axial_slice in lat.universes:
for ring in axial_slice:
for u in ring:
child_of[u].append(lat)
for e in elem.findall('cell'):
c = openmc.Cell.from_xml_element(e, surfaces, mats, get_universe)
if c.fill_type in ('universe', 'lattice'):
child_of[c.fill].append(c)
# Determine which universe is the root by finding one which is not a
# child of any other object
for u in universes.values():
if not child_of[u]:
return cls(u)
else:
raise ValueError('Error determining root universe.')
@classmethod
def from_xml(cls, path='geometry.xml', materials=None):
"""Generate geometry from XML file
@ -165,84 +271,15 @@ class Geometry:
Geometry object
"""
# Helper function for keeping a cache of Universe instances
universes = {}
def get_universe(univ_id):
if univ_id not in universes:
univ = openmc.Universe(univ_id)
universes[univ_id] = univ
return universes[univ_id]
tree = ET.parse(path)
root = tree.getroot()
# Get surfaces
surfaces = {}
periodic = {}
for surface in root.findall('surface'):
s = openmc.Surface.from_xml_element(surface)
surfaces[s.id] = s
# Check for periodic surface
other_id = xml.get_text(surface, 'periodic_surface_id')
if other_id is not None:
periodic[s.id] = int(other_id)
# Apply periodic surfaces
for s1, s2 in periodic.items():
surfaces[s1].periodic_surface = surfaces[s2]
# Add any DAGMC universes
for elem in root.findall('dagmc_universe'):
dag_univ = openmc.DAGMCUniverse.from_xml_element(elem)
universes[dag_univ.id] = dag_univ
# Dictionary that maps each universe to a list of cells/lattices that
# contain it (needed to determine which universe is the root)
child_of = defaultdict(list)
for elem in root.findall('lattice'):
lat = openmc.RectLattice.from_xml_element(elem, get_universe)
universes[lat.id] = lat
if lat.outer is not None:
child_of[lat.outer].append(lat)
for u in lat.universes.ravel():
child_of[u].append(lat)
for elem in root.findall('hex_lattice'):
lat = openmc.HexLattice.from_xml_element(elem, get_universe)
universes[lat.id] = lat
if lat.outer is not None:
child_of[lat.outer].append(lat)
if lat.ndim == 2:
for ring in lat.universes:
for u in ring:
child_of[u].append(lat)
else:
for axial_slice in lat.universes:
for ring in axial_slice:
for u in ring:
child_of[u].append(lat)
# Create dictionary to easily look up materials
if materials is None:
filename = Path(path).parent / 'materials.xml'
materials = openmc.Materials.from_xml(str(filename))
mats = {str(m.id): m for m in materials}
mats['void'] = None
for elem in root.findall('cell'):
c = openmc.Cell.from_xml_element(elem, surfaces, mats, get_universe)
if c.fill_type in ('universe', 'lattice'):
child_of[c.fill].append(c)
tree = ET.parse(path)
root = tree.getroot()
# Determine which universe is the root by finding one which is not a
# child of any other object
for u in universes.values():
if not child_of[u]:
return cls(u)
else:
raise ValueError('Error determining root universe.')
return cls.from_xml_element(root, materials)
def find(self, point):
"""Find cells/universes/lattices which contain a given point

View file

@ -1449,6 +1449,57 @@ class Materials(cv.CheckedList):
for material in self:
material.make_isotropic_in_lab()
def _write_xml(self, file, header=True, level=0, spaces_per_level=2, trailing_indent=True):
"""Writes XML content of the materials to an open file handle.
Parameters
----------
file : IOTextWrapper
Open file handle to write content into.
header : bool
Whether or not to write the XML header
level : int
Indentation level of materials element
spaces_per_level : int
Number of spaces per indentation
trailing_indentation : bool
Whether or not to write a trailing indentation for the materials element
"""
indentation = level*spaces_per_level*' '
# Write the header and the opening tag for the root element.
if header:
file.write("<?xml version='1.0' encoding='utf-8'?>\n")
file.write(indentation+'<materials>\n')
# Write the <cross_sections> element.
if self.cross_sections is not None:
element = ET.Element('cross_sections')
element.text = str(self.cross_sections)
clean_indentation(element, level=level+1)
element.tail = element.tail.strip(' ')
file.write((level+1)*spaces_per_level*' ')
reorder_attributes(element) # TODO: Remove when support is Python 3.8+
ET.ElementTree(element).write(file, encoding='unicode')
# Write the <material> elements.
for material in sorted(self, key=lambda x: x.id):
element = material.to_xml_element()
clean_indentation(element, level=level+1)
element.tail = element.tail.strip(' ')
file.write((level+1)*spaces_per_level*' ')
reorder_attributes(element) # TODO: Remove when support is Python 3.8+
ET.ElementTree(element).write(file, encoding='unicode')
# Write the closing tag for the root element.
file.write(indentation+'</materials>\n')
# Write a trailing indentation for the next element
# at this level if needed
if trailing_indent:
file.write(indentation)
def export_to_xml(self, path: PathLike = 'materials.xml'):
"""Export material collection to an XML file.
@ -1468,32 +1519,34 @@ class Materials(cv.CheckedList):
# one go.
with open(str(p), 'w', encoding='utf-8',
errors='xmlcharrefreplace') as fh:
self._write_xml(fh)
# Write the header and the opening tag for the root element.
fh.write("<?xml version='1.0' encoding='utf-8'?>\n")
fh.write('<materials>\n')
@classmethod
def from_xml_element(cls, elem):
"""Generate materials collection from XML file
# Write the <cross_sections> element.
if self.cross_sections is not None:
element = ET.Element('cross_sections')
element.text = str(self.cross_sections)
clean_indentation(element, level=1)
element.tail = element.tail.strip(' ')
fh.write(' ')
reorder_attributes(element) # TODO: Remove when support is Python 3.8+
ET.ElementTree(element).write(fh, encoding='unicode')
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
# Write the <material> elements.
for material in sorted(self, key=lambda x: x.id):
element = material.to_xml_element()
clean_indentation(element, level=1)
element.tail = element.tail.strip(' ')
fh.write(' ')
reorder_attributes(element) # TODO: Remove when support is Python 3.8+
ET.ElementTree(element).write(fh, encoding='unicode')
Returns
-------
openmc.Materials
Materials collection
# Write the closing tag for the root element.
fh.write('</materials>\n')
"""
# Generate each material
materials = cls()
for material in elem.findall('material'):
materials.append(Material.from_xml_element(material))
# Check for cross sections settings
xs = elem.find('cross_sections')
if xs is not None:
materials.cross_sections = xs.text
return materials
@classmethod
def from_xml(cls, path: PathLike = 'materials.xml'):
@ -1513,14 +1566,4 @@ class Materials(cv.CheckedList):
tree = ET.parse(path)
root = tree.getroot()
# Generate each material
materials = cls()
for material in root.findall('material'):
materials.append(Material.from_xml_element(material))
# Check for cross sections settings
xs = tree.find('cross_sections')
if xs is not None:
materials.cross_sections = xs.text
return materials
return cls.from_xml_element(root)

View file

@ -6,10 +6,12 @@ from pathlib import Path
from numbers import Integral
from tempfile import NamedTemporaryFile
import warnings
from xml.etree import ElementTree as ET
import h5py
import openmc
import openmc._xml as xml
from openmc.dummy_comm import DummyCommunicator
from openmc.executor import _process_CLI_arguments
from openmc.checkvalue import check_type, check_value
@ -238,6 +240,35 @@ class Model:
plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None
return cls(geometry, materials, settings, tallies, plots)
@classmethod
def from_model_xml(cls, path='model.xml'):
"""Create model from single XML file
.. vesionadded:: 0.13.3
Parameters
----------
path : str or Pathlike
Path to model.xml file
"""
tree = ET.parse(path)
root = tree.getroot()
model = cls()
meshes = {}
model.settings = openmc.Settings.from_xml_element(root.find('settings'), meshes)
model.materials = openmc.Materials.from_xml_element(root.find('materials'))
model.geometry = openmc.Geometry.from_xml_element(root.find('geometry'), model.materials)
if root.find('tallies'):
model.tallies = openmc.Tallies.from_xml_element(root.find('tallies'), meshes)
if root.find('plots'):
model.plots = openmc.Plots.from_xml_element(root.find('plots'))
return model
def init_lib(self, threads=None, geometry_debug=False, restart_file=None,
tracks=False, output=True, event_based=None, intracomm=None):
"""Initializes the model in memory via the C API
@ -399,7 +430,7 @@ class Model:
depletion_operator.finalize()
def export_to_xml(self, directory='.', remove_surfs=False):
"""Export model to XML files.
"""Export model to separate XML files.
Parameters
----------
@ -418,14 +449,7 @@ class Model:
d.mkdir(parents=True)
self.settings.export_to_xml(d)
if remove_surfs:
warnings.warn("remove_surfs kwarg will be deprecated soon, please "
"set the Geometry.merge_surfaces attribute instead.")
self.geometry.merge_surfaces = True
# Can be used to modify tallies in case any surfaces are redundant
redundant_surfaces = self.geometry.remove_redundant_surfaces()
self.geometry.export_to_xml(d)
self.geometry.export_to_xml(d, remove_surfs=remove_surfs)
# If a materials collection was specified, export it. Otherwise, look
# for all materials in the geometry and use that to automatically build
@ -442,6 +466,78 @@ class Model:
if self.plots:
self.plots.export_to_xml(d)
def export_to_model_xml(self, path='model.xml', remove_surfs=False):
"""Export model to a single XML file.
.. versionadded:: 0.13.3
Parameters
----------
path : str or Pathlike
Location of the XML file to write (default is 'model.xml'). Can be a
directory or file path.
remove_surfs : bool
Whether or not to remove redundant surfaces from the geometry when
exporting.
"""
xml_path = Path(path)
# if the provided path doesn't end with the XML extension, assume the
# input path is meant to be a directory. If the directory does not
# exist, create it and place a 'model.xml' file there.
if not str(xml_path).endswith('.xml') and not xml_path.exists():
os.mkdir(xml_path)
xml_path /= 'model.xml'
# if this is an XML file location and the file's parent directory does
# not exist, create it before continuing
elif not xml_path.parent.exists():
os.mkdir(xml_path.parent)
if remove_surfs:
warnings.warn("remove_surfs kwarg will be deprecated soon, please "
"set the Geometry.merge_surfaces attribute instead.")
self.geometry.merge_surfaces = True
# Can be used to modify tallies in case any surfaces are redundant
redundant_surfaces = self.geometry.remove_redundant_surfaces()
# provide a memo to track which meshes have been written
mesh_memo = set()
settings_element = self.settings.to_xml_element(mesh_memo)
geometry_element = self.geometry.to_xml_element()
xml.clean_indentation(geometry_element, level=1)
xml.clean_indentation(settings_element, level=1)
# If a materials collection was specified, export it. Otherwise, look
# for all materials in the geometry and use that to automatically build
# a collection.
if self.materials:
materials = self.materials
else:
materials = openmc.Materials(self.geometry.get_all_materials()
.values())
with open(xml_path, 'w', encoding='utf-8', errors='xmlcharrefreplace') as fh:
# write the XML header
fh.write("<?xml version='1.0' encoding='utf-8'?>\n")
fh.write("<model>\n")
# Write the materials collection to the open XML file first.
# This will write the XML header also
materials._write_xml(fh, False, level=1)
# Write remaining elements as a tree
ET.ElementTree(geometry_element).write(fh, encoding='unicode')
ET.ElementTree(settings_element).write(fh, encoding='unicode')
if self.tallies:
tallies_element = self.tallies.to_xml_element(mesh_memo)
xml.clean_indentation(tallies_element, level=1, trailing_indent=self.plots)
ET.ElementTree(tallies_element).write(fh, encoding='unicode')
if self.plots:
plots_element = self.plots.to_xml_element()
xml.clean_indentation(plots_element, level=1, trailing_indent=False)
ET.ElementTree(plots_element).write(fh, encoding='unicode')
fh.write("</model>\n")
def import_properties(self, filename):
"""Import physical properties

View file

@ -909,13 +909,13 @@ class Plots(cv.CheckedList):
self._plots_file.append(xml_element)
def export_to_xml(self, path='plots.xml'):
"""Export plot specifications to an XML file.
def to_xml_element(self):
"""Create a 'plots' element to be written to an XML file.
Parameters
----------
path : str
Path to file to write. Defaults to 'plots.xml'.
Returns
-------
element : xml.etree.ElementTree.Element
XML element containing all plot elements
"""
# Reset xml element tree
@ -925,17 +925,50 @@ class Plots(cv.CheckedList):
# Clean the indentation in the file to be user-readable
clean_indentation(self._plots_file)
reorder_attributes(self._plots_file) # TODO: Remove when support is Python 3.8+
return self._plots_file
def export_to_xml(self, path='plots.xml'):
"""Export plot specifications to an XML file.
Parameters
----------
path : str
Path to file to write. Defaults to 'plots.xml'.
"""
# Check if path is a directory
p = Path(path)
if p.is_dir():
p /= 'plots.xml'
self.to_xml_element()
# Write the XML Tree to the plots.xml file
reorder_attributes(self._plots_file) # TODO: Remove when support is Python 3.8+
tree = ET.ElementTree(self._plots_file)
tree.write(str(p), xml_declaration=True, encoding='utf-8')
@classmethod
def from_xml_element(cls, elem):
"""Generate plots collection from XML file
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.Plots
Plots collection
"""
# Generate each plot
plots = cls()
for e in elem.findall('plot'):
plots.append(Plot.from_xml_element(e))
return plots
@classmethod
def from_xml(cls, path='plots.xml'):
"""Generate plots collection from XML file
@ -953,9 +986,6 @@ class Plots(cv.CheckedList):
"""
tree = ET.parse(path)
root = tree.getroot()
return cls.from_xml_element(root)
# Generate each plot
plots = cls()
for elem in root.findall('plot'):
plots.append(Plot.from_xml_element(elem))
return plots

View file

@ -1087,25 +1087,35 @@ class Settings:
subelement = ET.SubElement(element, key)
subelement.text = str(value)
def _create_entropy_mesh_subelement(self, root):
if self.entropy_mesh is not None:
# use default heuristic for entropy mesh if not set by user
if self.entropy_mesh.dimension is None:
if self.particles is None:
raise RuntimeError("Number of particles must be set in order to " \
"use entropy mesh dimension heuristic")
else:
n = ceil((self.particles / 20.0)**(1.0 / 3.0))
d = len(self.entropy_mesh.lower_left)
self.entropy_mesh.dimension = (n,)*d
def _create_entropy_mesh_subelement(self, root, mesh_memo=None):
if self.entropy_mesh is None:
return
# See if a <mesh> element already exists -- if not, add it
path = f"./mesh[@id='{self.entropy_mesh.id}']"
if root.find(path) is None:
root.append(self.entropy_mesh.to_xml_element())
# use default heuristic for entropy mesh if not set by user
if self.entropy_mesh.dimension is None:
if self.particles is None:
raise RuntimeError("Number of particles must be set in order to " \
"use entropy mesh dimension heuristic")
else:
n = ceil((self.particles / 20.0)**(1.0 / 3.0))
d = len(self.entropy_mesh.lower_left)
self.entropy_mesh.dimension = (n,)*d
subelement = ET.SubElement(root, "entropy_mesh")
subelement.text = str(self.entropy_mesh.id)
# add mesh ID to this element
subelement = ET.SubElement(root, "entropy_mesh")
subelement.text = str(self.entropy_mesh.id)
# If this mesh has already been written outside the
# settings element, skip writing it again
if mesh_memo and self.entropy_mesh.id in mesh_memo:
return
# See if a <mesh> element already exists -- if not, add it
path = f"./mesh[@id='{self.entropy_mesh.id}']"
if root.find(path) is None:
root.append(self.entropy_mesh.to_xml_element())
if mesh_memo is not None:
mesh_memo.add(self.entropy_mesh.id)
def _create_trigger_subelement(self, root):
if self._trigger_active is not None:
@ -1156,15 +1166,21 @@ class Settings:
element = ET.SubElement(root, "track")
element.text = ' '.join(map(str, itertools.chain(*self._track)))
def _create_ufs_mesh_subelement(self, root):
if self.ufs_mesh is not None:
# See if a <mesh> element already exists -- if not, add it
path = f"./mesh[@id='{self.ufs_mesh.id}']"
if root.find(path) is None:
root.append(self.ufs_mesh.to_xml_element())
def _create_ufs_mesh_subelement(self, root, mesh_memo=None):
if self.ufs_mesh is None:
return
subelement = ET.SubElement(root, "ufs_mesh")
subelement.text = str(self.ufs_mesh.id)
subelement = ET.SubElement(root, "ufs_mesh")
subelement.text = str(self.ufs_mesh.id)
if mesh_memo and self.ufs_mesh.id in mesh_memo:
return
# See if a <mesh> element already exists -- if not, add it
path = f"./mesh[@id='{self.ufs_mesh.id}']"
if root.find(path) is None:
root.append(self.ufs_mesh.to_xml_element())
if mesh_memo is not None: mesh_memo.add(self.ufs_mesh.id)
def _create_resonance_scattering_subelement(self, root):
res = self.resonance_scattering
@ -1221,15 +1237,21 @@ class Settings:
elem = ET.SubElement(root, "write_initial_source")
elem.text = str(self._write_initial_source).lower()
def _create_weight_windows_subelement(self, root):
def _create_weight_windows_subelement(self, root, mesh_memo=None):
for ww in self._weight_windows:
# Add weight window information
root.append(ww.to_xml_element())
# if this mesh has already been written,
# skip writing the mesh element
if mesh_memo and ww.mesh.id in mesh_memo:
continue
# See if a <mesh> element already exists -- if not, add it
path = f"./mesh[@id='{ww.mesh.id}']"
if root.find(path) is None:
root.append(ww.mesh.to_xml_element())
if mesh_memo is not None: mesh_memo.add(ww.mesh.id)
if self._weight_windows_on is not None:
elem = ET.SubElement(root, "weight_windows_on")
@ -1412,13 +1434,15 @@ class Settings:
if value is not None:
self.cutoff[key] = float(value)
def _entropy_mesh_from_xml_element(self, root):
def _entropy_mesh_from_xml_element(self, root, meshes=None):
text = get_text(root, 'entropy_mesh')
if text is not None:
path = f"./mesh[@id='{int(text)}']"
elem = root.find(path)
if elem is not None:
self.entropy_mesh = RegularMesh.from_xml_element(elem)
if meshes is not None and self.entropy_mesh is not None:
meshes[self.entropy_mesh.id] = self.entropy_mesh
def _trigger_from_xml_element(self, root):
elem = root.find('trigger')
@ -1478,13 +1502,15 @@ class Settings:
values = [int(x) for x in text.split()]
self.track = list(zip(values[::3], values[1::3], values[2::3]))
def _ufs_mesh_from_xml_element(self, root):
def _ufs_mesh_from_xml_element(self, root, meshes=None):
text = get_text(root, 'ufs_mesh')
if text is not None:
path = f"./mesh[@id='{int(text)}']"
elem = root.find(path)
if elem is not None:
self.ufs_mesh = RegularMesh.from_xml_element(elem)
if meshes is not None and self.ufs_mesh is not None:
meshes[self.ufs_mesh.id] = self.ufs_mesh
def _resonance_scattering_from_xml_element(self, root):
elem = root.find('resonance_scattering')
@ -1536,7 +1562,7 @@ class Settings:
if text is not None:
self.write_initial_source = text in ('true', '1')
def _weight_windows_from_xml_element(self, root):
def _weight_windows_from_xml_element(self, root, meshes=None):
for elem in root.findall('weight_windows'):
ww = WeightWindows.from_xml_element(elem, root)
self.weight_windows.append(ww)
@ -1545,6 +1571,9 @@ class Settings:
if text is not None:
self.weight_windows_on = text in ('true', '1')
if meshes is not None and self.weight_windows:
meshes.update({ww.mesh.id: ww.mesh for ww in self.weight_windows})
def _max_splits_from_xml_element(self, root):
text = get_text(root, 'max_splits')
if text is not None:
@ -1555,6 +1584,68 @@ class Settings:
if text is not None:
self.max_tracks = int(text)
def to_xml_element(self, mesh_memo=None):
"""Create a 'settings' element to be written to an XML file.
Parameters
----------
mesh_memo : set of ints
A set of mesh IDs to keep track of whether a mesh has already been written.
"""
# Reset xml element tree
element = ET.Element("settings")
self._create_run_mode_subelement(element)
self._create_particles_subelement(element)
self._create_batches_subelement(element)
self._create_inactive_subelement(element)
self._create_max_lost_particles_subelement(element)
self._create_rel_max_lost_particles_subelement(element)
self._create_generations_per_batch_subelement(element)
self._create_keff_trigger_subelement(element)
self._create_source_subelement(element)
self._create_output_subelement(element)
self._create_statepoint_subelement(element)
self._create_sourcepoint_subelement(element)
self._create_surf_source_read_subelement(element)
self._create_surf_source_write_subelement(element)
self._create_confidence_intervals(element)
self._create_electron_treatment_subelement(element)
self._create_energy_mode_subelement(element)
self._create_max_order_subelement(element)
self._create_photon_transport_subelement(element)
self._create_ptables_subelement(element)
self._create_seed_subelement(element)
self._create_survival_biasing_subelement(element)
self._create_cutoff_subelement(element)
self._create_entropy_mesh_subelement(element, mesh_memo)
self._create_trigger_subelement(element)
self._create_no_reduce_subelement(element)
self._create_verbosity_subelement(element)
self._create_tabular_legendre_subelements(element)
self._create_temperature_subelements(element)
self._create_trace_subelement(element)
self._create_track_subelement(element)
self._create_ufs_mesh_subelement(element, mesh_memo)
self._create_resonance_scattering_subelement(element)
self._create_volume_calcs_subelement(element)
self._create_create_fission_neutrons_subelement(element)
self._create_delayed_photon_scaling_subelement(element)
self._create_event_based_subelement(element)
self._create_max_particles_in_flight_subelement(element)
self._create_material_cell_offsets_subelement(element)
self._create_log_grid_bins_subelement(element)
self._create_write_initial_source_subelement(element)
self._create_weight_windows_subelement(element, mesh_memo)
self._create_max_splits_subelement(element)
self._create_max_tracks_subelement(element)
# Clean the indentation in the file to be user-readable
clean_indentation(element)
reorder_attributes(element) # TODO: Remove when support is Python 3.8+
return element
def export_to_xml(self, path: PathLike = 'settings.xml'):
"""Export simulation settings to an XML file.
@ -1564,57 +1655,7 @@ class Settings:
Path to file to write. Defaults to 'settings.xml'.
"""
# Reset xml element tree
root_element = ET.Element("settings")
self._create_run_mode_subelement(root_element)
self._create_particles_subelement(root_element)
self._create_batches_subelement(root_element)
self._create_inactive_subelement(root_element)
self._create_max_lost_particles_subelement(root_element)
self._create_rel_max_lost_particles_subelement(root_element)
self._create_generations_per_batch_subelement(root_element)
self._create_keff_trigger_subelement(root_element)
self._create_source_subelement(root_element)
self._create_output_subelement(root_element)
self._create_statepoint_subelement(root_element)
self._create_sourcepoint_subelement(root_element)
self._create_surf_source_read_subelement(root_element)
self._create_surf_source_write_subelement(root_element)
self._create_confidence_intervals(root_element)
self._create_electron_treatment_subelement(root_element)
self._create_energy_mode_subelement(root_element)
self._create_max_order_subelement(root_element)
self._create_photon_transport_subelement(root_element)
self._create_ptables_subelement(root_element)
self._create_seed_subelement(root_element)
self._create_survival_biasing_subelement(root_element)
self._create_cutoff_subelement(root_element)
self._create_entropy_mesh_subelement(root_element)
self._create_trigger_subelement(root_element)
self._create_no_reduce_subelement(root_element)
self._create_verbosity_subelement(root_element)
self._create_tabular_legendre_subelements(root_element)
self._create_temperature_subelements(root_element)
self._create_trace_subelement(root_element)
self._create_track_subelement(root_element)
self._create_ufs_mesh_subelement(root_element)
self._create_resonance_scattering_subelement(root_element)
self._create_volume_calcs_subelement(root_element)
self._create_create_fission_neutrons_subelement(root_element)
self._create_delayed_photon_scaling_subelement(root_element)
self._create_event_based_subelement(root_element)
self._create_max_particles_in_flight_subelement(root_element)
self._create_material_cell_offsets_subelement(root_element)
self._create_log_grid_bins_subelement(root_element)
self._create_write_initial_source_subelement(root_element)
self._create_weight_windows_subelement(root_element)
self._create_max_splits_subelement(root_element)
self._create_max_tracks_subelement(root_element)
# Clean the indentation in the file to be user-readable
clean_indentation(root_element)
root_element = self.to_xml_element()
# Check if path is a directory
p = Path(path)
@ -1622,10 +1663,78 @@ class Settings:
p /= 'settings.xml'
# Write the XML Tree to the settings.xml file
reorder_attributes(root_element) # TODO: Remove when support is Python 3.8+
tree = ET.ElementTree(root_element)
tree.write(str(p), xml_declaration=True, encoding='utf-8')
@classmethod
def from_xml_element(cls, elem, meshes=None):
"""Generate settings from XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
meshes : dict or None
A dictionary with mesh IDs as keys and mesh instances as values that
have already been read from XML. Pre-existing meshes are used
and new meshes are added to when creating tally objects.
Returns
-------
openmc.Settings
Settings object
"""
settings = cls()
settings._eigenvalue_from_xml_element(elem)
settings._run_mode_from_xml_element(elem)
settings._particles_from_xml_element(elem)
settings._batches_from_xml_element(elem)
settings._inactive_from_xml_element(elem)
settings._max_lost_particles_from_xml_element(elem)
settings._rel_max_lost_particles_from_xml_element(elem)
settings._generations_per_batch_from_xml_element(elem)
settings._keff_trigger_from_xml_element(elem)
settings._source_from_xml_element(elem)
settings._volume_calcs_from_xml_element(elem)
settings._output_from_xml_element(elem)
settings._statepoint_from_xml_element(elem)
settings._sourcepoint_from_xml_element(elem)
settings._surf_source_read_from_xml_element(elem)
settings._surf_source_write_from_xml_element(elem)
settings._confidence_intervals_from_xml_element(elem)
settings._electron_treatment_from_xml_element(elem)
settings._energy_mode_from_xml_element(elem)
settings._max_order_from_xml_element(elem)
settings._photon_transport_from_xml_element(elem)
settings._ptables_from_xml_element(elem)
settings._seed_from_xml_element(elem)
settings._survival_biasing_from_xml_element(elem)
settings._cutoff_from_xml_element(elem)
settings._entropy_mesh_from_xml_element(elem, meshes)
settings._trigger_from_xml_element(elem)
settings._no_reduce_from_xml_element(elem)
settings._verbosity_from_xml_element(elem)
settings._tabular_legendre_from_xml_element(elem)
settings._temperature_from_xml_element(elem)
settings._trace_from_xml_element(elem)
settings._track_from_xml_element(elem)
settings._ufs_mesh_from_xml_element(elem, meshes)
settings._resonance_scattering_from_xml_element(elem)
settings._create_fission_neutrons_from_xml_element(elem)
settings._delayed_photon_scaling_from_xml_element(elem)
settings._event_based_from_xml_element(elem)
settings._max_particles_in_flight_from_xml_element(elem)
settings._material_cell_offsets_from_xml_element(elem)
settings._log_grid_bins_from_xml_element(elem)
settings._write_initial_source_from_xml_element(elem)
settings._weight_windows_from_xml_element(elem, meshes)
settings._max_splits_from_xml_element(elem)
settings._max_tracks_from_xml_element(elem)
# TODO: Get volume calculations
return settings
@classmethod
def from_xml(cls, path: PathLike = 'settings.xml'):
"""Generate settings from XML file
@ -1645,54 +1754,4 @@ class Settings:
"""
tree = ET.parse(path)
root = tree.getroot()
settings = cls()
settings._eigenvalue_from_xml_element(root)
settings._run_mode_from_xml_element(root)
settings._particles_from_xml_element(root)
settings._batches_from_xml_element(root)
settings._inactive_from_xml_element(root)
settings._max_lost_particles_from_xml_element(root)
settings._rel_max_lost_particles_from_xml_element(root)
settings._generations_per_batch_from_xml_element(root)
settings._keff_trigger_from_xml_element(root)
settings._source_from_xml_element(root)
settings._volume_calcs_from_xml_element(root)
settings._output_from_xml_element(root)
settings._statepoint_from_xml_element(root)
settings._sourcepoint_from_xml_element(root)
settings._surf_source_read_from_xml_element(root)
settings._surf_source_write_from_xml_element(root)
settings._confidence_intervals_from_xml_element(root)
settings._electron_treatment_from_xml_element(root)
settings._energy_mode_from_xml_element(root)
settings._max_order_from_xml_element(root)
settings._photon_transport_from_xml_element(root)
settings._ptables_from_xml_element(root)
settings._seed_from_xml_element(root)
settings._survival_biasing_from_xml_element(root)
settings._cutoff_from_xml_element(root)
settings._entropy_mesh_from_xml_element(root)
settings._trigger_from_xml_element(root)
settings._no_reduce_from_xml_element(root)
settings._verbosity_from_xml_element(root)
settings._tabular_legendre_from_xml_element(root)
settings._temperature_from_xml_element(root)
settings._trace_from_xml_element(root)
settings._track_from_xml_element(root)
settings._ufs_mesh_from_xml_element(root)
settings._resonance_scattering_from_xml_element(root)
settings._create_fission_neutrons_from_xml_element(root)
settings._delayed_photon_scaling_from_xml_element(root)
settings._event_based_from_xml_element(root)
settings._max_particles_in_flight_from_xml_element(root)
settings._material_cell_offsets_from_xml_element(root)
settings._log_grid_bins_from_xml_element(root)
settings._write_initial_source_from_xml_element(root)
settings._weight_windows_from_xml_element(root)
settings._max_splits_from_xml_element(root)
settings._max_tracks_from_xml_element(root)
# TODO: Get volume calculations
return settings
return cls.from_xml_element(root)

View file

@ -3119,17 +3119,17 @@ class Tallies(cv.CheckedList):
for tally in self:
root_element.append(tally.to_xml_element())
def _create_mesh_subelements(self, root_element):
already_written = set()
def _create_mesh_subelements(self, root_element, memo=None):
already_written = memo if memo else set()
for tally in self:
for f in tally.filters:
if isinstance(f, openmc.MeshFilter):
if f.mesh.id not in already_written:
if len(f.mesh.name) > 0:
root_element.append(ET.Comment(f.mesh.name))
root_element.append(f.mesh.to_xml_element())
already_written.add(f.mesh.id)
if f.mesh.id in already_written:
continue
if len(f.mesh.name) > 0:
root_element.append(ET.Comment(f.mesh.name))
root_element.append(f.mesh.to_xml_element())
already_written.add(f.mesh.id)
def _create_filter_subelements(self, root_element):
already_written = dict()
@ -3155,6 +3155,22 @@ class Tallies(cv.CheckedList):
for d in derivs:
root_element.append(d.to_xml_element())
def to_xml_element(self, memo=None):
"""Creates a 'tallies' element to be written to an XML file.
"""
element = ET.Element("tallies")
self._create_mesh_subelements(element, memo)
self._create_filter_subelements(element)
self._create_tally_subelements(element)
self._create_derivative_subelements(element)
# Clean the indentation in the file to be user-readable
clean_indentation(element)
reorder_attributes(element) # TODO: Remove when support is Python 3.8+
return element
def export_to_xml(self, path='tallies.xml'):
"""Create a tallies.xml file that can be used for a simulation.
@ -3164,15 +3180,7 @@ class Tallies(cv.CheckedList):
Path to file to write. Defaults to 'tallies.xml'.
"""
root_element = ET.Element("tallies")
self._create_mesh_subelements(root_element)
self._create_filter_subelements(root_element)
self._create_tally_subelements(root_element)
self._create_derivative_subelements(root_element)
# Clean the indentation in the file to be user-readable
clean_indentation(root_element)
root_element = self.to_xml_element()
# Check if path is a directory
p = Path(path)
@ -3180,10 +3188,56 @@ class Tallies(cv.CheckedList):
p /= 'tallies.xml'
# Write the XML Tree to the tallies.xml file
reorder_attributes(root_element) # TODO: Remove when support is Python 3.8+
tree = ET.ElementTree(root_element)
tree.write(str(p), xml_declaration=True, encoding='utf-8')
@classmethod
def from_xml_element(cls, elem, meshes=None):
"""Generate tallies from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
meshes : dict or None
A dictionary with mesh IDs as keys and mesh instances as values that
have already been read from XML. Pre-existing meshes are used
and new meshes are added to when creating tally objects.
Returns
-------
openmc.Tallies
Tallies object
"""
# Read mesh elements
meshes = {} if meshes is None else meshes
for e in elem.findall('mesh'):
mesh = MeshBase.from_xml_element(e)
meshes[mesh.id] = mesh
# Read filter elements
filters = {}
for e in elem.findall('filter'):
filter = openmc.Filter.from_xml_element(e, meshes=meshes)
filters[filter.id] = filter
# Read derivative elements
derivatives = {}
for e in elem.findall('derivative'):
deriv = openmc.TallyDerivative.from_xml_element(e)
derivatives[deriv.id] = deriv
# Read tally elements
tallies = []
for e in elem.findall('tally'):
tally = openmc.Tally.from_xml_element(
e, filters=filters, derivatives=derivatives
)
tallies.append(tally)
return cls(tallies)
@classmethod
def from_xml(cls, path='tallies.xml'):
"""Generate tallies from XML file
@ -3201,31 +3255,4 @@ class Tallies(cv.CheckedList):
"""
tree = ET.parse(path)
root = tree.getroot()
# Read mesh elements
meshes = {}
for elem in root.findall('mesh'):
mesh = MeshBase.from_xml_element(elem)
meshes[mesh.id] = mesh
# Read filter elements
filters = {}
for elem in root.findall('filter'):
filter = openmc.Filter.from_xml_element(elem, meshes=meshes)
filters[filter.id] = filter
# Read derivative elements
derivatives = {}
for elem in root.findall('derivative'):
deriv = openmc.TallyDerivative.from_xml_element(elem)
derivatives[deriv.id] = deriv
# Read tally elements
tallies = []
for elem in root.findall('tally'):
tally = openmc.Tally.from_xml_element(
elem, filters=filters, derivatives=derivatives
)
tallies.append(tally)
return cls(tallies)
return cls.from_xml_element(root)