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

openmc-dev / openmc / 21489819490

29 Jan 2026 06:21PM UTC coverage: 80.077% (-1.9%) from 81.953%
21489819490

Pull #3757

github

web-flow
Merge d08626053 into f7a734189
Pull Request #3757: Testing point detectors

16004 of 22621 branches covered (70.75%)

Branch coverage included in aggregate %.

94 of 518 new or added lines in 26 files covered. (18.15%)

1021 existing lines in 52 files now uncovered.

53779 of 64524 relevant lines covered (83.35%)

8016833.26 hits per line

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

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

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

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

18

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

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

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

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

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

61

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

70

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

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

110

111
class Surface(IDManagerMixin, ABC):
1✔
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
    next_id = 1
1✔
155
    used_ids = set()
1✔
156
    _atol = 1.e-12
1✔
157

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

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

170
    def __neg__(self):
1✔
171
        return Halfspace(self, '-')
1✔
172

173
    def __pos__(self):
1✔
174
        return Halfspace(self, '+')
1✔
175

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

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

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

193
        string += coefficients
1✔
194

195
        return string
1✔
196

197
    @property
1✔
198
    def name(self):
1✔
199
        return self._name
1✔
200

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

209
    @property
1✔
210
    def type(self):
1✔
211
        return self._type
1✔
212

213
    @property
1✔
214
    def boundary_type(self):
1✔
215
        return self._boundary_type
1✔
216

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

223
    @property
1✔
224
    def albedo(self):
1✔
225
        return self._albedo
1✔
226

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

233
    @property
1✔
234
    def coefficients(self):
1✔
235
        return self._coefficients
1✔
236

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

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

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

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

259
        """
260
        return BoundingBox.infinite()
1✔
261

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

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

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

276
        """
277

278
        if memo is None:
1✔
279
            memo = {}
1✔
280

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

286
            # Memoize the clone
287
            memo[self] = clone
1✔
288

289
        return memo[self]
1✔
290

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

294
        .. versionadded:: 0.12
295

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

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

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

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

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

324
        """
325
        coeffs1 = self.normalize(self._get_base_coeffs())
1✔
326
        coeffs2 = self.normalize(other._get_base_coeffs())
1✔
327

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

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

335
        """
336

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

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

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

352
        """
353

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

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

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

371
        """
372

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

377
        .. versionadded:: 0.12
378

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

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

409
        """
410

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

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

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

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

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

435
        return element
1✔
436

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

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

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

451
        """
452

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

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

467
        return cls(**kwargs)
1✔
468

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

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

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

483
        """
484

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

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

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

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

505
        return cls(*coeffs, **kwargs)
1✔
506

507

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

514
    @property
1✔
515
    def periodic_surface(self):
1✔
516
        return self._periodic_surface
1✔
517

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

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

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

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

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

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

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

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

575
        return BoundingBox(ll, ur)
1✔
576

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

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

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

591
        """
592

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

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

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

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

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

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

620
        surf = self if inplace else self.clone()
1✔
621

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

624
        return surf
1✔
625

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

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

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

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

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

650
        surf = Plane(a=a, b=b, c=c, d=d, **kwargs)
1✔
651

652
        return surf.translate(pivot, inplace=inplace)
1✔
653

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

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

662
        """
663
        element = super().to_xml_element()
1✔
664

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

672

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

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

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

727
    """
728

729
    _type = 'plane'
1✔
730
    _coeff_keys = ('a', 'b', 'c', 'd')
1✔
731

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

746
        super().__init__(**kwargs)
1✔
747

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

751
        for key, val in capdict.items():
1✔
752
            setattr(self, key, val)
×
753

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

760
    a = SurfaceCoefficient('a')
1✔
761
    b = SurfaceCoefficient('b')
1✔
762
    c = SurfaceCoefficient('c')
1✔
763
    d = SurfaceCoefficient('d')
1✔
764

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

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

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

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

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

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

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

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

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

813

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

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

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

856
    """
857

858
    _type = 'x-plane'
1✔
859
    _coeff_keys = ('x0',)
1✔
860

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

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

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

877

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

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

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

920
    """
921

922
    _type = 'y-plane'
1✔
923
    _coeff_keys = ('y0',)
1✔
924

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

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

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

941

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

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

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

984
    """
985

986
    _type = 'z-plane'
1✔
987
    _coeff_keys = ('z0',)
1✔
988

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

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

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

1005

1006
class QuadricMixin:
1✔
1007
    """A Mixin class implementing common functionality for quadric surfaces"""
1008

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

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

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

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

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

1036
        return A, bvec, k
1✔
1037

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

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

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

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

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

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

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

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

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

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

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

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

1100
        surf = self if inplace else self.clone()
1✔
1101

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

1111
        else:
1112
            A, bvec, cnst = self.get_Abc()
1✔
1113

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

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

1120
        return surf
1✔
1121

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

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

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

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

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

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

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

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

1172

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1315
        .. versionadded:: 0.12
1316

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

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

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

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

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

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

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

1359

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

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

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

1409
    """
1410

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

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

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

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

1434
    def _get_base_coeffs(self):
1✔
1435
        y0, z0, r = self.y0, self.z0, self.r
×
1436

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

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

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

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

1457

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

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

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

1507
    """
1508

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

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

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

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

1532
    def _get_base_coeffs(self):
1✔
1533
        x0, z0, r = self.x0, self.z0, self.r
×
1534

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

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

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

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

1555

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

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

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

1605
    """
1606

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

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

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

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

1630
    def _get_base_coeffs(self):
1✔
1631
        x0, y0, r = self.x0, self.y0, self.r
×
1632

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

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

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

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

1653

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

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

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

1705
    """
1706

1707
    _type = 'sphere'
1✔
1708
    _coeff_keys = ('x0', 'y0', 'z0', 'r')
1✔
1709

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

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

1722
    x0 = SurfaceCoefficient('x0')
1✔
1723
    y0 = SurfaceCoefficient('y0')
1✔
1724
    z0 = SurfaceCoefficient('z0')
1✔
1725
    r = SurfaceCoefficient('r')
1✔
1726

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

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

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

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

1751

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

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

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

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

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

1824
    """
1825

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

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

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

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

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

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

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

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

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

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

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

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

1907

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

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

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

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

1967
    """
1968

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

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

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

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

1992
    def _get_base_coeffs(self):
1✔
1993
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
1994

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

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

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

2009

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

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

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

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

2069
    """
2070

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

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

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

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

2094
    def _get_base_coeffs(self):
1✔
2095
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2096

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

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

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

2111

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

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

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

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

2171
    """
2172

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

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

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

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

2196
    def _get_base_coeffs(self):
1✔
2197
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2198

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

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

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

2213

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

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

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

2254
    """
2255

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

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

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

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

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

2281

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

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

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

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

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

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

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

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

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

2330
        # Compute new rotated torus center
2331
        center = Rmat @ center
1✔
2332

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

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

2349
        return surf.translate(pivot, inplace=inplace)
1✔
2350

2351
    def _get_base_coeffs(self):
1✔
2352
        raise NotImplementedError
×
2353

2354

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

2359
    .. versionadded:: 0.13.0
2360

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

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

2406
    """
2407
    _type = 'x-torus'
1✔
2408

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

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

2429

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

2434
    .. versionadded:: 0.13.0
2435

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

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

2481
    """
2482
    _type = 'y-torus'
1✔
2483

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

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

2504

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

2509
    .. versionadded:: 0.13.0
2510

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

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

2557
    _type = 'z-torus'
1✔
2558

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

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

2579

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

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

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

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

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

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

2615
    """
2616

2617
    def __init__(self, surface, side):
1✔
2618
        self.surface = surface
1✔
2619
        self.side = side
1✔
2620

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

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

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

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

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

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

2649
        """
2650

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

2654
    @property
1✔
2655
    def surface(self):
1✔
2656
        return self._surface
1✔
2657

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

2663
    @property
1✔
2664
    def side(self):
1✔
2665
        return self._side
1✔
2666

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

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

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

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

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

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

2694
        """
2695
        if surfaces is None:
1✔
2696
            surfaces = {}
1✔
2697

2698
        surfaces[self.surface.id] = self.surface
1✔
2699
        return surfaces
1✔
2700

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

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

2710
        """
2711

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

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

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

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

2731
        """
2732

2733
        if memo is None:
1✔
2734
            memo = dict
×
2735

2736
        clone = deepcopy(self)
1✔
2737
        clone.surface = self.surface.clone(memo)
1✔
2738
        return clone
1✔
2739

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

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

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

2755
        """
2756
        if memo is None:
1✔
2757
            memo = {}
×
2758

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

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

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

2771
        .. versionadded:: 0.12
2772

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

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

2805
        """
2806
        if memo is None:
1✔
2807
            memo = {}
×
2808

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

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

2818

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

2821

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