from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterable
from copy import deepcopy
import math
from numbers import Real
from warnings import warn, catch_warnings, simplefilter
import lxml.etree as ET
import numpy as np
from .checkvalue import check_type, check_value, check_length, check_greater_than
from .mixin import IDManagerMixin, IDWarning
from .region import Region, Intersection, Union
from .bounding_box import BoundingBox
_BOUNDARY_TYPES = {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
_ALBEDO_BOUNDARIES = {'reflective', 'periodic', 'white'}
_WARNING_UPPER = """\
"{}(...) accepts an argument named '{}', not '{}'. Future versions of OpenMC \
will not accept the capitalized version.\
"""
_WARNING_KWARGS = """\
"{}(...) accepts keyword arguments only for '{}'. Future versions of OpenMC \
will not accept positional parameters for superclass arguments.\
"""
class SurfaceCoefficient:
"""Descriptor class for surface coefficients.
Parameters
-----------
value : float or str
Value of the coefficient (float) or the name of the coefficient that
it is equivalent to (str).
"""
def __init__(self, value):
self.value = value
def __get__(self, instance, owner=None):
if instance is None:
return self
else:
if isinstance(self.value, str):
return instance._coefficients[self.value]
else:
return self.value
def __set__(self, instance, value):
if isinstance(self.value, Real):
raise AttributeError('This coefficient is read-only')
check_type(f'{self.value} coefficient', value, Real)
instance._coefficients[self.value] = value
def _future_kwargs_warning_helper(cls, *args, **kwargs):
# Warn if Surface parameters are passed by position, not by keyword
argsdict = dict(zip(('boundary_type', 'name', 'surface_id'), args))
for k in argsdict:
warn(_WARNING_KWARGS.format(cls.__name__, k), FutureWarning)
kwargs.update(argsdict)
return kwargs
def get_rotation_matrix(rotation, order='xyz'):
r"""Generate a 3x3 rotation matrix from input angles
.. versionadded:: 0.12
Parameters
----------
rotation : 3-tuple of float
A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where the
first element is the rotation about the x-axis in the fixed laboratory
frame, the second element is the rotation about the y-axis in the fixed
laboratory frame, and the third element is the rotation about the
z-axis in the fixed laboratory frame. The rotations are active
rotations.
order : str, optional
A string of 'x', 'y', and 'z' in some order specifying which rotation
to perform first, second, and third. Defaults to 'xyz' which means, the
rotation by angle :math:`\phi` about x will be applied first, followed
by :math:`\theta` about y and then :math:`\psi` about z. This
corresponds to an x-y-z extrinsic rotation as well as a z-y'-x''
intrinsic rotation using Tait-Bryan angles :math:`(\phi, \theta, \psi)`.
"""
check_type('surface rotation', rotation, Iterable, Real)
check_length('surface rotation', rotation, 3)
phi, theta, psi = np.array(rotation)*(math.pi/180.)
cx, sx = math.cos(phi), math.sin(phi)
cy, sy = math.cos(theta), math.sin(theta)
cz, sz = math.cos(psi), math.sin(psi)
R = {
'x': np.array([[1., 0., 0.], [0., cx, -sx], [0., sx, cx]]),
'y': np.array([[cy, 0., sy], [0., 1., 0.], [-sy, 0., cy]]),
'z': np.array([[cz, -sz, 0.], [sz, cz, 0.], [0., 0., 1.]]),
}
R1, R2, R3 = (R[xi] for xi in order)
return R3 @ R2 @ R1
[docs]class Surface(IDManagerMixin, ABC):
"""An implicit surface with an associated boundary condition.
An implicit surface is defined as the set of zeros of a function of the
three Cartesian coordinates. Surfaces in OpenMC are limited to a set of
algebraic surfaces, i.e., surfaces that are polynomial in x, y, and z.
Parameters
----------
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface. Note that periodic boundary conditions
can only be applied to x-, y-, and z-planes, and only axis-aligned
periodicity is supported.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the surface. If not specified, the name will be the empty
string.
Attributes
----------
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
next_id = 1
used_ids = set()
_atol = 1.e-12
def __init__(self, surface_id=None, boundary_type='transmission',
albedo=1., name=''):
self.id = surface_id
self.name = name
self.boundary_type = boundary_type
self.albedo = albedo
# A dictionary of the quadratic surface coefficients
# Key - coefficient name
# Value - coefficient value
self._coefficients = {}
def __neg__(self):
return Halfspace(self, '-')
def __pos__(self):
return Halfspace(self, '+')
def __repr__(self):
string = 'Surface\n'
string += '{0: <20}{1}{2}\n'.format('\tID', '=\t', self._id)
string += '{0: <20}{1}{2}\n'.format('\tName', '=\t', self._name)
string += '{0: <20}{1}{2}\n'.format('\tType', '=\t', self._type)
string += '{0: <20}{1}{2}\n'.format('\tBoundary', '=\t',
self._boundary_type)
if (self._boundary_type in _ALBEDO_BOUNDARIES and
not math.isclose(self._albedo, 1.0)):
string += '{0: <20}{1}{2}\n'.format('\tBoundary Albedo', '=\t',
self._albedo)
coefficients = '{0: <20}'.format('\tCoefficients') + '\n'
for coeff in self._coefficients:
coefficients += f'{coeff: <20}=\t{self._coefficients[coeff]}\n'
string += coefficients
return string
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if name is not None:
check_type('surface name', name, str)
self._name = name
else:
self._name = ''
@property
def type(self):
return self._type
@property
def boundary_type(self):
return self._boundary_type
@boundary_type.setter
def boundary_type(self, boundary_type):
check_type('boundary type', boundary_type, str)
check_value('boundary type', boundary_type, _BOUNDARY_TYPES)
self._boundary_type = boundary_type
@property
def albedo(self):
return self._albedo
@albedo.setter
def albedo(self, albedo):
check_type('albedo', albedo, Real)
check_greater_than('albedo', albedo, 0.0)
self._albedo = float(albedo)
@property
def coefficients(self):
return self._coefficients
[docs] def bounding_box(self, side):
"""Determine an axis-aligned bounding box.
An axis-aligned bounding box for surface half-spaces is represented by
its lower-left and upper-right coordinates. If the half-space is
unbounded in a particular direction, numpy.inf is used to represent
infinity.
Parameters
----------
side : {'+', '-'}
Indicates the negative or positive half-space
Returns
-------
numpy.ndarray
Lower-left coordinates of the axis-aligned bounding box for the
desired half-space
numpy.ndarray
Upper-right coordinates of the axis-aligned bounding box for the
desired half-space
"""
return BoundingBox.infinite()
[docs] def clone(self, memo=None):
"""Create a copy of this surface with a new unique ID.
Parameters
----------
memo : dict or None
A nested dictionary of previously cloned objects. This parameter
is used internally and should not be specified by the user.
Returns
-------
clone : openmc.Surface
The clone of this surface
"""
if memo is None:
memo = {}
# If no memoize'd clone exists, instantiate one
if self not in memo:
clone = deepcopy(self)
clone.id = None
# Memoize the clone
memo[self] = clone
return memo[self]
[docs] def normalize(self, coeffs=None):
"""Normalize coefficients by first nonzero value
.. versionadded:: 0.12
Parameters
----------
coeffs : tuple, optional
Tuple of surface coefficients to normalize. Defaults to None. If no
coefficients are supplied then the coefficients will be taken from
the current Surface.
Returns
-------
tuple of normalized coefficients
"""
if coeffs is None:
coeffs = self._get_base_coeffs()
coeffs = np.asarray(coeffs)
nonzeros = ~np.isclose(coeffs, 0., rtol=0., atol=self._atol)
norm_factor = np.abs(coeffs[nonzeros][0])
return tuple([c/norm_factor for c in coeffs])
[docs] def is_equal(self, other):
"""Determine if this Surface is equivalent to another
Parameters
----------
other : instance of openmc.Surface
Instance of openmc.Surface that should be compared to the current
surface
"""
coeffs1 = self.normalize(self._get_base_coeffs())
coeffs2 = self.normalize(other._get_base_coeffs())
return np.allclose(coeffs1, coeffs2, rtol=0., atol=self._atol)
@abstractmethod
def _get_base_coeffs(self):
"""Return polynomial coefficients representing the implicit surface
equation.
"""
[docs] @abstractmethod
def evaluate(self, point):
"""Evaluate the surface equation at a given point.
Parameters
----------
point : 3-tuple of float
The Cartesian coordinates, :math:`(x',y',z')`, at which the surface
equation should be evaluated.
Returns
-------
float
Evaluation of the surface polynomial at point :math:`(x',y',z')`
"""
[docs] @abstractmethod
def translate(self, vector, inplace=False):
"""Translate surface in given direction
Parameters
----------
vector : iterable of float
Direction in which surface should be translated
inplace : bool
Whether or not to return a new instance of this Surface or to
modify the coefficients of this Surface.
Returns
-------
instance of openmc.Surface
Translated surface
"""
[docs] @abstractmethod
def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
r"""Rotate surface by angles provided or by applying matrix directly.
.. versionadded:: 0.12
Parameters
----------
rotation : 3-tuple of float, or 3x3 iterable
A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where
the first element is the rotation about the x-axis in the fixed
laboratory frame, the second element is the rotation about the
y-axis in the fixed laboratory frame, and the third element is the
rotation about the z-axis in the fixed laboratory frame. The
rotations are active rotations. Additionally a 3x3 rotation matrix
can be specified directly either as a nested iterable or array.
pivot : iterable of float, optional
(x, y, z) coordinates for the point to rotate about. Defaults to
(0., 0., 0.)
order : str, optional
A string of 'x', 'y', and 'z' in some order specifying which
rotation to perform first, second, and third. Defaults to 'xyz'
which means, the rotation by angle :math:`\phi` about x will be
applied first, followed by :math:`\theta` about y and then
:math:`\psi` about z. This corresponds to an x-y-z extrinsic
rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan
angles :math:`(\phi, \theta, \psi)`.
inplace : bool
Whether or not to return a new instance of Surface or to modify the
coefficients of this Surface in place. Defaults to False.
Returns
-------
openmc.Surface
Rotated surface
"""
[docs] def to_xml_element(self):
"""Return XML representation of the surface
Returns
-------
element : lxml.etree._Element
XML element containing source data
"""
element = ET.Element("surface")
element.set("id", str(self._id))
if len(self._name) > 0:
element.set("name", str(self._name))
element.set("type", self._type)
if self.boundary_type != 'transmission':
element.set("boundary", self.boundary_type)
if (self.boundary_type in _ALBEDO_BOUNDARIES and
not math.isclose(self.albedo, 1.0)):
element.set("albedo", str(self.albedo))
element.set("coeffs", ' '.join([str(self._coefficients.setdefault(key, 0.0))
for key in self._coeff_keys]))
return element
[docs] @staticmethod
def from_xml_element(elem):
"""Generate surface from an XML element
Parameters
----------
elem : lxml.etree._Element
XML element
Returns
-------
openmc.Surface
Instance of a surface subclass
"""
# Determine appropriate class
surf_type = elem.get('type')
cls = _SURFACE_CLASSES[surf_type]
# Determine ID, boundary type, boundary albedo, coefficients
kwargs = {}
kwargs['surface_id'] = int(elem.get('id'))
kwargs['boundary_type'] = elem.get('boundary', 'transmission')
if kwargs['boundary_type'] in _ALBEDO_BOUNDARIES:
kwargs['albedo'] = float(elem.get('albedo', 1.0))
kwargs['name'] = elem.get('name')
coeffs = [float(x) for x in elem.get('coeffs').split()]
kwargs.update(dict(zip(cls._coeff_keys, coeffs)))
return cls(**kwargs)
[docs] @staticmethod
def from_hdf5(group):
"""Create surface from HDF5 group
Parameters
----------
group : h5py.Group
Group in HDF5 file
Returns
-------
openmc.Surface
Instance of surface subclass
"""
# If this is a DAGMC surface, do nothing for now
geom_type = group.get('geom_type')
if geom_type and geom_type[()].decode() == 'dagmc':
return
surface_id = int(group.name.split('/')[-1].lstrip('surface '))
name = group['name'][()].decode() if 'name' in group else ''
bc = group['boundary_type'][()].decode()
if 'albedo' in group:
bc_alb = float(group['albedo'][()].decode())
else:
bc_alb = 1.0
coeffs = group['coefficients'][...]
kwargs = {'boundary_type': bc, 'albedo': bc_alb, 'name': name,
'surface_id': surface_id}
surf_type = group['type'][()].decode()
cls = _SURFACE_CLASSES[surf_type]
return cls(*coeffs, **kwargs)
class PlaneMixin:
"""A Plane mixin class for all operations on order 1 surfaces"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._periodic_surface = None
@property
def periodic_surface(self):
return self._periodic_surface
@periodic_surface.setter
def periodic_surface(self, periodic_surface):
check_type('periodic surface', periodic_surface, Plane)
self._periodic_surface = periodic_surface
periodic_surface._periodic_surface = self
def _get_base_coeffs(self):
return (self.a, self.b, self.c, self.d)
def _get_normal(self):
a, b, c = self._get_base_coeffs()[:3]
return np.array((a, b, c)) / math.sqrt(a*a + b*b + c*c)
def bounding_box(self, side):
"""Determine an axis-aligned bounding box.
An axis-aligned bounding box for Plane half-spaces is represented by
its lower-left and upper-right coordinates. If the half-space is
unbounded in a particular direction, numpy.inf is used to represent
infinity.
Parameters
----------
side : {'+', '-'}
Indicates the negative or positive half-space
Returns
-------
numpy.ndarray
Lower-left coordinates of the axis-aligned bounding box for the
desired half-space
numpy.ndarray
Upper-right coordinates of the axis-aligned bounding box for the
desired half-space
"""
# Compute the bounding box based on the normal vector to the plane
nhat = self._get_normal()
ll = np.array([-np.inf, -np.inf, -np.inf])
ur = np.array([np.inf, np.inf, np.inf])
# If the plane is axis aligned, find the proper bounding box
if np.any(np.isclose(np.abs(nhat), 1., rtol=0., atol=self._atol)):
sign = nhat.sum()
a, b, c, d = self._get_base_coeffs()
vals = [d/val if not np.isclose(val, 0., rtol=0., atol=self._atol)
else np.nan for val in (a, b, c)]
if side == '-':
if sign > 0:
ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
else:
ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
elif side == '+':
if sign > 0:
ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
else:
ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
return BoundingBox(ll, ur)
def evaluate(self, point):
"""Evaluate the surface equation at a given point.
Parameters
----------
point : 3-tuple of float
The Cartesian coordinates, :math:`(x',y',z')`, at which the surface
equation should be evaluated.
Returns
-------
float
:math:`Ax' + By' + Cz' - D`
"""
x, y, z = point
a, b, c, d = self._get_base_coeffs()
return a*x + b*y + c*z - d
def translate(self, vector, inplace=False):
"""Translate surface in given direction
Parameters
----------
vector : iterable of float
Direction in which surface should be translated
inplace : bool
Whether or not to return a new instance of a Plane or to modify the
coefficients of this plane.
Returns
-------
openmc.Plane
Translated surface
"""
if np.allclose(vector, 0., rtol=0., atol=self._atol):
return self
a, b, c, d = self._get_base_coeffs()
d = d + np.dot([a, b, c], vector)
surf = self if inplace else self.clone()
setattr(surf, surf._coeff_keys[-1], d)
return surf
def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
pivot = np.asarray(pivot)
rotation = np.asarray(rotation, dtype=float)
# Allow rotation matrix to be passed in directly, otherwise build it
if rotation.ndim == 2:
check_length('surface rotation', rotation.ravel(), 9)
Rmat = rotation
else:
Rmat = get_rotation_matrix(rotation, order=order)
# Translate surface to pivot
surf = self.translate(-pivot, inplace=inplace)
a, b, c, d = surf._get_base_coeffs()
# Compute new rotated coefficients a, b, c
a, b, c = Rmat @ [a, b, c]
kwargs = {'boundary_type': surf.boundary_type,
'albedo': surf.albedo,
'name': surf.name}
if inplace:
kwargs['surface_id'] = surf.id
surf = Plane(a=a, b=b, c=c, d=d, **kwargs)
return surf.translate(pivot, inplace=inplace)
def to_xml_element(self):
"""Return XML representation of the surface
Returns
-------
element : lxml.etree._Element
XML element containing source data
"""
element = super().to_xml_element()
# Add periodic surface pair information
if self.boundary_type == 'periodic':
if self.periodic_surface is not None:
element.set("periodic_surface_id",
str(self.periodic_surface.id))
return element
[docs]class Plane(PlaneMixin, Surface):
"""An arbitrary plane of the form :math:`Ax + By + Cz = D`.
Parameters
----------
a : float, optional
The 'A' parameter for the plane. Defaults to 1.
b : float, optional
The 'B' parameter for the plane. Defaults to 0.
c : float, optional
The 'C' parameter for the plane. Defaults to 0.
d : float, optional
The 'D' parameter for the plane. Defaults to 0.
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the plane. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
a : float
The 'A' parameter for the plane
b : float
The 'B' parameter for the plane
c : float
The 'C' parameter for the plane
d : float
The 'D' parameter for the plane
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
periodic_surface : openmc.Surface
If a periodic boundary condition is used, the surface with which this
one is periodic with
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'plane'
_coeff_keys = ('a', 'b', 'c', 'd')
def __init__(self, a=1., b=0., c=0., d=0., *args, **kwargs):
# *args should ultimately be limited to a, b, c, d as specified in
# __init__, but to preserve the API it is allowed to accept Surface
# parameters for now, but will raise warnings if this is done.
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
# Warn if capital letter arguments are passed
capdict = {}
for k in 'ABCD':
val = kwargs.pop(k, None)
if val is not None:
warn(_WARNING_UPPER.format(type(self), k.lower(), k),
FutureWarning)
capdict[k.lower()] = val
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (a, b, c, d)):
setattr(self, key, val)
for key, val in capdict.items():
setattr(self, key, val)
@classmethod
def __subclasshook__(cls, c):
if cls is Plane and c in (XPlane, YPlane, ZPlane):
return True
return NotImplemented
a = SurfaceCoefficient('a')
b = SurfaceCoefficient('b')
c = SurfaceCoefficient('c')
d = SurfaceCoefficient('d')
[docs] @classmethod
def from_points(cls, p1, p2, p3, **kwargs):
"""Return a plane given three points that pass through it.
Parameters
----------
p1, p2, p3 : 3-tuples
Points that pass through the plane
kwargs : dict
Keyword arguments passed to the :class:`Plane` constructor
Returns
-------
Plane
Plane that passes through the three points
Raises
------
ValueError
If all three points lie along a line
"""
# Convert to numpy arrays
p1 = np.asarray(p1, dtype=float)
p2 = np.asarray(p2, dtype=float)
p3 = np.asarray(p3, dtype=float)
# Find normal vector to plane by taking cross product of two vectors
# connecting p1->p2 and p1->p3
n = np.cross(p2 - p1, p3 - p1)
# Check for points along a line
if np.allclose(n, 0.):
raise ValueError("All three points appear to lie along a line.")
# The equation of the plane will by n·(<x,y,z> - p1) = 0. Determine
# coefficients a, b, c, and d based on that
a, b, c = n
d = np.dot(n, p1)
return cls(a=a, b=b, c=c, d=d, **kwargs)
[docs] def flip_normal(self):
"""Modify plane coefficients to reverse the normal vector."""
self.a = -self.a
self.b = -self.b
self.c = -self.c
self.d = -self.d
[docs]class XPlane(PlaneMixin, Surface):
"""A plane perpendicular to the x axis of the form :math:`x - x_0 = 0`
Parameters
----------
x0 : float, optional
Location of the plane in [cm]. Defaults to 0.
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface. Only axis-aligned periodicity is
supported, i.e., x-planes can only be paired with x-planes.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the plane. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
Location of the plane in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
periodic_surface : openmc.Surface
If a periodic boundary condition is used, the surface with which this
one is periodic with
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'x-plane'
_coeff_keys = ('x0',)
def __init__(self, x0=0., *args, **kwargs):
# work around for accepting Surface kwargs as positional parameters
# until they are deprecated
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
self.x0 = x0
x0 = SurfaceCoefficient('x0')
a = SurfaceCoefficient(1.)
b = SurfaceCoefficient(0.)
c = SurfaceCoefficient(0.)
d = x0
[docs] def evaluate(self, point):
return point[0] - self.x0
[docs]class YPlane(PlaneMixin, Surface):
"""A plane perpendicular to the y axis of the form :math:`y - y_0 = 0`
Parameters
----------
y0 : float, optional
Location of the plane in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface. Only axis-aligned periodicity is
supported, i.e., y-planes can only be paired with y-planes.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the plane. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
y0 : float
Location of the plane in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
periodic_surface : openmc.Surface
If a periodic boundary condition is used, the surface with which this
one is periodic with
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'y-plane'
_coeff_keys = ('y0',)
def __init__(self, y0=0., *args, **kwargs):
# work around for accepting Surface kwargs as positional parameters
# until they are deprecated
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
self.y0 = y0
y0 = SurfaceCoefficient('y0')
a = SurfaceCoefficient(0.)
b = SurfaceCoefficient(1.)
c = SurfaceCoefficient(0.)
d = y0
[docs] def evaluate(self, point):
return point[1] - self.y0
[docs]class ZPlane(PlaneMixin, Surface):
"""A plane perpendicular to the z axis of the form :math:`z - z_0 = 0`
Parameters
----------
z0 : float, optional
Location of the plane in [cm]. Defaults to 0.
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface. Only axis-aligned periodicity is
supported, i.e., z-planes can only be paired with z-planes.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the plane. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
z0 : float
Location of the plane in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
periodic_surface : openmc.Surface
If a periodic boundary condition is used, the surface with which this
one is periodic with
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'z-plane'
_coeff_keys = ('z0',)
def __init__(self, z0=0., *args, **kwargs):
# work around for accepting Surface kwargs as positional parameters
# until they are deprecated
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
self.z0 = z0
z0 = SurfaceCoefficient('z0')
a = SurfaceCoefficient(0.)
b = SurfaceCoefficient(0.)
c = SurfaceCoefficient(1.)
d = z0
[docs] def evaluate(self, point):
return point[2] - self.z0
class QuadricMixin:
"""A Mixin class implementing common functionality for quadric surfaces"""
@property
def _origin(self):
return np.array((self.x0, self.y0, self.z0))
@property
def _axis(self):
axis = np.array((self.dx, self.dy, self.dz))
return axis / np.linalg.norm(axis)
def get_Abc(self, coeffs=None):
"""Compute matrix, vector, and scalar coefficients for this surface or
for a specified set of coefficients.
Parameters
----------
coeffs : tuple, optional
Tuple of coefficients from which to compute the quadric elements.
If none are supplied the coefficients of this surface will be used.
"""
if coeffs is None:
a, b, c, d, e, f, g, h, j, k = self._get_base_coeffs()
else:
a, b, c, d, e, f, g, h, j, k = coeffs
A = np.array([[a, d/2, f/2], [d/2, b, e/2], [f/2, e/2, c]])
bvec = np.array([g, h, j])
return A, bvec, k
def eigh(self, coeffs=None):
"""Wrapper method for returning eigenvalues and eigenvectors of this
quadric surface which is used for transformations.
Parameters
----------
coeffs : tuple, optional
Tuple of coefficients from which to compute the quadric elements.
If none are supplied the coefficients of this surface will be used.
Returns
-------
w, v : tuple of numpy arrays with shapes (3,) and (3,3) respectively
Returns the eigenvalues and eigenvectors of the quadric matrix A
that represents the supplied coefficients. The vector w contains
the eigenvalues in ascending order and the matrix v contains the
eigenvectors such that v[:,i] is the eigenvector corresponding to
the eigenvalue w[i].
"""
return np.linalg.eigh(self.get_Abc(coeffs=coeffs)[0])
def evaluate(self, point):
"""Evaluate the surface equation at a given point.
Parameters
----------
point : 3-tuple of float
The Cartesian coordinates, :math:`(x',y',z')`, in [cm] at which the
surface equation should be evaluated.
Returns
-------
float
:math:`Ax'^2 + By'^2 + Cz'^2 + Dx'y' + Ey'z' + Fx'z' + Gx' + Hy' +
Jz' + K = 0`
"""
x = np.asarray(point)
A, b, c = self.get_Abc()
return x.T @ A @ x + b.T @ x + c
def translate(self, vector, inplace=False):
"""Translate surface in given direction
Parameters
----------
vector : iterable of float
Direction in which surface should be translated
inplace : bool
Whether to return a clone of the Surface or the Surface itself.
Returns
-------
openmc.Surface
Translated surface
"""
vector = np.asarray(vector)
if np.allclose(vector, 0., rtol=0., atol=self._atol):
return self
surf = self if inplace else self.clone()
if hasattr(self, 'x0'):
for vi, xi in zip(vector, ('x0', 'y0', 'z0')):
val = getattr(surf, xi)
try:
setattr(surf, xi, val + vi)
except AttributeError:
# That attribute is read only i.e x0 for XCylinder
pass
else:
A, bvec, cnst = self.get_Abc()
g, h, j = bvec - 2*vector.T @ A
k = cnst + vector.T @ A @ vector - bvec.T @ vector
for key, val in zip(('g', 'h', 'j', 'k'), (g, h, j, k)):
setattr(surf, key, val)
return surf
def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
# Get pivot and rotation matrix
pivot = np.asarray(pivot)
rotation = np.asarray(rotation, dtype=float)
# Allow rotation matrix to be passed in directly, otherwise build it
if rotation.ndim == 2:
check_length('surface rotation', rotation.ravel(), 9)
Rmat = rotation
else:
Rmat = get_rotation_matrix(rotation, order=order)
# Translate surface to the pivot point
tsurf = self.translate(-pivot, inplace=inplace)
# If the surface is already generalized just clone it
if type(tsurf) is tsurf._virtual_base:
surf = tsurf if inplace else tsurf.clone()
else:
base_cls = type(tsurf)._virtual_base
# Copy necessary surface attributes to new kwargs dictionary
kwargs = {'boundary_type': tsurf.boundary_type,
'albedo': tsurf.albedo, 'name': tsurf.name}
if inplace:
kwargs['surface_id'] = tsurf.id
kwargs.update({k: getattr(tsurf, k) for k in base_cls._coeff_keys})
# Create new instance of the virtual base class
surf = base_cls(**kwargs)
# Perform rotations on axis, origin, or quadric coefficients
if hasattr(surf, 'dx'):
for key, val in zip(('dx', 'dy', 'dz'), Rmat @ tsurf._axis):
setattr(surf, key, val)
if hasattr(surf, 'x0'):
for key, val in zip(('x0', 'y0', 'z0'), Rmat @ tsurf._origin):
setattr(surf, key, val)
else:
A, bvec, k = surf.get_Abc()
Arot = Rmat @ A @ Rmat.T
a, b, c = np.diagonal(Arot)
d, e, f = 2*Arot[0, 1], 2*Arot[1, 2], 2*Arot[0, 2]
g, h, j = Rmat @ bvec
for key, val in zip(surf._coeff_keys, (a, b, c, d, e, f, g, h, j, k)):
setattr(surf, key, val)
# translate back to the original frame and return the surface
return surf.translate(pivot, inplace=inplace)
class Cylinder(QuadricMixin, Surface):
"""A cylinder with radius r, centered on the point (x0, y0, z0) with an
axis specified by the line through points (x0, y0, z0) and (x0+dx, y0+dy,
z0+dz)
Parameters
----------
x0 : float, optional
x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
y0 : float, optional
y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
z0 : float, optional
z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
r : float, optional
Radius of the cylinder in [cm]. Defaults to 1.
dx : float, optional
x-component of the vector representing the axis of the cylinder.
Defaults to 0.
dy : float, optional
y-component of the vector representing the axis of the cylinder.
Defaults to 0.
dz : float, optional
z-component of the vector representing the axis of the cylinder.
Defaults to 1.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the cylinder. If not specified, the name will be the empty
string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
x-coordinate for the origin of the Cylinder in [cm]
y0 : float
y-coordinate for the origin of the Cylinder in [cm]
z0 : float
z-coordinate for the origin of the Cylinder in [cm]
r : float
Radius of the cylinder in [cm]
dx : float
x-component of the vector representing the axis of the cylinder
dy : float
y-component of the vector representing the axis of the cylinder
dz : float
z-component of the vector representing the axis of the cylinder
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'cylinder'
_coeff_keys = ('x0', 'y0', 'z0', 'r', 'dx', 'dy', 'dz')
def __init__(self, x0=0., y0=0., z0=0., r=1., dx=0., dy=0., dz=1., *args,
**kwargs):
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, z0, r, dx, dy, dz)):
setattr(self, key, val)
@classmethod
def __subclasshook__(cls, c):
if cls is Cylinder and c in (XCylinder, YCylinder, ZCylinder):
return True
return NotImplemented
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
r = SurfaceCoefficient('r')
dx = SurfaceCoefficient('dx')
dy = SurfaceCoefficient('dy')
dz = SurfaceCoefficient('dz')
def bounding_box(self, side):
if side == '-':
r = self.r
ll = [xi - r if np.isclose(dxi, 0., rtol=0., atol=self._atol)
else -np.inf for xi, dxi in zip(self._origin, self._axis)]
ur = [xi + r if np.isclose(dxi, 0., rtol=0., atol=self._atol)
else np.inf for xi, dxi in zip(self._origin, self._axis)]
return BoundingBox(np.array(ll), np.array(ur))
elif side == '+':
return BoundingBox.infinite()
def _get_base_coeffs(self):
# Get x, y, z coordinates of two points
x1, y1, z1 = self._origin
x2, y2, z2 = self._origin + self._axis
r = self.r
# Define intermediate terms
dx = x2 - x1
dy = y2 - y1
dz = z2 - z1
cx = y1*z2 - y2*z1
cy = x2*z1 - x1*z2
cz = x1*y2 - x2*y1
# Given p=(x,y,z), p1=(x1, y1, z1), p2=(x2, y2, z2), the equation
# for the cylinder can be derived as
# r = |(p - p1) ⨯ (p - p2)| / |p2 - p1|.
# Expanding out all terms and grouping according to what Quadric
# expects gives the following coefficients.
a = dy*dy + dz*dz
b = dx*dx + dz*dz
c = dx*dx + dy*dy
d = -2*dx*dy
e = -2*dy*dz
f = -2*dx*dz
g = 2*(cy*dz - cz*dy)
h = 2*(cz*dx - cx*dz)
j = 2*(cx*dy - cy*dx)
k = cx*cx + cy*cy + cz*cz - (dx*dx + dy*dy + dz*dz)*r*r
return (a, b, c, d, e, f, g, h, j, k)
@classmethod
def from_points(cls, p1, p2, r=1., **kwargs):
"""Return a cylinder given points that define the axis and a radius.
.. versionadded:: 0.12
Parameters
----------
p1, p2 : 3-tuples
Points that pass through the cylinder axis.
r : float, optional
Radius of the cylinder in [cm]. Defaults to 1.
kwargs : dict
Keyword arguments passed to the :class:`Cylinder` constructor
Returns
-------
Cylinder
Cylinder that has an axis through the points p1 and p2, and a
radius r.
"""
# Convert to numpy arrays
p1 = np.asarray(p1)
p2 = np.asarray(p2)
x0, y0, z0 = p1
dx, dy, dz = p2 - p1
return cls(x0=x0, y0=y0, z0=z0, r=r, dx=dx, dy=dy, dz=dz, **kwargs)
def to_xml_element(self):
"""Return XML representation of the surface
Returns
-------
element : lxml.etree._Element
XML element containing source data
"""
# This method overrides Surface.to_xml_element to generate a Quadric
# since the C++ layer doesn't support Cylinders right now
with catch_warnings():
simplefilter('ignore', IDWarning)
kwargs = {'boundary_type': self.boundary_type, 'albedo': self.albedo,
'name': self.name, 'surface_id': self.id}
quad_rep = Quadric(*self._get_base_coeffs(), **kwargs)
return quad_rep.to_xml_element()
[docs]class XCylinder(QuadricMixin, Surface):
"""An infinite cylinder whose length is parallel to the x-axis of the form
:math:`(y - y_0)^2 + (z - z_0)^2 = r^2`.
Parameters
----------
y0 : float, optional
y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
z0 : float, optional
z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
r : float, optional
Radius of the cylinder in [cm]. Defaults to 1.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the cylinder. If not specified, the name will be the empty
string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
y0 : float
y-coordinate for the origin of the Cylinder in [cm]
z0 : float
z-coordinate for the origin of the Cylinder in [cm]
r : float
Radius of the cylinder in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'x-cylinder'
_coeff_keys = ('y0', 'z0', 'r')
def __init__(self, y0=0., z0=0., r=1., *args, **kwargs):
R = kwargs.pop('R', None)
if R is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
FutureWarning)
r = R
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (y0, z0, r)):
setattr(self, key, val)
x0 = SurfaceCoefficient(0.)
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
r = SurfaceCoefficient('r')
dx = SurfaceCoefficient(1.)
dy = SurfaceCoefficient(0.)
dz = SurfaceCoefficient(0.)
def _get_base_coeffs(self):
y0, z0, r = self.y0, self.z0, self.r
a = d = e = f = g = 0.
b = c = 1.
h, j, k = -2*y0, -2*z0, y0*y0 + z0*z0 - r*r
return (a, b, c, d, e, f, g, h, j, k)
[docs] def bounding_box(self, side):
if side == '-':
return BoundingBox(
np.array([-np.inf, self.y0 - self.r, self.z0 - self.r]),
np.array([np.inf, self.y0 + self.r, self.z0 + self.r])
)
elif side == '+':
return BoundingBox.infinite()
[docs] def evaluate(self, point):
y = point[1] - self.y0
z = point[2] - self.z0
return y*y + z*z - self.r**2
[docs]class YCylinder(QuadricMixin, Surface):
"""An infinite cylinder whose length is parallel to the y-axis of the form
:math:`(x - x_0)^2 + (z - z_0)^2 = r^2`.
Parameters
----------
x0 : float, optional
x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
z0 : float, optional
z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
r : float, optional
Radius of the cylinder in [cm]. Defaults to 1.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the cylinder. If not specified, the name will be the empty
string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
x-coordinate for the origin of the Cylinder in [cm]
z0 : float
z-coordinate for the origin of the Cylinder in [cm]
r : float
Radius of the cylinder in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'y-cylinder'
_coeff_keys = ('x0', 'z0', 'r')
def __init__(self, x0=0., z0=0., r=1., *args, **kwargs):
R = kwargs.pop('R', None)
if R is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
FutureWarning)
r = R
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, z0, r)):
setattr(self, key, val)
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient(0.)
z0 = SurfaceCoefficient('z0')
r = SurfaceCoefficient('r')
dx = SurfaceCoefficient(0.)
dy = SurfaceCoefficient(1.)
dz = SurfaceCoefficient(0.)
def _get_base_coeffs(self):
x0, z0, r = self.x0, self.z0, self.r
b = d = e = f = h = 0.
a = c = 1.
g, j, k = -2*x0, -2*z0, x0*x0 + z0*z0 - r*r
return (a, b, c, d, e, f, g, h, j, k)
[docs] def bounding_box(self, side):
if side == '-':
return BoundingBox(
np.array([self.x0 - self.r, -np.inf, self.z0 - self.r]),
np.array([self.x0 + self.r, np.inf, self.z0 + self.r])
)
elif side == '+':
return BoundingBox.infinite()
[docs] def evaluate(self, point):
x = point[0] - self.x0
z = point[2] - self.z0
return x*x + z*z - self.r**2
[docs]class ZCylinder(QuadricMixin, Surface):
"""An infinite cylinder whose length is parallel to the z-axis of the form
:math:`(x - x_0)^2 + (y - y_0)^2 = r^2`.
Parameters
----------
x0 : float, optional
x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
y0 : float, optional
y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
r : float, optional
Radius of the cylinder in [cm]. Defaults to 1.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the cylinder. If not specified, the name will be the empty
string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
x-coordinate for the origin of the Cylinder in [cm]
y0 : float
y-coordinate for the origin of the Cylinder in [cm]
r : float
Radius of the cylinder in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'z-cylinder'
_coeff_keys = ('x0', 'y0', 'r')
def __init__(self, x0=0., y0=0., r=1., *args, **kwargs):
R = kwargs.pop('R', None)
if R is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
FutureWarning)
r = R
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, r)):
setattr(self, key, val)
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient(0.)
r = SurfaceCoefficient('r')
dx = SurfaceCoefficient(0.)
dy = SurfaceCoefficient(0.)
dz = SurfaceCoefficient(1.)
def _get_base_coeffs(self):
x0, y0, r = self.x0, self.y0, self.r
c = d = e = f = j = 0.
a = b = 1.
g, h, k = -2*x0, -2*y0, x0*x0 + y0*y0 - r*r
return (a, b, c, d, e, f, g, h, j, k)
[docs] def bounding_box(self, side):
if side == '-':
return BoundingBox(
np.array([self.x0 - self.r, self.y0 - self.r, -np.inf]),
np.array([self.x0 + self.r, self.y0 + self.r, np.inf])
)
elif side == '+':
return BoundingBox.infinite()
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
return x*x + y*y - self.r**2
[docs]class Sphere(QuadricMixin, Surface):
"""A sphere of the form :math:`(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = r^2`.
Parameters
----------
x0 : float, optional
x-coordinate of the center of the sphere in [cm]. Defaults to 0.
y0 : float, optional
y-coordinate of the center of the sphere in [cm]. Defaults to 0.
z0 : float, optional
z-coordinate of the center of the sphere in [cm]. Defaults to 0.
r : float, optional
Radius of the sphere in [cm]. Defaults to 1.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the sphere. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
x-coordinate of the center of the sphere in [cm]
y0 : float
y-coordinate of the center of the sphere in [cm]
z0 : float
z-coordinate of the center of the sphere in [cm]
r : float
Radius of the sphere in [cm]
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'sphere'
_coeff_keys = ('x0', 'y0', 'z0', 'r')
def __init__(self, x0=0., y0=0., z0=0., r=1., *args, **kwargs):
R = kwargs.pop('R', None)
if R is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
FutureWarning)
r = R
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, z0, r)):
setattr(self, key, val)
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
r = SurfaceCoefficient('r')
def _get_base_coeffs(self):
x0, y0, z0, r = self.x0, self.y0, self.z0, self.r
a = b = c = 1.
d = e = f = 0.
g, h, j = -2*x0, -2*y0, -2*z0
k = x0*x0 + y0*y0 + z0*z0 - r*r
return (a, b, c, d, e, f, g, h, j, k)
[docs] def bounding_box(self, side):
if side == '-':
return BoundingBox(
np.array([self.x0 - self.r, self.y0 - self.r, self.z0 - self.r]),
np.array([self.x0 + self.r, self.y0 + self.r, self.z0 + self.r])
)
elif side == '+':
return BoundingBox.infinite()
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
z = point[2] - self.z0
return x*x + y*y + z*z - self.r**2
[docs]class Cone(QuadricMixin, Surface):
"""A conical surface parallel to the x-, y-, or z-axis.
.. Note::
This creates a double cone, which is two one-sided cones that meet at their apex.
For a one-sided cone see :class:`~openmc.model.XConeOneSided`,
:class:`~openmc.model.YConeOneSided`, and :class:`~openmc.model.ZConeOneSided`.
Parameters
----------
x0 : float, optional
x-coordinate of the apex in [cm]. Defaults to 0.
y0 : float, optional
y-coordinate of the apex in [cm]. Defaults to 0.
z0 : float, optional
z-coordinate of the apex in [cm]. Defaults to 0.
r2 : float, optional
Parameter related to the aperture [:math:`\\rm cm^2`].
It can be interpreted as the increase in the radius squared per cm along
the cone's axis of revolution.
dx : float, optional
x-component of the vector representing the axis of the cone.
Defaults to 0.
dy : float, optional
y-component of the vector representing the axis of the cone.
Defaults to 0.
dz : float, optional
z-component of the vector representing the axis of the cone.
Defaults to 1.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str
Name of the cone. If not specified, the name will be the empty string.
Attributes
----------
x0 : float
x-coordinate of the apex in [cm]
y0 : float
y-coordinate of the apex in [cm]
z0 : float
z-coordinate of the apex in [cm]
r2 : float
Parameter related to the aperature [cm^2]
dx : float
x-component of the vector representing the axis of the cone.
dy : float
y-component of the vector representing the axis of the cone.
dz : float
z-component of the vector representing the axis of the cone.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'cone'
_coeff_keys = ('x0', 'y0', 'z0', 'r2', 'dx', 'dy', 'dz')
def __init__(self, x0=0., y0=0., z0=0., r2=1., dx=0., dy=0., dz=1., *args,
**kwargs):
R2 = kwargs.pop('R2', None)
if R2 is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
FutureWarning)
r2 = R2
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, z0, r2, dx, dy, dz)):
setattr(self, key, val)
@classmethod
def __subclasshook__(cls, c):
if cls is Cone and c in (XCone, YCone, ZCone):
return True
return NotImplemented
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
r2 = SurfaceCoefficient('r2')
dx = SurfaceCoefficient('dx')
dy = SurfaceCoefficient('dy')
dz = SurfaceCoefficient('dz')
def _get_base_coeffs(self):
# The equation for a general cone with vertex at point p = (x0, y0, z0)
# and axis specified by the unit vector d = (dx, dy, dz) and opening
# half angle theta can be described by the equation
#
# (d*(r - p))^2 - (r - p)*(r - p)cos^2(theta) = 0
#
# where * is the dot product and the vector r is the evaluation point
# r = (x, y, z)
#
# The argument r2 for cones is actually tan^2(theta) so that
# cos^2(theta) = 1 / (1 + r2)
x0, y0, z0 = self._origin
dx, dy, dz = self._axis
cos2 = 1 / (1 + self.r2)
a = cos2 - dx*dx
b = cos2 - dy*dy
c = cos2 - dz*dz
d = -2*dx*dy
e = -2*dy*dz
f = -2*dx*dz
g = 2*(dx*(dy*y0 + dz*z0) - a*x0)
h = 2*(dy*(dx*x0 + dz*z0) - b*y0)
j = 2*(dz*(dx*x0 + dy*y0) - c*z0)
k = a*x0*x0 + b*y0*y0 + c*z0*z0 - 2*(dx*dy*x0*y0 + dy*dz*y0*z0 +
dx*dz*x0*z0)
return (a, b, c, d, e, f, g, h, j, k)
[docs] def to_xml_element(self):
"""Return XML representation of the surface
Returns
-------
element : lxml.etree._Element
XML element containing source data
"""
# This method overrides Surface.to_xml_element to generate a Quadric
# since the C++ layer doesn't support Cones right now
with catch_warnings():
simplefilter('ignore', IDWarning)
kwargs = {'boundary_type': self.boundary_type,
'albedo': self.albedo,
'name': self.name,
'surface_id': self.id}
quad_rep = Quadric(*self._get_base_coeffs(), **kwargs)
return quad_rep.to_xml_element()
[docs]class XCone(QuadricMixin, Surface):
"""A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 =
r^2 (x - x_0)^2`.
.. Note::
This creates a double cone, which is two one-sided cones that meet at their apex.
For a one-sided cone see :class:`~openmc.model.XConeOneSided`.
Parameters
----------
x0 : float, optional
x-coordinate of the apex in [cm]. Defaults to 0.
y0 : float, optional
y-coordinate of the apex in [cm]. Defaults to 0.
z0 : float, optional
z-coordinate of the apex in [cm]. Defaults to 0.
r2 : float, optional
Parameter related to the aperture [:math:`\\rm cm^2`].
It can be interpreted as the increase in the radius squared per cm along
the cone's axis of revolution.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the cone. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
x-coordinate of the apex in [cm]
y0 : float
y-coordinate of the apex in [cm]
z0 : float
z-coordinate of the apex in [cm]
r2 : float
Parameter related to the aperature
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'x-cone'
_coeff_keys = ('x0', 'y0', 'z0', 'r2')
def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
R2 = kwargs.pop('R2', None)
if R2 is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
FutureWarning)
r2 = R2
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
setattr(self, key, val)
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
r2 = SurfaceCoefficient('r2')
dx = SurfaceCoefficient(1.)
dy = SurfaceCoefficient(0.)
dz = SurfaceCoefficient(0.)
def _get_base_coeffs(self):
x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
a = -r2
b = c = 1.
d = e = f = 0.
g, h, j = 2*x0*r2, -2*y0, -2*z0
k = y0*y0 + z0*z0 - r2*x0*x0
return (a, b, c, d, e, f, g, h, j, k)
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
z = point[2] - self.z0
return y*y + z*z - self.r2*x*x
[docs]class YCone(QuadricMixin, Surface):
"""A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 =
r^2 (y - y_0)^2`.
.. Note::
This creates a double cone, which is two one-sided cones that meet at their apex.
For a one-sided cone see :class:`~openmc.model.YConeOneSided`.
Parameters
----------
x0 : float, optional
x-coordinate of the apex in [cm]. Defaults to 0.
y0 : float, optional
y-coordinate of the apex in [cm]. Defaults to 0.
z0 : float, optional
z-coordinate of the apex in [cm]. Defaults to 0.
r2 : float, optional
Parameter related to the aperture [:math:`\\rm cm^2`].
It can be interpreted as the increase in the radius squared per cm along
the cone's axis of revolution.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the cone. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
x-coordinate of the apex in [cm]
y0 : float
y-coordinate of the apex in [cm]
z0 : float
z-coordinate of the apex in [cm]
r2 : float
Parameter related to the aperature
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'y-cone'
_coeff_keys = ('x0', 'y0', 'z0', 'r2')
def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
R2 = kwargs.pop('R2', None)
if R2 is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
FutureWarning)
r2 = R2
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
setattr(self, key, val)
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
r2 = SurfaceCoefficient('r2')
dx = SurfaceCoefficient(0.)
dy = SurfaceCoefficient(1.)
dz = SurfaceCoefficient(0.)
def _get_base_coeffs(self):
x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
b = -r2
a = c = 1.
d = e = f = 0.
g, h, j = -2*x0, 2*y0*r2, -2*z0
k = x0*x0 + z0*z0 - r2*y0*y0
return (a, b, c, d, e, f, g, h, j, k)
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
z = point[2] - self.z0
return x*x + z*z - self.r2*y*y
[docs]class ZCone(QuadricMixin, Surface):
"""A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 =
r^2 (z - z_0)^2`.
.. Note::
This creates a double cone, which is two one-sided cones that meet at their apex.
For a one-sided cone see :class:`~openmc.model.ZConeOneSided`.
Parameters
----------
x0 : float, optional
x-coordinate of the apex in [cm]. Defaults to 0.
y0 : float, optional
y-coordinate of the apex in [cm]. Defaults to 0.
z0 : float, optional
z-coordinate of the apex in [cm]. Defaults to 0.
r2 : float, optional
Parameter related to the aperature [cm^2].
This is the square of the radius of the cone 1 cm from.
This can also be treated as the square of the slope of the cone relative to its axis.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the cone. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
x0 : float
x-coordinate of the apex in [cm]
y0 : float
y-coordinate of the apex in [cm]
z0 : float
z-coordinate of the apex in [cm]
r2 : float
Parameter related to the aperature
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'z-cone'
_coeff_keys = ('x0', 'y0', 'z0', 'r2')
def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
R2 = kwargs.pop('R2', None)
if R2 is not None:
warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
FutureWarning)
r2 = R2
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
setattr(self, key, val)
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
r2 = SurfaceCoefficient('r2')
dx = SurfaceCoefficient(0.)
dy = SurfaceCoefficient(0.)
dz = SurfaceCoefficient(1.)
def _get_base_coeffs(self):
x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
c = -r2
a = b = 1.
d = e = f = 0.
g, h, j = -2*x0, -2*y0, 2*z0*r2
k = x0*x0 + y0*y0 - r2*z0*z0
return (a, b, c, d, e, f, g, h, j, k)
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
z = point[2] - self.z0
return x*x + y*y - self.r2*z*z
[docs]class Quadric(QuadricMixin, Surface):
"""A surface of the form :math:`Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy +
Jz + K = 0`.
Parameters
----------
a, b, c, d, e, f, g, h, j, k : float, optional
coefficients for the surface. All default to 0.
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
Boundary condition that defines the behavior for particles hitting the
surface. Defaults to transmissive boundary condition where particles
freely pass through the surface.
albedo : float, optional
Albedo of the surfaces as a ratio of particle weight after interaction
with the surface to the initial weight. Values must be positive. Only
applicable if the boundary type is 'reflective', 'periodic', or 'white'.
name : str, optional
Name of the surface. If not specified, the name will be the empty string.
surface_id : int, optional
Unique identifier for the surface. If not specified, an identifier will
automatically be assigned.
Attributes
----------
a, b, c, d, e, f, g, h, j, k : float
coefficients for the surface
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'quadric'
_coeff_keys = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k')
def __init__(self, a=0., b=0., c=0., d=0., e=0., f=0., g=0., h=0., j=0.,
k=0., *args, **kwargs):
kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (a, b, c, d, e, f, g, h, j, k)):
setattr(self, key, val)
a = SurfaceCoefficient('a')
b = SurfaceCoefficient('b')
c = SurfaceCoefficient('c')
d = SurfaceCoefficient('d')
e = SurfaceCoefficient('e')
f = SurfaceCoefficient('f')
g = SurfaceCoefficient('g')
h = SurfaceCoefficient('h')
j = SurfaceCoefficient('j')
k = SurfaceCoefficient('k')
def _get_base_coeffs(self):
return tuple(getattr(self, c) for c in self._coeff_keys)
class TorusMixin:
"""A Mixin class implementing common functionality for torus surfaces"""
_coeff_keys = ('x0', 'y0', 'z0', 'a', 'b', 'c')
def __init__(self, x0=0., y0=0., z0=0., a=0., b=0., c=0., **kwargs):
super().__init__(**kwargs)
for key, val in zip(self._coeff_keys, (x0, y0, z0, a, b, c)):
setattr(self, key, val)
x0 = SurfaceCoefficient('x0')
y0 = SurfaceCoefficient('y0')
z0 = SurfaceCoefficient('z0')
a = SurfaceCoefficient('a')
b = SurfaceCoefficient('b')
c = SurfaceCoefficient('c')
def translate(self, vector, inplace=False):
surf = self if inplace else self.clone()
surf.x0 += vector[0]
surf.y0 += vector[1]
surf.z0 += vector[2]
return surf
def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
pivot = np.asarray(pivot)
rotation = np.asarray(rotation, dtype=float)
# Allow rotation matrix to be passed in directly, otherwise build it
if rotation.ndim == 2:
check_length('surface rotation', rotation.ravel(), 9)
Rmat = rotation
else:
Rmat = get_rotation_matrix(rotation, order=order)
# Only can handle trivial rotation matrices
close = np.isclose
if not np.all(close(Rmat, -1.0) | close(Rmat, 0.0) | close(Rmat, 1.0)):
raise NotImplementedError('Torus surfaces cannot handle generic rotations')
# Translate surface to pivot
surf = self.translate(-pivot, inplace=inplace)
# Determine "center" of torus and a point above it (along main axis)
center = [surf.x0, surf.y0, surf.z0]
above_center = center.copy()
index = ['x-torus', 'y-torus', 'z-torus'].index(surf._type)
above_center[index] += 1
# Compute new rotated torus center
center = Rmat @ center
# Figure out which axis should be used after rotation
above_center = Rmat @ above_center
new_index = np.where(np.isclose(np.abs(above_center - center), 1.0))[0][0]
cls = [XTorus, YTorus, ZTorus][new_index]
# Create rotated torus
kwargs = {
'boundary_type': surf.boundary_type,
'albedo': surf.albedo,
'name': surf.name,
'a': surf.a, 'b': surf.b, 'c': surf.c
}
if inplace:
kwargs['surface_id'] = surf.id
surf = cls(x0=center[0], y0=center[1], z0=center[2], **kwargs)
return surf.translate(pivot, inplace=inplace)
def _get_base_coeffs(self):
raise NotImplementedError
[docs]class XTorus(TorusMixin, Surface):
r"""A torus of the form :math:`(x - x_0)^2/B^2 + (\sqrt{(y - y_0)^2 + (z -
z_0)^2} - A)^2/C^2 - 1 = 0`.
.. versionadded:: 0.13.0
Parameters
----------
x0 : float
x-coordinate of the center of the axis of revolution in [cm]
y0 : float
y-coordinate of the center of the axis of revolution in [cm]
z0 : float
z-coordinate of the center of the axis of revolution in [cm]
a : float
Major radius of the torus in [cm]
b : float
Minor radius of the torus in [cm] (parallel to axis of revolution)
c : float
Minor radius of the torus in [cm] (perpendicular to axis of revolution)
kwargs : dict
Keyword arguments passed to the :class:`Surface` constructor
Attributes
----------
x0 : float
x-coordinate of the center of the axis of revolution in [cm]
y0 : float
y-coordinate of the center of the axis of revolution in [cm]
z0 : float
z-coordinate of the center of the axis of revolution in [cm]
a : float
Major radius of the torus in [cm]
b : float
Minor radius of the torus in [cm] (parallel to axis of revolution)
c : float
Minor radius of the torus in [cm] (perpendicular to axis of revolution)
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'x-torus'
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
z = point[2] - self.z0
a = self.a
b = self.b
c = self.c
return (x*x)/(b*b) + (math.sqrt(y*y + z*z) - a)**2/(c*c) - 1
[docs] def bounding_box(self, side):
x0, y0, z0 = self.x0, self.y0, self.z0
a, b, c = self.a, self.b, self.c
if side == '-':
return BoundingBox(
np.array([x0 - b, y0 - a - c, z0 - a - c]),
np.array([x0 + b, y0 + a + c, z0 + a + c])
)
elif side == '+':
return BoundingBox.infinite()
[docs]class YTorus(TorusMixin, Surface):
r"""A torus of the form :math:`(y - y_0)^2/B^2 + (\sqrt{(x - x_0)^2 + (z -
z_0)^2} - A)^2/C^2 - 1 = 0`.
.. versionadded:: 0.13.0
Parameters
----------
x0 : float
x-coordinate of the center of the axis of revolution in [cm]
y0 : float
y-coordinate of the center of the axis of revolution in [cm]
z0 : float
z-coordinate of the center of the axis of revolution in [cm]
a : float
Major radius of the torus in [cm]
b : float
Minor radius of the torus in [cm] (parallel to axis of revolution)
c : float
Minor radius of the torus in [cm] (perpendicular to axis of revolution)
kwargs : dict
Keyword arguments passed to the :class:`Surface` constructor
Attributes
----------
x0 : float
x-coordinate of the center of the axis of revolution in [cm]
y0 : float
y-coordinate of the center of the axis of revolution in [cm]
z0 : float
z-coordinate of the center of the axis of revolution in [cm]
a : float
Major radius of the torus in [cm]
b : float
Minor radius of the torus in [cm] (parallel to axis of revolution)
c : float
Minor radius of the torus (perpendicular to axis of revolution)
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'y-torus'
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
z = point[2] - self.z0
a = self.a
b = self.b
c = self.c
return (y*y)/(b*b) + (math.sqrt(x*x + z*z) - a)**2/(c*c) - 1
[docs] def bounding_box(self, side):
x0, y0, z0 = self.x0, self.y0, self.z0
a, b, c = self.a, self.b, self.c
if side == '-':
return BoundingBox(
np.array([x0 - a - c, y0 - b, z0 - a - c]),
np.array([x0 + a + c, y0 + b, z0 + a + c])
)
elif side == '+':
return BoundingBox.infinite()
[docs]class ZTorus(TorusMixin, Surface):
r"""A torus of the form :math:`(z - z_0)^2/B^2 + (\sqrt{(x - x_0)^2 + (y -
y_0)^2} - A)^2/C^2 - 1 = 0`.
.. versionadded:: 0.13.0
Parameters
----------
x0 : float
x-coordinate of the center of the axis of revolution in [cm]
y0 : float
y-coordinate of the center of the axis of revolution in [cm]
z0 : float
z-coordinate of the center of the axis of revolution in [cm]
a : float
Major radius of the torus in [cm]
b : float
Minor radius of the torus in [cm] (parallel to axis of revolution)
c : float
Minor radius of the torus in [cm] (perpendicular to axis of revolution)
kwargs : dict
Keyword arguments passed to the :class:`Surface` constructor
Attributes
----------
x0 : float
x-coordinate of the center of the axis of revolution in [cm]
y0 : float
y-coordinate of the center of the axis of revolution in [cm]
z0 : float
z-coordinate of the center of the axis of revolution in [cm]
a : float
Major radius of the torus in [cm]
b : float
Minor radius of the torus in [cm] (parallel to axis of revolution)
c : float
Minor radius of the torus in [cm] (perpendicular to axis of revolution)
boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
Boundary condition that defines the behavior for particles hitting the
surface.
albedo : float
Boundary albedo as a positive multiplier of particle weight
coefficients : dict
Dictionary of surface coefficients
id : int
Unique identifier for the surface
name : str
Name of the surface
type : str
Type of the surface
"""
_type = 'z-torus'
[docs] def evaluate(self, point):
x = point[0] - self.x0
y = point[1] - self.y0
z = point[2] - self.z0
a = self.a
b = self.b
c = self.c
return (z*z)/(b*b) + (math.sqrt(x*x + y*y) - a)**2/(c*c) - 1
[docs] def bounding_box(self, side):
x0, y0, z0 = self.x0, self.y0, self.z0
a, b, c = self.a, self.b, self.c
if side == '-':
return BoundingBox(
np.array([x0 - a - c, y0 - a - c, z0 - b]),
np.array([x0 + a + c, y0 + a + c, z0 + b])
)
elif side == '+':
return BoundingBox.infinite()
[docs]class Halfspace(Region):
"""A positive or negative half-space region.
A half-space is either of the two parts into which a two-dimension surface
divides the three-dimensional Euclidean space. If the equation of the
surface is :math:`f(x,y,z) = 0`, the region for which :math:`f(x,y,z) < 0`
is referred to as the negative half-space and the region for which
:math:`f(x,y,z) > 0` is referred to as the positive half-space.
Instances of Halfspace are generally not instantiated directly. Rather, they
can be created from an existing Surface through the __neg__ and __pos__
operators, as the following example demonstrates:
>>> sphere = openmc.Sphere(surface_id=1, r=10.0)
>>> inside_sphere = -sphere
>>> outside_sphere = +sphere
>>> type(inside_sphere)
<class 'openmc.surface.Halfspace'>
Parameters
----------
surface : openmc.Surface
Surface which divides Euclidean space.
side : {'+', '-'}
Indicates whether the positive or negative half-space is used.
Attributes
----------
surface : openmc.Surface
Surface which divides Euclidean space.
side : {'+', '-'}
Indicates whether the positive or negative half-space is used.
bounding_box : openmc.BoundingBox
Lower-left and upper-right coordinates of an axis-aligned bounding box
"""
def __init__(self, surface, side):
self.surface = surface
self.side = side
def __and__(self, other):
if isinstance(other, Intersection):
return Intersection([self] + other[:])
else:
return Intersection((self, other))
def __or__(self, other):
if isinstance(other, Union):
return Union([self] + other[:])
else:
return Union((self, other))
def __invert__(self) -> Halfspace:
return -self.surface if self.side == '+' else +self.surface
def __contains__(self, point):
"""Check whether a point is contained in the half-space.
Parameters
----------
point : 3-tuple of float
Cartesian coordinates, :math:`(x',y',z')`, of the point
Returns
-------
bool
Whether the point is in the half-space
"""
val = self.surface.evaluate(point)
return val >= 0. if self.side == '+' else val < 0.
@property
def surface(self):
return self._surface
@surface.setter
def surface(self, surface):
check_type('surface', surface, Surface)
self._surface = surface
@property
def side(self):
return self._side
@side.setter
def side(self, side):
check_value('side', side, ('+', '-'))
self._side = side
@property
def bounding_box(self):
return self.surface.bounding_box(self.side)
def __str__(self):
return '-' + str(self.surface.id) if self.side == '-' \
else str(self.surface.id)
[docs] def get_surfaces(self, surfaces=None):
"""
Returns the surface that this is a halfspace of.
Parameters
----------
surfaces : dict, optional
Dictionary mapping surface IDs to :class:`openmc.Surface` instances
Returns
-------
surfaces : dict
Dictionary mapping surface IDs to :class:`openmc.Surface` instances
"""
if surfaces is None:
surfaces = {}
surfaces[self.surface.id] = self.surface
return surfaces
[docs] def remove_redundant_surfaces(self, redundant_surfaces):
"""Recursively remove all redundant surfaces referenced by this region
Parameters
----------
redundant_surfaces : dict
Dictionary mapping redundant surface IDs to surface IDs for the
:class:`openmc.Surface` instances that should replace them.
"""
surf = redundant_surfaces.get(self.surface.id)
if surf is not None:
self.surface = surf
[docs] def clone(self, memo=None):
"""Create a copy of this halfspace, with a cloned surface with a
unique ID.
Parameters
----------
memo : dict or None
A nested dictionary of previously cloned objects. This parameter
is used internally and should not be specified by the user.
Returns
-------
clone : openmc.Halfspace
The clone of this halfspace
"""
if memo is None:
memo = dict
clone = deepcopy(self)
clone.surface = self.surface.clone(memo)
return clone
[docs] def translate(self, vector, inplace=False, memo=None):
"""Translate half-space in given direction
Parameters
----------
vector : iterable of float
Direction in which region should be translated
memo : dict or None
Dictionary used for memoization
Returns
-------
openmc.Halfspace
Translated half-space
"""
if memo is None:
memo = {}
# If translated surface not in memo, add it
key = (self.surface, tuple(vector))
if key not in memo:
memo[key] = self.surface.translate(vector, inplace)
# Return translated half-space
return type(self)(memo[key], self.side)
[docs] def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False,
memo=None):
r"""Rotate surface by angles provided or by applying matrix directly.
.. versionadded:: 0.12
Parameters
----------
rotation : 3-tuple of float, or 3x3 iterable
A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where
the first element is the rotation about the x-axis in the fixed
laboratory frame, the second element is the rotation about the
y-axis in the fixed laboratory frame, and the third element is the
rotation about the z-axis in the fixed laboratory frame. The
rotations are active rotations. Additionally a 3x3 rotation matrix
can be specified directly either as a nested iterable or array.
pivot : iterable of float, optional
(x, y, z) coordinates for the point to rotate about. Defaults to
(0., 0., 0.)
order : str, optional
A string of 'x', 'y', and 'z' in some order specifying which
rotation to perform first, second, and third. Defaults to 'xyz'
which means, the rotation by angle :math:`\phi` about x will be
applied first, followed by :math:`\theta` about y and then
:math:`\psi` about z. This corresponds to an x-y-z extrinsic
rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan
angles :math:`(\phi, \theta, \psi)`.
inplace : bool
Whether or not to return a new instance of Surface or to modify the
coefficients of this Surface in place. Defaults to False.
memo : dict or None
Dictionary used for memoization
Returns
-------
openmc.Halfspace
Translated half-space
"""
if memo is None:
memo = {}
# If rotated surface not in memo, add it
key = (self.surface, tuple(np.ravel(rotation)), tuple(pivot), order, inplace)
if key not in memo:
memo[key] = self.surface.rotate(rotation, pivot=pivot, order=order,
inplace=inplace)
# Return rotated half-space
return type(self)(memo[key], self.side)
_SURFACE_CLASSES = {cls._type: cls for cls in Surface.__subclasses__()}
# Set virtual base classes for "casting" up the hierarchy
Plane._virtual_base = Plane
XPlane._virtual_base = Plane
YPlane._virtual_base = Plane
ZPlane._virtual_base = Plane
Cylinder._virtual_base = Cylinder
XCylinder._virtual_base = Cylinder
YCylinder._virtual_base = Cylinder
ZCylinder._virtual_base = Cylinder
Cone._virtual_base = Cone
XCone._virtual_base = Cone
YCone._virtual_base = Cone
ZCone._virtual_base = Cone
Sphere._virtual_base = Sphere
Quadric._virtual_base = Quadric