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

openmc-dev / openmc / 22007957997

14 Feb 2026 12:50AM UTC coverage: 81.717% (-0.05%) from 81.763%
22007957997

Pull #3765

github

web-flow
Merge 6111ffe2e into 19c0aafdc
Pull Request #3765: Store atomic mass in ParticleType.

17526 of 24626 branches covered (71.17%)

Branch coverage included in aggregate %.

6 of 7 new or added lines in 2 files covered. (85.71%)

1820 existing lines in 34 files now uncovered.

56937 of 66497 relevant lines covered (85.62%)

44324210.02 hits per line

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

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

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

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

18

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

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

27
_WARNING_KWARGS = """\
10✔
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:
10✔
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):
10✔
44
        self.value = value
10✔
45

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

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

61

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

70

71
def get_rotation_matrix(rotation, order='xyz'):
10✔
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)
10✔
95
    check_length('surface rotation', rotation, 3)
10✔
96

97
    phi, theta, psi = np.array(rotation)*(math.pi/180.)
10✔
98
    cx, sx = math.cos(phi), math.sin(phi)
10✔
99
    cy, sy = math.cos(theta), math.sin(theta)
10✔
100
    cz, sz = math.cos(psi), math.sin(psi)
10✔
101
    R = {
10✔
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)
10✔
108
    return R3 @ R2 @ R1
10✔
109

110

111
class Surface(IDManagerMixin, ABC):
10✔
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 only axis-aligned
127
        periodicity is supported around the x-, y-, and z-axes.
128
    albedo : float, optional
129
        Albedo of the surfaces as a ratio of particle weight after interaction
130
        with the surface to the initial weight. Values must be positive. Only
131
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
132
    name : str, optional
133
        Name of the surface. If not specified, the name will be the empty
134
        string.
135

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

152
    """
153

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

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

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

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

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

177
    def __repr__(self):
10✔
178
        string = 'Surface\n'
10✔
179
        string += '{0: <20}{1}{2}\n'.format('\tID', '=\t', self._id)
10✔
180
        string += '{0: <20}{1}{2}\n'.format('\tName', '=\t', self._name)
10✔
181
        string += '{0: <20}{1}{2}\n'.format('\tType', '=\t', self._type)
10✔
182
        string += '{0: <20}{1}{2}\n'.format('\tBoundary', '=\t',
10✔
183
                                            self._boundary_type)
184
        if (self._boundary_type in _ALBEDO_BOUNDARIES and
10✔
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'
10✔
190

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

194
        string += coefficients
10✔
195

196
        return string
10✔
197

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

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

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

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

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

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

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

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

238
    def bounding_box(self, side):
10✔
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()
10✔
262

263
    def clone(self, memo=None):
10✔
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:
10✔
280
            memo = {}
10✔
281

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

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

290
        return memo[self]
10✔
291

292
    def normalize(self, coeffs=None):
10✔
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:
10✔
310
            coeffs = self._get_base_coeffs()
10✔
311
        coeffs = np.asarray(coeffs)
10✔
312
        nonzeros = ~np.isclose(coeffs, 0., rtol=0., atol=self._atol)
10✔
313
        norm_factor = coeffs[nonzeros][0]
10✔
314
        return tuple([c/norm_factor for c in coeffs])
10✔
315

316
    def is_equal(self, other):
10✔
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())
10✔
327
        coeffs2 = self.normalize(other._get_base_coeffs())
10✔
328

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

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

336
        """
337

338
    @abstractmethod
10✔
339
    def evaluate(self, point):
10✔
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
10✔
356
    def translate(self, vector, inplace=False):
10✔
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
10✔
375
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
10✔
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):
10✔
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")
10✔
422
        element.set("id", str(self._id))
10✔
423

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

427
        element.set("type", self._type)
10✔
428
        if self.boundary_type != 'transmission':
10✔
429
            element.set("boundary", self.boundary_type)
10✔
430
            if (self.boundary_type in _ALBEDO_BOUNDARIES and
10✔
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))
10✔
434
                                        for key in self._coeff_keys]))
435

436
        return element
10✔
437

438
    @staticmethod
10✔
439
    def from_xml_element(elem):
10✔
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")
10✔
456
        cls = _SURFACE_CLASSES[surf_type]
10✔
457

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

468
        return cls(**kwargs)
10✔
469

470
    @staticmethod
10✔
471
    def from_hdf5(group):
10✔
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')
10✔
488
        if geom_type and geom_type[()].decode() == 'dagmc':
10✔
489
            return
1✔
490

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

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

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

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

508

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

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

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

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

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

532
    def bounding_box(self, side):
10✔
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()
10✔
557
        ll = np.array([-np.inf, -np.inf, -np.inf])
10✔
558
        ur = np.array([np.inf, np.inf, np.inf])
10✔
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)):
10✔
561
            sign = nhat.sum()
10✔
562
            a, b, c, d = self._get_base_coeffs()
10✔
563
            vals = [d/val if not np.isclose(val, 0., rtol=0., atol=self._atol)
10✔
564
                    else np.nan for val in (a, b, c)]
565
            if side == '-':
10✔
566
                if sign > 0:
10✔
567
                    ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
10✔
568
                else:
569
                    ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
10✔
570
            elif side == '+':
10✔
571
                if sign > 0:
10✔
572
                    ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
10✔
573
                else:
574
                    ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
10✔
575

576
        return BoundingBox(ll, ur)
10✔
577

578
    def evaluate(self, point):
10✔
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
10✔
595
        a, b, c, d = self._get_base_coeffs()
10✔
596
        return a*x + b*y + c*z - d
10✔
597

598
    def translate(self, vector, inplace=False):
10✔
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):
10✔
616
            return self
10✔
617

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

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

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

625
        return surf
10✔
626

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

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

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

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

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

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

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

655
    def to_xml_element(self):
10✔
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()
10✔
665

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

673

674
class Plane(PlaneMixin, Surface):
10✔
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'
10✔
731
    _coeff_keys = ('a', 'b', 'c', 'd')
10✔
732

733
    def __init__(self, a=1., b=0., c=0., d=0., *args, **kwargs):
10✔
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)
10✔
738
        # Warn if capital letter arguments are passed
739
        capdict = {}
10✔
740
        for k in 'ABCD':
10✔
741
            val = kwargs.pop(k, None)
10✔
742
            if val is not None:
10✔
UNCOV
743
                warn(_WARNING_UPPER.format(type(self), k.lower(), k),
×
744
                     FutureWarning)
UNCOV
745
                capdict[k.lower()] = val
×
746

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

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

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

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

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

766
    @classmethod
10✔
767
    def from_points(cls, p1, p2, p3, **kwargs):
10✔
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)
10✔
790
        p2 = np.asarray(p2, dtype=float)
10✔
791
        p3 = np.asarray(p3, dtype=float)
10✔
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)
10✔
796

797
        # Check for points along a line
798
        if np.allclose(n, 0.):
10✔
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
10✔
804
        d = np.dot(n, p1)
10✔
805
        return cls(a=a, b=b, c=c, d=d, **kwargs)
10✔
806

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

814

815
class XPlane(PlaneMixin, Surface):
10✔
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.
826
    albedo : float, optional
827
        Albedo of the surfaces as a ratio of particle weight after interaction
828
        with the surface to the initial weight. Values must be positive. Only
829
        applicable if the boundary type is 'reflective', 'periodic', or 'white'.
830
    name : str, optional
831
        Name of the plane. If not specified, the name will be the empty string.
832
    surface_id : int, optional
833
        Unique identifier for the surface. If not specified, an identifier will
834
        automatically be assigned.
835

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

857
    """
858

859
    _type = 'x-plane'
10✔
860
    _coeff_keys = ('x0',)
10✔
861

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

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

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

878

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

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

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

921
    """
922

923
    _type = 'y-plane'
10✔
924
    _coeff_keys = ('y0',)
10✔
925

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

933
    y0 = SurfaceCoefficient('y0')
10✔
934
    a = SurfaceCoefficient(0.)
10✔
935
    b = SurfaceCoefficient(1.)
10✔
936
    c = SurfaceCoefficient(0.)
10✔
937
    d = y0
10✔
938

939
    def evaluate(self, point):
10✔
940
        return point[1] - self.y0
10✔
941

942

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

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

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

985
    """
986

987
    _type = 'z-plane'
10✔
988
    _coeff_keys = ('z0',)
10✔
989

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

997
    z0 = SurfaceCoefficient('z0')
10✔
998
    a = SurfaceCoefficient(0.)
10✔
999
    b = SurfaceCoefficient(0.)
10✔
1000
    c = SurfaceCoefficient(1.)
10✔
1001
    d = z0
10✔
1002

1003
    def evaluate(self, point):
10✔
1004
        return point[2] - self.z0
10✔
1005

1006

1007
class QuadricMixin:
10✔
1008
    """A Mixin class implementing common functionality for quadric surfaces"""
1009

1010
    @property
10✔
1011
    def _origin(self):
10✔
1012
        return np.array((self.x0, self.y0, self.z0))
10✔
1013

1014
    @property
10✔
1015
    def _axis(self):
10✔
1016
        axis = np.array((self.dx, self.dy, self.dz))
10✔
1017
        return axis / np.linalg.norm(axis)
10✔
1018

1019
    def get_Abc(self, coeffs=None):
10✔
1020
        """Compute matrix, vector, and scalar coefficients for this surface or
1021
        for a specified set of coefficients.
1022

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

1034
        A = np.array([[a, d/2, f/2], [d/2, b, e/2], [f/2, e/2, c]])
10✔
1035
        bvec = np.array([g, h, j])
10✔
1036

1037
        return A, bvec, k
10✔
1038

1039
    def eigh(self, coeffs=None):
10✔
1040
        """Wrapper method for returning eigenvalues and eigenvectors of this
1041
        quadric surface which is used for transformations.
1042

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

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

1058
        """
UNCOV
1059
        return np.linalg.eigh(self.get_Abc(coeffs=coeffs)[0])
×
1060

1061
    def evaluate(self, point):
10✔
1062
        """Evaluate the surface equation at a given point.
1063

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

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

1076
        """
1077
        x = np.asarray(point)
10✔
1078
        A, b, c = self.get_Abc()
10✔
1079
        return x.T @ A @ x + b.T @ x + c
10✔
1080

1081
    def translate(self, vector, inplace=False):
10✔
1082
        """Translate surface in given direction
1083

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

1091
        Returns
1092
        -------
1093
        openmc.Surface
1094
            Translated surface
1095

1096
        """
1097
        vector = np.asarray(vector)
10✔
1098
        if np.allclose(vector, 0., rtol=0., atol=self._atol):
10✔
1099
            return self
10✔
1100

1101
        surf = self if inplace else self.clone()
10✔
1102

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

1112
        else:
1113
            A, bvec, cnst = self.get_Abc()
10✔
1114

1115
            g, h, j = bvec - 2*vector.T @ A
10✔
1116
            k = cnst + vector.T @ A @ vector - bvec.T @ vector
10✔
1117

1118
            for key, val in zip(('g', 'h', 'j', 'k'), (g, h, j, k)):
10✔
1119
                setattr(surf, key, val)
10✔
1120

1121
        return surf
10✔
1122

1123
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
10✔
1124
        # Get pivot and rotation matrix
1125
        pivot = np.asarray(pivot)
10✔
1126
        rotation = np.asarray(rotation, dtype=float)
10✔
1127

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

1135
        # Translate surface to the pivot point
1136
        tsurf = self.translate(-pivot, inplace=inplace)
10✔
1137

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

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

1163
            a, b, c = np.diagonal(Arot)
10✔
1164
            d, e, f = 2*Arot[0, 1], 2*Arot[1, 2], 2*Arot[0, 2]
10✔
1165
            g, h, j = Rmat @ bvec
10✔
1166

1167
            for key, val in zip(surf._coeff_keys, (a, b, c, d, e, f, g, h, j, k)):
10✔
1168
                setattr(surf, key, val)
10✔
1169

1170
        # translate back to the original frame and return the surface
1171
        return surf.translate(pivot, inplace=inplace)
10✔
1172

1173

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

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

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

1243
    """
1244
    _type = 'cylinder'
10✔
1245
    _coeff_keys = ('x0', 'y0', 'z0', 'r', 'dx', 'dy', 'dz')
10✔
1246

1247
    def __init__(self, x0=0., y0=0., z0=0., r=1., dx=0., dy=0., dz=1., *args,
10✔
1248
                 **kwargs):
1249
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
10✔
1250
        super().__init__(**kwargs)
10✔
1251

1252
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r, dx, dy, dz)):
10✔
1253
            setattr(self, key, val)
10✔
1254

1255
    @classmethod
10✔
1256
    def __subclasshook__(cls, c):
10✔
1257
        if cls is Cylinder and c in (XCylinder, YCylinder, ZCylinder):
10✔
1258
            return True
10✔
UNCOV
1259
        return NotImplemented
×
1260

1261
    x0 = SurfaceCoefficient('x0')
10✔
1262
    y0 = SurfaceCoefficient('y0')
10✔
1263
    z0 = SurfaceCoefficient('z0')
10✔
1264
    r = SurfaceCoefficient('r')
10✔
1265
    dx = SurfaceCoefficient('dx')
10✔
1266
    dy = SurfaceCoefficient('dy')
10✔
1267
    dz = SurfaceCoefficient('dz')
10✔
1268

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

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

1286
        # Define intermediate terms
1287
        dx = x2 - x1
10✔
1288
        dy = y2 - y1
10✔
1289
        dz = z2 - z1
10✔
1290
        cx = y1*z2 - y2*z1
10✔
1291
        cy = x2*z1 - x1*z2
10✔
1292
        cz = x1*y2 - x2*y1
10✔
1293

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

1310
        return (a, b, c, d, e, f, g, h, j, k)
10✔
1311

1312
    @classmethod
10✔
1313
    def from_points(cls, p1, p2, r=1., **kwargs):
10✔
1314
        """Return a cylinder given points that define the axis and a radius.
1315

1316
        .. versionadded:: 0.12
1317

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

1327
        Returns
1328
        -------
1329
        Cylinder
1330
            Cylinder that has an axis through the points p1 and p2, and a
1331
            radius r.
1332

1333
        """
1334
        # Convert to numpy arrays
1335
        p1 = np.asarray(p1)
10✔
1336
        p2 = np.asarray(p2)
10✔
1337
        x0, y0, z0 = p1
10✔
1338
        dx, dy, dz = p2 - p1
10✔
1339

1340
        return cls(x0=x0, y0=y0, z0=z0, r=r, dx=dx, dy=dy, dz=dz, **kwargs)
10✔
1341

1342
    def to_xml_element(self):
10✔
1343
        """Return XML representation of the surface
1344

1345
        Returns
1346
        -------
1347
        element : lxml.etree._Element
1348
            XML element containing source data
1349

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

1360

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

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

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

1410
    """
1411

1412
    _type = 'x-cylinder'
10✔
1413
    _coeff_keys = ('y0', 'z0', 'r')
10✔
1414

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

1424
        for key, val in zip(self._coeff_keys, (y0, z0, r)):
10✔
1425
            setattr(self, key, val)
10✔
1426

1427
    x0 = SurfaceCoefficient(0.)
10✔
1428
    y0 = SurfaceCoefficient('y0')
10✔
1429
    z0 = SurfaceCoefficient('z0')
10✔
1430
    r = SurfaceCoefficient('r')
10✔
1431
    dx = SurfaceCoefficient(1.)
10✔
1432
    dy = SurfaceCoefficient(0.)
10✔
1433
    dz = SurfaceCoefficient(0.)
10✔
1434

1435
    def _get_base_coeffs(self):
10✔
UNCOV
1436
        y0, z0, r = self.y0, self.z0, self.r
×
1437

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

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

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

1453
    def evaluate(self, point):
10✔
1454
        y = point[1] - self.y0
10✔
1455
        z = point[2] - self.z0
10✔
1456
        return y*y + z*z - self.r**2
10✔
1457

1458

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

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

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

1508
    """
1509

1510
    _type = 'y-cylinder'
10✔
1511
    _coeff_keys = ('x0', 'z0', 'r')
10✔
1512

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

1522
        for key, val in zip(self._coeff_keys, (x0, z0, r)):
10✔
1523
            setattr(self, key, val)
10✔
1524

1525
    x0 = SurfaceCoefficient('x0')
10✔
1526
    y0 = SurfaceCoefficient(0.)
10✔
1527
    z0 = SurfaceCoefficient('z0')
10✔
1528
    r = SurfaceCoefficient('r')
10✔
1529
    dx = SurfaceCoefficient(0.)
10✔
1530
    dy = SurfaceCoefficient(1.)
10✔
1531
    dz = SurfaceCoefficient(0.)
10✔
1532

1533
    def _get_base_coeffs(self):
10✔
UNCOV
1534
        x0, z0, r = self.x0, self.z0, self.r
×
1535

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

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

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

1551
    def evaluate(self, point):
10✔
1552
        x = point[0] - self.x0
10✔
1553
        z = point[2] - self.z0
10✔
1554
        return x*x + z*z - self.r**2
10✔
1555

1556

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

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

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

1606
    """
1607

1608
    _type = 'z-cylinder'
10✔
1609
    _coeff_keys = ('x0', 'y0', 'r')
10✔
1610

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

1620
        for key, val in zip(self._coeff_keys, (x0, y0, r)):
10✔
1621
            setattr(self, key, val)
10✔
1622

1623
    x0 = SurfaceCoefficient('x0')
10✔
1624
    y0 = SurfaceCoefficient('y0')
10✔
1625
    z0 = SurfaceCoefficient(0.)
10✔
1626
    r = SurfaceCoefficient('r')
10✔
1627
    dx = SurfaceCoefficient(0.)
10✔
1628
    dy = SurfaceCoefficient(0.)
10✔
1629
    dz = SurfaceCoefficient(1.)
10✔
1630

1631
    def _get_base_coeffs(self):
10✔
UNCOV
1632
        x0, y0, r = self.x0, self.y0, self.r
×
1633

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

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

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

1649
    def evaluate(self, point):
10✔
1650
        x = point[0] - self.x0
10✔
1651
        y = point[1] - self.y0
10✔
1652
        return x*x + y*y - self.r**2
10✔
1653

1654

1655
class Sphere(QuadricMixin, Surface):
10✔
1656
    """A sphere of the form :math:`(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = r^2`.
1657

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

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

1706
    """
1707

1708
    _type = 'sphere'
10✔
1709
    _coeff_keys = ('x0', 'y0', 'z0', 'r')
10✔
1710

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

1720
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r)):
10✔
1721
            setattr(self, key, val)
10✔
1722

1723
    x0 = SurfaceCoefficient('x0')
10✔
1724
    y0 = SurfaceCoefficient('y0')
10✔
1725
    z0 = SurfaceCoefficient('z0')
10✔
1726
    r = SurfaceCoefficient('r')
10✔
1727

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

1735
        return (a, b, c, d, e, f, g, h, j, k)
10✔
1736

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

1746
    def evaluate(self, point):
10✔
1747
        x = point[0] - self.x0
10✔
1748
        y = point[1] - self.y0
10✔
1749
        z = point[2] - self.z0
10✔
1750
        return x*x + y*y + z*z - self.r**2
10✔
1751

1752

1753
class Cone(QuadricMixin, Surface):
10✔
1754
    r"""A conical surface parallel to the x-, y-, or z-axis.
1755

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

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

1792
    name : str
1793
        Name of the cone. If not specified, the name will be the empty string.
1794

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

1825
    """
1826

1827
    _type = 'cone'
10✔
1828
    _coeff_keys = ('x0', 'y0', 'z0', 'r2', 'dx', 'dy', 'dz')
10✔
1829

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

1840
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2, dx, dy, dz)):
10✔
1841
            setattr(self, key, val)
10✔
1842

1843
    @classmethod
10✔
1844
    def __subclasshook__(cls, c):
10✔
1845
        if cls is Cone and c in (XCone, YCone, ZCone):
×
1846
            return True
×
UNCOV
1847
        return NotImplemented
×
1848

1849
    x0 = SurfaceCoefficient('x0')
10✔
1850
    y0 = SurfaceCoefficient('y0')
10✔
1851
    z0 = SurfaceCoefficient('z0')
10✔
1852
    r2 = SurfaceCoefficient('r2')
10✔
1853
    dx = SurfaceCoefficient('dx')
10✔
1854
    dy = SurfaceCoefficient('dy')
10✔
1855
    dz = SurfaceCoefficient('dz')
10✔
1856

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

1870
        x0, y0, z0 = self._origin
10✔
1871
        dx, dy, dz = self._axis
10✔
1872
        cos2 = 1 / (1 + self.r2)
10✔
1873

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

1886
        return (a, b, c, d, e, f, g, h, j, k)
10✔
1887

1888
    def to_xml_element(self):
10✔
1889
        """Return XML representation of the surface
1890

1891
        Returns
1892
        -------
1893
        element : lxml.etree._Element
1894
            XML element containing source data
1895

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

1908

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

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

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

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

1968
    """
1969

1970
    _type = 'x-cone'
10✔
1971
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
10✔
1972

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

1982
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
10✔
1983
            setattr(self, key, val)
10✔
1984

1985
    x0 = SurfaceCoefficient('x0')
10✔
1986
    y0 = SurfaceCoefficient('y0')
10✔
1987
    z0 = SurfaceCoefficient('z0')
10✔
1988
    r2 = SurfaceCoefficient('r2')
10✔
1989
    dx = SurfaceCoefficient(1.)
10✔
1990
    dy = SurfaceCoefficient(0.)
10✔
1991
    dz = SurfaceCoefficient(0.)
10✔
1992

1993
    def _get_base_coeffs(self):
10✔
UNCOV
1994
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
1995

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

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

2004
    def evaluate(self, point):
10✔
2005
        x = point[0] - self.x0
10✔
2006
        y = point[1] - self.y0
10✔
2007
        z = point[2] - self.z0
10✔
2008
        return y*y + z*z - self.r2*x*x
10✔
2009

2010

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

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

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

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

2070
    """
2071

2072
    _type = 'y-cone'
10✔
2073
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
10✔
2074

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

2084
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
10✔
2085
            setattr(self, key, val)
10✔
2086

2087
    x0 = SurfaceCoefficient('x0')
10✔
2088
    y0 = SurfaceCoefficient('y0')
10✔
2089
    z0 = SurfaceCoefficient('z0')
10✔
2090
    r2 = SurfaceCoefficient('r2')
10✔
2091
    dx = SurfaceCoefficient(0.)
10✔
2092
    dy = SurfaceCoefficient(1.)
10✔
2093
    dz = SurfaceCoefficient(0.)
10✔
2094

2095
    def _get_base_coeffs(self):
10✔
UNCOV
2096
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2097

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

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

2106
    def evaluate(self, point):
10✔
2107
        x = point[0] - self.x0
10✔
2108
        y = point[1] - self.y0
10✔
2109
        z = point[2] - self.z0
10✔
2110
        return x*x + z*z - self.r2*y*y
10✔
2111

2112

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

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

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

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

2172
    """
2173

2174
    _type = 'z-cone'
10✔
2175
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
10✔
2176

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

2186
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
10✔
2187
            setattr(self, key, val)
10✔
2188

2189
    x0 = SurfaceCoefficient('x0')
10✔
2190
    y0 = SurfaceCoefficient('y0')
10✔
2191
    z0 = SurfaceCoefficient('z0')
10✔
2192
    r2 = SurfaceCoefficient('r2')
10✔
2193
    dx = SurfaceCoefficient(0.)
10✔
2194
    dy = SurfaceCoefficient(0.)
10✔
2195
    dz = SurfaceCoefficient(1.)
10✔
2196

2197
    def _get_base_coeffs(self):
10✔
UNCOV
2198
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2199

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

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

2208
    def evaluate(self, point):
10✔
2209
        x = point[0] - self.x0
10✔
2210
        y = point[1] - self.y0
10✔
2211
        z = point[2] - self.z0
10✔
2212
        return x*x + y*y - self.r2*z*z
10✔
2213

2214

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

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

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

2255
    """
2256

2257
    _type = 'quadric'
10✔
2258
    _coeff_keys = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k')
10✔
2259

2260
    def __init__(self, a=0., b=0., c=0., d=0., e=0., f=0., g=0., h=0., j=0.,
10✔
2261
                 k=0., *args, **kwargs):
2262
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
10✔
2263
        super().__init__(**kwargs)
10✔
2264

2265
        for key, val in zip(self._coeff_keys, (a, b, c, d, e, f, g, h, j, k)):
10✔
2266
            setattr(self, key, val)
10✔
2267

2268
    a = SurfaceCoefficient('a')
10✔
2269
    b = SurfaceCoefficient('b')
10✔
2270
    c = SurfaceCoefficient('c')
10✔
2271
    d = SurfaceCoefficient('d')
10✔
2272
    e = SurfaceCoefficient('e')
10✔
2273
    f = SurfaceCoefficient('f')
10✔
2274
    g = SurfaceCoefficient('g')
10✔
2275
    h = SurfaceCoefficient('h')
10✔
2276
    j = SurfaceCoefficient('j')
10✔
2277
    k = SurfaceCoefficient('k')
10✔
2278

2279
    def _get_base_coeffs(self):
10✔
2280
        return tuple(getattr(self, c) for c in self._coeff_keys)
10✔
2281

2282

2283
class TorusMixin:
10✔
2284
    """A Mixin class implementing common functionality for torus surfaces"""
2285
    _coeff_keys = ('x0', 'y0', 'z0', 'a', 'b', 'c')
10✔
2286

2287
    def __init__(self, x0=0., y0=0., z0=0., a=0., b=0., c=0., **kwargs):
10✔
2288
        super().__init__(**kwargs)
10✔
2289
        for key, val in zip(self._coeff_keys, (x0, y0, z0, a, b, c)):
10✔
2290
            setattr(self, key, val)
10✔
2291

2292
    x0 = SurfaceCoefficient('x0')
10✔
2293
    y0 = SurfaceCoefficient('y0')
10✔
2294
    z0 = SurfaceCoefficient('z0')
10✔
2295
    a = SurfaceCoefficient('a')
10✔
2296
    b = SurfaceCoefficient('b')
10✔
2297
    c = SurfaceCoefficient('c')
10✔
2298

2299
    def translate(self, vector, inplace=False):
10✔
2300
        surf = self if inplace else self.clone()
10✔
2301
        surf.x0 += vector[0]
10✔
2302
        surf.y0 += vector[1]
10✔
2303
        surf.z0 += vector[2]
10✔
2304
        return surf
10✔
2305

2306
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
10✔
2307
        pivot = np.asarray(pivot)
10✔
2308
        rotation = np.asarray(rotation, dtype=float)
10✔
2309

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

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

2322
        # Translate surface to pivot
2323
        surf = self.translate(-pivot, inplace=inplace)
10✔
2324

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

2331
        # Compute new rotated torus center
2332
        center = Rmat @ center
10✔
2333

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

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

2350
        return surf.translate(pivot, inplace=inplace)
10✔
2351

2352
    def _get_base_coeffs(self):
10✔
UNCOV
2353
        raise NotImplementedError
×
2354

2355

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

2360
    .. versionadded:: 0.13.0
2361

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

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

2407
    """
2408
    _type = 'x-torus'
10✔
2409

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

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

2430

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

2435
    .. versionadded:: 0.13.0
2436

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

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

2482
    """
2483
    _type = 'y-torus'
10✔
2484

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

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

2505

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

2510
    .. versionadded:: 0.13.0
2511

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

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

2558
    _type = 'z-torus'
10✔
2559

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

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

2580

2581
class Halfspace(Region):
10✔
2582
    """A positive or negative half-space region.
2583

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

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

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

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

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

2616
    """
2617

2618
    def __init__(self, surface, side):
10✔
2619
        self.surface = surface
10✔
2620
        self.side = side
10✔
2621

2622
    def __and__(self, other):
10✔
2623
        if isinstance(other, Intersection):
10✔
2624
            return Intersection([self] + other[:])
10✔
2625
        else:
2626
            return Intersection((self, other))
10✔
2627

2628
    def __or__(self, other):
10✔
2629
        if isinstance(other, Union):
10✔
UNCOV
2630
            return Union([self] + other[:])
×
2631
        else:
2632
            return Union((self, other))
10✔
2633

2634
    def __invert__(self) -> Halfspace:
10✔
2635
        return -self.surface if self.side == '+' else +self.surface
10✔
2636

2637
    def __contains__(self, point):
10✔
2638
        """Check whether a point is contained in the half-space.
2639

2640
        Parameters
2641
        ----------
2642
        point : 3-tuple of float
2643
            Cartesian coordinates, :math:`(x',y',z')`, of the point
2644

2645
        Returns
2646
        -------
2647
        bool
2648
            Whether the point is in the half-space
2649

2650
        """
2651

2652
        val = self.surface.evaluate(point)
10✔
2653
        return val >= 0. if self.side == '+' else val < 0.
10✔
2654

2655
    @property
10✔
2656
    def surface(self):
10✔
2657
        return self._surface
10✔
2658

2659
    @surface.setter
10✔
2660
    def surface(self, surface):
10✔
2661
        check_type('surface', surface, Surface)
10✔
2662
        self._surface = surface
10✔
2663

2664
    @property
10✔
2665
    def side(self):
10✔
2666
        return self._side
10✔
2667

2668
    @side.setter
10✔
2669
    def side(self, side):
10✔
2670
        check_value('side', side, ('+', '-'))
10✔
2671
        self._side = side
10✔
2672

2673
    @property
10✔
2674
    def bounding_box(self):
10✔
2675
        return self.surface.bounding_box(self.side)
10✔
2676

2677
    def __str__(self):
10✔
2678
        return '-' + str(self.surface.id) if self.side == '-' \
10✔
2679
            else str(self.surface.id)
2680

2681
    def get_surfaces(self, surfaces=None):
10✔
2682
        """
2683
        Returns the surface that this is a halfspace of.
2684

2685
        Parameters
2686
        ----------
2687
        surfaces : dict, optional
2688
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
2689

2690
        Returns
2691
        -------
2692
        surfaces : dict
2693
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
2694

2695
        """
2696
        if surfaces is None:
10✔
2697
            surfaces = {}
10✔
2698

2699
        surfaces[self.surface.id] = self.surface
10✔
2700
        return surfaces
10✔
2701

2702
    def remove_redundant_surfaces(self, redundant_surfaces):
10✔
2703
        """Recursively remove all redundant surfaces referenced by this region
2704

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

2711
        """
2712

2713
        surf = redundant_surfaces.get(self.surface.id)
10✔
2714
        if surf is not None:
10✔
2715
            self.surface = surf
10✔
2716

2717
    def clone(self, memo=None):
10✔
2718
        """Create a copy of this halfspace, with a cloned surface with a
2719
        unique ID.
2720

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

2727
        Returns
2728
        -------
2729
        clone : openmc.Halfspace
2730
            The clone of this halfspace
2731

2732
        """
2733

2734
        if memo is None:
10✔
UNCOV
2735
            memo = dict
×
2736

2737
        clone = deepcopy(self)
10✔
2738
        clone.surface = self.surface.clone(memo)
10✔
2739
        return clone
10✔
2740

2741
    def translate(self, vector, inplace=False, memo=None):
10✔
2742
        """Translate half-space in given direction
2743

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

2751
        Returns
2752
        -------
2753
        openmc.Halfspace
2754
            Translated half-space
2755

2756
        """
2757
        if memo is None:
10✔
UNCOV
2758
            memo = {}
×
2759

2760
        # If translated surface not in memo, add it
2761
        key = (self.surface, tuple(vector))
10✔
2762
        if key not in memo:
10✔
2763
            memo[key] = self.surface.translate(vector, inplace)
10✔
2764

2765
        # Return translated half-space
2766
        return type(self)(memo[key], self.side)
10✔
2767

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

2772
        .. versionadded:: 0.12
2773

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

2801
        Returns
2802
        -------
2803
        openmc.Halfspace
2804
            Translated half-space
2805

2806
        """
2807
        if memo is None:
10✔
UNCOV
2808
            memo = {}
×
2809

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

2816
        # Return rotated half-space
2817
        return type(self)(memo[key], self.side)
10✔
2818

2819

2820
_SURFACE_CLASSES = {cls._type: cls for cls in Surface.__subclasses__()}
10✔
2821

2822

2823
# Set virtual base classes for "casting" up the hierarchy
2824
Plane._virtual_base = Plane
10✔
2825
XPlane._virtual_base = Plane
10✔
2826
YPlane._virtual_base = Plane
10✔
2827
ZPlane._virtual_base = Plane
10✔
2828
Cylinder._virtual_base = Cylinder
10✔
2829
XCylinder._virtual_base = Cylinder
10✔
2830
YCylinder._virtual_base = Cylinder
10✔
2831
ZCylinder._virtual_base = Cylinder
10✔
2832
Cone._virtual_base = Cone
10✔
2833
XCone._virtual_base = Cone
10✔
2834
YCone._virtual_base = Cone
10✔
2835
ZCone._virtual_base = Cone
10✔
2836
Sphere._virtual_base = Sphere
10✔
2837
Quadric._virtual_base = Quadric
10✔
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