• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmc-dev / openmc / 19058781736

04 Nov 2025 05:26AM UTC coverage: 82.008% (-3.1%) from 85.155%
19058781736

Pull #3252

github

web-flow
Merge b8a72730f into bd76fc056
Pull Request #3252: Adding vtkhdf option to write vtk data

16714 of 23236 branches covered (71.93%)

Branch coverage included in aggregate %.

61 of 66 new or added lines in 1 file covered. (92.42%)

3175 existing lines in 103 files now uncovered.

54243 of 63288 relevant lines covered (85.71%)

43393337.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.95
/openmc/surface.py
1
from __future__ import annotations
11✔
2
from abc import ABC, abstractmethod
11✔
3
from collections.abc import Iterable
11✔
4
from copy import deepcopy
11✔
5
import math
11✔
6
from numbers import Real
11✔
7
from warnings import warn, catch_warnings, simplefilter
11✔
8

9
import lxml.etree as ET
11✔
10
import numpy as np
11✔
11

12
from .checkvalue import check_type, check_value, check_length, check_greater_than
11✔
13
from .mixin import IDManagerMixin, IDWarning
11✔
14
from .region import Region, Intersection, Union
11✔
15
from .bounding_box import BoundingBox
11✔
16
from ._xml import get_elem_list, get_text
11✔
17

18

19
_BOUNDARY_TYPES = {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
11✔
20
_ALBEDO_BOUNDARIES = {'reflective', 'periodic', 'white'}
11✔
21

22
_WARNING_UPPER = """\
11✔
23
"{}(...) accepts an argument named '{}', not '{}'. Future versions of OpenMC \
24
will not accept the capitalized version.\
25
"""
26

27
_WARNING_KWARGS = """\
11✔
28
"{}(...) accepts keyword arguments only for '{}'. Future versions of OpenMC \
29
will not accept positional parameters for superclass arguments.\
30
"""
31

32

33
class SurfaceCoefficient:
11✔
34
    """Descriptor class for surface coefficients.
35

36
    Parameters
37
    -----------
38
    value : float or str
39
        Value of the coefficient (float) or the name of the coefficient that
40
        it is equivalent to (str).
41

42
    """
43
    def __init__(self, value):
11✔
44
        self.value = value
11✔
45

46
    def __get__(self, instance, owner=None):
11✔
47
        if instance is None:
11✔
UNCOV
48
            return self
×
49
        else:
50
            if isinstance(self.value, str):
11✔
51
                return instance._coefficients[self.value]
11✔
52
            else:
53
                return self.value
11✔
54

55
    def __set__(self, instance, value):
11✔
56
        if isinstance(self.value, Real):
11✔
57
            raise AttributeError('This coefficient is read-only')
11✔
58
        check_type(f'{self.value} coefficient', value, Real)
11✔
59
        instance._coefficients[self.value] = value
11✔
60

61

62
def _future_kwargs_warning_helper(cls, *args, **kwargs):
11✔
63
    # Warn if Surface parameters are passed by position, not by keyword
64
    argsdict = dict(zip(('boundary_type', 'name', 'surface_id'), args))
11✔
65
    for k in argsdict:
11✔
UNCOV
66
        warn(_WARNING_KWARGS.format(cls.__name__, k), FutureWarning)
×
67
    kwargs.update(argsdict)
11✔
68
    return kwargs
11✔
69

70

71
def get_rotation_matrix(rotation, order='xyz'):
11✔
72
    r"""Generate a 3x3 rotation matrix from input angles
73

74
    .. versionadded:: 0.12
75

76
    Parameters
77
    ----------
78
    rotation : 3-tuple of float
79
        A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where the
80
        first element is the rotation about the x-axis in the fixed laboratory
81
        frame, the second element is the rotation about the y-axis in the fixed
82
        laboratory frame, and the third element is the rotation about the
83
        z-axis in the fixed laboratory frame. The rotations are active
84
        rotations.
85
    order : str, optional
86
        A string of 'x', 'y', and 'z' in some order specifying which rotation
87
        to perform first, second, and third. Defaults to 'xyz' which means, the
88
        rotation by angle :math:`\phi` about x will be applied first, followed
89
        by :math:`\theta` about y and then :math:`\psi` about z. This
90
        corresponds to an x-y-z extrinsic rotation as well as a z-y'-x''
91
        intrinsic rotation using Tait-Bryan angles :math:`(\phi, \theta, \psi)`.
92

93
    """
94
    check_type('surface rotation', rotation, Iterable, Real)
11✔
95
    check_length('surface rotation', rotation, 3)
11✔
96

97
    phi, theta, psi = np.array(rotation)*(math.pi/180.)
11✔
98
    cx, sx = math.cos(phi), math.sin(phi)
11✔
99
    cy, sy = math.cos(theta), math.sin(theta)
11✔
100
    cz, sz = math.cos(psi), math.sin(psi)
11✔
101
    R = {
11✔
102
        'x': np.array([[1., 0., 0.], [0., cx, -sx], [0., sx, cx]]),
103
        'y': np.array([[cy, 0., sy], [0., 1., 0.], [-sy, 0., cy]]),
104
        'z': np.array([[cz, -sz, 0.], [sz, cz, 0.], [0., 0., 1.]]),
105
    }
106

107
    R1, R2, R3 = (R[xi] for xi in order)
11✔
108
    return R3 @ R2 @ R1
11✔
109

110

111
class Surface(IDManagerMixin, ABC):
11✔
112
    """An implicit surface with an associated boundary condition.
113

114
    An implicit surface is defined as the set of zeros of a function of the
115
    three Cartesian coordinates. Surfaces in OpenMC are limited to a set of
116
    algebraic surfaces, i.e., surfaces that are polynomial in x, y, and z.
117

118
    Parameters
119
    ----------
120
    surface_id : int, optional
121
        Unique identifier for the surface. If not specified, an identifier will
122
        automatically be assigned.
123
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
124
        Boundary condition that defines the behavior for particles hitting the
125
        surface. Defaults to transmissive boundary condition where particles
126
        freely pass through the surface. Note that periodic boundary conditions
127
        can only be applied to x-, y-, and z-planes, and only axis-aligned
128
        periodicity is supported.
129
    albedo : float, optional
130
        Albedo of the surfaces as a ratio of particle weight after interaction
131
        with the surface to the initial weight. Values must be positive. Only
132
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
133
    name : str, optional
134
        Name of the surface. If not specified, the name will be the empty
135
        string.
136

137
    Attributes
138
    ----------
139
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
140
        Boundary condition that defines the behavior for particles hitting the
141
        surface.
142
    albedo : float
143
        Boundary albedo as a positive multiplier of particle weight
144
    coefficients : dict
145
        Dictionary of surface coefficients
146
    id : int
147
        Unique identifier for the surface
148
    name : str
149
        Name of the surface
150
    type : str
151
        Type of the surface
152

153
    """
154

155
    next_id = 1
11✔
156
    used_ids = set()
11✔
157
    _atol = 1.e-12
11✔
158

159
    def __init__(self, surface_id=None, boundary_type='transmission',
11✔
160
                 albedo=1., name=''):
161
        self.id = surface_id
11✔
162
        self.name = name
11✔
163
        self.boundary_type = boundary_type
11✔
164
        self.albedo = albedo
11✔
165

166
        # A dictionary of the quadratic surface coefficients
167
        # Key      - coefficient name
168
        # Value    - coefficient value
169
        self._coefficients = {}
11✔
170

171
    def __neg__(self):
11✔
172
        return Halfspace(self, '-')
11✔
173

174
    def __pos__(self):
11✔
175
        return Halfspace(self, '+')
11✔
176

177
    def __repr__(self):
11✔
178
        string = 'Surface\n'
11✔
179
        string += '{0: <20}{1}{2}\n'.format('\tID', '=\t', self._id)
11✔
180
        string += '{0: <20}{1}{2}\n'.format('\tName', '=\t', self._name)
11✔
181
        string += '{0: <20}{1}{2}\n'.format('\tType', '=\t', self._type)
11✔
182
        string += '{0: <20}{1}{2}\n'.format('\tBoundary', '=\t',
11✔
183
                                            self._boundary_type)
184
        if (self._boundary_type in _ALBEDO_BOUNDARIES and
11✔
185
            not math.isclose(self._albedo, 1.0)):
UNCOV
186
            string += '{0: <20}{1}{2}\n'.format('\tBoundary Albedo', '=\t',
×
187
                                                self._albedo)
188

189
        coefficients = '{0: <20}'.format('\tCoefficients') + '\n'
11✔
190

191
        for coeff in self._coefficients:
11✔
192
            coefficients += f'{coeff: <20}=\t{self._coefficients[coeff]}\n'
11✔
193

194
        string += coefficients
11✔
195

196
        return string
11✔
197

198
    @property
11✔
199
    def name(self):
11✔
200
        return self._name
11✔
201

202
    @name.setter
11✔
203
    def name(self, name):
11✔
204
        if name is not None:
11✔
205
            check_type('surface name', name, str)
11✔
206
            self._name = name
11✔
207
        else:
208
            self._name = ''
11✔
209

210
    @property
11✔
211
    def type(self):
11✔
212
        return self._type
11✔
213

214
    @property
11✔
215
    def boundary_type(self):
11✔
216
        return self._boundary_type
11✔
217

218
    @boundary_type.setter
11✔
219
    def boundary_type(self, boundary_type):
11✔
220
        check_type('boundary type', boundary_type, str)
11✔
221
        check_value('boundary type', boundary_type, _BOUNDARY_TYPES)
11✔
222
        self._boundary_type = boundary_type
11✔
223

224
    @property
11✔
225
    def albedo(self):
11✔
226
        return self._albedo
11✔
227

228
    @albedo.setter
11✔
229
    def albedo(self, albedo):
11✔
230
        check_type('albedo', albedo, Real)
11✔
231
        check_greater_than('albedo', albedo, 0.0)
11✔
232
        self._albedo = float(albedo)
11✔
233

234
    @property
11✔
235
    def coefficients(self):
11✔
236
        return self._coefficients
11✔
237

238
    def bounding_box(self, side):
11✔
239
        """Determine an axis-aligned bounding box.
240

241
        An axis-aligned bounding box for surface half-spaces is represented by
242
        its lower-left and upper-right coordinates. If the half-space is
243
        unbounded in a particular direction, numpy.inf is used to represent
244
        infinity.
245

246
        Parameters
247
        ----------
248
        side : {'+', '-'}
249
            Indicates the negative or positive half-space
250

251
        Returns
252
        -------
253
        numpy.ndarray
254
            Lower-left coordinates of the axis-aligned bounding box for the
255
            desired half-space
256
        numpy.ndarray
257
            Upper-right coordinates of the axis-aligned bounding box for the
258
            desired half-space
259

260
        """
261
        return BoundingBox.infinite()
11✔
262

263
    def clone(self, memo=None):
11✔
264
        """Create a copy of this surface with a new unique ID.
265

266
        Parameters
267
        ----------
268
        memo : dict or None
269
            A nested dictionary of previously cloned objects. This parameter
270
            is used internally and should not be specified by the user.
271

272
        Returns
273
        -------
274
        clone : openmc.Surface
275
            The clone of this surface
276

277
        """
278

279
        if memo is None:
11✔
280
            memo = {}
11✔
281

282
        # If no memoize'd clone exists, instantiate one
283
        if self not in memo:
11✔
284
            clone = deepcopy(self)
11✔
285
            clone.id = None
11✔
286

287
            # Memoize the clone
288
            memo[self] = clone
11✔
289

290
        return memo[self]
11✔
291

292
    def normalize(self, coeffs=None):
11✔
293
        """Normalize coefficients by first nonzero value
294

295
        .. versionadded:: 0.12
296

297
        Parameters
298
        ----------
299
        coeffs : tuple, optional
300
            Tuple of surface coefficients to normalize. Defaults to None. If no
301
            coefficients are supplied then the coefficients will be taken from
302
            the current Surface.
303

304
        Returns
305
        -------
306
        tuple of normalized coefficients
307

308
        """
309
        if coeffs is None:
11✔
310
            coeffs = self._get_base_coeffs()
11✔
311
        coeffs = np.asarray(coeffs)
11✔
312
        nonzeros = ~np.isclose(coeffs, 0., rtol=0., atol=self._atol)
11✔
313
        norm_factor = coeffs[nonzeros][0]
11✔
314
        return tuple([c/norm_factor for c in coeffs])
11✔
315

316
    def is_equal(self, other):
11✔
317
        """Determine if this Surface is equivalent to another
318

319
        Parameters
320
        ----------
321
        other : instance of openmc.Surface
322
            Instance of openmc.Surface that should be compared to the current
323
            surface
324

325
        """
326
        coeffs1 = self.normalize(self._get_base_coeffs())
11✔
327
        coeffs2 = self.normalize(other._get_base_coeffs())
11✔
328

329
        return np.allclose(coeffs1, coeffs2, rtol=0., atol=self._atol)
11✔
330

331
    @abstractmethod
11✔
332
    def _get_base_coeffs(self):
11✔
333
        """Return polynomial coefficients representing the implicit surface
334
        equation.
335

336
        """
337

338
    @abstractmethod
11✔
339
    def evaluate(self, point):
11✔
340
        """Evaluate the surface equation at a given point.
341

342
        Parameters
343
        ----------
344
        point : 3-tuple of float
345
            The Cartesian coordinates, :math:`(x',y',z')`, at which the surface
346
            equation should be evaluated.
347

348
        Returns
349
        -------
350
        float
351
            Evaluation of the surface polynomial at point :math:`(x',y',z')`
352

353
        """
354

355
    @abstractmethod
11✔
356
    def translate(self, vector, inplace=False):
11✔
357
        """Translate surface in given direction
358

359
        Parameters
360
        ----------
361
        vector : iterable of float
362
            Direction in which surface should be translated
363
        inplace : bool
364
            Whether or not to return a new instance of this Surface or to
365
            modify the coefficients of this Surface.
366

367
        Returns
368
        -------
369
        instance of openmc.Surface
370
            Translated surface
371

372
        """
373

374
    @abstractmethod
11✔
375
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
11✔
376
        r"""Rotate surface by angles provided or by applying matrix directly.
377

378
        .. versionadded:: 0.12
379

380
        Parameters
381
        ----------
382
        rotation : 3-tuple of float, or 3x3 iterable
383
            A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where
384
            the first element is the rotation about the x-axis in the fixed
385
            laboratory frame, the second element is the rotation about the
386
            y-axis in the fixed laboratory frame, and the third element is the
387
            rotation about the z-axis in the fixed laboratory frame. The
388
            rotations are active rotations. Additionally a 3x3 rotation matrix
389
            can be specified directly either as a nested iterable or array.
390
        pivot : iterable of float, optional
391
            (x, y, z) coordinates for the point to rotate about. Defaults to
392
            (0., 0., 0.)
393
        order : str, optional
394
            A string of 'x', 'y', and 'z' in some order specifying which
395
            rotation to perform first, second, and third. Defaults to 'xyz'
396
            which means, the rotation by angle :math:`\phi` about x will be
397
            applied first, followed by :math:`\theta` about y and then
398
            :math:`\psi` about z. This corresponds to an x-y-z extrinsic
399
            rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan
400
            angles :math:`(\phi, \theta, \psi)`.
401
        inplace : bool
402
            Whether or not to return a new instance of Surface or to modify the
403
            coefficients of this Surface in place. Defaults to False.
404

405
        Returns
406
        -------
407
        openmc.Surface
408
            Rotated surface
409

410
        """
411

412
    def to_xml_element(self):
11✔
413
        """Return XML representation of the surface
414

415
        Returns
416
        -------
417
        element : lxml.etree._Element
418
            XML element containing source data
419

420
        """
421
        element = ET.Element("surface")
11✔
422
        element.set("id", str(self._id))
11✔
423

424
        if len(self._name) > 0:
11✔
425
            element.set("name", str(self._name))
11✔
426

427
        element.set("type", self._type)
11✔
428
        if self.boundary_type != 'transmission':
11✔
429
            element.set("boundary", self.boundary_type)
11✔
430
            if (self.boundary_type in _ALBEDO_BOUNDARIES and
11✔
431
                not math.isclose(self.albedo, 1.0)):
UNCOV
432
                element.set("albedo", str(self.albedo))
×
433
        element.set("coeffs", ' '.join([str(self._coefficients.setdefault(key, 0.0))
11✔
434
                                        for key in self._coeff_keys]))
435

436
        return element
11✔
437

438
    @staticmethod
11✔
439
    def from_xml_element(elem):
11✔
440
        """Generate surface from an XML element
441

442
        Parameters
443
        ----------
444
        elem : lxml.etree._Element
445
            XML element
446

447
        Returns
448
        -------
449
        openmc.Surface
450
            Instance of a surface subclass
451

452
        """
453

454
        # Determine appropriate class
455
        surf_type = get_text(elem, "type")
11✔
456
        cls = _SURFACE_CLASSES[surf_type]
11✔
457

458
        # Determine ID, boundary type, boundary albedo, coefficients
459
        kwargs = {}
11✔
460
        kwargs['surface_id'] = int(get_text(elem, "id"))
11✔
461
        kwargs['boundary_type'] = get_text(elem, "boundary", "transmission")
11✔
462
        if kwargs['boundary_type'] in _ALBEDO_BOUNDARIES:
11✔
463
            kwargs['albedo'] = float(get_text(elem, "albedo", 1.0))
11✔
464
        kwargs['name'] = get_text(elem, "name")
11✔
465
        coeffs = get_elem_list(elem, "coeffs", float)
11✔
466
        kwargs.update(dict(zip(cls._coeff_keys, coeffs)))
11✔
467

468
        return cls(**kwargs)
11✔
469

470
    @staticmethod
11✔
471
    def from_hdf5(group):
11✔
472
        """Create surface from HDF5 group
473

474
        Parameters
475
        ----------
476
        group : h5py.Group
477
            Group in HDF5 file
478

479
        Returns
480
        -------
481
        openmc.Surface
482
            Instance of surface subclass
483

484
        """
485

486
        # If this is a DAGMC surface, do nothing for now
487
        geom_type = group.get('geom_type')
11✔
488
        if geom_type and geom_type[()].decode() == 'dagmc':
11✔
489
            return
1✔
490

491
        surface_id = int(group.name.split('/')[-1].lstrip('surface '))
11✔
492
        name = group['name'][()].decode() if 'name' in group else ''
11✔
493

494
        bc = group['boundary_type'][()].decode()
11✔
495
        if 'albedo' in group:
11✔
496
            bc_alb = float(group['albedo'][()].decode())
11✔
497
        else:
498
            bc_alb = 1.0
11✔
499
        coeffs = group['coefficients'][...]
11✔
500
        kwargs = {'boundary_type': bc, 'albedo': bc_alb, 'name': name,
11✔
501
                  'surface_id': surface_id}
502

503
        surf_type = group['type'][()].decode()
11✔
504
        cls = _SURFACE_CLASSES[surf_type]
11✔
505

506
        return cls(*coeffs, **kwargs)
11✔
507

508

509
class PlaneMixin:
11✔
510
    """A Plane mixin class for all operations on order 1 surfaces"""
511
    def __init__(self, **kwargs):
11✔
512
        super().__init__(**kwargs)
11✔
513
        self._periodic_surface = None
11✔
514

515
    @property
11✔
516
    def periodic_surface(self):
11✔
517
        return self._periodic_surface
11✔
518

519
    @periodic_surface.setter
11✔
520
    def periodic_surface(self, periodic_surface):
11✔
521
        check_type('periodic surface', periodic_surface, Plane)
11✔
522
        self._periodic_surface = periodic_surface
11✔
523
        periodic_surface._periodic_surface = self
11✔
524

525
    def _get_base_coeffs(self):
11✔
526
        return (self.a, self.b, self.c, self.d)
11✔
527

528
    def _get_normal(self):
11✔
529
        a, b, c = self._get_base_coeffs()[:3]
11✔
530
        return np.array((a, b, c)) / math.sqrt(a*a + b*b + c*c)
11✔
531

532
    def bounding_box(self, side):
11✔
533
        """Determine an axis-aligned bounding box.
534

535
        An axis-aligned bounding box for Plane half-spaces is represented by
536
        its lower-left and upper-right coordinates. If the half-space is
537
        unbounded in a particular direction, numpy.inf is used to represent
538
        infinity.
539

540
        Parameters
541
        ----------
542
        side : {'+', '-'}
543
            Indicates the negative or positive half-space
544

545
        Returns
546
        -------
547
        numpy.ndarray
548
            Lower-left coordinates of the axis-aligned bounding box for the
549
            desired half-space
550
        numpy.ndarray
551
            Upper-right coordinates of the axis-aligned bounding box for the
552
            desired half-space
553

554
        """
555
        # Compute the bounding box based on the normal vector to the plane
556
        nhat = self._get_normal()
11✔
557
        ll = np.array([-np.inf, -np.inf, -np.inf])
11✔
558
        ur = np.array([np.inf, np.inf, np.inf])
11✔
559
        # If the plane is axis aligned, find the proper bounding box
560
        if np.any(np.isclose(np.abs(nhat), 1., rtol=0., atol=self._atol)):
11✔
561
            sign = nhat.sum()
11✔
562
            a, b, c, d = self._get_base_coeffs()
11✔
563
            vals = [d/val if not np.isclose(val, 0., rtol=0., atol=self._atol)
11✔
564
                    else np.nan for val in (a, b, c)]
565
            if side == '-':
11✔
566
                if sign > 0:
11✔
567
                    ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
11✔
568
                else:
569
                    ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
11✔
570
            elif side == '+':
11✔
571
                if sign > 0:
11✔
572
                    ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
11✔
573
                else:
574
                    ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
11✔
575

576
        return BoundingBox(ll, ur)
11✔
577

578
    def evaluate(self, point):
11✔
579
        """Evaluate the surface equation at a given point.
580

581
        Parameters
582
        ----------
583
        point : 3-tuple of float
584
            The Cartesian coordinates, :math:`(x',y',z')`, at which the surface
585
            equation should be evaluated.
586

587
        Returns
588
        -------
589
        float
590
            :math:`Ax' + By' + Cz' - D`
591

592
        """
593

594
        x, y, z = point
11✔
595
        a, b, c, d = self._get_base_coeffs()
11✔
596
        return a*x + b*y + c*z - d
11✔
597

598
    def translate(self, vector, inplace=False):
11✔
599
        """Translate surface in given direction
600

601
        Parameters
602
        ----------
603
        vector : iterable of float
604
            Direction in which surface should be translated
605
        inplace : bool
606
            Whether or not to return a new instance of a Plane or to modify the
607
            coefficients of this plane.
608

609
        Returns
610
        -------
611
        openmc.Plane
612
            Translated surface
613

614
        """
615
        if np.allclose(vector, 0., rtol=0., atol=self._atol):
11✔
616
            return self
11✔
617

618
        a, b, c, d = self._get_base_coeffs()
11✔
619
        d = d + np.dot([a, b, c], vector)
11✔
620

621
        surf = self if inplace else self.clone()
11✔
622

623
        setattr(surf, surf._coeff_keys[-1], d)
11✔
624

625
        return surf
11✔
626

627
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
11✔
628
        pivot = np.asarray(pivot)
11✔
629
        rotation = np.asarray(rotation, dtype=float)
11✔
630

631
        # Allow rotation matrix to be passed in directly, otherwise build it
632
        if rotation.ndim == 2:
11✔
633
            check_length('surface rotation', rotation.ravel(), 9)
11✔
634
            Rmat = rotation
11✔
635
        else:
636
            Rmat = get_rotation_matrix(rotation, order=order)
11✔
637

638
        # Translate surface to pivot
639
        surf = self.translate(-pivot, inplace=inplace)
11✔
640

641
        a, b, c, d = surf._get_base_coeffs()
11✔
642
        # Compute new rotated coefficients a, b, c
643
        a, b, c = Rmat @ [a, b, c]
11✔
644

645
        kwargs = {'boundary_type': surf.boundary_type,
11✔
646
                  'albedo': surf.albedo,
647
                  'name': surf.name}
648
        if inplace:
11✔
UNCOV
649
            kwargs['surface_id'] = surf.id
×
650

651
        surf = Plane(a=a, b=b, c=c, d=d, **kwargs)
11✔
652

653
        return surf.translate(pivot, inplace=inplace)
11✔
654

655
    def to_xml_element(self):
11✔
656
        """Return XML representation of the surface
657

658
        Returns
659
        -------
660
        element : lxml.etree._Element
661
            XML element containing source data
662

663
        """
664
        element = super().to_xml_element()
11✔
665

666
        # Add periodic surface pair information
667
        if self.boundary_type == 'periodic':
11✔
668
            if self.periodic_surface is not None:
11✔
669
                element.set("periodic_surface_id",
11✔
670
                            str(self.periodic_surface.id))
671
        return element
11✔
672

673

674
class Plane(PlaneMixin, Surface):
11✔
675
    """An arbitrary plane of the form :math:`Ax + By + Cz = D`.
676

677
    Parameters
678
    ----------
679
    a : float, optional
680
        The 'A' parameter for the plane. Defaults to 1.
681
    b : float, optional
682
        The 'B' parameter for the plane. Defaults to 0.
683
    c : float, optional
684
        The 'C' parameter for the plane. Defaults to 0.
685
    d : float, optional
686
        The 'D' parameter for the plane. Defaults to 0.
687
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
688
        Boundary condition that defines the behavior for particles hitting the
689
        surface. Defaults to transmissive boundary condition where particles
690
        freely pass through the surface.
691
    albedo : float, optional
692
        Albedo of the surfaces as a ratio of particle weight after interaction
693
        with the surface to the initial weight. Values must be positive. Only
694
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
695
    name : str, optional
696
        Name of the plane. If not specified, the name will be the empty string.
697
    surface_id : int, optional
698
        Unique identifier for the surface. If not specified, an identifier will
699
        automatically be assigned.
700

701
    Attributes
702
    ----------
703
    a : float
704
        The 'A' parameter for the plane
705
    b : float
706
        The 'B' parameter for the plane
707
    c : float
708
        The 'C' parameter for the plane
709
    d : float
710
        The 'D' parameter for the plane
711
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
712
        Boundary condition that defines the behavior for particles hitting the
713
        surface.
714
    albedo : float
715
        Boundary albedo as a positive multiplier of particle weight
716
    periodic_surface : openmc.Surface
717
        If a periodic boundary condition is used, the surface with which this
718
        one is periodic with
719
    coefficients : dict
720
        Dictionary of surface coefficients
721
    id : int
722
        Unique identifier for the surface
723
    name : str
724
        Name of the surface
725
    type : str
726
        Type of the surface
727

728
    """
729

730
    _type = 'plane'
11✔
731
    _coeff_keys = ('a', 'b', 'c', 'd')
11✔
732

733
    def __init__(self, a=1., b=0., c=0., d=0., *args, **kwargs):
11✔
734
        # *args should ultimately be limited to a, b, c, d as specified in
735
        # __init__, but to preserve the API it is allowed to accept Surface
736
        # parameters for now, but will raise warnings if this is done.
737
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
738
        # Warn if capital letter arguments are passed
739
        capdict = {}
11✔
740
        for k in 'ABCD':
11✔
741
            val = kwargs.pop(k, None)
11✔
742
            if val is not None:
11✔
UNCOV
743
                warn(_WARNING_UPPER.format(type(self), k.lower(), k),
×
744
                     FutureWarning)
UNCOV
745
                capdict[k.lower()] = val
×
746

747
        super().__init__(**kwargs)
11✔
748

749
        for key, val in zip(self._coeff_keys, (a, b, c, d)):
11✔
750
            setattr(self, key, val)
11✔
751

752
        for key, val in capdict.items():
11✔
UNCOV
753
            setattr(self, key, val)
×
754

755
    @classmethod
11✔
756
    def __subclasshook__(cls, c):
11✔
757
        if cls is Plane and c in (XPlane, YPlane, ZPlane):
11✔
758
            return True
11✔
759
        return NotImplemented
11✔
760

761
    a = SurfaceCoefficient('a')
11✔
762
    b = SurfaceCoefficient('b')
11✔
763
    c = SurfaceCoefficient('c')
11✔
764
    d = SurfaceCoefficient('d')
11✔
765

766
    @classmethod
11✔
767
    def from_points(cls, p1, p2, p3, **kwargs):
11✔
768
        """Return a plane given three points that pass through it.
769

770
        Parameters
771
        ----------
772
        p1, p2, p3 : 3-tuples
773
            Points that pass through the plane
774
        kwargs : dict
775
            Keyword arguments passed to the :class:`Plane` constructor
776

777
        Returns
778
        -------
779
        Plane
780
            Plane that passes through the three points
781

782
        Raises
783
        ------
784
        ValueError
785
            If all three points lie along a line
786

787
        """
788
        # Convert to numpy arrays
789
        p1 = np.asarray(p1, dtype=float)
11✔
790
        p2 = np.asarray(p2, dtype=float)
11✔
791
        p3 = np.asarray(p3, dtype=float)
11✔
792

793
        # Find normal vector to plane by taking cross product of two vectors
794
        # connecting p1->p2 and p1->p3
795
        n = np.cross(p2 - p1, p3 - p1)
11✔
796

797
        # Check for points along a line
798
        if np.allclose(n, 0.):
11✔
UNCOV
799
            raise ValueError("All three points appear to lie along a line.")
×
800

801
        # The equation of the plane will by n·(<x,y,z> - p1) = 0. Determine
802
        # coefficients a, b, c, and d based on that
803
        a, b, c = n
11✔
804
        d = np.dot(n, p1)
11✔
805
        return cls(a=a, b=b, c=c, d=d, **kwargs)
11✔
806

807
    def flip_normal(self):
11✔
808
        """Modify plane coefficients to reverse the normal vector."""
809
        self.a = -self.a
11✔
810
        self.b = -self.b
11✔
811
        self.c = -self.c
11✔
812
        self.d = -self.d
11✔
813

814

815
class XPlane(PlaneMixin, Surface):
11✔
816
    """A plane perpendicular to the x axis of the form :math:`x - x_0 = 0`
817

818
    Parameters
819
    ----------
820
    x0 : float, optional
821
        Location of the plane in [cm]. Defaults to 0.
822
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
823
        Boundary condition that defines the behavior for particles hitting the
824
        surface. Defaults to transmissive boundary condition where particles
825
        freely pass through the surface. Only axis-aligned periodicity is
826
        supported, i.e., x-planes can only be paired with x-planes.
827
    albedo : float, optional
828
        Albedo of the surfaces as a ratio of particle weight after interaction
829
        with the surface to the initial weight. Values must be positive. Only
830
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
831
    name : str, optional
832
        Name of the plane. If not specified, the name will be the empty string.
833
    surface_id : int, optional
834
        Unique identifier for the surface. If not specified, an identifier will
835
        automatically be assigned.
836

837
    Attributes
838
    ----------
839
    x0 : float
840
        Location of the plane in [cm]
841
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
842
        Boundary condition that defines the behavior for particles hitting the
843
        surface.
844
    albedo : float
845
        Boundary albedo as a positive multiplier of particle weight
846
    periodic_surface : openmc.Surface
847
        If a periodic boundary condition is used, the surface with which this
848
        one is periodic with
849
    coefficients : dict
850
        Dictionary of surface coefficients
851
    id : int
852
        Unique identifier for the surface
853
    name : str
854
        Name of the surface
855
    type : str
856
        Type of the surface
857

858
    """
859

860
    _type = 'x-plane'
11✔
861
    _coeff_keys = ('x0',)
11✔
862

863
    def __init__(self, x0=0., *args, **kwargs):
11✔
864
        # work around for accepting Surface kwargs as positional parameters
865
        # until they are deprecated
866
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
867
        super().__init__(**kwargs)
11✔
868
        self.x0 = x0
11✔
869

870
    x0 = SurfaceCoefficient('x0')
11✔
871
    a = SurfaceCoefficient(1.)
11✔
872
    b = SurfaceCoefficient(0.)
11✔
873
    c = SurfaceCoefficient(0.)
11✔
874
    d = x0
11✔
875

876
    def evaluate(self, point):
11✔
877
        return point[0] - self.x0
11✔
878

879

880
class YPlane(PlaneMixin, Surface):
11✔
881
    """A plane perpendicular to the y axis of the form :math:`y - y_0 = 0`
882

883
    Parameters
884
    ----------
885
    y0 : float, optional
886
        Location of the plane in [cm]
887
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
888
        Boundary condition that defines the behavior for particles hitting the
889
        surface. Defaults to transmissive boundary condition where particles
890
        freely pass through the surface. Only axis-aligned periodicity is
891
        supported, i.e., y-planes can only be paired with y-planes.
892
    albedo : float, optional
893
        Albedo of the surfaces as a ratio of particle weight after interaction
894
        with the surface to the initial weight. Values must be positive. Only
895
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
896
    name : str, optional
897
        Name of the plane. If not specified, the name will be the empty string.
898
    surface_id : int, optional
899
        Unique identifier for the surface. If not specified, an identifier will
900
        automatically be assigned.
901

902
    Attributes
903
    ----------
904
    y0 : float
905
        Location of the plane in [cm]
906
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
907
        Boundary condition that defines the behavior for particles hitting the
908
        surface.
909
    albedo : float
910
        Boundary albedo as a positive multiplier of particle weight
911
    periodic_surface : openmc.Surface
912
        If a periodic boundary condition is used, the surface with which this
913
        one is periodic with
914
    coefficients : dict
915
        Dictionary of surface coefficients
916
    id : int
917
        Unique identifier for the surface
918
    name : str
919
        Name of the surface
920
    type : str
921
        Type of the surface
922

923
    """
924

925
    _type = 'y-plane'
11✔
926
    _coeff_keys = ('y0',)
11✔
927

928
    def __init__(self, y0=0., *args, **kwargs):
11✔
929
        # work around for accepting Surface kwargs as positional parameters
930
        # until they are deprecated
931
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
932
        super().__init__(**kwargs)
11✔
933
        self.y0 = y0
11✔
934

935
    y0 = SurfaceCoefficient('y0')
11✔
936
    a = SurfaceCoefficient(0.)
11✔
937
    b = SurfaceCoefficient(1.)
11✔
938
    c = SurfaceCoefficient(0.)
11✔
939
    d = y0
11✔
940

941
    def evaluate(self, point):
11✔
942
        return point[1] - self.y0
11✔
943

944

945
class ZPlane(PlaneMixin, Surface):
11✔
946
    """A plane perpendicular to the z axis of the form :math:`z - z_0 = 0`
947

948
    Parameters
949
    ----------
950
    z0 : float, optional
951
        Location of the plane in [cm]. Defaults to 0.
952
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
953
        Boundary condition that defines the behavior for particles hitting the
954
        surface. Defaults to transmissive boundary condition where particles
955
        freely pass through the surface. Only axis-aligned periodicity is
956
        supported, i.e., z-planes can only be paired with z-planes.
957
    albedo : float, optional
958
        Albedo of the surfaces as a ratio of particle weight after interaction
959
        with the surface to the initial weight. Values must be positive. Only
960
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
961
    name : str, optional
962
        Name of the plane. If not specified, the name will be the empty string.
963
    surface_id : int, optional
964
        Unique identifier for the surface. If not specified, an identifier will
965
        automatically be assigned.
966

967
    Attributes
968
    ----------
969
    z0 : float
970
        Location of the plane in [cm]
971
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}
972
        Boundary condition that defines the behavior for particles hitting the
973
        surface.
974
    albedo : float
975
        Boundary albedo as a positive multiplier of particle weight
976
    periodic_surface : openmc.Surface
977
        If a periodic boundary condition is used, the surface with which this
978
        one is periodic with
979
    coefficients : dict
980
        Dictionary of surface coefficients
981
    id : int
982
        Unique identifier for the surface
983
    name : str
984
        Name of the surface
985
    type : str
986
        Type of the surface
987

988
    """
989

990
    _type = 'z-plane'
11✔
991
    _coeff_keys = ('z0',)
11✔
992

993
    def __init__(self, z0=0., *args, **kwargs):
11✔
994
        # work around for accepting Surface kwargs as positional parameters
995
        # until they are deprecated
996
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
997
        super().__init__(**kwargs)
11✔
998
        self.z0 = z0
11✔
999

1000
    z0 = SurfaceCoefficient('z0')
11✔
1001
    a = SurfaceCoefficient(0.)
11✔
1002
    b = SurfaceCoefficient(0.)
11✔
1003
    c = SurfaceCoefficient(1.)
11✔
1004
    d = z0
11✔
1005

1006
    def evaluate(self, point):
11✔
1007
        return point[2] - self.z0
11✔
1008

1009

1010
class QuadricMixin:
11✔
1011
    """A Mixin class implementing common functionality for quadric surfaces"""
1012

1013
    @property
11✔
1014
    def _origin(self):
11✔
1015
        return np.array((self.x0, self.y0, self.z0))
11✔
1016

1017
    @property
11✔
1018
    def _axis(self):
11✔
1019
        axis = np.array((self.dx, self.dy, self.dz))
11✔
1020
        return axis / np.linalg.norm(axis)
11✔
1021

1022
    def get_Abc(self, coeffs=None):
11✔
1023
        """Compute matrix, vector, and scalar coefficients for this surface or
1024
        for a specified set of coefficients.
1025

1026
        Parameters
1027
        ----------
1028
        coeffs : tuple, optional
1029
            Tuple of coefficients from which to compute the quadric elements.
1030
            If none are supplied the coefficients of this surface will be used.
1031
        """
1032
        if coeffs is None:
11✔
1033
            a, b, c, d, e, f, g, h, j, k = self._get_base_coeffs()
11✔
1034
        else:
UNCOV
1035
            a, b, c, d, e, f, g, h, j, k = coeffs
×
1036

1037
        A = np.array([[a, d/2, f/2], [d/2, b, e/2], [f/2, e/2, c]])
11✔
1038
        bvec = np.array([g, h, j])
11✔
1039

1040
        return A, bvec, k
11✔
1041

1042
    def eigh(self, coeffs=None):
11✔
1043
        """Wrapper method for returning eigenvalues and eigenvectors of this
1044
        quadric surface which is used for transformations.
1045

1046
        Parameters
1047
        ----------
1048
        coeffs : tuple, optional
1049
            Tuple of coefficients from which to compute the quadric elements.
1050
            If none are supplied the coefficients of this surface will be used.
1051

1052
        Returns
1053
        -------
1054
        w, v : tuple of numpy arrays with shapes (3,) and (3,3) respectively
1055
            Returns the eigenvalues and eigenvectors of the quadric matrix A
1056
            that represents the supplied coefficients. The vector w contains
1057
            the eigenvalues in ascending order and the matrix v contains the
1058
            eigenvectors such that v[:,i] is the eigenvector corresponding to
1059
            the eigenvalue w[i].
1060

1061
        """
UNCOV
1062
        return np.linalg.eigh(self.get_Abc(coeffs=coeffs)[0])
×
1063

1064
    def evaluate(self, point):
11✔
1065
        """Evaluate the surface equation at a given point.
1066

1067
        Parameters
1068
        ----------
1069
        point : 3-tuple of float
1070
            The Cartesian coordinates, :math:`(x',y',z')`, in [cm] at which the
1071
            surface equation should be evaluated.
1072

1073
        Returns
1074
        -------
1075
        float
1076
            :math:`Ax'^2 + By'^2 + Cz'^2 + Dx'y' + Ey'z' + Fx'z' + Gx' + Hy' +
1077
            Jz' + K = 0`
1078

1079
        """
1080
        x = np.asarray(point)
11✔
1081
        A, b, c = self.get_Abc()
11✔
1082
        return x.T @ A @ x + b.T @ x + c
11✔
1083

1084
    def translate(self, vector, inplace=False):
11✔
1085
        """Translate surface in given direction
1086

1087
        Parameters
1088
        ----------
1089
        vector : iterable of float
1090
            Direction in which surface should be translated
1091
        inplace : bool
1092
            Whether to return a clone of the Surface or the Surface itself.
1093

1094
        Returns
1095
        -------
1096
        openmc.Surface
1097
            Translated surface
1098

1099
        """
1100
        vector = np.asarray(vector)
11✔
1101
        if np.allclose(vector, 0., rtol=0., atol=self._atol):
11✔
1102
            return self
11✔
1103

1104
        surf = self if inplace else self.clone()
11✔
1105

1106
        if hasattr(self, 'x0'):
11✔
1107
            for vi, xi in zip(vector, ('x0', 'y0', 'z0')):
11✔
1108
                val = getattr(surf, xi)
11✔
1109
                try:
11✔
1110
                    setattr(surf, xi, val + vi)
11✔
1111
                except AttributeError:
11✔
1112
                    # That attribute is read only i.e x0 for XCylinder
1113
                    pass
11✔
1114

1115
        else:
1116
            A, bvec, cnst = self.get_Abc()
11✔
1117

1118
            g, h, j = bvec - 2*vector.T @ A
11✔
1119
            k = cnst + vector.T @ A @ vector - bvec.T @ vector
11✔
1120

1121
            for key, val in zip(('g', 'h', 'j', 'k'), (g, h, j, k)):
11✔
1122
                setattr(surf, key, val)
11✔
1123

1124
        return surf
11✔
1125

1126
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
11✔
1127
        # Get pivot and rotation matrix
1128
        pivot = np.asarray(pivot)
11✔
1129
        rotation = np.asarray(rotation, dtype=float)
11✔
1130

1131
        # Allow rotation matrix to be passed in directly, otherwise build it
1132
        if rotation.ndim == 2:
11✔
1133
            check_length('surface rotation', rotation.ravel(), 9)
11✔
1134
            Rmat = rotation
11✔
1135
        else:
1136
            Rmat = get_rotation_matrix(rotation, order=order)
11✔
1137

1138
        # Translate surface to the pivot point
1139
        tsurf = self.translate(-pivot, inplace=inplace)
11✔
1140

1141
        # If the surface is already generalized just clone it
1142
        if type(tsurf) is tsurf._virtual_base:
11✔
1143
            surf = tsurf if inplace else tsurf.clone()
11✔
1144
        else:
1145
            base_cls = type(tsurf)._virtual_base
11✔
1146
            # Copy necessary surface attributes to new kwargs dictionary
1147
            kwargs = {'boundary_type': tsurf.boundary_type,
11✔
1148
                      'albedo': tsurf.albedo, 'name': tsurf.name}
1149
            if inplace:
11✔
UNCOV
1150
                kwargs['surface_id'] = tsurf.id
×
1151
            kwargs.update({k: getattr(tsurf, k) for k in base_cls._coeff_keys})
11✔
1152
            # Create new instance of the virtual base class
1153
            surf = base_cls(**kwargs)
11✔
1154

1155
        # Perform rotations on axis, origin, or quadric coefficients
1156
        if hasattr(surf, 'dx'):
11✔
1157
            for key, val in zip(('dx', 'dy', 'dz'), Rmat @ tsurf._axis):
11✔
1158
                setattr(surf, key, val)
11✔
1159
        if hasattr(surf, 'x0'):
11✔
1160
            for key, val in zip(('x0', 'y0', 'z0'), Rmat @ tsurf._origin):
11✔
1161
                setattr(surf, key, val)
11✔
1162
        else:
1163
            A, bvec, k = surf.get_Abc()
11✔
1164
            Arot = Rmat @ A @ Rmat.T
11✔
1165

1166
            a, b, c = np.diagonal(Arot)
11✔
1167
            d, e, f = 2*Arot[0, 1], 2*Arot[1, 2], 2*Arot[0, 2]
11✔
1168
            g, h, j = Rmat @ bvec
11✔
1169

1170
            for key, val in zip(surf._coeff_keys, (a, b, c, d, e, f, g, h, j, k)):
11✔
1171
                setattr(surf, key, val)
11✔
1172

1173
        # translate back to the original frame and return the surface
1174
        return surf.translate(pivot, inplace=inplace)
11✔
1175

1176

1177
class Cylinder(QuadricMixin, Surface):
11✔
1178
    """A cylinder with radius r, centered on the point (x0, y0, z0) with an
1179
    axis specified by the line through points (x0, y0, z0) and (x0+dx, y0+dy,
1180
    z0+dz)
1181

1182
    Parameters
1183
    ----------
1184
    x0 : float, optional
1185
        x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1186
    y0 : float, optional
1187
        y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1188
    z0 : float, optional
1189
        z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1190
    r : float, optional
1191
        Radius of the cylinder in [cm]. Defaults to 1.
1192
    dx : float, optional
1193
        x-component of the vector representing the axis of the cylinder.
1194
        Defaults to 0.
1195
    dy : float, optional
1196
        y-component of the vector representing the axis of the cylinder.
1197
        Defaults to 0.
1198
    dz : float, optional
1199
        z-component of the vector representing the axis of the cylinder.
1200
        Defaults to 1.
1201
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
1202
        Boundary condition that defines the behavior for particles hitting the
1203
        surface. Defaults to transmissive boundary condition where particles
1204
        freely pass through the surface.
1205
    albedo : float, optional
1206
        Albedo of the surfaces as a ratio of particle weight after interaction
1207
        with the surface to the initial weight. Values must be positive. Only
1208
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
1209
    name : str, optional
1210
        Name of the cylinder. If not specified, the name will be the empty
1211
        string.
1212
    surface_id : int, optional
1213
        Unique identifier for the surface. If not specified, an identifier will
1214
        automatically be assigned.
1215

1216
    Attributes
1217
    ----------
1218
    x0 : float
1219
        x-coordinate for the origin of the Cylinder in [cm]
1220
    y0 : float
1221
        y-coordinate for the origin of the Cylinder in [cm]
1222
    z0 : float
1223
        z-coordinate for the origin of the Cylinder in [cm]
1224
    r : float
1225
        Radius of the cylinder in [cm]
1226
    dx : float
1227
        x-component of the vector representing the axis of the cylinder
1228
    dy : float
1229
        y-component of the vector representing the axis of the cylinder
1230
    dz : float
1231
        z-component of the vector representing the axis of the cylinder
1232
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
1233
        Boundary condition that defines the behavior for particles hitting the
1234
        surface.
1235
    albedo : float
1236
        Boundary albedo as a positive multiplier of particle weight
1237
    coefficients : dict
1238
        Dictionary of surface coefficients
1239
    id : int
1240
        Unique identifier for the surface
1241
    name : str
1242
        Name of the surface
1243
    type : str
1244
        Type of the surface
1245

1246
    """
1247
    _type = 'cylinder'
11✔
1248
    _coeff_keys = ('x0', 'y0', 'z0', 'r', 'dx', 'dy', 'dz')
11✔
1249

1250
    def __init__(self, x0=0., y0=0., z0=0., r=1., dx=0., dy=0., dz=1., *args,
11✔
1251
                 **kwargs):
1252
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1253
        super().__init__(**kwargs)
11✔
1254

1255
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r, dx, dy, dz)):
11✔
1256
            setattr(self, key, val)
11✔
1257

1258
    @classmethod
11✔
1259
    def __subclasshook__(cls, c):
11✔
1260
        if cls is Cylinder and c in (XCylinder, YCylinder, ZCylinder):
11✔
1261
            return True
11✔
UNCOV
1262
        return NotImplemented
×
1263

1264
    x0 = SurfaceCoefficient('x0')
11✔
1265
    y0 = SurfaceCoefficient('y0')
11✔
1266
    z0 = SurfaceCoefficient('z0')
11✔
1267
    r = SurfaceCoefficient('r')
11✔
1268
    dx = SurfaceCoefficient('dx')
11✔
1269
    dy = SurfaceCoefficient('dy')
11✔
1270
    dz = SurfaceCoefficient('dz')
11✔
1271

1272
    def bounding_box(self, side):
11✔
1273
        if side == '-':
11✔
1274
            r = self.r
11✔
1275
            ll = [xi - r if np.isclose(dxi, 0., rtol=0., atol=self._atol)
11✔
1276
                  else -np.inf for xi, dxi in zip(self._origin, self._axis)]
1277
            ur = [xi + r if np.isclose(dxi, 0., rtol=0., atol=self._atol)
11✔
1278
                  else np.inf for xi, dxi in zip(self._origin, self._axis)]
1279
            return BoundingBox(np.array(ll), np.array(ur))
11✔
1280
        elif side == '+':
11✔
1281
            return BoundingBox.infinite()
11✔
1282

1283
    def _get_base_coeffs(self):
11✔
1284
        # Get x, y, z coordinates of two points
1285
        x1, y1, z1 = self._origin
11✔
1286
        x2, y2, z2 = self._origin + self._axis
11✔
1287
        r = self.r
11✔
1288

1289
        # Define intermediate terms
1290
        dx = x2 - x1
11✔
1291
        dy = y2 - y1
11✔
1292
        dz = z2 - z1
11✔
1293
        cx = y1*z2 - y2*z1
11✔
1294
        cy = x2*z1 - x1*z2
11✔
1295
        cz = x1*y2 - x2*y1
11✔
1296

1297
        # Given p=(x,y,z), p1=(x1, y1, z1), p2=(x2, y2, z2), the equation
1298
        # for the cylinder can be derived as
1299
        # r = |(p - p1) ⨯ (p - p2)| / |p2 - p1|.
1300
        # Expanding out all terms and grouping according to what Quadric
1301
        # expects gives the following coefficients.
1302
        a = dy*dy + dz*dz
11✔
1303
        b = dx*dx + dz*dz
11✔
1304
        c = dx*dx + dy*dy
11✔
1305
        d = -2*dx*dy
11✔
1306
        e = -2*dy*dz
11✔
1307
        f = -2*dx*dz
11✔
1308
        g = 2*(cy*dz - cz*dy)
11✔
1309
        h = 2*(cz*dx - cx*dz)
11✔
1310
        j = 2*(cx*dy - cy*dx)
11✔
1311
        k = cx*cx + cy*cy + cz*cz - (dx*dx + dy*dy + dz*dz)*r*r
11✔
1312

1313
        return (a, b, c, d, e, f, g, h, j, k)
11✔
1314

1315
    @classmethod
11✔
1316
    def from_points(cls, p1, p2, r=1., **kwargs):
11✔
1317
        """Return a cylinder given points that define the axis and a radius.
1318

1319
        .. versionadded:: 0.12
1320

1321
        Parameters
1322
        ----------
1323
        p1, p2 : 3-tuples
1324
            Points that pass through the cylinder axis.
1325
        r : float, optional
1326
            Radius of the cylinder in [cm]. Defaults to 1.
1327
        kwargs : dict
1328
            Keyword arguments passed to the :class:`Cylinder` constructor
1329

1330
        Returns
1331
        -------
1332
        Cylinder
1333
            Cylinder that has an axis through the points p1 and p2, and a
1334
            radius r.
1335

1336
        """
1337
        # Convert to numpy arrays
1338
        p1 = np.asarray(p1)
11✔
1339
        p2 = np.asarray(p2)
11✔
1340
        x0, y0, z0 = p1
11✔
1341
        dx, dy, dz = p2 - p1
11✔
1342

1343
        return cls(x0=x0, y0=y0, z0=z0, r=r, dx=dx, dy=dy, dz=dz, **kwargs)
11✔
1344

1345
    def to_xml_element(self):
11✔
1346
        """Return XML representation of the surface
1347

1348
        Returns
1349
        -------
1350
        element : lxml.etree._Element
1351
            XML element containing source data
1352

1353
        """
1354
        # This method overrides Surface.to_xml_element to generate a Quadric
1355
        # since the C++ layer doesn't support Cylinders right now
1356
        with catch_warnings():
×
1357
            simplefilter('ignore', IDWarning)
×
UNCOV
1358
            kwargs = {'boundary_type': self.boundary_type, 'albedo': self.albedo,
×
1359
                      'name': self.name, 'surface_id': self.id}
1360
            quad_rep = Quadric(*self._get_base_coeffs(), **kwargs)
×
UNCOV
1361
        return quad_rep.to_xml_element()
×
1362

1363

1364
class XCylinder(QuadricMixin, Surface):
11✔
1365
    """An infinite cylinder whose length is parallel to the x-axis of the form
1366
    :math:`(y - y_0)^2 + (z - z_0)^2 = r^2`.
1367

1368
    Parameters
1369
    ----------
1370
    y0 : float, optional
1371
        y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1372
    z0 : float, optional
1373
        z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1374
    r : float, optional
1375
        Radius of the cylinder in [cm]. Defaults to 1.
1376
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
1377
        Boundary condition that defines the behavior for particles hitting the
1378
        surface. Defaults to transmissive boundary condition where particles
1379
        freely pass through the surface.
1380
    albedo : float, optional
1381
        Albedo of the surfaces as a ratio of particle weight after interaction
1382
        with the surface to the initial weight. Values must be positive. Only
1383
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
1384
    name : str, optional
1385
        Name of the cylinder. If not specified, the name will be the empty
1386
        string.
1387
    surface_id : int, optional
1388
        Unique identifier for the surface. If not specified, an identifier will
1389
        automatically be assigned.
1390

1391
    Attributes
1392
    ----------
1393
    y0 : float
1394
        y-coordinate for the origin of the Cylinder in [cm]
1395
    z0 : float
1396
        z-coordinate for the origin of the Cylinder in [cm]
1397
    r : float
1398
        Radius of the cylinder in [cm]
1399
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
1400
        Boundary condition that defines the behavior for particles hitting the
1401
        surface.
1402
    albedo : float
1403
        Boundary albedo as a positive multiplier of particle weight
1404
    coefficients : dict
1405
        Dictionary of surface coefficients
1406
    id : int
1407
        Unique identifier for the surface
1408
    name : str
1409
        Name of the surface
1410
    type : str
1411
        Type of the surface
1412

1413
    """
1414

1415
    _type = 'x-cylinder'
11✔
1416
    _coeff_keys = ('y0', 'z0', 'r')
11✔
1417

1418
    def __init__(self, y0=0., z0=0., r=1., *args, **kwargs):
11✔
1419
        R = kwargs.pop('R', None)
11✔
1420
        if R is not None:
11✔
UNCOV
1421
            warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
×
1422
                 FutureWarning)
UNCOV
1423
            r = R
×
1424
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1425
        super().__init__(**kwargs)
11✔
1426

1427
        for key, val in zip(self._coeff_keys, (y0, z0, r)):
11✔
1428
            setattr(self, key, val)
11✔
1429

1430
    x0 = SurfaceCoefficient(0.)
11✔
1431
    y0 = SurfaceCoefficient('y0')
11✔
1432
    z0 = SurfaceCoefficient('z0')
11✔
1433
    r = SurfaceCoefficient('r')
11✔
1434
    dx = SurfaceCoefficient(1.)
11✔
1435
    dy = SurfaceCoefficient(0.)
11✔
1436
    dz = SurfaceCoefficient(0.)
11✔
1437

1438
    def _get_base_coeffs(self):
11✔
UNCOV
1439
        y0, z0, r = self.y0, self.z0, self.r
×
1440

1441
        a = d = e = f = g = 0.
×
1442
        b = c = 1.
×
UNCOV
1443
        h, j, k = -2*y0, -2*z0, y0*y0 + z0*z0 - r*r
×
1444

UNCOV
1445
        return (a, b, c, d, e, f, g, h, j, k)
×
1446

1447
    def bounding_box(self, side):
11✔
1448
        if side == '-':
11✔
1449
            return BoundingBox(
11✔
1450
                np.array([-np.inf, self.y0 - self.r, self.z0 - self.r]),
1451
                np.array([np.inf, self.y0 + self.r, self.z0 + self.r])
1452
            )
1453
        elif side == '+':
11✔
1454
            return BoundingBox.infinite()
11✔
1455

1456
    def evaluate(self, point):
11✔
1457
        y = point[1] - self.y0
11✔
1458
        z = point[2] - self.z0
11✔
1459
        return y*y + z*z - self.r**2
11✔
1460

1461

1462
class YCylinder(QuadricMixin, Surface):
11✔
1463
    """An infinite cylinder whose length is parallel to the y-axis of the form
1464
    :math:`(x - x_0)^2 + (z - z_0)^2 = r^2`.
1465

1466
    Parameters
1467
    ----------
1468
    x0 : float, optional
1469
        x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1470
    z0 : float, optional
1471
        z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1472
    r : float, optional
1473
        Radius of the cylinder in [cm]. Defaults to 1.
1474
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
1475
        Boundary condition that defines the behavior for particles hitting the
1476
        surface. Defaults to transmissive boundary condition where particles
1477
        freely pass through the surface.
1478
    albedo : float, optional
1479
        Albedo of the surfaces as a ratio of particle weight after interaction
1480
        with the surface to the initial weight. Values must be positive. Only
1481
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
1482
    name : str, optional
1483
        Name of the cylinder. If not specified, the name will be the empty
1484
        string.
1485
    surface_id : int, optional
1486
        Unique identifier for the surface. If not specified, an identifier will
1487
        automatically be assigned.
1488

1489
    Attributes
1490
    ----------
1491
    x0 : float
1492
        x-coordinate for the origin of the Cylinder in [cm]
1493
    z0 : float
1494
        z-coordinate for the origin of the Cylinder in [cm]
1495
    r : float
1496
        Radius of the cylinder in [cm]
1497
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
1498
        Boundary condition that defines the behavior for particles hitting the
1499
        surface.
1500
    albedo : float
1501
        Boundary albedo as a positive multiplier of particle weight
1502
    coefficients : dict
1503
        Dictionary of surface coefficients
1504
    id : int
1505
        Unique identifier for the surface
1506
    name : str
1507
        Name of the surface
1508
    type : str
1509
        Type of the surface
1510

1511
    """
1512

1513
    _type = 'y-cylinder'
11✔
1514
    _coeff_keys = ('x0', 'z0', 'r')
11✔
1515

1516
    def __init__(self, x0=0., z0=0., r=1., *args, **kwargs):
11✔
1517
        R = kwargs.pop('R', None)
11✔
1518
        if R is not None:
11✔
UNCOV
1519
            warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
×
1520
                 FutureWarning)
UNCOV
1521
            r = R
×
1522
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1523
        super().__init__(**kwargs)
11✔
1524

1525
        for key, val in zip(self._coeff_keys, (x0, z0, r)):
11✔
1526
            setattr(self, key, val)
11✔
1527

1528
    x0 = SurfaceCoefficient('x0')
11✔
1529
    y0 = SurfaceCoefficient(0.)
11✔
1530
    z0 = SurfaceCoefficient('z0')
11✔
1531
    r = SurfaceCoefficient('r')
11✔
1532
    dx = SurfaceCoefficient(0.)
11✔
1533
    dy = SurfaceCoefficient(1.)
11✔
1534
    dz = SurfaceCoefficient(0.)
11✔
1535

1536
    def _get_base_coeffs(self):
11✔
UNCOV
1537
        x0, z0, r = self.x0, self.z0, self.r
×
1538

1539
        b = d = e = f = h = 0.
×
1540
        a = c = 1.
×
UNCOV
1541
        g, j, k = -2*x0, -2*z0, x0*x0 + z0*z0 - r*r
×
1542

UNCOV
1543
        return (a, b, c, d, e, f, g, h, j, k)
×
1544

1545
    def bounding_box(self, side):
11✔
1546
        if side == '-':
11✔
1547
            return BoundingBox(
11✔
1548
                np.array([self.x0 - self.r, -np.inf, self.z0 - self.r]),
1549
                np.array([self.x0 + self.r, np.inf, self.z0 + self.r])
1550
            )
1551
        elif side == '+':
11✔
1552
            return BoundingBox.infinite()
11✔
1553

1554
    def evaluate(self, point):
11✔
1555
        x = point[0] - self.x0
11✔
1556
        z = point[2] - self.z0
11✔
1557
        return x*x + z*z - self.r**2
11✔
1558

1559

1560
class ZCylinder(QuadricMixin, Surface):
11✔
1561
    """An infinite cylinder whose length is parallel to the z-axis of the form
1562
    :math:`(x - x_0)^2 + (y - y_0)^2 = r^2`.
1563

1564
    Parameters
1565
    ----------
1566
    x0 : float, optional
1567
        x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1568
    y0 : float, optional
1569
        y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1570
    r : float, optional
1571
        Radius of the cylinder in [cm]. Defaults to 1.
1572
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
1573
        Boundary condition that defines the behavior for particles hitting the
1574
        surface. Defaults to transmissive boundary condition where particles
1575
        freely pass through the surface.
1576
    albedo : float, optional
1577
        Albedo of the surfaces as a ratio of particle weight after interaction
1578
        with the surface to the initial weight. Values must be positive. Only
1579
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
1580
    name : str, optional
1581
        Name of the cylinder. If not specified, the name will be the empty
1582
        string.
1583
    surface_id : int, optional
1584
        Unique identifier for the surface. If not specified, an identifier will
1585
        automatically be assigned.
1586

1587
    Attributes
1588
    ----------
1589
    x0 : float
1590
        x-coordinate for the origin of the Cylinder in [cm]
1591
    y0 : float
1592
        y-coordinate for the origin of the Cylinder in [cm]
1593
    r : float
1594
        Radius of the cylinder in [cm]
1595
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
1596
        Boundary condition that defines the behavior for particles hitting the
1597
        surface.
1598
    albedo : float
1599
        Boundary albedo as a positive multiplier of particle weight
1600
    coefficients : dict
1601
        Dictionary of surface coefficients
1602
    id : int
1603
        Unique identifier for the surface
1604
    name : str
1605
        Name of the surface
1606
    type : str
1607
        Type of the surface
1608

1609
    """
1610

1611
    _type = 'z-cylinder'
11✔
1612
    _coeff_keys = ('x0', 'y0', 'r')
11✔
1613

1614
    def __init__(self, x0=0., y0=0., r=1., *args, **kwargs):
11✔
1615
        R = kwargs.pop('R', None)
11✔
1616
        if R is not None:
11✔
UNCOV
1617
            warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
×
1618
                 FutureWarning)
UNCOV
1619
            r = R
×
1620
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1621
        super().__init__(**kwargs)
11✔
1622

1623
        for key, val in zip(self._coeff_keys, (x0, y0, r)):
11✔
1624
            setattr(self, key, val)
11✔
1625

1626
    x0 = SurfaceCoefficient('x0')
11✔
1627
    y0 = SurfaceCoefficient('y0')
11✔
1628
    z0 = SurfaceCoefficient(0.)
11✔
1629
    r = SurfaceCoefficient('r')
11✔
1630
    dx = SurfaceCoefficient(0.)
11✔
1631
    dy = SurfaceCoefficient(0.)
11✔
1632
    dz = SurfaceCoefficient(1.)
11✔
1633

1634
    def _get_base_coeffs(self):
11✔
UNCOV
1635
        x0, y0, r = self.x0, self.y0, self.r
×
1636

1637
        c = d = e = f = j = 0.
×
1638
        a = b = 1.
×
UNCOV
1639
        g, h, k = -2*x0, -2*y0, x0*x0 + y0*y0 - r*r
×
1640

UNCOV
1641
        return (a, b, c, d, e, f, g, h, j, k)
×
1642

1643
    def bounding_box(self, side):
11✔
1644
        if side == '-':
11✔
1645
            return BoundingBox(
11✔
1646
                np.array([self.x0 - self.r, self.y0 - self.r, -np.inf]),
1647
                np.array([self.x0 + self.r, self.y0 + self.r, np.inf])
1648
            )
1649
        elif side == '+':
11✔
1650
            return BoundingBox.infinite()
11✔
1651

1652
    def evaluate(self, point):
11✔
1653
        x = point[0] - self.x0
11✔
1654
        y = point[1] - self.y0
11✔
1655
        return x*x + y*y - self.r**2
11✔
1656

1657

1658
class Sphere(QuadricMixin, Surface):
11✔
1659
    """A sphere of the form :math:`(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = r^2`.
1660

1661
    Parameters
1662
    ----------
1663
    x0 : float, optional
1664
        x-coordinate of the center of the sphere in [cm]. Defaults to 0.
1665
    y0 : float, optional
1666
        y-coordinate of the center of the sphere in [cm]. Defaults to 0.
1667
    z0 : float, optional
1668
        z-coordinate of the center of the sphere in [cm]. Defaults to 0.
1669
    r : float, optional
1670
        Radius of the sphere in [cm]. Defaults to 1.
1671
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
1672
        Boundary condition that defines the behavior for particles hitting the
1673
        surface. Defaults to transmissive boundary condition where particles
1674
        freely pass through the surface.
1675
    albedo : float, optional
1676
        Albedo of the surfaces as a ratio of particle weight after interaction
1677
        with the surface to the initial weight. Values must be positive. Only
1678
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
1679
    name : str, optional
1680
        Name of the sphere. If not specified, the name will be the empty string.
1681
    surface_id : int, optional
1682
        Unique identifier for the surface. If not specified, an identifier will
1683
        automatically be assigned.
1684

1685
    Attributes
1686
    ----------
1687
    x0 : float
1688
        x-coordinate of the center of the sphere in [cm]
1689
    y0 : float
1690
        y-coordinate of the center of the sphere in [cm]
1691
    z0 : float
1692
        z-coordinate of the center of the sphere in [cm]
1693
    r : float
1694
        Radius of the sphere in [cm]
1695
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
1696
        Boundary condition that defines the behavior for particles hitting the
1697
        surface.
1698
    albedo : float
1699
        Boundary albedo as a positive multiplier of particle weight
1700
    coefficients : dict
1701
        Dictionary of surface coefficients
1702
    id : int
1703
        Unique identifier for the surface
1704
    name : str
1705
        Name of the surface
1706
    type : str
1707
        Type of the surface
1708

1709
    """
1710

1711
    _type = 'sphere'
11✔
1712
    _coeff_keys = ('x0', 'y0', 'z0', 'r')
11✔
1713

1714
    def __init__(self, x0=0., y0=0., z0=0., r=1., *args, **kwargs):
11✔
1715
        R = kwargs.pop('R', None)
11✔
1716
        if R is not None:
11✔
1717
            warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
1✔
1718
                 FutureWarning)
1719
            r = R
1✔
1720
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1721
        super().__init__(**kwargs)
11✔
1722

1723
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r)):
11✔
1724
            setattr(self, key, val)
11✔
1725

1726
    x0 = SurfaceCoefficient('x0')
11✔
1727
    y0 = SurfaceCoefficient('y0')
11✔
1728
    z0 = SurfaceCoefficient('z0')
11✔
1729
    r = SurfaceCoefficient('r')
11✔
1730

1731
    def _get_base_coeffs(self):
11✔
1732
        x0, y0, z0, r = self.x0, self.y0, self.z0, self.r
11✔
1733
        a = b = c = 1.
11✔
1734
        d = e = f = 0.
11✔
1735
        g, h, j = -2*x0, -2*y0, -2*z0
11✔
1736
        k = x0*x0 + y0*y0 + z0*z0 - r*r
11✔
1737

1738
        return (a, b, c, d, e, f, g, h, j, k)
11✔
1739

1740
    def bounding_box(self, side):
11✔
1741
        if side == '-':
11✔
1742
            return BoundingBox(
11✔
1743
                np.array([self.x0 - self.r, self.y0 - self.r, self.z0 - self.r]),
1744
                np.array([self.x0 + self.r, self.y0 + self.r, self.z0 + self.r])
1745
            )
1746
        elif side == '+':
11✔
1747
            return BoundingBox.infinite()
11✔
1748

1749
    def evaluate(self, point):
11✔
1750
        x = point[0] - self.x0
11✔
1751
        y = point[1] - self.y0
11✔
1752
        z = point[2] - self.z0
11✔
1753
        return x*x + y*y + z*z - self.r**2
11✔
1754

1755

1756
class Cone(QuadricMixin, Surface):
11✔
1757
    r"""A conical surface parallel to the x-, y-, or z-axis.
1758

1759
    .. Note::
1760
        This creates a double cone, which is two one-sided cones that meet at their apex.
1761
        For a one-sided cone see :class:`~openmc.model.XConeOneSided`,
1762
        :class:`~openmc.model.YConeOneSided`, and :class:`~openmc.model.ZConeOneSided`.
1763

1764
    Parameters
1765
    ----------
1766
    x0 : float, optional
1767
        x-coordinate of the apex in [cm].
1768
    y0 : float, optional
1769
        y-coordinate of the apex in [cm].
1770
    z0 : float, optional
1771
        z-coordinate of the apex in [cm].
1772
    r2 : float, optional
1773
        The square of the slope of the cone. It is defined as
1774
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
1775
        distance :math:`h` from the apex. An easy way to define this quantity is
1776
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
1777
    dx : float, optional
1778
        x-component of the vector representing the axis of the cone.
1779
    dy : float, optional
1780
        y-component of the vector representing the axis of the cone.
1781
    dz : float, optional
1782
        z-component of the vector representing the axis of the cone.
1783
    surface_id : int, optional
1784
        Unique identifier for the surface. If not specified, an identifier will
1785
        automatically be assigned.
1786
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
1787
        Boundary condition that defines the behavior for particles hitting the
1788
        surface. Defaults to transmissive boundary condition where particles
1789
        freely pass through the surface.
1790
    albedo : float, optional
1791
        Albedo of the surfaces as a ratio of particle weight after interaction
1792
        with the surface to the initial weight. Values must be positive. Only
1793
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
1794

1795
    name : str
1796
        Name of the cone. If not specified, the name will be the empty string.
1797

1798
    Attributes
1799
    ----------
1800
    x0 : float
1801
        x-coordinate of the apex in [cm]
1802
    y0 : float
1803
        y-coordinate of the apex in [cm]
1804
    z0 : float
1805
        z-coordinate of the apex in [cm]
1806
    r2 : float
1807
        Parameter related to the aperture
1808
    dx : float
1809
        x-component of the vector representing the axis of the cone.
1810
    dy : float
1811
        y-component of the vector representing the axis of the cone.
1812
    dz : float
1813
        z-component of the vector representing the axis of the cone.
1814
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
1815
        Boundary condition that defines the behavior for particles hitting the
1816
        surface.
1817
    albedo : float
1818
        Boundary albedo as a positive multiplier of particle weight
1819
    coefficients : dict
1820
        Dictionary of surface coefficients
1821
    id : int
1822
        Unique identifier for the surface
1823
    name : str
1824
        Name of the surface
1825
    type : str
1826
        Type of the surface
1827

1828
    """
1829

1830
    _type = 'cone'
11✔
1831
    _coeff_keys = ('x0', 'y0', 'z0', 'r2', 'dx', 'dy', 'dz')
11✔
1832

1833
    def __init__(self, x0=0., y0=0., z0=0., r2=1., dx=0., dy=0., dz=1., *args,
11✔
1834
                 **kwargs):
1835
        R2 = kwargs.pop('R2', None)
11✔
1836
        if R2 is not None:
11✔
UNCOV
1837
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
1838
                 FutureWarning)
UNCOV
1839
            r2 = R2
×
1840
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1841
        super().__init__(**kwargs)
11✔
1842

1843
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2, dx, dy, dz)):
11✔
1844
            setattr(self, key, val)
11✔
1845

1846
    @classmethod
11✔
1847
    def __subclasshook__(cls, c):
11✔
1848
        if cls is Cone and c in (XCone, YCone, ZCone):
×
1849
            return True
×
UNCOV
1850
        return NotImplemented
×
1851

1852
    x0 = SurfaceCoefficient('x0')
11✔
1853
    y0 = SurfaceCoefficient('y0')
11✔
1854
    z0 = SurfaceCoefficient('z0')
11✔
1855
    r2 = SurfaceCoefficient('r2')
11✔
1856
    dx = SurfaceCoefficient('dx')
11✔
1857
    dy = SurfaceCoefficient('dy')
11✔
1858
    dz = SurfaceCoefficient('dz')
11✔
1859

1860
    def _get_base_coeffs(self):
11✔
1861
        # The equation for a general cone with vertex at point p = (x0, y0, z0)
1862
        # and axis specified by the unit vector d = (dx, dy, dz) and opening
1863
        # half angle theta can be described by the equation
1864
        #
1865
        # (d*(r - p))^2 - (r - p)*(r - p)cos^2(theta) = 0
1866
        #
1867
        # where * is the dot product and the vector r is the evaluation point
1868
        # r = (x, y, z)
1869
        #
1870
        # The argument r2 for cones is actually tan^2(theta) so that
1871
        # cos^2(theta) = 1 / (1 + r2)
1872

1873
        x0, y0, z0 = self._origin
11✔
1874
        dx, dy, dz = self._axis
11✔
1875
        cos2 = 1 / (1 + self.r2)
11✔
1876

1877
        a = cos2 - dx*dx
11✔
1878
        b = cos2 - dy*dy
11✔
1879
        c = cos2 - dz*dz
11✔
1880
        d = -2*dx*dy
11✔
1881
        e = -2*dy*dz
11✔
1882
        f = -2*dx*dz
11✔
1883
        g = 2*(dx*(dy*y0 + dz*z0) - a*x0)
11✔
1884
        h = 2*(dy*(dx*x0 + dz*z0) - b*y0)
11✔
1885
        j = 2*(dz*(dx*x0 + dy*y0) - c*z0)
11✔
1886
        k = a*x0*x0 + b*y0*y0 + c*z0*z0 - 2*(dx*dy*x0*y0 + dy*dz*y0*z0 +
11✔
1887
                                             dx*dz*x0*z0)
1888

1889
        return (a, b, c, d, e, f, g, h, j, k)
11✔
1890

1891
    def to_xml_element(self):
11✔
1892
        """Return XML representation of the surface
1893

1894
        Returns
1895
        -------
1896
        element : lxml.etree._Element
1897
            XML element containing source data
1898

1899
        """
1900
        # This method overrides Surface.to_xml_element to generate a Quadric
1901
        # since the C++ layer doesn't support Cones right now
1902
        with catch_warnings():
×
1903
            simplefilter('ignore', IDWarning)
×
UNCOV
1904
            kwargs = {'boundary_type': self.boundary_type,
×
1905
                      'albedo': self.albedo,
1906
                      'name': self.name,
1907
                      'surface_id': self.id}
1908
            quad_rep = Quadric(*self._get_base_coeffs(), **kwargs)
×
UNCOV
1909
        return quad_rep.to_xml_element()
×
1910

1911

1912
class XCone(QuadricMixin, Surface):
11✔
1913
    r"""A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 =
1914
    r^2 (x - x_0)^2`.
1915

1916
    .. Note::
1917
        This creates a double cone, which is two one-sided cones that meet at their apex.
1918
        For a one-sided cone see :class:`~openmc.model.XConeOneSided`.
1919

1920
    Parameters
1921
    ----------
1922
    x0 : float, optional
1923
        x-coordinate of the apex in [cm].
1924
    y0 : float, optional
1925
        y-coordinate of the apex in [cm].
1926
    z0 : float, optional
1927
        z-coordinate of the apex in [cm].
1928
    r2 : float, optional
1929
        The square of the slope of the cone. It is defined as
1930
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
1931
        distance :math:`h` from the apex. An easy way to define this quantity is
1932
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
1933
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
1934
        Boundary condition that defines the behavior for particles hitting the
1935
        surface. Defaults to transmissive boundary condition where particles
1936
        freely pass through the surface.
1937
    albedo : float, optional
1938
        Albedo of the surfaces as a ratio of particle weight after interaction
1939
        with the surface to the initial weight. Values must be positive. Only
1940
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
1941
    name : str, optional
1942
        Name of the cone. If not specified, the name will be the empty string.
1943
    surface_id : int, optional
1944
        Unique identifier for the surface. If not specified, an identifier will
1945
        automatically be assigned.
1946

1947
    Attributes
1948
    ----------
1949
    x0 : float
1950
        x-coordinate of the apex in [cm]
1951
    y0 : float
1952
        y-coordinate of the apex in [cm]
1953
    z0 : float
1954
        z-coordinate of the apex in [cm]
1955
    r2 : float
1956
        Parameter related to the aperture
1957
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
1958
        Boundary condition that defines the behavior for particles hitting the
1959
        surface.
1960
    albedo : float
1961
        Boundary albedo as a positive multiplier of particle weight
1962
    coefficients : dict
1963
        Dictionary of surface coefficients
1964
    id : int
1965
        Unique identifier for the surface
1966
    name : str
1967
        Name of the surface
1968
    type : str
1969
        Type of the surface
1970

1971
    """
1972

1973
    _type = 'x-cone'
11✔
1974
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
11✔
1975

1976
    def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
11✔
1977
        R2 = kwargs.pop('R2', None)
11✔
1978
        if R2 is not None:
11✔
UNCOV
1979
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
1980
                 FutureWarning)
UNCOV
1981
            r2 = R2
×
1982
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1983
        super().__init__(**kwargs)
11✔
1984

1985
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
11✔
1986
            setattr(self, key, val)
11✔
1987

1988
    x0 = SurfaceCoefficient('x0')
11✔
1989
    y0 = SurfaceCoefficient('y0')
11✔
1990
    z0 = SurfaceCoefficient('z0')
11✔
1991
    r2 = SurfaceCoefficient('r2')
11✔
1992
    dx = SurfaceCoefficient(1.)
11✔
1993
    dy = SurfaceCoefficient(0.)
11✔
1994
    dz = SurfaceCoefficient(0.)
11✔
1995

1996
    def _get_base_coeffs(self):
11✔
UNCOV
1997
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
1998

1999
        a = -r2
×
2000
        b = c = 1.
×
2001
        d = e = f = 0.
×
2002
        g, h, j = 2*x0*r2, -2*y0, -2*z0
×
UNCOV
2003
        k = y0*y0 + z0*z0 - r2*x0*x0
×
2004

UNCOV
2005
        return (a, b, c, d, e, f, g, h, j, k)
×
2006

2007
    def evaluate(self, point):
11✔
2008
        x = point[0] - self.x0
11✔
2009
        y = point[1] - self.y0
11✔
2010
        z = point[2] - self.z0
11✔
2011
        return y*y + z*z - self.r2*x*x
11✔
2012

2013

2014
class YCone(QuadricMixin, Surface):
11✔
2015
    r"""A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 =
2016
    r^2 (y - y_0)^2`.
2017

2018
    .. Note::
2019
        This creates a double cone, which is two one-sided cones that meet at their apex.
2020
        For a one-sided cone see :class:`~openmc.model.YConeOneSided`.
2021

2022
    Parameters
2023
    ----------
2024
    x0 : float, optional
2025
        x-coordinate of the apex in [cm].
2026
    y0 : float, optional
2027
        y-coordinate of the apex in [cm].
2028
    z0 : float, optional
2029
        z-coordinate of the apex in [cm].
2030
    r2 : float, optional
2031
        The square of the slope of the cone. It is defined as
2032
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
2033
        distance :math:`h` from the apex. An easy way to define this quantity is
2034
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
2035
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
2036
        Boundary condition that defines the behavior for particles hitting the
2037
        surface. Defaults to transmissive boundary condition where particles
2038
        freely pass through the surface.
2039
    albedo : float, optional
2040
        Albedo of the surfaces as a ratio of particle weight after interaction
2041
        with the surface to the initial weight. Values must be positive. Only
2042
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
2043
    name : str, optional
2044
        Name of the cone. If not specified, the name will be the empty string.
2045
    surface_id : int, optional
2046
        Unique identifier for the surface. If not specified, an identifier will
2047
        automatically be assigned.
2048

2049
    Attributes
2050
    ----------
2051
    x0 : float
2052
        x-coordinate of the apex in [cm]
2053
    y0 : float
2054
        y-coordinate of the apex in [cm]
2055
    z0 : float
2056
        z-coordinate of the apex in [cm]
2057
    r2 : float
2058
        Parameter related to the aperture
2059
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
2060
        Boundary condition that defines the behavior for particles hitting the
2061
        surface.
2062
    albedo : float
2063
        Boundary albedo as a positive multiplier of particle weight
2064
    coefficients : dict
2065
        Dictionary of surface coefficients
2066
    id : int
2067
        Unique identifier for the surface
2068
    name : str
2069
        Name of the surface
2070
    type : str
2071
        Type of the surface
2072

2073
    """
2074

2075
    _type = 'y-cone'
11✔
2076
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
11✔
2077

2078
    def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
11✔
2079
        R2 = kwargs.pop('R2', None)
11✔
2080
        if R2 is not None:
11✔
UNCOV
2081
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
2082
                 FutureWarning)
UNCOV
2083
            r2 = R2
×
2084
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
2085
        super().__init__(**kwargs)
11✔
2086

2087
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
11✔
2088
            setattr(self, key, val)
11✔
2089

2090
    x0 = SurfaceCoefficient('x0')
11✔
2091
    y0 = SurfaceCoefficient('y0')
11✔
2092
    z0 = SurfaceCoefficient('z0')
11✔
2093
    r2 = SurfaceCoefficient('r2')
11✔
2094
    dx = SurfaceCoefficient(0.)
11✔
2095
    dy = SurfaceCoefficient(1.)
11✔
2096
    dz = SurfaceCoefficient(0.)
11✔
2097

2098
    def _get_base_coeffs(self):
11✔
UNCOV
2099
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2100

2101
        b = -r2
×
2102
        a = c = 1.
×
2103
        d = e = f = 0.
×
2104
        g, h, j = -2*x0, 2*y0*r2, -2*z0
×
UNCOV
2105
        k = x0*x0 + z0*z0 - r2*y0*y0
×
2106

UNCOV
2107
        return (a, b, c, d, e, f, g, h, j, k)
×
2108

2109
    def evaluate(self, point):
11✔
2110
        x = point[0] - self.x0
11✔
2111
        y = point[1] - self.y0
11✔
2112
        z = point[2] - self.z0
11✔
2113
        return x*x + z*z - self.r2*y*y
11✔
2114

2115

2116
class ZCone(QuadricMixin, Surface):
11✔
2117
    r"""A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 =
2118
    r^2 (z - z_0)^2`.
2119

2120
    .. Note::
2121
        This creates a double cone, which is two one-sided cones that meet at their apex.
2122
        For a one-sided cone see :class:`~openmc.model.ZConeOneSided`.
2123

2124
    Parameters
2125
    ----------
2126
    x0 : float, optional
2127
        x-coordinate of the apex in [cm].
2128
    y0 : float, optional
2129
        y-coordinate of the apex in [cm].
2130
    z0 : float, optional
2131
        z-coordinate of the apex in [cm].
2132
    r2 : float, optional
2133
        The square of the slope of the cone. It is defined as
2134
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
2135
        distance :math:`h` from the apex. An easy way to define this quantity is
2136
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
2137
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
2138
        Boundary condition that defines the behavior for particles hitting the
2139
        surface. Defaults to transmissive boundary condition where particles
2140
        freely pass through the surface.
2141
    albedo : float, optional
2142
        Albedo of the surfaces as a ratio of particle weight after interaction
2143
        with the surface to the initial weight. Values must be positive. Only
2144
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
2145
    name : str, optional
2146
        Name of the cone. If not specified, the name will be the empty string.
2147
    surface_id : int, optional
2148
        Unique identifier for the surface. If not specified, an identifier will
2149
        automatically be assigned.
2150

2151
    Attributes
2152
    ----------
2153
    x0 : float
2154
        x-coordinate of the apex in [cm]
2155
    y0 : float
2156
        y-coordinate of the apex in [cm]
2157
    z0 : float
2158
        z-coordinate of the apex in [cm]
2159
    r2 : float
2160
        Parameter related to the aperture.
2161
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
2162
        Boundary condition that defines the behavior for particles hitting the
2163
        surface.
2164
    albedo : float
2165
        Boundary albedo as a positive multiplier of particle weight
2166
    coefficients : dict
2167
        Dictionary of surface coefficients
2168
    id : int
2169
        Unique identifier for the surface
2170
    name : str
2171
        Name of the surface
2172
    type : str
2173
        Type of the surface
2174

2175
    """
2176

2177
    _type = 'z-cone'
11✔
2178
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
11✔
2179

2180
    def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
11✔
2181
        R2 = kwargs.pop('R2', None)
11✔
2182
        if R2 is not None:
11✔
UNCOV
2183
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
2184
                 FutureWarning)
UNCOV
2185
            r2 = R2
×
2186
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
2187
        super().__init__(**kwargs)
11✔
2188

2189
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
11✔
2190
            setattr(self, key, val)
11✔
2191

2192
    x0 = SurfaceCoefficient('x0')
11✔
2193
    y0 = SurfaceCoefficient('y0')
11✔
2194
    z0 = SurfaceCoefficient('z0')
11✔
2195
    r2 = SurfaceCoefficient('r2')
11✔
2196
    dx = SurfaceCoefficient(0.)
11✔
2197
    dy = SurfaceCoefficient(0.)
11✔
2198
    dz = SurfaceCoefficient(1.)
11✔
2199

2200
    def _get_base_coeffs(self):
11✔
UNCOV
2201
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2202

2203
        c = -r2
×
2204
        a = b = 1.
×
2205
        d = e = f = 0.
×
2206
        g, h, j = -2*x0, -2*y0, 2*z0*r2
×
UNCOV
2207
        k = x0*x0 + y0*y0 - r2*z0*z0
×
2208

UNCOV
2209
        return (a, b, c, d, e, f, g, h, j, k)
×
2210

2211
    def evaluate(self, point):
11✔
2212
        x = point[0] - self.x0
11✔
2213
        y = point[1] - self.y0
11✔
2214
        z = point[2] - self.z0
11✔
2215
        return x*x + y*y - self.r2*z*z
11✔
2216

2217

2218
class Quadric(QuadricMixin, Surface):
11✔
2219
    """A surface of the form :math:`Ax^2 + By^2 + Cz^2 + Dxy + Eyz + Fxz + Gx + Hy +
2220
    Jz + K = 0`.
2221

2222
    Parameters
2223
    ----------
2224
    a, b, c, d, e, f, g, h, j, k : float, optional
2225
        coefficients for the surface. All default to 0.
2226
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional
2227
        Boundary condition that defines the behavior for particles hitting the
2228
        surface. Defaults to transmissive boundary condition where particles
2229
        freely pass through the surface.
2230
    albedo : float, optional
2231
        Albedo of the surfaces as a ratio of particle weight after interaction
2232
        with the surface to the initial weight. Values must be positive. Only
2233
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
2234
    name : str, optional
2235
        Name of the surface. If not specified, the name will be the empty string.
2236
    surface_id : int, optional
2237
        Unique identifier for the surface. If not specified, an identifier will
2238
        automatically be assigned.
2239

2240
    Attributes
2241
    ----------
2242
    a, b, c, d, e, f, g, h, j, k : float
2243
        coefficients for the surface
2244
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
2245
        Boundary condition that defines the behavior for particles hitting the
2246
        surface.
2247
    albedo : float
2248
        Boundary albedo as a positive multiplier of particle weight
2249
    coefficients : dict
2250
        Dictionary of surface coefficients
2251
    id : int
2252
        Unique identifier for the surface
2253
    name : str
2254
        Name of the surface
2255
    type : str
2256
        Type of the surface
2257

2258
    """
2259

2260
    _type = 'quadric'
11✔
2261
    _coeff_keys = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k')
11✔
2262

2263
    def __init__(self, a=0., b=0., c=0., d=0., e=0., f=0., g=0., h=0., j=0.,
11✔
2264
                 k=0., *args, **kwargs):
2265
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
2266
        super().__init__(**kwargs)
11✔
2267

2268
        for key, val in zip(self._coeff_keys, (a, b, c, d, e, f, g, h, j, k)):
11✔
2269
            setattr(self, key, val)
11✔
2270

2271
    a = SurfaceCoefficient('a')
11✔
2272
    b = SurfaceCoefficient('b')
11✔
2273
    c = SurfaceCoefficient('c')
11✔
2274
    d = SurfaceCoefficient('d')
11✔
2275
    e = SurfaceCoefficient('e')
11✔
2276
    f = SurfaceCoefficient('f')
11✔
2277
    g = SurfaceCoefficient('g')
11✔
2278
    h = SurfaceCoefficient('h')
11✔
2279
    j = SurfaceCoefficient('j')
11✔
2280
    k = SurfaceCoefficient('k')
11✔
2281

2282
    def _get_base_coeffs(self):
11✔
2283
        return tuple(getattr(self, c) for c in self._coeff_keys)
11✔
2284

2285

2286
class TorusMixin:
11✔
2287
    """A Mixin class implementing common functionality for torus surfaces"""
2288
    _coeff_keys = ('x0', 'y0', 'z0', 'a', 'b', 'c')
11✔
2289

2290
    def __init__(self, x0=0., y0=0., z0=0., a=0., b=0., c=0., **kwargs):
11✔
2291
        super().__init__(**kwargs)
11✔
2292
        for key, val in zip(self._coeff_keys, (x0, y0, z0, a, b, c)):
11✔
2293
            setattr(self, key, val)
11✔
2294

2295
    x0 = SurfaceCoefficient('x0')
11✔
2296
    y0 = SurfaceCoefficient('y0')
11✔
2297
    z0 = SurfaceCoefficient('z0')
11✔
2298
    a = SurfaceCoefficient('a')
11✔
2299
    b = SurfaceCoefficient('b')
11✔
2300
    c = SurfaceCoefficient('c')
11✔
2301

2302
    def translate(self, vector, inplace=False):
11✔
2303
        surf = self if inplace else self.clone()
11✔
2304
        surf.x0 += vector[0]
11✔
2305
        surf.y0 += vector[1]
11✔
2306
        surf.z0 += vector[2]
11✔
2307
        return surf
11✔
2308

2309
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
11✔
2310
        pivot = np.asarray(pivot)
11✔
2311
        rotation = np.asarray(rotation, dtype=float)
11✔
2312

2313
        # Allow rotation matrix to be passed in directly, otherwise build it
2314
        if rotation.ndim == 2:
11✔
2315
            check_length('surface rotation', rotation.ravel(), 9)
×
UNCOV
2316
            Rmat = rotation
×
2317
        else:
2318
            Rmat = get_rotation_matrix(rotation, order=order)
11✔
2319

2320
        # Only can handle trivial rotation matrices
2321
        close = np.isclose
11✔
2322
        if not np.all(close(Rmat, -1.0) | close(Rmat, 0.0) | close(Rmat, 1.0)):
11✔
2323
            raise NotImplementedError('Torus surfaces cannot handle generic rotations')
11✔
2324

2325
        # Translate surface to pivot
2326
        surf = self.translate(-pivot, inplace=inplace)
11✔
2327

2328
        # Determine "center" of torus and a point above it (along main axis)
2329
        center = [surf.x0, surf.y0, surf.z0]
11✔
2330
        above_center = center.copy()
11✔
2331
        index = ['x-torus', 'y-torus', 'z-torus'].index(surf._type)
11✔
2332
        above_center[index] += 1
11✔
2333

2334
        # Compute new rotated torus center
2335
        center = Rmat @ center
11✔
2336

2337
        # Figure out which axis should be used after rotation
2338
        above_center = Rmat @ above_center
11✔
2339
        new_index = np.where(np.isclose(np.abs(above_center - center), 1.0))[0][0]
11✔
2340
        cls = [XTorus, YTorus, ZTorus][new_index]
11✔
2341

2342
        # Create rotated torus
2343
        kwargs = {
11✔
2344
            'boundary_type': surf.boundary_type,
2345
            'albedo': surf.albedo,
2346
            'name': surf.name,
2347
            'a': surf.a, 'b': surf.b, 'c': surf.c
2348
        }
2349
        if inplace:
11✔
UNCOV
2350
            kwargs['surface_id'] = surf.id
×
2351
        surf = cls(x0=center[0], y0=center[1], z0=center[2], **kwargs)
11✔
2352

2353
        return surf.translate(pivot, inplace=inplace)
11✔
2354

2355
    def _get_base_coeffs(self):
11✔
UNCOV
2356
        raise NotImplementedError
×
2357

2358

2359
class XTorus(TorusMixin, Surface):
11✔
2360
    r"""A torus of the form :math:`(x - x_0)^2/B^2 + (\sqrt{(y - y_0)^2 + (z -
2361
    z_0)^2} - A)^2/C^2 - 1 = 0`.
2362

2363
    .. versionadded:: 0.13.0
2364

2365
    Parameters
2366
    ----------
2367
    x0 : float
2368
        x-coordinate of the center of the axis of revolution in [cm]
2369
    y0 : float
2370
        y-coordinate of the center of the axis of revolution in [cm]
2371
    z0 : float
2372
        z-coordinate of the center of the axis of revolution in [cm]
2373
    a : float
2374
        Major radius of the torus in [cm]
2375
    b : float
2376
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2377
    c : float
2378
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2379
    kwargs : dict
2380
        Keyword arguments passed to the :class:`Surface` constructor
2381

2382
    Attributes
2383
    ----------
2384
    x0 : float
2385
        x-coordinate of the center of the axis of revolution in [cm]
2386
    y0 : float
2387
        y-coordinate of the center of the axis of revolution in [cm]
2388
    z0 : float
2389
        z-coordinate of the center of the axis of revolution in [cm]
2390
    a : float
2391
        Major radius of the torus in [cm]
2392
    b : float
2393
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2394
    c : float
2395
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2396
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
2397
        Boundary condition that defines the behavior for particles hitting the
2398
        surface.
2399
    albedo : float
2400
        Boundary albedo as a positive multiplier of particle weight
2401
    coefficients : dict
2402
        Dictionary of surface coefficients
2403
    id : int
2404
        Unique identifier for the surface
2405
    name : str
2406
        Name of the surface
2407
    type : str
2408
        Type of the surface
2409

2410
    """
2411
    _type = 'x-torus'
11✔
2412

2413
    def evaluate(self, point):
11✔
2414
        x = point[0] - self.x0
11✔
2415
        y = point[1] - self.y0
11✔
2416
        z = point[2] - self.z0
11✔
2417
        a = self.a
11✔
2418
        b = self.b
11✔
2419
        c = self.c
11✔
2420
        return (x*x)/(b*b) + (math.sqrt(y*y + z*z) - a)**2/(c*c) - 1
11✔
2421

2422
    def bounding_box(self, side):
11✔
2423
        x0, y0, z0 = self.x0, self.y0, self.z0
11✔
2424
        a, b, c = self.a, self.b, self.c
11✔
2425
        if side == '-':
11✔
2426
            return BoundingBox(
11✔
2427
                np.array([x0 - b, y0 - a - c, z0 - a - c]),
2428
                np.array([x0 + b, y0 + a + c, z0 + a + c])
2429
            )
2430
        elif side == '+':
11✔
2431
            return BoundingBox.infinite()
11✔
2432

2433

2434
class YTorus(TorusMixin, Surface):
11✔
2435
    r"""A torus of the form :math:`(y - y_0)^2/B^2 + (\sqrt{(x - x_0)^2 + (z -
2436
    z_0)^2} - A)^2/C^2 - 1 = 0`.
2437

2438
    .. versionadded:: 0.13.0
2439

2440
    Parameters
2441
    ----------
2442
    x0 : float
2443
        x-coordinate of the center of the axis of revolution in [cm]
2444
    y0 : float
2445
        y-coordinate of the center of the axis of revolution in [cm]
2446
    z0 : float
2447
        z-coordinate of the center of the axis of revolution in [cm]
2448
    a : float
2449
        Major radius of the torus in [cm]
2450
    b : float
2451
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2452
    c : float
2453
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2454
    kwargs : dict
2455
        Keyword arguments passed to the :class:`Surface` constructor
2456

2457
    Attributes
2458
    ----------
2459
    x0 : float
2460
        x-coordinate of the center of the axis of revolution in [cm]
2461
    y0 : float
2462
        y-coordinate of the center of the axis of revolution in [cm]
2463
    z0 : float
2464
        z-coordinate of the center of the axis of revolution in [cm]
2465
    a : float
2466
        Major radius of the torus in [cm]
2467
    b : float
2468
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2469
    c : float
2470
        Minor radius of the torus (perpendicular to axis of revolution)
2471
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
2472
        Boundary condition that defines the behavior for particles hitting the
2473
        surface.
2474
    albedo : float
2475
        Boundary albedo as a positive multiplier of particle weight
2476
    coefficients : dict
2477
        Dictionary of surface coefficients
2478
    id : int
2479
        Unique identifier for the surface
2480
    name : str
2481
        Name of the surface
2482
    type : str
2483
        Type of the surface
2484

2485
    """
2486
    _type = 'y-torus'
11✔
2487

2488
    def evaluate(self, point):
11✔
2489
        x = point[0] - self.x0
11✔
2490
        y = point[1] - self.y0
11✔
2491
        z = point[2] - self.z0
11✔
2492
        a = self.a
11✔
2493
        b = self.b
11✔
2494
        c = self.c
11✔
2495
        return (y*y)/(b*b) + (math.sqrt(x*x + z*z) - a)**2/(c*c) - 1
11✔
2496

2497
    def bounding_box(self, side):
11✔
2498
        x0, y0, z0 = self.x0, self.y0, self.z0
11✔
2499
        a, b, c = self.a, self.b, self.c
11✔
2500
        if side == '-':
11✔
2501
            return BoundingBox(
11✔
2502
                np.array([x0 - a - c, y0 - b, z0 - a - c]),
2503
                np.array([x0 + a + c, y0 + b, z0 + a + c])
2504
            )
2505
        elif side == '+':
11✔
2506
            return BoundingBox.infinite()
11✔
2507

2508

2509
class ZTorus(TorusMixin, Surface):
11✔
2510
    r"""A torus of the form :math:`(z - z_0)^2/B^2 + (\sqrt{(x - x_0)^2 + (y -
2511
    y_0)^2} - A)^2/C^2 - 1 = 0`.
2512

2513
    .. versionadded:: 0.13.0
2514

2515
    Parameters
2516
    ----------
2517
    x0 : float
2518
        x-coordinate of the center of the axis of revolution in [cm]
2519
    y0 : float
2520
        y-coordinate of the center of the axis of revolution in [cm]
2521
    z0 : float
2522
        z-coordinate of the center of the axis of revolution in [cm]
2523
    a : float
2524
        Major radius of the torus in [cm]
2525
    b : float
2526
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2527
    c : float
2528
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2529
    kwargs : dict
2530
        Keyword arguments passed to the :class:`Surface` constructor
2531

2532
    Attributes
2533
    ----------
2534
    x0 : float
2535
        x-coordinate of the center of the axis of revolution in [cm]
2536
    y0 : float
2537
        y-coordinate of the center of the axis of revolution in [cm]
2538
    z0 : float
2539
        z-coordinate of the center of the axis of revolution in [cm]
2540
    a : float
2541
        Major radius of the torus in [cm]
2542
    b : float
2543
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2544
    c : float
2545
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2546
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}
2547
        Boundary condition that defines the behavior for particles hitting the
2548
        surface.
2549
    albedo : float
2550
        Boundary albedo as a positive multiplier of particle weight
2551
    coefficients : dict
2552
        Dictionary of surface coefficients
2553
    id : int
2554
        Unique identifier for the surface
2555
    name : str
2556
        Name of the surface
2557
    type : str
2558
        Type of the surface
2559
    """
2560

2561
    _type = 'z-torus'
11✔
2562

2563
    def evaluate(self, point):
11✔
2564
        x = point[0] - self.x0
11✔
2565
        y = point[1] - self.y0
11✔
2566
        z = point[2] - self.z0
11✔
2567
        a = self.a
11✔
2568
        b = self.b
11✔
2569
        c = self.c
11✔
2570
        return (z*z)/(b*b) + (math.sqrt(x*x + y*y) - a)**2/(c*c) - 1
11✔
2571

2572
    def bounding_box(self, side):
11✔
2573
        x0, y0, z0 = self.x0, self.y0, self.z0
11✔
2574
        a, b, c = self.a, self.b, self.c
11✔
2575
        if side == '-':
11✔
2576
            return BoundingBox(
11✔
2577
                np.array([x0 - a - c, y0 - a - c, z0 - b]),
2578
                np.array([x0 + a + c, y0 + a + c, z0 + b])
2579
            )
2580
        elif side == '+':
11✔
2581
            return BoundingBox.infinite()
11✔
2582

2583

2584
class Halfspace(Region):
11✔
2585
    """A positive or negative half-space region.
2586

2587
    A half-space is either of the two parts into which a two-dimension surface
2588
    divides the three-dimensional Euclidean space. If the equation of the
2589
    surface is :math:`f(x,y,z) = 0`, the region for which :math:`f(x,y,z) < 0`
2590
    is referred to as the negative half-space and the region for which
2591
    :math:`f(x,y,z) > 0` is referred to as the positive half-space.
2592

2593
    Instances of Halfspace are generally not instantiated directly. Rather, they
2594
    can be created from an existing Surface through the __neg__ and __pos__
2595
    operators, as the following example demonstrates:
2596

2597
    >>> sphere = openmc.Sphere(surface_id=1, r=10.0)
2598
    >>> inside_sphere = -sphere
2599
    >>> outside_sphere = +sphere
2600
    >>> type(inside_sphere)
2601
    <class 'openmc.surface.Halfspace'>
2602

2603
    Parameters
2604
    ----------
2605
    surface : openmc.Surface
2606
        Surface which divides Euclidean space.
2607
    side : {'+', '-'}
2608
        Indicates whether the positive or negative half-space is used.
2609

2610
    Attributes
2611
    ----------
2612
    surface : openmc.Surface
2613
        Surface which divides Euclidean space.
2614
    side : {'+', '-'}
2615
        Indicates whether the positive or negative half-space is used.
2616
    bounding_box : openmc.BoundingBox
2617
        Lower-left and upper-right coordinates of an axis-aligned bounding box
2618

2619
    """
2620

2621
    def __init__(self, surface, side):
11✔
2622
        self.surface = surface
11✔
2623
        self.side = side
11✔
2624

2625
    def __and__(self, other):
11✔
2626
        if isinstance(other, Intersection):
11✔
2627
            return Intersection([self] + other[:])
11✔
2628
        else:
2629
            return Intersection((self, other))
11✔
2630

2631
    def __or__(self, other):
11✔
2632
        if isinstance(other, Union):
11✔
UNCOV
2633
            return Union([self] + other[:])
×
2634
        else:
2635
            return Union((self, other))
11✔
2636

2637
    def __invert__(self) -> Halfspace:
11✔
2638
        return -self.surface if self.side == '+' else +self.surface
11✔
2639

2640
    def __contains__(self, point):
11✔
2641
        """Check whether a point is contained in the half-space.
2642

2643
        Parameters
2644
        ----------
2645
        point : 3-tuple of float
2646
            Cartesian coordinates, :math:`(x',y',z')`, of the point
2647

2648
        Returns
2649
        -------
2650
        bool
2651
            Whether the point is in the half-space
2652

2653
        """
2654

2655
        val = self.surface.evaluate(point)
11✔
2656
        return val >= 0. if self.side == '+' else val < 0.
11✔
2657

2658
    @property
11✔
2659
    def surface(self):
11✔
2660
        return self._surface
11✔
2661

2662
    @surface.setter
11✔
2663
    def surface(self, surface):
11✔
2664
        check_type('surface', surface, Surface)
11✔
2665
        self._surface = surface
11✔
2666

2667
    @property
11✔
2668
    def side(self):
11✔
2669
        return self._side
11✔
2670

2671
    @side.setter
11✔
2672
    def side(self, side):
11✔
2673
        check_value('side', side, ('+', '-'))
11✔
2674
        self._side = side
11✔
2675

2676
    @property
11✔
2677
    def bounding_box(self):
11✔
2678
        return self.surface.bounding_box(self.side)
11✔
2679

2680
    def __str__(self):
11✔
2681
        return '-' + str(self.surface.id) if self.side == '-' \
11✔
2682
            else str(self.surface.id)
2683

2684
    def get_surfaces(self, surfaces=None):
11✔
2685
        """
2686
        Returns the surface that this is a halfspace of.
2687

2688
        Parameters
2689
        ----------
2690
        surfaces : dict, optional
2691
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
2692

2693
        Returns
2694
        -------
2695
        surfaces : dict
2696
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
2697

2698
        """
2699
        if surfaces is None:
11✔
2700
            surfaces = {}
11✔
2701

2702
        surfaces[self.surface.id] = self.surface
11✔
2703
        return surfaces
11✔
2704

2705
    def remove_redundant_surfaces(self, redundant_surfaces):
11✔
2706
        """Recursively remove all redundant surfaces referenced by this region
2707

2708
        Parameters
2709
        ----------
2710
        redundant_surfaces : dict
2711
            Dictionary mapping redundant surface IDs to surface IDs for the
2712
            :class:`openmc.Surface` instances that should replace them.
2713

2714
        """
2715

2716
        surf = redundant_surfaces.get(self.surface.id)
11✔
2717
        if surf is not None:
11✔
2718
            self.surface = surf
11✔
2719

2720
    def clone(self, memo=None):
11✔
2721
        """Create a copy of this halfspace, with a cloned surface with a
2722
        unique ID.
2723

2724
        Parameters
2725
        ----------
2726
        memo : dict or None
2727
            A nested dictionary of previously cloned objects. This parameter
2728
            is used internally and should not be specified by the user.
2729

2730
        Returns
2731
        -------
2732
        clone : openmc.Halfspace
2733
            The clone of this halfspace
2734

2735
        """
2736

2737
        if memo is None:
11✔
UNCOV
2738
            memo = dict
×
2739

2740
        clone = deepcopy(self)
11✔
2741
        clone.surface = self.surface.clone(memo)
11✔
2742
        return clone
11✔
2743

2744
    def translate(self, vector, inplace=False, memo=None):
11✔
2745
        """Translate half-space in given direction
2746

2747
        Parameters
2748
        ----------
2749
        vector : iterable of float
2750
            Direction in which region should be translated
2751
        memo : dict or None
2752
            Dictionary used for memoization
2753

2754
        Returns
2755
        -------
2756
        openmc.Halfspace
2757
            Translated half-space
2758

2759
        """
2760
        if memo is None:
11✔
UNCOV
2761
            memo = {}
×
2762

2763
        # If translated surface not in memo, add it
2764
        key = (self.surface, tuple(vector))
11✔
2765
        if key not in memo:
11✔
2766
            memo[key] = self.surface.translate(vector, inplace)
11✔
2767

2768
        # Return translated half-space
2769
        return type(self)(memo[key], self.side)
11✔
2770

2771
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False,
11✔
2772
               memo=None):
2773
        r"""Rotate surface by angles provided or by applying matrix directly.
2774

2775
        .. versionadded:: 0.12
2776

2777
        Parameters
2778
        ----------
2779
        rotation : 3-tuple of float, or 3x3 iterable
2780
            A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where
2781
            the first element is the rotation about the x-axis in the fixed
2782
            laboratory frame, the second element is the rotation about the
2783
            y-axis in the fixed laboratory frame, and the third element is the
2784
            rotation about the z-axis in the fixed laboratory frame. The
2785
            rotations are active rotations. Additionally a 3x3 rotation matrix
2786
            can be specified directly either as a nested iterable or array.
2787
        pivot : iterable of float, optional
2788
            (x, y, z) coordinates for the point to rotate about. Defaults to
2789
            (0., 0., 0.)
2790
        order : str, optional
2791
            A string of 'x', 'y', and 'z' in some order specifying which
2792
            rotation to perform first, second, and third. Defaults to 'xyz'
2793
            which means, the rotation by angle :math:`\phi` about x will be
2794
            applied first, followed by :math:`\theta` about y and then
2795
            :math:`\psi` about z. This corresponds to an x-y-z extrinsic
2796
            rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan
2797
            angles :math:`(\phi, \theta, \psi)`.
2798
        inplace : bool
2799
            Whether or not to return a new instance of Surface or to modify the
2800
            coefficients of this Surface in place. Defaults to False.
2801
        memo : dict or None
2802
            Dictionary used for memoization
2803

2804
        Returns
2805
        -------
2806
        openmc.Halfspace
2807
            Translated half-space
2808

2809
        """
2810
        if memo is None:
11✔
UNCOV
2811
            memo = {}
×
2812

2813
        # If rotated surface not in memo, add it
2814
        key = (self.surface, tuple(np.ravel(rotation)), tuple(pivot), order, inplace)
11✔
2815
        if key not in memo:
11✔
2816
            memo[key] = self.surface.rotate(rotation, pivot=pivot, order=order,
11✔
2817
                                            inplace=inplace)
2818

2819
        # Return rotated half-space
2820
        return type(self)(memo[key], self.side)
11✔
2821

2822

2823
_SURFACE_CLASSES = {cls._type: cls for cls in Surface.__subclasses__()}
11✔
2824

2825

2826
# Set virtual base classes for "casting" up the hierarchy
2827
Plane._virtual_base = Plane
11✔
2828
XPlane._virtual_base = Plane
11✔
2829
YPlane._virtual_base = Plane
11✔
2830
ZPlane._virtual_base = Plane
11✔
2831
Cylinder._virtual_base = Cylinder
11✔
2832
XCylinder._virtual_base = Cylinder
11✔
2833
YCylinder._virtual_base = Cylinder
11✔
2834
ZCylinder._virtual_base = Cylinder
11✔
2835
Cone._virtual_base = Cone
11✔
2836
XCone._virtual_base = Cone
11✔
2837
YCone._virtual_base = Cone
11✔
2838
ZCone._virtual_base = Cone
11✔
2839
Sphere._virtual_base = Sphere
11✔
2840
Quadric._virtual_base = Quadric
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc