Merge pull request #1955 from paulromano/from-xml-complete

Add missing from_xml capabilities
This commit is contained in:
Patrick Shriwise 2022-01-24 10:50:46 -06:00 committed by GitHub
commit 6450d669cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 714 additions and 141 deletions

View file

@ -16,6 +16,7 @@ from .material import Material
from .mixin import IDManagerMixin
from .surface import Surface
from .universe import UniverseBase
from ._xml import get_text
_FILTER_TYPES = (
@ -231,9 +232,43 @@ class Filter(IDManagerMixin, metaclass=FilterMeta):
subelement = ET.SubElement(element, 'bins')
subelement.text = ' '.join(str(b) for b in self.bins)
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
"""Generate a filter from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
**kwargs
Keyword arguments (e.g., mesh information)
Returns
-------
openmc.Filter
Filter object
"""
filter_type = elem.get('type')
# If the filter type matches this class's short_name, then
# there is no overriden from_xml_element method
if filter_type == cls.short_name.lower():
# Get bins from element -- the default here works for any filters
# that just store a list of bins that can be represented as integers
filter_id = int(elem.get('id'))
bins = [int(x) for x in get_text(elem, 'bins').split()]
return cls(bins, filter_id=filter_id)
# Search through all subclasses and find the one matching the HDF5
# 'type'. Call that class's from_hdf5 method
for subclass in cls._recursive_subclasses():
if filter_type == subclass.short_name.lower():
return subclass.from_xml_element(elem, **kwargs)
def can_merge(self, other):
"""Determine if filter can be merged with another.
@ -622,6 +657,13 @@ class CellInstanceFilter(Filter):
subelement.text = ' '.join(str(i) for i in self.bins.ravel())
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
bins = [int(x) for x in get_text(elem, 'bins').split()]
cell_instances = list(zip(bins[::2], bins[1::2]))
return cls(cell_instances, filter_id=filter_id)
class SurfaceFilter(WithIDFilter):
"""Filters particles by surface crossing
@ -661,8 +703,8 @@ class ParticleFilter(Filter):
Attributes
----------
bins : Iterable of Integral
The Particles to tally
bins : iterable of str
The particles to tally
id : int
Unique identifier for the filter
num_bins : Integral
@ -698,6 +740,12 @@ class ParticleFilter(Filter):
filter_id = int(group.name.split('/')[-1].lstrip('filter '))
return cls(particles, filter_id=filter_id)
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
bins = get_text(elem, 'bins').split()
return cls(bins, filter_id=filter_id)
class MeshFilter(Filter):
"""Bins tally event locations onto a regular, rectangular mesh.
@ -877,6 +925,18 @@ class MeshFilter(Filter):
element.set('translation', ' '.join(map(str, self.translation)))
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
mesh_id = int(get_text(elem, 'bins'))
mesh_obj = kwargs['meshes'][mesh_id]
filter_id = int(elem.get('id'))
out = cls(mesh_obj, filter_id=filter_id)
translation = elem.get('translation')
if translation:
out.translation = [float(x) for x in translation.split()]
return out
class MeshSurfaceFilter(MeshFilter):
"""Filter events by surface crossings on a regular, rectangular mesh.
@ -1019,35 +1079,12 @@ class CollisionFilter(Filter):
self.bins = np.asarray(bins)
self.id = filter_id
def __repr__(self):
string = type(self).__name__ + '\n'
string += '{: <16}=\t{}\n'.format('\tValues', self.bins)
string += '{: <16}=\t{}\n'.format('\tID', self.id)
return string
@Filter.bins.setter
def bins(self, bins):
Filter.bins.__set__(self, np.asarray(bins))
def check_bins(self, bins):
for x in bins:
# Values should be integers
cv.check_type('filter value', x, Integral)
cv.check_greater_than('filter value', x, 0, equality=True)
def to_xml_element(self):
"""Return XML Element representing the Filter.
Returns
-------
element : xml.etree.ElementTree.Element
XML element containing filter data
"""
element = super().to_xml_element()
element[0].text = ' '.join(str(x) for x in self.bins)
return element
class RealFilter(Filter):
"""Tally modifier that describes phase-space and other characteristics
@ -1236,6 +1273,12 @@ class RealFilter(Filter):
element[0].text = ' '.join(str(x) for x in self.values)
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
bins = [float(x) for x in get_text(elem, 'bins').split()]
return cls(bins, filter_id=filter_id)
class EnergyFilter(RealFilter):
"""Bins tally events based on incident particle energy.
@ -1969,6 +2012,13 @@ class EnergyFunctionFilter(Filter):
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
energy = [float(x) for x in get_text(elem, 'energy').split()]
y = [float(x) for x in get_text(elem, 'y').split()]
return cls(energy, y, filter_id=filter_id)
def can_merge(self, other):
return False

View file

@ -46,6 +46,12 @@ class ExpansionFilter(Filter):
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
order = int(elem.find('order').text)
return cls(order, filter_id=filter_id)
class LegendreFilter(ExpansionFilter):
r"""Score Legendre expansion moments up to specified order.
@ -226,6 +232,15 @@ class SpatialLegendreFilter(ExpansionFilter):
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
order = int(elem.find('order').text)
axis = elem.find('axis').text
minimum = float(elem.find('min').text)
maximum = float(elem.find('max').text)
return cls(order, axis, minimum, maximum, filter_id=filter_id)
class SphericalHarmonicsFilter(ExpansionFilter):
r"""Score spherical harmonic expansion moments up to specified order.
@ -316,6 +331,14 @@ class SphericalHarmonicsFilter(ExpansionFilter):
element.set('cosine', self.cosine)
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
order = int(elem.find('order').text)
filter = cls(order, filter_id=filter_id)
filter.cosine = elem.get('cosine')
return filter
class ZernikeFilter(ExpansionFilter):
r"""Score Zernike expansion moments in space up to specified order.
@ -358,7 +381,7 @@ class ZernikeFilter(ExpansionFilter):
x-coordinate of center of circle for normalization
y : float
y-coordinate of center of circle for normalization
r : int or None
r : float
Radius of circle for normalization
Attributes
@ -369,7 +392,7 @@ class ZernikeFilter(ExpansionFilter):
x-coordinate of center of circle for normalization
y : float
y-coordinate of center of circle for normalization
r : int or None
r : float
Radius of circle for normalization
id : int
Unique identifier for the filter
@ -464,6 +487,15 @@ class ZernikeFilter(ExpansionFilter):
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
filter_id = int(elem.get('id'))
order = int(elem.find('order').text)
x = float(elem.find('x').text)
y = float(elem.find('y').text)
r = float(elem.find('r').text)
return cls(order, x, y, r, filter_id=filter_id)
class ZernikeRadialFilter(ZernikeFilter):
r"""Score the :math:`m = 0` (radial variation only) Zernike moments up to
@ -499,7 +531,7 @@ class ZernikeRadialFilter(ZernikeFilter):
x-coordinate of center of circle for normalization
y : float
y-coordinate of center of circle for normalization
r : int or None
r : float
Radius of circle for normalization
Attributes
@ -510,7 +542,7 @@ class ZernikeRadialFilter(ZernikeFilter):
x-coordinate of center of circle for normalization
y : float
y-coordinate of center of circle for normalization
r : int or None
r : float
Radius of circle for normalization
id : int
Unique identifier for the filter

View file

@ -285,6 +285,18 @@ class RectilinearMesh(Mesh):
return (lower_left, upper_right, dimension, width)
def set_grid(self, x_grid, y_grid, z_grid):
"""Set grid values
Parameters
----------
x_grid : iterable of float
Mesh boundary points along the x-axis
y_grid : iterable of float
Mesh boundary points along the y-axis
z_grid : iterable of float
Mesh boundary points along the z-axis
"""
nx = len(x_grid)
x_grid = (c_double*nx)(*x_grid)
ny = len(y_grid)
@ -371,15 +383,27 @@ class CylindricalMesh(Mesh):
return (lower_left, upper_right, dimension, width)
def set_grid(self, x_grid, y_grid, z_grid):
nx = len(x_grid)
x_grid = (c_double*nx)(*x_grid)
ny = len(y_grid)
y_grid = (c_double*ny)(*y_grid)
def set_grid(self, r_grid, phi_grid, z_grid):
"""Set grid values
Parameters
----------
r_grid : iterable of float
Mesh boundary points along the r-axis
phi_grid : Iterable of float
Mesh boundary points along the phi-axis
z_grid : Iterable of float
Mesh boundary points along the z-axis
"""
nr = len(r_grid)
r_grid = (c_double*nr)(*r_grid)
nphi = len(phi_grid)
phi_grid = (c_double*nphi)(*phi_grid)
nz = len(z_grid)
z_grid = (c_double*nz)(*z_grid)
_dll.openmc_cylindrical_mesh_set_grid(self._index, x_grid, nx, y_grid,
ny, z_grid, nz)
_dll.openmc_cylindrical_mesh_set_grid(self._index, r_grid, nr, phi_grid,
nphi, z_grid, nz)
class SphericalMesh(Mesh):
"""SphericalMesh stored internally.
@ -457,15 +481,27 @@ class SphericalMesh(Mesh):
return (lower_left, upper_right, dimension, width)
def set_grid(self, x_grid, y_grid, z_grid):
nx = len(x_grid)
x_grid = (c_double*nx)(*x_grid)
ny = len(y_grid)
y_grid = (c_double*ny)(*y_grid)
nz = len(z_grid)
z_grid = (c_double*nz)(*z_grid)
_dll.openmc_spherical_mesh_set_grid(self._index, x_grid, nx, y_grid,
ny, z_grid, nz)
def set_grid(self, r_grid, theta_grid, phi_grid):
"""Set grid values
Parameters
----------
r_grid : iterable of float
Mesh boundary points along the r-axis
theta_grid : Iterable of float
Mesh boundary points along the theta-axis
phi_grid : Iterable of float
Mesh boundary points along the phi-axis
"""
nr = len(r_grid)
r_grid = (c_double*nr)(*r_grid)
ntheta = len(theta_grid)
theta_grid = (c_double*ntheta)(*theta_grid)
nphi = len(phi_grid)
phi_grid = (c_double*nphi)(*phi_grid)
_dll.openmc_spherical_mesh_set_grid(self._index, r_grid, nr, theta_grid,
ntheta, phi_grid, nphi)
_MESH_TYPE_MAP = {

View file

@ -1,5 +1,6 @@
from abc import ABC
from collections.abc import Iterable
from math import pi
from numbers import Real, Integral
import warnings
from xml.etree import ElementTree as ET
@ -89,7 +90,7 @@ class MeshBase(IDManagerMixin, ABC):
raise ValueError('Unrecognized mesh type: "' + mesh_type + '"')
@classmethod
def from_xml(cls, elem):
def from_xml_element(cls, elem):
"""Generates a mesh from an XML element
Parameters
@ -109,6 +110,10 @@ class MeshBase(IDManagerMixin, ABC):
return RegularMesh.from_xml_element(elem)
elif mesh_type == 'rectilinear':
return RectilinearMesh.from_xml_element(elem)
elif mesh_type == 'cylindrical':
return CylindricalMesh.from_xml_element(elem)
elif mesh_type == 'spherical':
return SphericalMesh.from_xml_element(elem)
elif mesh_type == 'unstructured':
return UnstructuredMesh.from_xml_element(elem)
else:
@ -697,11 +702,11 @@ class CylindricalMesh(MeshBase):
n_dimension : int
Number of mesh dimensions (always 3 for a CylindricalMesh).
r_grid : Iterable of float
Mesh boundary points along the r-axis.
Mesh boundary points along the r-axis.
Requirement is r >= 0.
phi_grid : Iterable of float
Mesh boundary points along the phi-axis.
The default value is [0, 360], i.e. the full phi range.
Mesh boundary points along the phi-axis.
The default value is [0, 2π], i.e. the full phi range.
z_grid : Iterable of float
Mesh boundary points along the z-axis.
indices : Iterable of tuple
@ -714,7 +719,7 @@ class CylindricalMesh(MeshBase):
super().__init__(mesh_id, name)
self._r_grid = None
self._phi_grid = [0, 360]
self._phi_grid = [0.0, 2*pi]
self._z_grid = None
@property
@ -757,12 +762,12 @@ class CylindricalMesh(MeshBase):
@phi_grid.setter
def phi_grid(self, grid):
cv.check_type('mesh phi_grid', grid, Iterable, Real)
self._phi_grid = np.array(grid)
self._phi_grid = np.asarray(grid)
@z_grid.setter
def z_grid(self, grid):
cv.check_type('mesh z_grid', grid, Iterable, Real)
self._z_grid = np.array(grid)
self._z_grid = np.asarray(grid)
def __repr__(self):
fmt = '{0: <16}{1}{2}\n'
@ -792,7 +797,7 @@ class CylindricalMesh(MeshBase):
# Read and assign mesh properties
mesh = cls(mesh_id)
mesh.r_grid = group['r_grid'][()]
mesh.phi_grid = 180 / np.pi * group['phi_grid'][()]
mesh.phi_grid = group['phi_grid'][()]
mesh.z_grid = group['z_grid'][()]
return mesh
@ -822,6 +827,29 @@ class CylindricalMesh(MeshBase):
return element
@classmethod
def from_xml_element(cls, elem):
"""Generate a cylindrical mesh from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.CylindricalMesh
Cylindrical mesh object
"""
mesh_id = int(get_text(elem, 'id'))
mesh = cls(mesh_id)
mesh.r_grid = [float(x) for x in get_text(elem, "r_grid").split()]
mesh.phi_grid = [float(x) for x in get_text(elem, "phi_grid").split()]
mesh.z_grid = [float(x) for x in get_text(elem, "z_grid").split()]
return mesh
def calc_mesh_volumes(self):
"""Return Volumes for every mesh cell
@ -832,9 +860,9 @@ class CylindricalMesh(MeshBase):
"""
V_r = np.diff(np.array(self.r_grid)**2 / 2)
V_p = np.diff(np.array(self.phi_grid) * np.pi / 180.0)
V_z = np.diff(np.array(self.z_grid))
V_r = np.diff(np.asarray(self.r_grid)**2 / 2)
V_p = np.diff(self.phi_grid)
V_z = np.diff(self.z_grid)
return np.multiply.outer(np.outer(V_r, V_p), V_z)
@ -864,10 +892,10 @@ class SphericalMesh(MeshBase):
Requirement is r >= 0.
theta_grid : Iterable of float
Mesh boundary points along the theta-axis in degrees.
The default value is [0, 180], i.e. the full theta range.
The default value is [0, π], i.e. the full theta range.
phi_grid : Iterable of float
Mesh boundary points along the phi-axis in degrees.
The default value is [0, 360], i.e. the full phi range.
The default value is [0, 2π], i.e. the full phi range.
indices : Iterable of tuple
An iterable of mesh indices for each mesh element, e.g. [(1, 1, 1),
(2, 1, 1), ...]
@ -878,8 +906,8 @@ class SphericalMesh(MeshBase):
super().__init__(mesh_id, name)
self._r_grid = None
self._theta_grid = [0, 180]
self._phi_grid = [0, 360]
self._theta_grid = [0, pi]
self._phi_grid = [0, 2*pi]
@property
def dimension(self):
@ -921,12 +949,12 @@ class SphericalMesh(MeshBase):
@theta_grid.setter
def theta_grid(self, grid):
cv.check_type('mesh theta_grid', grid, Iterable, Real)
self._theta_grid = np.array(grid)
self._theta_grid = np.asarray(grid)
@phi_grid.setter
def phi_grid(self, grid):
cv.check_type('mesh phi_grid', grid, Iterable, Real)
self._phi_grid = np.array(grid)
self._phi_grid = np.asarray(grid)
def __repr__(self):
fmt = '{0: <16}{1}{2}\n'
@ -956,8 +984,8 @@ class SphericalMesh(MeshBase):
# Read and assign mesh properties
mesh = cls(mesh_id)
mesh.r_grid = group['r_grid'][()]
mesh.theta_grid = 180 / np.pi * group['theta_grid'][()]
mesh.phi_grid = 180 / np.pi * group['phi_grid'][()]
mesh.theta_grid = group['theta_grid'][()]
mesh.phi_grid = group['phi_grid'][()]
return mesh
@ -986,6 +1014,29 @@ class SphericalMesh(MeshBase):
return element
@classmethod
def from_xml_element(cls, elem):
"""Generate a spherical mesh from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.SphericalMesh
Spherical mesh object
"""
mesh_id = int(get_text(elem, 'id'))
mesh = cls(mesh_id)
mesh.r_grid = [float(x) for x in get_text(elem, "r_grid").split()]
mesh.theta_grid = [float(x) for x in get_text(elem, "theta_grid").split()]
mesh.phi_grid = [float(x) for x in get_text(elem, "phi_grid").split()]
return mesh
def calc_mesh_volumes(self):
"""Return Volumes for every mesh cell
@ -996,15 +1047,13 @@ class SphericalMesh(MeshBase):
"""
V_r = np.diff(np.array(self.r_grid)**3 / 3)
V_t = np.diff(-np.cos(np.pi * np.array(self.theta_grid) / 180.0))
V_p = np.diff(np.array(self.phi_grid) * np.pi / 180)
V_r = np.diff(np.asarray(self.r_grid)**3 / 3)
V_t = np.diff(-np.cos(self.theta_grid))
V_p = np.diff(self.phi_grid)
return np.multiply.outer(np.outer(V_r, V_t), V_p)
class UnstructuredMesh(MeshBase):
"""A 3D unstructured mesh
@ -1017,6 +1066,8 @@ class UnstructuredMesh(MeshBase):
----------
filename : str
Location of the unstructured mesh file
library : {'moab', 'libmesh'}
Mesh library used for the unstructured mesh tally
mesh_id : int
Unique identifier for the mesh
name : str
@ -1034,7 +1085,7 @@ class UnstructuredMesh(MeshBase):
Name of the file containing the unstructured mesh
length_multiplier: float
Multiplicative factor to apply to mesh coordinates
library : str
library : {'moab', 'libmesh'}
Mesh library used for the unstructured mesh tally
output : bool
Indicates whether or not automatic tally output should
@ -1291,4 +1342,4 @@ class UnstructuredMesh(MeshBase):
library = get_text(elem, 'library')
length_multiplier = float(get_text(elem, 'length_multiplier', 1.0))
return cls(filename, library, mesh_id, '', length_multiplier)
return cls(filename, library, mesh_id, '', length_multiplier)

View file

@ -5,7 +5,6 @@ import os
from pathlib import Path
from numbers import Integral
from tempfile import NamedTemporaryFile
import time
import h5py
@ -204,10 +203,9 @@ class Model:
@classmethod
def from_xml(cls, geometry='geometry.xml', materials='materials.xml',
settings='settings.xml'):
settings='settings.xml', tallies='tallies.xml',
plots='plots.xml'):
"""Create model from existing XML files
When initializing this way, the user must manually load plots and
tallies.
Parameters
----------
@ -217,6 +215,14 @@ class Model:
Path to materials.xml file
settings : str
Path to settings.xml file
tallies : str
Path to tallies.xml file
.. versionadded:: 0.13.0
plots : str
Path to plots.xml file
.. versionadded:: 0.13.0
Returns
-------
@ -227,7 +233,9 @@ class Model:
materials = openmc.Materials.from_xml(materials)
geometry = openmc.Geometry.from_xml(geometry, materials)
settings = openmc.Settings.from_xml(settings)
return cls(geometry, materials, settings)
tallies = openmc.Tallies.from_xml(tallies) if Path(tallies).exists() else None
plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None
return cls(geometry, materials, settings, tallies, plots)
def init_lib(self, threads=None, geometry_debug=False, restart_file=None,
tracks=False, output=True, event_based=None, intracomm=None):

View file

@ -684,6 +684,85 @@ class Plot(IDManagerMixin):
return element
@classmethod
def from_xml_element(cls, elem):
"""Generate plot object from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.Plot
Plot object
"""
plot_id = int(elem.get("id"))
plot = cls(plot_id)
if "filename" in elem.keys():
plot.filename = elem.get("filename")
plot.color_by = elem.get("color_by")
plot.type = elem.get("type")
plot.basis = elem.get("basis")
# Helper function to get a tuple of values
def get_tuple(elem, name, dtype=int):
subelem = elem.find(name)
if subelem is not None:
return tuple([dtype(x) for x in subelem.text.split()])
plot.origin = get_tuple(elem, "origin", float)
plot.width = get_tuple(elem, "width", float)
plot.pixels = get_tuple(elem, "pixels")
plot._background = get_tuple(elem, "background")
# Set plot colors
colors = {}
for color_elem in elem.findall("color"):
uid = color_elem.get("id")
colors[uid] = tuple([int(x) for x in color_elem.get("rgb").split()])
# TODO: set colors (needs geometry information)
# Set masking information
mask_elem = elem.find("mask")
if mask_elem is not None:
mask_components = [int(x) for x in mask_elem.get("components").split()]
# TODO: set mask components (needs geometry information)
background = mask_elem.get("background")
if background is not None:
plot.mask_background = tuple([int(x) for x in background.split()])
# show overlaps
overlap_elem = elem.find("show_overlaps")
if overlap_elem is not None:
plot.show_overlaps = (overlap_elem.text in ('true', '1'))
overlap_color = get_tuple(elem, "overlap_color")
if overlap_color is not None:
plot.overlap_color = overlap_color
# Set universe level
level = elem.find("level")
if level is not None:
plot.level = int(level.text)
# Set meshlines
mesh_elem = elem.find("meshlines")
if mesh_elem is not None:
meshlines = {'type': mesh_elem.get('meshtype')}
if 'id' in mesh_elem.keys():
meshlines['id'] = int(mesh_elem.get('id'))
if 'linewidth' in mesh_elem.keys():
meshlines['linewidth'] = int(mesh_elem.get('linewidth'))
if 'color' in mesh_elem.keys():
meshlines['color'] = tuple(
[int(x) for x in mesh_elem.get('color').split()]
)
plot.meshlines = meshlines
return plot
def to_ipython_image(self, openmc_exec='openmc', cwd='.'):
"""Render plot as an image
@ -848,3 +927,27 @@ class Plots(cv.CheckedList):
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(cls, path='plots.xml'):
"""Generate plots collection from XML file
Parameters
----------
path : str, optional
Path to plots XML file
Returns
-------
openmc.Plots
Plots collection
"""
tree = ET.parse(path)
root = tree.getroot()
# Generate each plot
plots = cls()
for elem in root.findall('plot'):
plots.append(Plot.from_xml_element(elem))
return plots

View file

@ -1247,6 +1247,12 @@ class Settings:
for elem in root.findall('source'):
self.source.append(Source.from_xml_element(elem))
def _volume_calcs_from_xml_element(self, root):
volume_elems = root.findall("volume_calc")
if volume_elems:
self.volume_calculations = [VolumeCalculation.from_xml_element(elem)
for elem in volume_elems]
def _output_from_xml_element(self, root):
elem = root.find('output')
if elem is not None:
@ -1585,6 +1591,7 @@ class Settings:
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)

View file

@ -492,6 +492,7 @@ class SphericalIndependent(Spatial):
origin = [float(x) for x in elem.get('origin').split()]
return cls(r, theta, phi, origin=origin)
class CylindricalIndependent(Spatial):
r"""Spatial distribution represented in cylindrical coordinates.

View file

@ -10,12 +10,14 @@ from xml.etree import ElementTree as ET
import h5py
import numpy as np
import pandas as pd
from scipy.misc import derivative
import scipy.sparse as sps
import openmc
import openmc.checkvalue as cv
from ._xml import clean_indentation, reorder_attributes
from ._xml import clean_indentation, reorder_attributes, get_text
from .mixin import IDManagerMixin
from .mesh import MeshBase
# The tally arithmetic product types. The tensor product performs the full
@ -844,13 +846,8 @@ class Tally(IDManagerMixin):
'not contain any scores'
raise ValueError(msg)
else:
scores = ''
for score in self.scores:
scores += f'{score} '
subelement = ET.SubElement(element, "scores")
subelement.text = scores.rstrip(' ')
subelement = ET.SubElement(element, "scores")
subelement.text = ' '.join(str(x) for x in self.scores)
# Tally estimator type
if self.estimator is not None:
@ -859,7 +856,7 @@ class Tally(IDManagerMixin):
# Optional Triggers
for trigger in self.triggers:
trigger.get_trigger_xml(element)
element.append(trigger.to_xml_element())
# Optional derivatives
if self.derivative is not None:
@ -868,6 +865,60 @@ class Tally(IDManagerMixin):
return element
@classmethod
def from_xml_element(cls, elem, **kwargs):
"""Generate tally object from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.Tally
Tally object
"""
tally_id = int(elem.get('id'))
name = elem.get('name', '')
tally = cls(tally_id=tally_id, name=name)
# Read filters
filters_elem = elem.find('filters')
if filters_elem is not None:
filter_ids = [int(x) for x in filters_elem.text.split()]
tally.filters = [kwargs['filters'][uid] for uid in filter_ids]
# Read nuclides
nuclides_elem = elem.find('nuclides')
if nuclides_elem is not None:
tally.nuclides = nuclides_elem.text.split()
# Read scores
scores_elem = elem.find('scores')
if scores_elem is not None:
tally.scores = scores_elem.text.split()
# Set estimator
estimator_elem = elem.find('estimator')
if estimator_elem is not None:
tally.estimator = estimator_elem.text
# Read triggers
tally.triggers = [
openmc.Trigger.from_xml_element(trigger_elem)
for trigger_elem in elem.findall('trigger')
]
# Read tally derivative
deriv_elem = elem.find('derivative')
if deriv_elem is not None:
deriv_id = int(deriv_elem.text)
tally.derivative = kwargs['derivatives'][deriv_id]
return tally
def contains_filter(self, filter_type):
"""Looks for a filter in the tally that matches a specified type
@ -3143,3 +3194,49 @@ class Tallies(cv.CheckedList):
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(cls, path='tallies.xml'):
"""Generate tallies from XML file
Parameters
----------
path : str, optional
Path to tallies XML file
Returns
-------
openmc.Tallies
Tallies object
"""
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)

View file

@ -105,3 +105,24 @@ class TallyDerivative(EqualityMixin, IDManagerMixin):
if self.variable == 'nuclide_density':
element.set("nuclide", self.nuclide)
return element
@classmethod
def from_xml_element(cls, elem):
"""Generate tally derivative from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.TallyDerivative
Tally derivative object
"""
derivative_id = int(elem.get("id"))
variable = elem.get("variable")
material = int(elem.get("material"))
nuclide = elem.get("nuclide") if variable == "nuclide_density" else None
return cls(derivative_id, variable, material, nuclide)

View file

@ -74,7 +74,7 @@ class Trigger(EqualityMixin):
if score not in self._scores:
self._scores.append(score)
def get_trigger_xml(self, element):
def to_xml_element(self):
"""Return XML representation of the trigger
Returns
@ -84,8 +84,36 @@ class Trigger(EqualityMixin):
"""
subelement = ET.SubElement(element, "trigger")
subelement.set("type", self._trigger_type)
subelement.set("threshold", str(self._threshold))
element = ET.Element("trigger")
element.set("type", self._trigger_type)
element.set("threshold", str(self._threshold))
if len(self._scores) != 0:
subelement.set("scores", ' '.join(map(str, self._scores)))
element.set("scores", ' '.join(self._scores))
return element
@classmethod
def from_xml_element(cls, elem):
"""Generate trigger object from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.Trigger
Trigger object
"""
# Generate trigger object
trigger_type = elem.get("type")
threshold = float(elem.get("threshold"))
trigger = cls(trigger_type, threshold)
# Add scores if present
scores = elem.get("scores")
if scores is not None:
trigger.scores = scores.split()
return trigger

View file

@ -11,6 +11,7 @@ from uncertainties import ufloat
import openmc
import openmc.checkvalue as cv
from openmc._xml import get_text
_VERSION_VOLUME = 1
@ -352,3 +353,49 @@ class VolumeCalculation:
trigger_elem.set("type", self.trigger_type)
trigger_elem.set("threshold", str(self.threshold))
return element
@classmethod
def from_xml_element(cls, elem):
"""Generate volume calculation object from an XML element
Parameters
----------
elem : xml.etree.ElementTree.Element
XML element
Returns
-------
openmc.VolumeCalculation
Volume calculation object
"""
domain_type = get_text(elem, "domain_type")
domain_ids = get_text(elem, "domain_ids").split()
ids = [int(x) for x in domain_ids]
samples = int(get_text(elem, "samples"))
lower_left = get_text(elem, "lower_left").split()
lower_left = tuple([float(x) for x in lower_left])
upper_right = get_text(elem, "upper_right").split()
upper_right = tuple([float(x) for x in upper_right])
# Instantiate some throw-away domains that are used by the constructor
# to assign IDs
with warnings.catch_warnings():
warnings.simplefilter('ignore', openmc.IDWarning)
if domain_type == 'cell':
domains = [openmc.Cell(uid) for uid in ids]
elif domain_type == 'material':
domains = [openmc.Material(uid) for uid in ids]
elif domain_type == 'universe':
domains = [openmc.Universe(uid) for uid in ids]
vol = cls(domains, samples, lower_left, upper_right)
# Check for trigger
trigger_elem = elem.find("threshold")
if trigger_elem is not None:
trigger_type = get_text(trigger_elem, "type")
threshold = float(get_text(trigger_elem, "threshold"))
vol.set_trigger(threshold, trigger_type)
return vol

View file

@ -270,7 +270,7 @@ class WeightWindows(IDManagerMixin):
path = f"./mesh[@id='{mesh_id}']"
mesh_elem = root.find(path)
if mesh_elem is not None:
mesh = MeshBase.from_xml(mesh_elem)
mesh = MeshBase.from_xml_element(mesh_elem)
# Read all other parameters
lower_ww_bounds = [float(l) for l in get_text(elem, 'lower_ww_bounds').split()]

View file

@ -1001,7 +1001,7 @@ double CylindricalMesh::find_r_crossing(
double CylindricalMesh::find_phi_crossing(
const Position& r, const Direction& u, double l, int shell) const
{
// Phi grid is [0, 360], thus there is no real surface to cross
// Phi grid is [0, ], thus there is no real surface to cross
if (full_phi_ && (shape_[1] == 1))
return INFTY;
@ -1104,18 +1104,14 @@ int CylindricalMesh::set_grid()
"cylindrical meshes must start at phi >= 0.");
return OPENMC_E_INVALID_ARGUMENT;
}
if (grid_[1].back() > 360) {
if (grid_[1].back() > 2.0 * PI) {
set_errmsg("phi-grids for "
"cylindrical meshes must end with theta <= 360 degree.");
"cylindrical meshes must end with theta <= 2*pi.");
return OPENMC_E_INVALID_ARGUMENT;
}
full_phi_ = (grid_[1].front() == 0.0) && (grid_[1].back() == 360.0);
// Transform phi-grid from degrees to radians
std::transform(grid_[1].begin(), grid_[1].end(), grid_[1].begin(),
[](double d) { return M_PI * d / 180.0; });
full_phi_ = (grid_[1].front() == 0.0) && (grid_[1].back() == 2.0 * PI);
lower_left_ = {grid_[0].front(), grid_[1].front(), grid_[2].front()};
upper_right_ = {grid_[0].back(), grid_[1].back(), grid_[2].back()};
@ -1225,7 +1221,7 @@ double SphericalMesh::find_r_crossing(
double SphericalMesh::find_theta_crossing(
const Position& r, const Direction& u, double l, int shell) const
{
// Theta grid is [0, 180], thus there is no real surface to cross
// Theta grid is [0, π], thus there is no real surface to cross
if (full_theta_ && (shape_[1] == 1))
return INFTY;
@ -1287,7 +1283,7 @@ double SphericalMesh::find_theta_crossing(
double SphericalMesh::find_phi_crossing(
const Position& r, const Direction& u, double l, int shell) const
{
// Phi grid is [0, 360], thus there is no real surface to cross
// Phi grid is [0, ], thus there is no real surface to cross
if (full_phi_ && (shape_[2] == 1))
return INFTY;
@ -1368,25 +1364,20 @@ int SphericalMesh::set_grid()
return OPENMC_E_INVALID_ARGUMENT;
}
}
if (grid_[1].back() > 180) {
if (grid_[1].back() > PI) {
set_errmsg("theta-grids for "
"spherical meshes must end with theta <= 180 degree.");
"spherical meshes must end with theta <= pi.");
return OPENMC_E_INVALID_ARGUMENT;
}
if (grid_[2].back() > 360) {
if (grid_[2].back() > 2 * PI) {
set_errmsg("phi-grids for "
"spherical meshes must end with phi <= 180 degree.");
"spherical meshes must end with phi <= 2*pi.");
return OPENMC_E_INVALID_ARGUMENT;
}
full_theta_ = (grid_[1].front() == 0.0) && (grid_[1].back() == 180.0);
full_phi_ = (grid_[2].front() == 0.0) && (grid_[2].back() == 360.0);
// Transform theta- and phi-grid from degrees to radians
for (int i = 1; i < 3; i++)
std::transform(grid_[i].begin(), grid_[i].end(), grid_[i].begin(),
[](double d) { return M_PI * d / 180.0; });
full_theta_ = (grid_[1].front() == 0.0) && (grid_[1].back() == PI);
full_phi_ = (grid_[2].front() == 0.0) && (grid_[2].back() == 2 * PI);
lower_left_ = {grid_[0].front(), grid_[1].front(), grid_[2].front()};
upper_right_ = {grid_[0].back(), grid_[1].back(), grid_[2].back()};

View file

@ -55,13 +55,13 @@
</mesh>
<mesh id="5" type="cylindrical">
<r_grid>0.0 0.4411764705882353 0.8823529411764706 1.3235294117647058 1.7647058823529411 2.2058823529411766 2.6470588235294117 3.0882352941176467 3.5294117647058822 3.9705882352941178 4.411764705882353 4.852941176470588 5.294117647058823 5.735294117647059 6.1764705882352935 6.617647058823529 7.0588235294117645 7.5</r_grid>
<phi_grid>0.0 20.0 40.0 60.0 80.0 100.0 120.0 140.0 160.0 180.0 200.0 220.0 240.0 260.0 280.0 300.0 320.0 340.0 360.0</phi_grid>
<phi_grid>0.0 0.3490658503988659 0.6981317007977318 1.0471975511965976 1.3962634015954636 1.7453292519943295 2.0943951023931953 2.443460952792061 2.792526803190927 3.141592653589793 3.490658503988659 3.839724354387525 4.1887902047863905 4.537856055185257 4.886921905584122 5.235987755982989 5.585053606381854 5.93411945678072 6.283185307179586</phi_grid>
<z_grid>-7.5 -6.5625 -5.625 -4.6875 -3.75 -2.8125 -1.875 -0.9375 0.0 0.9375 1.875 2.8125 3.75 4.6875 5.625 6.5625 7.5</z_grid>
</mesh>
<mesh id="6" type="spherical">
<r_grid>0.0 0.4411764705882353 0.8823529411764706 1.3235294117647058 1.7647058823529411 2.2058823529411766 2.6470588235294117 3.0882352941176467 3.5294117647058822 3.9705882352941178 4.411764705882353 4.852941176470588 5.294117647058823 5.735294117647059 6.1764705882352935 6.617647058823529 7.0588235294117645 7.5</r_grid>
<theta_grid>0.0 22.5 45.0 67.5 90.0 112.5 135.0 157.5 180.0</theta_grid>
<phi_grid>0.0 20.0 40.0 60.0 80.0 100.0 120.0 140.0 160.0 180.0 200.0 220.0 240.0 260.0 280.0 300.0 320.0 340.0 360.0</phi_grid>
<theta_grid>0.0 0.39269908169872414 0.7853981633974483 1.1780972450961724 1.5707963267948966 1.9634954084936207 2.356194490192345 2.748893571891069 3.141592653589793</theta_grid>
<phi_grid>0.0 0.3490658503988659 0.6981317007977318 1.0471975511965976 1.3962634015954636 1.7453292519943295 2.0943951023931953 2.443460952792061 2.792526803190927 3.141592653589793 3.490658503988659 3.839724354387525 4.1887902047863905 4.537856055185257 4.886921905584122 5.235987755982989 5.585053606381854 5.93411945678072 6.283185307179586</phi_grid>
</mesh>
<filter id="1" type="mesh">
<bins>1</bins>

View file

@ -1,4 +1,5 @@
import numpy as np
from math import pi
import openmc
import pytest
@ -53,13 +54,13 @@ def model():
cyl_mesh = openmc.CylindricalMesh()
cyl_mesh.r_grid = np.linspace(0, 7.5, 18)
cyl_mesh.phi_grid = np.linspace(0, 360, 19)
cyl_mesh.phi_grid = np.linspace(0, 2*pi, 19)
cyl_mesh.z_grid = np.linspace(-7.5, 7.5, 17)
sph_mesh = openmc.SphericalMesh()
sph_mesh.r_grid = np.linspace(0, 7.5, 18)
sph_mesh.theta_grid = np.linspace(0, 180, 9)
sph_mesh.phi_grid = np.linspace(0, 360, 19)
sph_mesh.theta_grid = np.linspace(0, pi, 9)
sph_mesh.phi_grid = np.linspace(0, 2*pi, 19)
# Create filters
reg_filters = [

View file

@ -1,5 +1,4 @@
from math import sqrt, pi
import numpy as np
import openmc
from pytest import fixture, approx
@ -38,6 +37,11 @@ def test_cell_instance():
assert all(x == c1.id for x in bins[:6:2])
assert all(x == c2.id for x in bins[6::2])
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
assert new_f.id == f.id
assert np.all(new_f.bins == f.bins)
# get_pandas_dataframe()
df = f.get_pandas_dataframe(f.num_bins, 1)
cells = df['cellinstance', 'cell']
@ -61,6 +65,11 @@ def test_collision():
assert elem.tag == 'filter'
assert elem.attrib['type'] == 'collision'
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
assert new_f.id == f.id
assert np.all(new_f.bins == f.bins)
def test_legendre():
n = 5
@ -79,6 +88,11 @@ def test_legendre():
assert elem.attrib['type'] == 'legendre'
assert elem.find('order').text == str(n)
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
assert new_f.id == f.id
assert new_f.bins, f.bins
def test_spatial_legendre():
n = 5
@ -102,6 +116,12 @@ def test_spatial_legendre():
assert elem.find('order').text == str(n)
assert elem.find('axis').text == str(axis)
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
assert new_f.id == f.id
assert new_f.order == f.order
assert new_f.axis == f.axis
def test_spherical_harmonics():
n = 3
@ -122,6 +142,12 @@ def test_spherical_harmonics():
assert elem.attrib['cosine'] == f.cosine
assert elem.find('order').text == str(n)
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
assert new_f.id == f.id
assert new_f.order == f.order
assert new_f.cosine == f.cosine
def test_zernike():
n = 4
@ -140,6 +166,12 @@ def test_zernike():
assert elem.attrib['type'] == 'zernike'
assert elem.find('order').text == str(n)
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
for attr in ('id', 'order', 'x', 'y', 'r'):
assert getattr(new_f, attr) == getattr(f, attr)
def test_zernike_radial():
n = 4
f = openmc.ZernikeRadialFilter(n, 0., 0., 1.)
@ -157,6 +189,11 @@ def test_zernike_radial():
assert elem.attrib['type'] == 'zernikeradial'
assert elem.find('order').text == str(n)
# from_xml_element()
new_f = openmc.Filter.from_xml_element(elem)
for attr in ('id', 'order', 'x', 'y', 'r'):
assert getattr(new_f, attr) == getattr(f, attr)
def test_first_moment(run_in_tmpdir, box_model):
plain_tally = openmc.Tally()

View file

@ -583,17 +583,17 @@ def test_rectilinear_mesh(lib_init):
def test_cylindrical_mesh(lib_init):
deg2rad = lambda deg: deg*np.pi/180
mesh = openmc.lib.CylindricalMesh()
x_grid = [0., 5., 10.]
y_grid = [0., 10., 20.]
r_grid = [0., 5., 10.]
phi_grid = np.radians([0., 10., 20.])
z_grid = [10., 20., 30.]
mesh.set_grid(x_grid, y_grid, z_grid)
mesh.set_grid(r_grid, phi_grid, z_grid)
assert np.all(mesh.lower_left == (0., 0., 10.))
assert np.all(mesh.upper_right == (10., deg2rad(20.), 30.))
assert np.all(mesh.dimension == (2, 2, 2))
for i, diff_x in enumerate(np.diff(x_grid)):
for j, diff_y in enumerate(np.diff(y_grid)):
for k, diff_z in enumerate(np.diff(z_grid)):
assert np.all(mesh.width[i, j, k, :] == (5, deg2rad(10), 10))
for i, _ in enumerate(np.diff(r_grid)):
for j, _ in enumerate(np.diff(phi_grid)):
for k, _ in enumerate(np.diff(z_grid)):
assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), 10))
with pytest.raises(exc.AllocationError):
mesh2 = openmc.lib.CylindricalMesh(mesh.id)
@ -614,17 +614,17 @@ def test_cylindrical_mesh(lib_init):
def test_spherical_mesh(lib_init):
deg2rad = lambda deg: deg*np.pi/180
mesh = openmc.lib.SphericalMesh()
x_grid = [0., 5., 10.]
y_grid = [0., 10., 20.]
z_grid = [10., 20., 30.]
mesh.set_grid(x_grid, y_grid, z_grid)
r_grid = [0., 5., 10.]
theta_grid = np.radians([0., 10., 20.])
phi_grid = np.radians([10., 20., 30.])
mesh.set_grid(r_grid, theta_grid, phi_grid)
assert np.all(mesh.lower_left == (0., 0., deg2rad(10.)))
assert np.all(mesh.upper_right == (10., deg2rad(20.), deg2rad(30.)))
assert np.all(mesh.dimension == (2, 2, 2))
for i, diff_x in enumerate(np.diff(x_grid)):
for j, diff_y in enumerate(np.diff(y_grid)):
for k, diff_z in enumerate(np.diff(z_grid)):
assert np.all(abs(mesh.width[i, j, k, :] - (5, deg2rad(10), deg2rad(10))) < 1e-16)
for i, _ in enumerate(np.diff(r_grid)):
for j, _ in enumerate(np.diff(theta_grid)):
for k, _ in enumerate(np.diff(phi_grid)):
assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), deg2rad(10)))
with pytest.raises(exc.AllocationError):
mesh2 = openmc.lib.SphericalMesh(mesh.id)

View file

@ -195,8 +195,8 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes):
keys = sorted(k for k in settings.__dict__.keys() if k not in no_test)
for ref_k in keys:
assert test_model.settings.__dict__[ref_k] == settings.__dict__[ref_k]
assert len(test_model.tallies) == 0
assert len(test_model.plots) == 0
assert len(test_model.tallies) == 1
assert len(test_model.plots) == 2
assert test_model._materials_by_id == \
{1: test_model.materials[0], 2: test_model.materials[1],
3: test_model.materials[2]}

View file

@ -1,3 +1,4 @@
import numpy as np
import openmc
import openmc.examples
import pytest
@ -12,8 +13,8 @@ def myplot():
plot.filename = 'myplot'
plot.type = 'slice'
plot.basis = 'yz'
plot.background = (0, 0, 0)
plot.background = 'black'
plot.background = (0, 0, 0)
plot.color_by = 'material'
m1, m2 = openmc.Material(), openmc.Material()
@ -21,8 +22,8 @@ def myplot():
plot.colors = {m1: 'green', m2: 'blue'}
plot.mask_components = [openmc.Material()]
plot.mask_background = (255, 255, 255)
plot.mask_background = 'white'
plot.mask_background = (255, 255, 255)
plot.overlap_color = (255, 211, 0)
plot.overlap_color = 'yellow'
@ -70,7 +71,7 @@ def test_highlight_domains():
plots.highlight_domains(model.geometry, mats)
def test_to_xml_element(myplot):
def test_xml_element(myplot):
elem = myplot.to_xml_element()
assert 'id' in elem.attrib
assert 'color_by' in elem.attrib
@ -80,10 +81,19 @@ def test_to_xml_element(myplot):
assert elem.find('pixels') is not None
assert elem.find('background').text == '0 0 0'
newplot = openmc.Plot.from_xml_element(elem)
attributes = ('id', 'color_by', 'filename', 'type', 'basis', 'level',
'meshlines', 'show_overlaps', 'origin', 'width', 'pixels',
'background', 'mask_background')
for attr in attributes:
assert getattr(newplot, attr) == getattr(myplot, attr), attr
def test_plots(run_in_tmpdir):
p1 = openmc.Plot(name='plot1')
p1.origin = (5., 5., 5.)
p2 = openmc.Plot(name='plot2')
p2.origin = (-3., -3., -3.)
plots = openmc.Plots([p1, p2])
assert len(plots) == 2
@ -92,3 +102,9 @@ def test_plots(run_in_tmpdir):
assert len(plots) == 3
plots.export_to_xml()
# from_xml
new_plots = openmc.Plots.from_xml()
assert len(plots)
assert plots[0].origin == p1.origin
assert plots[1].origin == p2.origin

View file

@ -112,3 +112,10 @@ def test_export_to_xml(run_in_tmpdir):
assert not s.photon_transport
assert s.electron_treatment == 'led'
assert s.write_initial_source == True
assert len(s.volume_calculations) == 1
vol = s.volume_calculations[0]
assert vol.domain_type == 'cell'
assert len(vol.ids) == 1
assert vol.samples == 1000
assert vol.lower_left == (-10., -10., -10.)
assert vol.upper_right == (10., 10., 10.)

View file

@ -0,0 +1,40 @@
import numpy as np
import openmc
def test_xml_roundtrip(run_in_tmpdir):
# Create a tally with all possible gizmos
mesh = openmc.RegularMesh()
mesh.lower_left = (-10., -10., -10.)
mesh.upper_right = (10., 10., 10.,)
mesh.dimension = (5, 5, 5)
mesh_filter = openmc.MeshFilter(mesh)
tally = openmc.Tally()
tally.filters = [mesh_filter]
tally.nuclides = ['U235', 'I135', 'Li6']
tally.scores = ['total', 'fission', 'heating']
tally.derivative = openmc.TallyDerivative(
variable='nuclide_density', material=1, nuclide='Li6'
)
tally.triggers = [openmc.Trigger('rel_err', 0.025)]
tally.triggers[0].scores = ['total', 'fission']
tallies = openmc.Tallies([tally])
# Roundtrip through XML and make sure we get what we started with
tallies.export_to_xml()
new_tallies = openmc.Tallies.from_xml()
assert len(new_tallies) == 1
new_tally = new_tallies[0]
assert new_tally.id == tally.id
assert len(new_tally.filters) == 1
assert isinstance(new_tally.filters[0], openmc.MeshFilter)
assert np.allclose(new_tally.filters[0].mesh.lower_left, mesh.lower_left)
assert new_tally.nuclides == tally.nuclides
assert new_tally.scores == tally.scores
assert new_tally.derivative.variable == tally.derivative.variable
assert new_tally.derivative.material == tally.derivative.material
assert new_tally.derivative.nuclide == tally.derivative.nuclide
assert len(new_tally.triggers) == 1
assert new_tally.triggers[0].trigger_type == tally.triggers[0].trigger_type
assert new_tally.triggers[0].threshold == tally.triggers[0].threshold
assert new_tally.triggers[0].scores == tally.triggers[0].scores