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

openmc-dev / openmc / 15542373011

09 Jun 2025 07:02PM UTC coverage: 85.098% (-0.1%) from 85.2%
15542373011

Pull #3402

github

web-flow
Merge f0ae3624c into 7fda3eb84
Pull Request #3402: Add transformation boundary condition

74 of 111 new or added lines in 4 files covered. (66.67%)

897 existing lines in 38 files now uncovered.

52390 of 61564 relevant lines covered (85.1%)

36484613.85 hits per line

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

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

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

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

17

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

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

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

31

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

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

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

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

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

60

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

72

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

76
    .. versionadded:: 0.12
77

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

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

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

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

112

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

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

120
    Parameters
121
    ----------
122
    surface_id : int, optional
123
        Unique identifier for the surface. If not specified, an identifier will
124
        automatically be assigned.
125
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}, optional
126
        Boundary condition that defines the behavior for particles hitting the
127
        surface. Defaults to transmissive boundary condition where particles
128
        freely pass through the surface. Note that periodic boundary conditions
129
        can only be applied to x-, y-, and z-planes, and only axis-aligned
130
        periodicity is supported. In addition, to enable greatest flexibility
131
        for transformation boundary conditions, it is up to the user to ensure
132
        valid transformations for transport.
133
    albedo : float, optional
134
        Albedo of the surfaces as a ratio of particle weight after interaction
135
        with the surface to the initial weight. Values must be positive. Only
136
        applicable if the boundary type is 'reflective', 'periodic', 'white',
137
        or 'transformation'.
138
    name : str, optional
139
        Name of the surface. If not specified, the name will be the empty
140
        string.
141
    transformation : dict, optional
142
        If a transformation boundary condition is used, the dictionary
143
        describing the affine transfomations by which particle position and
144
        direction are modified. Valid keys are:
145
            'direction' : iterable of float, optional
146
                The affine transformation matrix by which particle directions
147
                are transformed. Should be a 3x4 matrix given in row-major
148
                order (i.e., a flat array with a length of 12). In matrix
149
                representation, the first three columns correspond to a linear
150
                map affecting direction via matrix multiplication and the
151
                fourth column corresponds to a translation affecting direction
152
                via vector addition. Defaults to the 3x3 identity matrix with
153
                an additional zero vector column.
154
            'position' : iterable of float, optional
155
                The affine transformation matrix by which particle positions
156
                are transformed. Should be a 3x4 matrix given in row-major
157
                order (i.e., a flat array with a length of 12). In matrix
158
                representation, the first three columns correspond to a linear
159
                map affecting position via matrix multiplication and the fourth
160
                column corresponds to a translation affecting position via
161
                vector addition. Defaults to the 3x3 identity matrix with an
162
                additional zero vector column.
163
    
164
    Attributes
165
    ----------
166
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}
167
        Boundary condition that defines the behavior for particles hitting the
168
        surface.
169
    albedo : float
170
        Boundary albedo as a positive multiplier of particle weight
171
    coefficients : dict
172
        Dictionary of surface coefficients
173
    id : int
174
        Unique identifier for the surface
175
    name : str
176
        Name of the surface
177
    type : str
178
        Type of the surface
179
    transformation : dict
180
        Dictionary describing affine transformation of particle positions and
181
        directions
182
    """
183

184
    next_id = 1
11✔
185
    used_ids = set()
11✔
186
    _atol = 1.e-12
11✔
187

188
    def __init__(self, surface_id=None, boundary_type='transmission',
11✔
189
                 albedo=1., name='', transformation={}):
190
        self.id = surface_id
11✔
191
        self.name = name
11✔
192
        self.boundary_type = boundary_type
11✔
193
        self.albedo = albedo
11✔
194
        self.transformation = transformation
11✔
195

196
        # A dictionary of the quadratic surface coefficients
197
        # Key      - coefficient name
198
        # Value    - coefficient value
199
        self._coefficients = {}
11✔
200

201
    def __neg__(self):
11✔
202
        return Halfspace(self, '-')
11✔
203

204
    def __pos__(self):
11✔
205
        return Halfspace(self, '+')
11✔
206

207
    def __repr__(self):
11✔
208
        string = 'Surface\n'
11✔
209
        string += '{0: <20}{1}{2}\n'.format('\tID', '=\t', self._id)
11✔
210
        string += '{0: <20}{1}{2}\n'.format('\tName', '=\t', self._name)
11✔
211
        string += '{0: <20}{1}{2}\n'.format('\tType', '=\t', self._type)
11✔
212
        string += '{0: <20}{1}{2}\n'.format('\tBoundary', '=\t',
11✔
213
                                            self._boundary_type)
214
        if (self._boundary_type in _ALBEDO_BOUNDARIES and
11✔
215
            not math.isclose(self._albedo, 1.0)):
216
            string += '{0: <20}{1}{2}\n'.format('\tBoundary Albedo', '=\t',
×
217
                                                self._albedo)
218

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

221
        for coeff in self._coefficients:
11✔
222
            coefficients += f'{coeff: <20}=\t{self._coefficients[coeff]}\n'
11✔
223

224
        string += coefficients
11✔
225

226
        return string
11✔
227

228
    @property
11✔
229
    def name(self):
11✔
230
        return self._name
11✔
231

232
    @name.setter
11✔
233
    def name(self, name):
11✔
234
        if name is not None:
11✔
235
            check_type('surface name', name, str)
11✔
236
            self._name = name
11✔
237
        else:
238
            self._name = ''
11✔
239

240
    @property
11✔
241
    def type(self):
11✔
242
        return self._type
11✔
243

244
    @property
11✔
245
    def boundary_type(self):
11✔
246
        return self._boundary_type
11✔
247

248
    @boundary_type.setter
11✔
249
    def boundary_type(self, boundary_type):
11✔
250
        check_type('boundary type', boundary_type, str)
11✔
251
        check_value('boundary type', boundary_type, _BOUNDARY_TYPES)
11✔
252
        self._boundary_type = boundary_type
11✔
253

254
    @property
11✔
255
    def albedo(self):
11✔
256
        return self._albedo
11✔
257

258
    @albedo.setter
11✔
259
    def albedo(self, albedo):
11✔
260
        check_type('albedo', albedo, Real)
11✔
261
        check_greater_than('albedo', albedo, 0.0)
11✔
262
        self._albedo = float(albedo)
11✔
263
    
264
    @property
11✔
265
    def transformation(self):
11✔
266
        return self._transformation
11✔
267

268
    @transformation.setter
11✔
269
    def transformation(self, transformation):
11✔
270
        if not isinstance(transformation, Mapping):
11✔
NEW
271
            msg = (
×
272
                f'Unable to set transformation from "{transformation}", which '
273
                'is not a Python dictionary')
NEW
274
            raise ValueError(msg)
×
275
        
276
        if transformation:
11✔
277
            if self.boundary_type != "transformation":
11✔
NEW
278
                warn(
×
279
                    f"boundary_type of surface {self.id} is set to "
280
                    f"{self.boundary_type}; transformation dictionary will "
281
                    "have no effect."
282
                )
283
            else:
284
                valid_keys = {"direction", "position"}
11✔
285
                
286
                for key in set(transformation) - valid_keys:
11✔
NEW
287
                    msg = (
×
288
                        f'Unable to set transformation parameter "{key}", '
289
                        'which is unsupported by OpenMC'
290
                    )
NEW
291
                    raise ValueError(msg)
×
292
                
293
                for key in valid_keys:
11✔
294
                    if key in transformation:
11✔
295
                        check_type(
11✔
296
                            'transformation',
297
                            transformation[key],
298
                            Iterable,
299
                            Real)
300
                        check_length(
11✔
301
                            'transformation', transformation[key], 12)
302
                    else:
303
                        transformation[key] = np.append(
11✔
304
                            np.identity(3), np.zeros((3,1)), axis=1).flatten()
305
        
306
        self._transformation = transformation
11✔
307

308
    @property
11✔
309
    def coefficients(self):
11✔
310
        return self._coefficients
11✔
311

312
    def bounding_box(self, side):
11✔
313
        """Determine an axis-aligned bounding box.
314

315
        An axis-aligned bounding box for surface half-spaces is represented by
316
        its lower-left and upper-right coordinates. If the half-space is
317
        unbounded in a particular direction, numpy.inf is used to represent
318
        infinity.
319

320
        Parameters
321
        ----------
322
        side : {'+', '-'}
323
            Indicates the negative or positive half-space
324

325
        Returns
326
        -------
327
        numpy.ndarray
328
            Lower-left coordinates of the axis-aligned bounding box for the
329
            desired half-space
330
        numpy.ndarray
331
            Upper-right coordinates of the axis-aligned bounding box for the
332
            desired half-space
333

334
        """
335
        return BoundingBox.infinite()
11✔
336

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

340
        Parameters
341
        ----------
342
        memo : dict or None
343
            A nested dictionary of previously cloned objects. This parameter
344
            is used internally and should not be specified by the user.
345

346
        Returns
347
        -------
348
        clone : openmc.Surface
349
            The clone of this surface
350

351
        """
352

353
        if memo is None:
11✔
354
            memo = {}
11✔
355

356
        # If no memoize'd clone exists, instantiate one
357
        if self not in memo:
11✔
358
            clone = deepcopy(self)
11✔
359
            clone.id = None
11✔
360

361
            # Memoize the clone
362
            memo[self] = clone
11✔
363

364
        return memo[self]
11✔
365

366
    def normalize(self, coeffs=None):
11✔
367
        """Normalize coefficients by first nonzero value
368

369
        .. versionadded:: 0.12
370

371
        Parameters
372
        ----------
373
        coeffs : tuple, optional
374
            Tuple of surface coefficients to normalize. Defaults to None. If no
375
            coefficients are supplied then the coefficients will be taken from
376
            the current Surface.
377

378
        Returns
379
        -------
380
        tuple of normalized coefficients
381

382
        """
383
        if coeffs is None:
11✔
384
            coeffs = self._get_base_coeffs()
11✔
385
        coeffs = np.asarray(coeffs)
11✔
386
        nonzeros = ~np.isclose(coeffs, 0., rtol=0., atol=self._atol)
11✔
387
        norm_factor = coeffs[nonzeros][0]
11✔
388
        return tuple([c/norm_factor for c in coeffs])
11✔
389

390
    def is_equal(self, other):
11✔
391
        """Determine if this Surface is equivalent to another
392

393
        Parameters
394
        ----------
395
        other : instance of openmc.Surface
396
            Instance of openmc.Surface that should be compared to the current
397
            surface
398

399
        """
400
        coeffs1 = self.normalize(self._get_base_coeffs())
11✔
401
        coeffs2 = self.normalize(other._get_base_coeffs())
11✔
402

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

405
    @abstractmethod
11✔
406
    def _get_base_coeffs(self):
11✔
407
        """Return polynomial coefficients representing the implicit surface
408
        equation.
409

410
        """
411

412
    @abstractmethod
11✔
413
    def evaluate(self, point):
11✔
414
        """Evaluate the surface equation at a given point.
415

416
        Parameters
417
        ----------
418
        point : 3-tuple of float
419
            The Cartesian coordinates, :math:`(x',y',z')`, at which the surface
420
            equation should be evaluated.
421

422
        Returns
423
        -------
424
        float
425
            Evaluation of the surface polynomial at point :math:`(x',y',z')`
426

427
        """
428

429
    @abstractmethod
11✔
430
    def translate(self, vector, inplace=False):
11✔
431
        """Translate surface in given direction
432

433
        Parameters
434
        ----------
435
        vector : iterable of float
436
            Direction in which surface should be translated
437
        inplace : bool
438
            Whether or not to return a new instance of this Surface or to
439
            modify the coefficients of this Surface.
440

441
        Returns
442
        -------
443
        instance of openmc.Surface
444
            Translated surface
445

446
        """
447

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

452
        .. versionadded:: 0.12
453

454
        Parameters
455
        ----------
456
        rotation : 3-tuple of float, or 3x3 iterable
457
            A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where
458
            the first element is the rotation about the x-axis in the fixed
459
            laboratory frame, the second element is the rotation about the
460
            y-axis in the fixed laboratory frame, and the third element is the
461
            rotation about the z-axis in the fixed laboratory frame. The
462
            rotations are active rotations. Additionally a 3x3 rotation matrix
463
            can be specified directly either as a nested iterable or array.
464
        pivot : iterable of float, optional
465
            (x, y, z) coordinates for the point to rotate about. Defaults to
466
            (0., 0., 0.)
467
        order : str, optional
468
            A string of 'x', 'y', and 'z' in some order specifying which
469
            rotation to perform first, second, and third. Defaults to 'xyz'
470
            which means, the rotation by angle :math:`\phi` about x will be
471
            applied first, followed by :math:`\theta` about y and then
472
            :math:`\psi` about z. This corresponds to an x-y-z extrinsic
473
            rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan
474
            angles :math:`(\phi, \theta, \psi)`.
475
        inplace : bool
476
            Whether or not to return a new instance of Surface or to modify the
477
            coefficients of this Surface in place. Defaults to False.
478

479
        Returns
480
        -------
481
        openmc.Surface
482
            Rotated surface
483

484
        """
485

486
    def to_xml_element(self):
11✔
487
        """Return XML representation of the surface
488

489
        Returns
490
        -------
491
        element : lxml.etree._Element
492
            XML element containing source data
493

494
        """
495
        element = ET.Element("surface")
11✔
496
        element.set("id", str(self._id))
11✔
497

498
        if len(self._name) > 0:
11✔
499
            element.set("name", str(self._name))
11✔
500

501
        element.set("type", self._type)
11✔
502
        if self.boundary_type != 'transmission':
11✔
503
            element.set("boundary", self.boundary_type)
11✔
504
            if (self.boundary_type in _ALBEDO_BOUNDARIES and
11✔
505
                not math.isclose(self.albedo, 1.0)):
506
                element.set("albedo", str(self.albedo))
×
507
        element.set("coeffs", ' '.join([str(self._coefficients.setdefault(key, 0.0))
11✔
508
                                        for key in self._coeff_keys]))
509
        
510
        if self.boundary_type == 'transformation':
11✔
511
            element.set(
11✔
512
                "direction_transformation",
513
                ' '.join([str(elem)
514
                          for elem in self.transformation["direction"]]))
515
            element.set(
11✔
516
                "position_transformation",
517
                ' '.join([str(elem)
518
                          for elem in self.transformation["position"]]))
519

520
        return element
11✔
521

522
    @staticmethod
11✔
523
    def from_xml_element(elem):
11✔
524
        """Generate surface from an XML element
525

526
        Parameters
527
        ----------
528
        elem : lxml.etree._Element
529
            XML element
530

531
        Returns
532
        -------
533
        openmc.Surface
534
            Instance of a surface subclass
535

536
        """
537

538
        # Determine appropriate class
539
        surf_type = elem.get('type')
11✔
540
        cls = _SURFACE_CLASSES[surf_type]
11✔
541

542
        # Determine ID, boundary type, boundary albedo, coefficients
543
        kwargs = {}
11✔
544
        kwargs['surface_id'] = int(elem.get('id'))
11✔
545
        kwargs['boundary_type'] = elem.get('boundary', 'transmission')
11✔
546
        if kwargs['boundary_type'] in _ALBEDO_BOUNDARIES:
11✔
547
            kwargs['albedo'] = float(elem.get('albedo', 1.0))
11✔
548
        kwargs['name'] = elem.get('name')
11✔
549
        coeffs = [float(x) for x in elem.get('coeffs').split()]
11✔
550
        kwargs.update(dict(zip(cls._coeff_keys, coeffs)))
11✔
551
        if kwargs['boundary_type'] == 'transformation':
11✔
NEW
552
            transformation = {}
×
NEW
553
            if elem.get('direction_transformation'):
×
NEW
554
                transformation['direction'] = [
×
555
                    float(x) for x in elem.get('direction_transformation').split()
556
                ]
NEW
557
            if elem.get('position_transformation'):
×
NEW
558
                transformation['position'] = [
×
559
                    float(x) for x in elem.get('position_transformation').split()
560
                ]
NEW
561
            kwargs['transformation'] = transformation
×
562

563
        return cls(**kwargs)
11✔
564

565
    @staticmethod
11✔
566
    def from_hdf5(group):
11✔
567
        """Create surface from HDF5 group
568

569
        Parameters
570
        ----------
571
        group : h5py.Group
572
            Group in HDF5 file
573

574
        Returns
575
        -------
576
        openmc.Surface
577
            Instance of surface subclass
578

579
        """
580

581
        # If this is a DAGMC surface, do nothing for now
582
        geom_type = group.get('geom_type')
11✔
583
        if geom_type and geom_type[()].decode() == 'dagmc':
11✔
584
            return
1✔
585

586
        surface_id = int(group.name.split('/')[-1].lstrip('surface '))
11✔
587
        name = group['name'][()].decode() if 'name' in group else ''
11✔
588

589
        bc = group['boundary_type'][()].decode()
11✔
590
        if 'albedo' in group:
11✔
591
            bc_alb = float(group['albedo'][()].decode())
11✔
592
        else:
593
            bc_alb = 1.0
11✔
594
        coeffs = group['coefficients'][...]
11✔
595
        kwargs = {'boundary_type': bc, 'albedo': bc_alb, 'name': name,
11✔
596
                  'surface_id': surface_id}
597
        
598
        transformation = {}
11✔
599
        
600
        if 'direction_transformation' in group:
11✔
NEW
601
            string_array = group['direction_transformation'][()].decode()
×
NEW
602
            direction_transformation = [float(x) for x in string_array]
×
NEW
603
            transformation['direction'] = direction_transformation
×
604
        if 'position_transformation' in group:
11✔
NEW
605
            string_array = group['position_transformation'][()].decode()
×
NEW
606
            position_transformation = [float(x) for x in string_array]
×
NEW
607
            transformation['position'] = position_transformation
×
608
        
609
        kwargs['transformation'] = transformation
11✔
610

611
        surf_type = group['type'][()].decode()
11✔
612
        cls = _SURFACE_CLASSES[surf_type]
11✔
613

614
        return cls(*coeffs, **kwargs)
11✔
615

616

617
class PlaneMixin:
11✔
618
    """A Plane mixin class for all operations on order 1 surfaces"""
619
    def __init__(self, **kwargs):
11✔
620
        super().__init__(**kwargs)
11✔
621
        self._periodic_surface = None
11✔
622

623
    @property
11✔
624
    def periodic_surface(self):
11✔
625
        return self._periodic_surface
11✔
626

627
    @periodic_surface.setter
11✔
628
    def periodic_surface(self, periodic_surface):
11✔
629
        check_type('periodic surface', periodic_surface, Plane)
11✔
630
        self._periodic_surface = periodic_surface
11✔
631
        periodic_surface._periodic_surface = self
11✔
632

633
    def _get_base_coeffs(self):
11✔
634
        return (self.a, self.b, self.c, self.d)
11✔
635

636
    def _get_normal(self):
11✔
637
        a, b, c = self._get_base_coeffs()[:3]
11✔
638
        return np.array((a, b, c)) / math.sqrt(a*a + b*b + c*c)
11✔
639

640
    def bounding_box(self, side):
11✔
641
        """Determine an axis-aligned bounding box.
642

643
        An axis-aligned bounding box for Plane half-spaces is represented by
644
        its lower-left and upper-right coordinates. If the half-space is
645
        unbounded in a particular direction, numpy.inf is used to represent
646
        infinity.
647

648
        Parameters
649
        ----------
650
        side : {'+', '-'}
651
            Indicates the negative or positive half-space
652

653
        Returns
654
        -------
655
        numpy.ndarray
656
            Lower-left coordinates of the axis-aligned bounding box for the
657
            desired half-space
658
        numpy.ndarray
659
            Upper-right coordinates of the axis-aligned bounding box for the
660
            desired half-space
661

662
        """
663
        # Compute the bounding box based on the normal vector to the plane
664
        nhat = self._get_normal()
11✔
665
        ll = np.array([-np.inf, -np.inf, -np.inf])
11✔
666
        ur = np.array([np.inf, np.inf, np.inf])
11✔
667
        # If the plane is axis aligned, find the proper bounding box
668
        if np.any(np.isclose(np.abs(nhat), 1., rtol=0., atol=self._atol)):
11✔
669
            sign = nhat.sum()
11✔
670
            a, b, c, d = self._get_base_coeffs()
11✔
671
            vals = [d/val if not np.isclose(val, 0., rtol=0., atol=self._atol)
11✔
672
                    else np.nan for val in (a, b, c)]
673
            if side == '-':
11✔
674
                if sign > 0:
11✔
675
                    ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
11✔
676
                else:
677
                    ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
11✔
678
            elif side == '+':
11✔
679
                if sign > 0:
11✔
680
                    ll = np.array([v if not np.isnan(v) else -np.inf for v in vals])
11✔
681
                else:
682
                    ur = np.array([v if not np.isnan(v) else np.inf for v in vals])
11✔
683

684
        return BoundingBox(ll, ur)
11✔
685

686
    def evaluate(self, point):
11✔
687
        """Evaluate the surface equation at a given point.
688

689
        Parameters
690
        ----------
691
        point : 3-tuple of float
692
            The Cartesian coordinates, :math:`(x',y',z')`, at which the surface
693
            equation should be evaluated.
694

695
        Returns
696
        -------
697
        float
698
            :math:`Ax' + By' + Cz' - D`
699

700
        """
701

702
        x, y, z = point
11✔
703
        a, b, c, d = self._get_base_coeffs()
11✔
704
        return a*x + b*y + c*z - d
11✔
705

706
    def translate(self, vector, inplace=False):
11✔
707
        """Translate surface in given direction
708

709
        Parameters
710
        ----------
711
        vector : iterable of float
712
            Direction in which surface should be translated
713
        inplace : bool
714
            Whether or not to return a new instance of a Plane or to modify the
715
            coefficients of this plane.
716

717
        Returns
718
        -------
719
        openmc.Plane
720
            Translated surface
721

722
        """
723
        if np.allclose(vector, 0., rtol=0., atol=self._atol):
11✔
724
            return self
11✔
725

726
        a, b, c, d = self._get_base_coeffs()
11✔
727
        d = d + np.dot([a, b, c], vector)
11✔
728

729
        surf = self if inplace else self.clone()
11✔
730

731
        setattr(surf, surf._coeff_keys[-1], d)
11✔
732

733
        return surf
11✔
734

735
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
11✔
736
        pivot = np.asarray(pivot)
11✔
737
        rotation = np.asarray(rotation, dtype=float)
11✔
738

739
        # Allow rotation matrix to be passed in directly, otherwise build it
740
        if rotation.ndim == 2:
11✔
741
            check_length('surface rotation', rotation.ravel(), 9)
11✔
742
            Rmat = rotation
11✔
743
        else:
744
            Rmat = get_rotation_matrix(rotation, order=order)
11✔
745

746
        # Translate surface to pivot
747
        surf = self.translate(-pivot, inplace=inplace)
11✔
748

749
        a, b, c, d = surf._get_base_coeffs()
11✔
750
        # Compute new rotated coefficients a, b, c
751
        a, b, c = Rmat @ [a, b, c]
11✔
752

753
        kwargs = {'boundary_type': surf.boundary_type,
11✔
754
                  'albedo': surf.albedo,
755
                  'name': surf.name,
756
                  'transformation': surf.transformation}
757
        if inplace:
11✔
758
            kwargs['surface_id'] = surf.id
×
759

760
        surf = Plane(a=a, b=b, c=c, d=d, **kwargs)
11✔
761

762
        return surf.translate(pivot, inplace=inplace)
11✔
763

764
    def to_xml_element(self):
11✔
765
        """Return XML representation of the surface
766

767
        Returns
768
        -------
769
        element : lxml.etree._Element
770
            XML element containing source data
771

772
        """
773
        element = super().to_xml_element()
11✔
774

775
        # Add periodic surface pair information
776
        if self.boundary_type == 'periodic':
11✔
777
            if self.periodic_surface is not None:
11✔
778
                element.set("periodic_surface_id",
11✔
779
                            str(self.periodic_surface.id))
780
        return element
11✔
781

782

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

786
    Parameters
787
    ----------
788
    a : float, optional
789
        The 'A' parameter for the plane. Defaults to 1.
790
    b : float, optional
791
        The 'B' parameter for the plane. Defaults to 0.
792
    c : float, optional
793
        The 'C' parameter for the plane. Defaults to 0.
794
    d : float, optional
795
        The 'D' parameter for the plane. Defaults to 0.
796
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}, optional
797
        Boundary condition that defines the behavior for particles hitting the
798
        surface. Defaults to transmissive boundary condition where particles
799
        freely pass through the surface.
800
    albedo : float, optional
801
        Albedo of the surfaces as a ratio of particle weight after interaction
802
        with the surface to the initial weight. Values must be positive. Only
803
        applicable if the boundary type is 'reflective', 'periodic', 'white',
804
        or 'transformation'.
805
    name : str, optional
806
        Name of the plane. If not specified, the name will be the empty string.
807
    surface_id : int, optional
808
        Unique identifier for the surface. If not specified, an identifier will
809
        automatically be assigned.
810

811
    Attributes
812
    ----------
813
    a : float
814
        The 'A' parameter for the plane
815
    b : float
816
        The 'B' parameter for the plane
817
    c : float
818
        The 'C' parameter for the plane
819
    d : float
820
        The 'D' parameter for the plane
821
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}
822
        Boundary condition that defines the behavior for particles hitting the
823
        surface.
824
    albedo : float
825
        Boundary albedo as a positive multiplier of particle weight
826
    periodic_surface : openmc.Surface
827
        If a periodic boundary condition is used, the surface with which this
828
        one is periodic with
829
    coefficients : dict
830
        Dictionary of surface coefficients
831
    id : int
832
        Unique identifier for the surface
833
    name : str
834
        Name of the surface
835
    type : str
836
        Type of the surface
837

838
    """
839

840
    _type = 'plane'
11✔
841
    _coeff_keys = ('a', 'b', 'c', 'd')
11✔
842

843
    def __init__(self, a=1., b=0., c=0., d=0., *args, **kwargs):
11✔
844
        # *args should ultimately be limited to a, b, c, d as specified in
845
        # __init__, but to preserve the API it is allowed to accept Surface
846
        # parameters for now, but will raise warnings if this is done.
847
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
848
        # Warn if capital letter arguments are passed
849
        capdict = {}
11✔
850
        for k in 'ABCD':
11✔
851
            val = kwargs.pop(k, None)
11✔
852
            if val is not None:
11✔
853
                warn(_WARNING_UPPER.format(type(self), k.lower(), k),
×
854
                     FutureWarning)
855
                capdict[k.lower()] = val
×
856

857
        super().__init__(**kwargs)
11✔
858

859
        for key, val in zip(self._coeff_keys, (a, b, c, d)):
11✔
860
            setattr(self, key, val)
11✔
861

862
        for key, val in capdict.items():
11✔
863
            setattr(self, key, val)
×
864

865
    @classmethod
11✔
866
    def __subclasshook__(cls, c):
11✔
867
        if cls is Plane and c in (XPlane, YPlane, ZPlane):
11✔
868
            return True
11✔
869
        return NotImplemented
11✔
870

871
    a = SurfaceCoefficient('a')
11✔
872
    b = SurfaceCoefficient('b')
11✔
873
    c = SurfaceCoefficient('c')
11✔
874
    d = SurfaceCoefficient('d')
11✔
875

876
    @classmethod
11✔
877
    def from_points(cls, p1, p2, p3, **kwargs):
11✔
878
        """Return a plane given three points that pass through it.
879

880
        Parameters
881
        ----------
882
        p1, p2, p3 : 3-tuples
883
            Points that pass through the plane
884
        kwargs : dict
885
            Keyword arguments passed to the :class:`Plane` constructor
886

887
        Returns
888
        -------
889
        Plane
890
            Plane that passes through the three points
891

892
        Raises
893
        ------
894
        ValueError
895
            If all three points lie along a line
896

897
        """
898
        # Convert to numpy arrays
899
        p1 = np.asarray(p1, dtype=float)
11✔
900
        p2 = np.asarray(p2, dtype=float)
11✔
901
        p3 = np.asarray(p3, dtype=float)
11✔
902

903
        # Find normal vector to plane by taking cross product of two vectors
904
        # connecting p1->p2 and p1->p3
905
        n = np.cross(p2 - p1, p3 - p1)
11✔
906

907
        # Check for points along a line
908
        if np.allclose(n, 0.):
11✔
909
            raise ValueError("All three points appear to lie along a line.")
×
910

911
        # The equation of the plane will by n·(<x,y,z> - p1) = 0. Determine
912
        # coefficients a, b, c, and d based on that
913
        a, b, c = n
11✔
914
        d = np.dot(n, p1)
11✔
915
        return cls(a=a, b=b, c=c, d=d, **kwargs)
11✔
916

917
    def flip_normal(self):
11✔
918
        """Modify plane coefficients to reverse the normal vector."""
919
        self.a = -self.a
11✔
920
        self.b = -self.b
11✔
921
        self.c = -self.c
11✔
922
        self.d = -self.d
11✔
923

924

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

928
    Parameters
929
    ----------
930
    x0 : float, optional
931
        Location of the plane in [cm]. Defaults to 0.
932
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}, optional
933
        Boundary condition that defines the behavior for particles hitting the
934
        surface. Defaults to transmissive boundary condition where particles
935
        freely pass through the surface. Only axis-aligned periodicity is
936
        supported, i.e., x-planes can only be paired with x-planes.
937
    albedo : float, optional
938
        Albedo of the surfaces as a ratio of particle weight after interaction
939
        with the surface to the initial weight. Values must be positive. Only
940
        applicable if the boundary type is 'reflective', 'periodic', 'white',
941
        or 'transformation'.
942
    name : str, optional
943
        Name of the plane. If not specified, the name will be the empty string.
944
    surface_id : int, optional
945
        Unique identifier for the surface. If not specified, an identifier will
946
        automatically be assigned.
947

948
    Attributes
949
    ----------
950
    x0 : float
951
        Location of the plane in [cm]
952
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}
953
        Boundary condition that defines the behavior for particles hitting the
954
        surface.
955
    albedo : float
956
        Boundary albedo as a positive multiplier of particle weight
957
    periodic_surface : openmc.Surface
958
        If a periodic boundary condition is used, the surface with which this
959
        one is periodic with
960
    coefficients : dict
961
        Dictionary of surface coefficients
962
    id : int
963
        Unique identifier for the surface
964
    name : str
965
        Name of the surface
966
    type : str
967
        Type of the surface
968

969
    """
970

971
    _type = 'x-plane'
11✔
972
    _coeff_keys = ('x0',)
11✔
973

974
    def __init__(self, x0=0., *args, **kwargs):
11✔
975
        # work around for accepting Surface kwargs as positional parameters
976
        # until they are deprecated
977
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
978
        super().__init__(**kwargs)
11✔
979
        self.x0 = x0
11✔
980

981
    x0 = SurfaceCoefficient('x0')
11✔
982
    a = SurfaceCoefficient(1.)
11✔
983
    b = SurfaceCoefficient(0.)
11✔
984
    c = SurfaceCoefficient(0.)
11✔
985
    d = x0
11✔
986

987
    def evaluate(self, point):
11✔
988
        return point[0] - self.x0
11✔
989

990

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

994
    Parameters
995
    ----------
996
    y0 : float, optional
997
        Location of the plane in [cm]
998
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}, optional
999
        Boundary condition that defines the behavior for particles hitting the
1000
        surface. Defaults to transmissive boundary condition where particles
1001
        freely pass through the surface. Only axis-aligned periodicity is
1002
        supported, i.e., y-planes can only be paired with y-planes.
1003
    albedo : float, optional
1004
        Albedo of the surfaces as a ratio of particle weight after interaction
1005
        with the surface to the initial weight. Values must be positive. Only
1006
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1007
        or 'transformation'.
1008
    name : str, optional
1009
        Name of the plane. If not specified, the name will be the empty string.
1010
    surface_id : int, optional
1011
        Unique identifier for the surface. If not specified, an identifier will
1012
        automatically be assigned.
1013

1014
    Attributes
1015
    ----------
1016
    y0 : float
1017
        Location of the plane in [cm]
1018
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}
1019
        Boundary condition that defines the behavior for particles hitting the
1020
        surface.
1021
    albedo : float
1022
        Boundary albedo as a positive multiplier of particle weight
1023
    periodic_surface : openmc.Surface
1024
        If a periodic boundary condition is used, the surface with which this
1025
        one is periodic with
1026
    coefficients : dict
1027
        Dictionary of surface coefficients
1028
    id : int
1029
        Unique identifier for the surface
1030
    name : str
1031
        Name of the surface
1032
    type : str
1033
        Type of the surface
1034

1035
    """
1036

1037
    _type = 'y-plane'
11✔
1038
    _coeff_keys = ('y0',)
11✔
1039

1040
    def __init__(self, y0=0., *args, **kwargs):
11✔
1041
        # work around for accepting Surface kwargs as positional parameters
1042
        # until they are deprecated
1043
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1044
        super().__init__(**kwargs)
11✔
1045
        self.y0 = y0
11✔
1046

1047
    y0 = SurfaceCoefficient('y0')
11✔
1048
    a = SurfaceCoefficient(0.)
11✔
1049
    b = SurfaceCoefficient(1.)
11✔
1050
    c = SurfaceCoefficient(0.)
11✔
1051
    d = y0
11✔
1052

1053
    def evaluate(self, point):
11✔
1054
        return point[1] - self.y0
11✔
1055

1056

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

1060
    Parameters
1061
    ----------
1062
    z0 : float, optional
1063
        Location of the plane in [cm]. Defaults to 0.
1064
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}, optional
1065
        Boundary condition that defines the behavior for particles hitting the
1066
        surface. Defaults to transmissive boundary condition where particles
1067
        freely pass through the surface. Only axis-aligned periodicity is
1068
        supported, i.e., z-planes can only be paired with z-planes.
1069
    albedo : float, optional
1070
        Albedo of the surfaces as a ratio of particle weight after interaction
1071
        with the surface to the initial weight. Values must be positive. Only
1072
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1073
        or 'transformation'.
1074
    name : str, optional
1075
        Name of the plane. If not specified, the name will be the empty string.
1076
    surface_id : int, optional
1077
        Unique identifier for the surface. If not specified, an identifier will
1078
        automatically be assigned.
1079

1080
    Attributes
1081
    ----------
1082
    z0 : float
1083
        Location of the plane in [cm]
1084
    boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white', 'transformation'}
1085
        Boundary condition that defines the behavior for particles hitting the
1086
        surface.
1087
    albedo : float
1088
        Boundary albedo as a positive multiplier of particle weight
1089
    periodic_surface : openmc.Surface
1090
        If a periodic boundary condition is used, the surface with which this
1091
        one is periodic with
1092
    coefficients : dict
1093
        Dictionary of surface coefficients
1094
    id : int
1095
        Unique identifier for the surface
1096
    name : str
1097
        Name of the surface
1098
    type : str
1099
        Type of the surface
1100

1101
    """
1102

1103
    _type = 'z-plane'
11✔
1104
    _coeff_keys = ('z0',)
11✔
1105

1106
    def __init__(self, z0=0., *args, **kwargs):
11✔
1107
        # work around for accepting Surface kwargs as positional parameters
1108
        # until they are deprecated
1109
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1110
        super().__init__(**kwargs)
11✔
1111
        self.z0 = z0
11✔
1112

1113
    z0 = SurfaceCoefficient('z0')
11✔
1114
    a = SurfaceCoefficient(0.)
11✔
1115
    b = SurfaceCoefficient(0.)
11✔
1116
    c = SurfaceCoefficient(1.)
11✔
1117
    d = z0
11✔
1118

1119
    def evaluate(self, point):
11✔
1120
        return point[2] - self.z0
11✔
1121

1122

1123
class QuadricMixin:
11✔
1124
    """A Mixin class implementing common functionality for quadric surfaces"""
1125

1126
    @property
11✔
1127
    def _origin(self):
11✔
1128
        return np.array((self.x0, self.y0, self.z0))
11✔
1129

1130
    @property
11✔
1131
    def _axis(self):
11✔
1132
        axis = np.array((self.dx, self.dy, self.dz))
11✔
1133
        return axis / np.linalg.norm(axis)
11✔
1134

1135
    def get_Abc(self, coeffs=None):
11✔
1136
        """Compute matrix, vector, and scalar coefficients for this surface or
1137
        for a specified set of coefficients.
1138

1139
        Parameters
1140
        ----------
1141
        coeffs : tuple, optional
1142
            Tuple of coefficients from which to compute the quadric elements.
1143
            If none are supplied the coefficients of this surface will be used.
1144
        """
1145
        if coeffs is None:
11✔
1146
            a, b, c, d, e, f, g, h, j, k = self._get_base_coeffs()
11✔
1147
        else:
1148
            a, b, c, d, e, f, g, h, j, k = coeffs
×
1149

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

1153
        return A, bvec, k
11✔
1154

1155
    def eigh(self, coeffs=None):
11✔
1156
        """Wrapper method for returning eigenvalues and eigenvectors of this
1157
        quadric surface which is used for transformations.
1158

1159
        Parameters
1160
        ----------
1161
        coeffs : tuple, optional
1162
            Tuple of coefficients from which to compute the quadric elements.
1163
            If none are supplied the coefficients of this surface will be used.
1164

1165
        Returns
1166
        -------
1167
        w, v : tuple of numpy arrays with shapes (3,) and (3,3) respectively
1168
            Returns the eigenvalues and eigenvectors of the quadric matrix A
1169
            that represents the supplied coefficients. The vector w contains
1170
            the eigenvalues in ascending order and the matrix v contains the
1171
            eigenvectors such that v[:,i] is the eigenvector corresponding to
1172
            the eigenvalue w[i].
1173

1174
        """
1175
        return np.linalg.eigh(self.get_Abc(coeffs=coeffs)[0])
×
1176

1177
    def evaluate(self, point):
11✔
1178
        """Evaluate the surface equation at a given point.
1179

1180
        Parameters
1181
        ----------
1182
        point : 3-tuple of float
1183
            The Cartesian coordinates, :math:`(x',y',z')`, in [cm] at which the
1184
            surface equation should be evaluated.
1185

1186
        Returns
1187
        -------
1188
        float
1189
            :math:`Ax'^2 + By'^2 + Cz'^2 + Dx'y' + Ey'z' + Fx'z' + Gx' + Hy' +
1190
            Jz' + K = 0`
1191

1192
        """
1193
        x = np.asarray(point)
11✔
1194
        A, b, c = self.get_Abc()
11✔
1195
        return x.T @ A @ x + b.T @ x + c
11✔
1196

1197
    def translate(self, vector, inplace=False):
11✔
1198
        """Translate surface in given direction
1199

1200
        Parameters
1201
        ----------
1202
        vector : iterable of float
1203
            Direction in which surface should be translated
1204
        inplace : bool
1205
            Whether to return a clone of the Surface or the Surface itself.
1206

1207
        Returns
1208
        -------
1209
        openmc.Surface
1210
            Translated surface
1211

1212
        """
1213
        vector = np.asarray(vector)
11✔
1214
        if np.allclose(vector, 0., rtol=0., atol=self._atol):
11✔
1215
            return self
11✔
1216

1217
        surf = self if inplace else self.clone()
11✔
1218

1219
        if hasattr(self, 'x0'):
11✔
1220
            for vi, xi in zip(vector, ('x0', 'y0', 'z0')):
11✔
1221
                val = getattr(surf, xi)
11✔
1222
                try:
11✔
1223
                    setattr(surf, xi, val + vi)
11✔
1224
                except AttributeError:
11✔
1225
                    # That attribute is read only i.e x0 for XCylinder
1226
                    pass
11✔
1227

1228
        else:
1229
            A, bvec, cnst = self.get_Abc()
11✔
1230

1231
            g, h, j = bvec - 2*vector.T @ A
11✔
1232
            k = cnst + vector.T @ A @ vector - bvec.T @ vector
11✔
1233

1234
            for key, val in zip(('g', 'h', 'j', 'k'), (g, h, j, k)):
11✔
1235
                setattr(surf, key, val)
11✔
1236

1237
        return surf
11✔
1238

1239
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
11✔
1240
        # Get pivot and rotation matrix
1241
        pivot = np.asarray(pivot)
11✔
1242
        rotation = np.asarray(rotation, dtype=float)
11✔
1243

1244
        # Allow rotation matrix to be passed in directly, otherwise build it
1245
        if rotation.ndim == 2:
11✔
1246
            check_length('surface rotation', rotation.ravel(), 9)
11✔
1247
            Rmat = rotation
11✔
1248
        else:
1249
            Rmat = get_rotation_matrix(rotation, order=order)
11✔
1250

1251
        # Translate surface to the pivot point
1252
        tsurf = self.translate(-pivot, inplace=inplace)
11✔
1253

1254
        # If the surface is already generalized just clone it
1255
        if type(tsurf) is tsurf._virtual_base:
11✔
1256
            surf = tsurf if inplace else tsurf.clone()
11✔
1257
        else:
1258
            base_cls = type(tsurf)._virtual_base
11✔
1259
            # Copy necessary surface attributes to new kwargs dictionary
1260
            kwargs = {'boundary_type': tsurf.boundary_type,
11✔
1261
                      'albedo': tsurf.albedo, 'name': tsurf.name,
1262
                      'transformation': tsurf.transformation}
1263
            if inplace:
11✔
1264
                kwargs['surface_id'] = tsurf.id
×
1265
            kwargs.update({k: getattr(tsurf, k) for k in base_cls._coeff_keys})
11✔
1266
            # Create new instance of the virtual base class
1267
            surf = base_cls(**kwargs)
11✔
1268

1269
        # Perform rotations on axis, origin, or quadric coefficients
1270
        if hasattr(surf, 'dx'):
11✔
1271
            for key, val in zip(('dx', 'dy', 'dz'), Rmat @ tsurf._axis):
11✔
1272
                setattr(surf, key, val)
11✔
1273
        if hasattr(surf, 'x0'):
11✔
1274
            for key, val in zip(('x0', 'y0', 'z0'), Rmat @ tsurf._origin):
11✔
1275
                setattr(surf, key, val)
11✔
1276
        else:
1277
            A, bvec, k = surf.get_Abc()
11✔
1278
            Arot = Rmat @ A @ Rmat.T
11✔
1279

1280
            a, b, c = np.diagonal(Arot)
11✔
1281
            d, e, f = 2*Arot[0, 1], 2*Arot[1, 2], 2*Arot[0, 2]
11✔
1282
            g, h, j = Rmat @ bvec
11✔
1283

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

1287
        # translate back to the original frame and return the surface
1288
        return surf.translate(pivot, inplace=inplace)
11✔
1289

1290

1291
class Cylinder(QuadricMixin, Surface):
11✔
1292
    """A cylinder with radius r, centered on the point (x0, y0, z0) with an
1293
    axis specified by the line through points (x0, y0, z0) and (x0+dx, y0+dy,
1294
    z0+dz)
1295

1296
    Parameters
1297
    ----------
1298
    x0 : float, optional
1299
        x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1300
    y0 : float, optional
1301
        y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1302
    z0 : float, optional
1303
        z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1304
    r : float, optional
1305
        Radius of the cylinder in [cm]. Defaults to 1.
1306
    dx : float, optional
1307
        x-component of the vector representing the axis of the cylinder.
1308
        Defaults to 0.
1309
    dy : float, optional
1310
        y-component of the vector representing the axis of the cylinder.
1311
        Defaults to 0.
1312
    dz : float, optional
1313
        z-component of the vector representing the axis of the cylinder.
1314
        Defaults to 1.
1315
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
1316
        Boundary condition that defines the behavior for particles hitting the
1317
        surface. Defaults to transmissive boundary condition where particles
1318
        freely pass through the surface.
1319
    albedo : float, optional
1320
        Albedo of the surfaces as a ratio of particle weight after interaction
1321
        with the surface to the initial weight. Values must be positive. Only
1322
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1323
        or 'transformation'.
1324
    name : str, optional
1325
        Name of the cylinder. If not specified, the name will be the empty
1326
        string.
1327
    surface_id : int, optional
1328
        Unique identifier for the surface. If not specified, an identifier will
1329
        automatically be assigned.
1330

1331
    Attributes
1332
    ----------
1333
    x0 : float
1334
        x-coordinate for the origin of the Cylinder in [cm]
1335
    y0 : float
1336
        y-coordinate for the origin of the Cylinder in [cm]
1337
    z0 : float
1338
        z-coordinate for the origin of the Cylinder in [cm]
1339
    r : float
1340
        Radius of the cylinder in [cm]
1341
    dx : float
1342
        x-component of the vector representing the axis of the cylinder
1343
    dy : float
1344
        y-component of the vector representing the axis of the cylinder
1345
    dz : float
1346
        z-component of the vector representing the axis of the cylinder
1347
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
1348
        Boundary condition that defines the behavior for particles hitting the
1349
        surface.
1350
    albedo : float
1351
        Boundary albedo as a positive multiplier of particle weight
1352
    coefficients : dict
1353
        Dictionary of surface coefficients
1354
    id : int
1355
        Unique identifier for the surface
1356
    name : str
1357
        Name of the surface
1358
    type : str
1359
        Type of the surface
1360

1361
    """
1362
    _type = 'cylinder'
11✔
1363
    _coeff_keys = ('x0', 'y0', 'z0', 'r', 'dx', 'dy', 'dz')
11✔
1364

1365
    def __init__(self, x0=0., y0=0., z0=0., r=1., dx=0., dy=0., dz=1., *args,
11✔
1366
                 **kwargs):
1367
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1368
        super().__init__(**kwargs)
11✔
1369

1370
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r, dx, dy, dz)):
11✔
1371
            setattr(self, key, val)
11✔
1372

1373
    @classmethod
11✔
1374
    def __subclasshook__(cls, c):
11✔
1375
        if cls is Cylinder and c in (XCylinder, YCylinder, ZCylinder):
11✔
1376
            return True
11✔
1377
        return NotImplemented
×
1378

1379
    x0 = SurfaceCoefficient('x0')
11✔
1380
    y0 = SurfaceCoefficient('y0')
11✔
1381
    z0 = SurfaceCoefficient('z0')
11✔
1382
    r = SurfaceCoefficient('r')
11✔
1383
    dx = SurfaceCoefficient('dx')
11✔
1384
    dy = SurfaceCoefficient('dy')
11✔
1385
    dz = SurfaceCoefficient('dz')
11✔
1386

1387
    def bounding_box(self, side):
11✔
1388
        if side == '-':
11✔
1389
            r = self.r
11✔
1390
            ll = [xi - r if np.isclose(dxi, 0., rtol=0., atol=self._atol)
11✔
1391
                  else -np.inf for xi, dxi in zip(self._origin, self._axis)]
1392
            ur = [xi + r if np.isclose(dxi, 0., rtol=0., atol=self._atol)
11✔
1393
                  else np.inf for xi, dxi in zip(self._origin, self._axis)]
1394
            return BoundingBox(np.array(ll), np.array(ur))
11✔
1395
        elif side == '+':
11✔
1396
            return BoundingBox.infinite()
11✔
1397

1398
    def _get_base_coeffs(self):
11✔
1399
        # Get x, y, z coordinates of two points
1400
        x1, y1, z1 = self._origin
11✔
1401
        x2, y2, z2 = self._origin + self._axis
11✔
1402
        r = self.r
11✔
1403

1404
        # Define intermediate terms
1405
        dx = x2 - x1
11✔
1406
        dy = y2 - y1
11✔
1407
        dz = z2 - z1
11✔
1408
        cx = y1*z2 - y2*z1
11✔
1409
        cy = x2*z1 - x1*z2
11✔
1410
        cz = x1*y2 - x2*y1
11✔
1411

1412
        # Given p=(x,y,z), p1=(x1, y1, z1), p2=(x2, y2, z2), the equation
1413
        # for the cylinder can be derived as
1414
        # r = |(p - p1) ⨯ (p - p2)| / |p2 - p1|.
1415
        # Expanding out all terms and grouping according to what Quadric
1416
        # expects gives the following coefficients.
1417
        a = dy*dy + dz*dz
11✔
1418
        b = dx*dx + dz*dz
11✔
1419
        c = dx*dx + dy*dy
11✔
1420
        d = -2*dx*dy
11✔
1421
        e = -2*dy*dz
11✔
1422
        f = -2*dx*dz
11✔
1423
        g = 2*(cy*dz - cz*dy)
11✔
1424
        h = 2*(cz*dx - cx*dz)
11✔
1425
        j = 2*(cx*dy - cy*dx)
11✔
1426
        k = cx*cx + cy*cy + cz*cz - (dx*dx + dy*dy + dz*dz)*r*r
11✔
1427

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

1430
    @classmethod
11✔
1431
    def from_points(cls, p1, p2, r=1., **kwargs):
11✔
1432
        """Return a cylinder given points that define the axis and a radius.
1433

1434
        .. versionadded:: 0.12
1435

1436
        Parameters
1437
        ----------
1438
        p1, p2 : 3-tuples
1439
            Points that pass through the cylinder axis.
1440
        r : float, optional
1441
            Radius of the cylinder in [cm]. Defaults to 1.
1442
        kwargs : dict
1443
            Keyword arguments passed to the :class:`Cylinder` constructor
1444

1445
        Returns
1446
        -------
1447
        Cylinder
1448
            Cylinder that has an axis through the points p1 and p2, and a
1449
            radius r.
1450

1451
        """
1452
        # Convert to numpy arrays
1453
        p1 = np.asarray(p1)
11✔
1454
        p2 = np.asarray(p2)
11✔
1455
        x0, y0, z0 = p1
11✔
1456
        dx, dy, dz = p2 - p1
11✔
1457

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

1460
    def to_xml_element(self):
11✔
1461
        """Return XML representation of the surface
1462

1463
        Returns
1464
        -------
1465
        element : lxml.etree._Element
1466
            XML element containing source data
1467

1468
        """
1469
        # This method overrides Surface.to_xml_element to generate a Quadric
1470
        # since the C++ layer doesn't support Cylinders right now
1471
        with catch_warnings():
×
1472
            simplefilter('ignore', IDWarning)
×
1473
            kwargs = {'boundary_type': self.boundary_type, 'albedo': self.albedo,
×
1474
                      'name': self.name, 'surface_id': self.id,
1475
                      'transformation': self.transformation}
1476
            quad_rep = Quadric(*self._get_base_coeffs(), **kwargs)
×
1477
        return quad_rep.to_xml_element()
×
1478

1479

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

1484
    Parameters
1485
    ----------
1486
    y0 : float, optional
1487
        y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1488
    z0 : float, optional
1489
        z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1490
    r : float, optional
1491
        Radius of the cylinder in [cm]. Defaults to 1.
1492
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
1493
        Boundary condition that defines the behavior for particles hitting the
1494
        surface. Defaults to transmissive boundary condition where particles
1495
        freely pass through the surface.
1496
    albedo : float, optional
1497
        Albedo of the surfaces as a ratio of particle weight after interaction
1498
        with the surface to the initial weight. Values must be positive. Only
1499
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1500
        or 'transformation'.
1501
    name : str, optional
1502
        Name of the cylinder. If not specified, the name will be the empty
1503
        string.
1504
    surface_id : int, optional
1505
        Unique identifier for the surface. If not specified, an identifier will
1506
        automatically be assigned.
1507

1508
    Attributes
1509
    ----------
1510
    y0 : float
1511
        y-coordinate for the origin of the Cylinder in [cm]
1512
    z0 : float
1513
        z-coordinate for the origin of the Cylinder in [cm]
1514
    r : float
1515
        Radius of the cylinder in [cm]
1516
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
1517
        Boundary condition that defines the behavior for particles hitting the
1518
        surface.
1519
    albedo : float
1520
        Boundary albedo as a positive multiplier of particle weight
1521
    coefficients : dict
1522
        Dictionary of surface coefficients
1523
    id : int
1524
        Unique identifier for the surface
1525
    name : str
1526
        Name of the surface
1527
    type : str
1528
        Type of the surface
1529

1530
    """
1531

1532
    _type = 'x-cylinder'
11✔
1533
    _coeff_keys = ('y0', 'z0', 'r')
11✔
1534

1535
    def __init__(self, y0=0., z0=0., r=1., *args, **kwargs):
11✔
1536
        R = kwargs.pop('R', None)
11✔
1537
        if R is not None:
11✔
1538
            warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
×
1539
                 FutureWarning)
1540
            r = R
×
1541
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1542
        super().__init__(**kwargs)
11✔
1543

1544
        for key, val in zip(self._coeff_keys, (y0, z0, r)):
11✔
1545
            setattr(self, key, val)
11✔
1546

1547
    x0 = SurfaceCoefficient(0.)
11✔
1548
    y0 = SurfaceCoefficient('y0')
11✔
1549
    z0 = SurfaceCoefficient('z0')
11✔
1550
    r = SurfaceCoefficient('r')
11✔
1551
    dx = SurfaceCoefficient(1.)
11✔
1552
    dy = SurfaceCoefficient(0.)
11✔
1553
    dz = SurfaceCoefficient(0.)
11✔
1554

1555
    def _get_base_coeffs(self):
11✔
1556
        y0, z0, r = self.y0, self.z0, self.r
×
1557

1558
        a = d = e = f = g = 0.
×
1559
        b = c = 1.
×
1560
        h, j, k = -2*y0, -2*z0, y0*y0 + z0*z0 - r*r
×
1561

1562
        return (a, b, c, d, e, f, g, h, j, k)
×
1563

1564
    def bounding_box(self, side):
11✔
1565
        if side == '-':
11✔
1566
            return BoundingBox(
11✔
1567
                np.array([-np.inf, self.y0 - self.r, self.z0 - self.r]),
1568
                np.array([np.inf, self.y0 + self.r, self.z0 + self.r])
1569
            )
1570
        elif side == '+':
11✔
1571
            return BoundingBox.infinite()
11✔
1572

1573
    def evaluate(self, point):
11✔
1574
        y = point[1] - self.y0
11✔
1575
        z = point[2] - self.z0
11✔
1576
        return y*y + z*z - self.r**2
11✔
1577

1578

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

1583
    Parameters
1584
    ----------
1585
    x0 : float, optional
1586
        x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1587
    z0 : float, optional
1588
        z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1589
    r : float, optional
1590
        Radius of the cylinder in [cm]. Defaults to 1.
1591
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
1592
        Boundary condition that defines the behavior for particles hitting the
1593
        surface. Defaults to transmissive boundary condition where particles
1594
        freely pass through the surface.
1595
    albedo : float, optional
1596
        Albedo of the surfaces as a ratio of particle weight after interaction
1597
        with the surface to the initial weight. Values must be positive. Only
1598
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1599
        or 'transformation'.
1600
    name : str, optional
1601
        Name of the cylinder. If not specified, the name will be the empty
1602
        string.
1603
    surface_id : int, optional
1604
        Unique identifier for the surface. If not specified, an identifier will
1605
        automatically be assigned.
1606

1607
    Attributes
1608
    ----------
1609
    x0 : float
1610
        x-coordinate for the origin of the Cylinder in [cm]
1611
    z0 : float
1612
        z-coordinate for the origin of the Cylinder in [cm]
1613
    r : float
1614
        Radius of the cylinder in [cm]
1615
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
1616
        Boundary condition that defines the behavior for particles hitting the
1617
        surface.
1618
    albedo : float
1619
        Boundary albedo as a positive multiplier of particle weight
1620
    coefficients : dict
1621
        Dictionary of surface coefficients
1622
    id : int
1623
        Unique identifier for the surface
1624
    name : str
1625
        Name of the surface
1626
    type : str
1627
        Type of the surface
1628

1629
    """
1630

1631
    _type = 'y-cylinder'
11✔
1632
    _coeff_keys = ('x0', 'z0', 'r')
11✔
1633

1634
    def __init__(self, x0=0., z0=0., r=1., *args, **kwargs):
11✔
1635
        R = kwargs.pop('R', None)
11✔
1636
        if R is not None:
11✔
1637
            warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
×
1638
                 FutureWarning)
1639
            r = R
×
1640
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1641
        super().__init__(**kwargs)
11✔
1642

1643
        for key, val in zip(self._coeff_keys, (x0, z0, r)):
11✔
1644
            setattr(self, key, val)
11✔
1645

1646
    x0 = SurfaceCoefficient('x0')
11✔
1647
    y0 = SurfaceCoefficient(0.)
11✔
1648
    z0 = SurfaceCoefficient('z0')
11✔
1649
    r = SurfaceCoefficient('r')
11✔
1650
    dx = SurfaceCoefficient(0.)
11✔
1651
    dy = SurfaceCoefficient(1.)
11✔
1652
    dz = SurfaceCoefficient(0.)
11✔
1653

1654
    def _get_base_coeffs(self):
11✔
1655
        x0, z0, r = self.x0, self.z0, self.r
×
1656

1657
        b = d = e = f = h = 0.
×
1658
        a = c = 1.
×
1659
        g, j, k = -2*x0, -2*z0, x0*x0 + z0*z0 - r*r
×
1660

1661
        return (a, b, c, d, e, f, g, h, j, k)
×
1662

1663
    def bounding_box(self, side):
11✔
1664
        if side == '-':
11✔
1665
            return BoundingBox(
11✔
1666
                np.array([self.x0 - self.r, -np.inf, self.z0 - self.r]),
1667
                np.array([self.x0 + self.r, np.inf, self.z0 + self.r])
1668
            )
1669
        elif side == '+':
11✔
1670
            return BoundingBox.infinite()
11✔
1671

1672
    def evaluate(self, point):
11✔
1673
        x = point[0] - self.x0
11✔
1674
        z = point[2] - self.z0
11✔
1675
        return x*x + z*z - self.r**2
11✔
1676

1677

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

1682
    Parameters
1683
    ----------
1684
    x0 : float, optional
1685
        x-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1686
    y0 : float, optional
1687
        y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0
1688
    r : float, optional
1689
        Radius of the cylinder in [cm]. Defaults to 1.
1690
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
1691
        Boundary condition that defines the behavior for particles hitting the
1692
        surface. Defaults to transmissive boundary condition where particles
1693
        freely pass through the surface.
1694
    albedo : float, optional
1695
        Albedo of the surfaces as a ratio of particle weight after interaction
1696
        with the surface to the initial weight. Values must be positive. Only
1697
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1698
        or 'transformation'.
1699
    name : str, optional
1700
        Name of the cylinder. If not specified, the name will be the empty
1701
        string.
1702
    surface_id : int, optional
1703
        Unique identifier for the surface. If not specified, an identifier will
1704
        automatically be assigned.
1705

1706
    Attributes
1707
    ----------
1708
    x0 : float
1709
        x-coordinate for the origin of the Cylinder in [cm]
1710
    y0 : float
1711
        y-coordinate for the origin of the Cylinder in [cm]
1712
    r : float
1713
        Radius of the cylinder in [cm]
1714
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
1715
        Boundary condition that defines the behavior for particles hitting the
1716
        surface.
1717
    albedo : float
1718
        Boundary albedo as a positive multiplier of particle weight
1719
    coefficients : dict
1720
        Dictionary of surface coefficients
1721
    id : int
1722
        Unique identifier for the surface
1723
    name : str
1724
        Name of the surface
1725
    type : str
1726
        Type of the surface
1727

1728
    """
1729

1730
    _type = 'z-cylinder'
11✔
1731
    _coeff_keys = ('x0', 'y0', 'r')
11✔
1732

1733
    def __init__(self, x0=0., y0=0., r=1., *args, **kwargs):
11✔
1734
        R = kwargs.pop('R', None)
11✔
1735
        if R is not None:
11✔
1736
            warn(_WARNING_UPPER.format(type(self).__name__, 'r', 'R'),
×
1737
                 FutureWarning)
1738
            r = R
×
1739
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1740
        super().__init__(**kwargs)
11✔
1741

1742
        for key, val in zip(self._coeff_keys, (x0, y0, r)):
11✔
1743
            setattr(self, key, val)
11✔
1744

1745
    x0 = SurfaceCoefficient('x0')
11✔
1746
    y0 = SurfaceCoefficient('y0')
11✔
1747
    z0 = SurfaceCoefficient(0.)
11✔
1748
    r = SurfaceCoefficient('r')
11✔
1749
    dx = SurfaceCoefficient(0.)
11✔
1750
    dy = SurfaceCoefficient(0.)
11✔
1751
    dz = SurfaceCoefficient(1.)
11✔
1752

1753
    def _get_base_coeffs(self):
11✔
1754
        x0, y0, r = self.x0, self.y0, self.r
×
1755

1756
        c = d = e = f = j = 0.
×
1757
        a = b = 1.
×
1758
        g, h, k = -2*x0, -2*y0, x0*x0 + y0*y0 - r*r
×
1759

1760
        return (a, b, c, d, e, f, g, h, j, k)
×
1761

1762
    def bounding_box(self, side):
11✔
1763
        if side == '-':
11✔
1764
            return BoundingBox(
11✔
1765
                np.array([self.x0 - self.r, self.y0 - self.r, -np.inf]),
1766
                np.array([self.x0 + self.r, self.y0 + self.r, np.inf])
1767
            )
1768
        elif side == '+':
11✔
1769
            return BoundingBox.infinite()
11✔
1770

1771
    def evaluate(self, point):
11✔
1772
        x = point[0] - self.x0
11✔
1773
        y = point[1] - self.y0
11✔
1774
        return x*x + y*y - self.r**2
11✔
1775

1776

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

1780
    Parameters
1781
    ----------
1782
    x0 : float, optional
1783
        x-coordinate of the center of the sphere in [cm]. Defaults to 0.
1784
    y0 : float, optional
1785
        y-coordinate of the center of the sphere in [cm]. Defaults to 0.
1786
    z0 : float, optional
1787
        z-coordinate of the center of the sphere in [cm]. Defaults to 0.
1788
    r : float, optional
1789
        Radius of the sphere in [cm]. Defaults to 1.
1790
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
1791
        Boundary condition that defines the behavior for particles hitting the
1792
        surface. Defaults to transmissive boundary condition where particles
1793
        freely pass through the surface.
1794
    albedo : float, optional
1795
        Albedo of the surfaces as a ratio of particle weight after interaction
1796
        with the surface to the initial weight. Values must be positive. Only
1797
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1798
        or 'transformation'.
1799
    name : str, optional
1800
        Name of the sphere. If not specified, the name will be the empty string.
1801
    surface_id : int, optional
1802
        Unique identifier for the surface. If not specified, an identifier will
1803
        automatically be assigned.
1804

1805
    Attributes
1806
    ----------
1807
    x0 : float
1808
        x-coordinate of the center of the sphere in [cm]
1809
    y0 : float
1810
        y-coordinate of the center of the sphere in [cm]
1811
    z0 : float
1812
        z-coordinate of the center of the sphere in [cm]
1813
    r : float
1814
        Radius of the sphere in [cm]
1815
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
1816
        Boundary condition that defines the behavior for particles hitting the
1817
        surface.
1818
    albedo : float
1819
        Boundary albedo as a positive multiplier of particle weight
1820
    coefficients : dict
1821
        Dictionary of surface coefficients
1822
    id : int
1823
        Unique identifier for the surface
1824
    name : str
1825
        Name of the surface
1826
    type : str
1827
        Type of the surface
1828

1829
    """
1830

1831
    _type = 'sphere'
11✔
1832
    _coeff_keys = ('x0', 'y0', 'z0', 'r')
11✔
1833

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

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

1846
    x0 = SurfaceCoefficient('x0')
11✔
1847
    y0 = SurfaceCoefficient('y0')
11✔
1848
    z0 = SurfaceCoefficient('z0')
11✔
1849
    r = SurfaceCoefficient('r')
11✔
1850

1851
    def _get_base_coeffs(self):
11✔
1852
        x0, y0, z0, r = self.x0, self.y0, self.z0, self.r
11✔
1853
        a = b = c = 1.
11✔
1854
        d = e = f = 0.
11✔
1855
        g, h, j = -2*x0, -2*y0, -2*z0
11✔
1856
        k = x0*x0 + y0*y0 + z0*z0 - r*r
11✔
1857

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

1860
    def bounding_box(self, side):
11✔
1861
        if side == '-':
11✔
1862
            return BoundingBox(
11✔
1863
                np.array([self.x0 - self.r, self.y0 - self.r, self.z0 - self.r]),
1864
                np.array([self.x0 + self.r, self.y0 + self.r, self.z0 + self.r])
1865
            )
1866
        elif side == '+':
11✔
1867
            return BoundingBox.infinite()
11✔
1868

1869
    def evaluate(self, point):
11✔
1870
        x = point[0] - self.x0
11✔
1871
        y = point[1] - self.y0
11✔
1872
        z = point[2] - self.z0
11✔
1873
        return x*x + y*y + z*z - self.r**2
11✔
1874

1875

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

1879
    .. Note::
1880
        This creates a double cone, which is two one-sided cones that meet at their apex.
1881
        For a one-sided cone see :class:`~openmc.model.XConeOneSided`,
1882
        :class:`~openmc.model.YConeOneSided`, and :class:`~openmc.model.ZConeOneSided`.
1883

1884
    Parameters
1885
    ----------
1886
    x0 : float, optional
1887
        x-coordinate of the apex in [cm].
1888
    y0 : float, optional
1889
        y-coordinate of the apex in [cm].
1890
    z0 : float, optional
1891
        z-coordinate of the apex in [cm].
1892
    r2 : float, optional
1893
        The square of the slope of the cone. It is defined as
1894
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
1895
        distance :math:`h` from the apex. An easy way to define this quantity is
1896
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
1897
    dx : float, optional
1898
        x-component of the vector representing the axis of the cone.
1899
    dy : float, optional
1900
        y-component of the vector representing the axis of the cone.
1901
    dz : float, optional
1902
        z-component of the vector representing the axis of the cone.
1903
    surface_id : int, optional
1904
        Unique identifier for the surface. If not specified, an identifier will
1905
        automatically be assigned.
1906
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
1907
        Boundary condition that defines the behavior for particles hitting the
1908
        surface. Defaults to transmissive boundary condition where particles
1909
        freely pass through the surface.
1910
    albedo : float, optional
1911
        Albedo of the surfaces as a ratio of particle weight after interaction
1912
        with the surface to the initial weight. Values must be positive. Only
1913
        applicable if the boundary type is 'reflective', 'periodic', 'white',
1914
        or 'transformation'.
1915

1916
    name : str
1917
        Name of the cone. If not specified, the name will be the empty string.
1918

1919
    Attributes
1920
    ----------
1921
    x0 : float
1922
        x-coordinate of the apex in [cm]
1923
    y0 : float
1924
        y-coordinate of the apex in [cm]
1925
    z0 : float
1926
        z-coordinate of the apex in [cm]
1927
    r2 : float
1928
        Parameter related to the aperture
1929
    dx : float
1930
        x-component of the vector representing the axis of the cone.
1931
    dy : float
1932
        y-component of the vector representing the axis of the cone.
1933
    dz : float
1934
        z-component of the vector representing the axis of the cone.
1935
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
1936
        Boundary condition that defines the behavior for particles hitting the
1937
        surface.
1938
    albedo : float
1939
        Boundary albedo as a positive multiplier of particle weight
1940
    coefficients : dict
1941
        Dictionary of surface coefficients
1942
    id : int
1943
        Unique identifier for the surface
1944
    name : str
1945
        Name of the surface
1946
    type : str
1947
        Type of the surface
1948

1949
    """
1950

1951
    _type = 'cone'
11✔
1952
    _coeff_keys = ('x0', 'y0', 'z0', 'r2', 'dx', 'dy', 'dz')
11✔
1953

1954
    def __init__(self, x0=0., y0=0., z0=0., r2=1., dx=0., dy=0., dz=1., *args,
11✔
1955
                 **kwargs):
1956
        R2 = kwargs.pop('R2', None)
11✔
1957
        if R2 is not None:
11✔
1958
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
1959
                 FutureWarning)
1960
            r2 = R2
×
1961
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
1962
        super().__init__(**kwargs)
11✔
1963

1964
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2, dx, dy, dz)):
11✔
1965
            setattr(self, key, val)
11✔
1966

1967
    @classmethod
11✔
1968
    def __subclasshook__(cls, c):
11✔
1969
        if cls is Cone and c in (XCone, YCone, ZCone):
×
1970
            return True
×
1971
        return NotImplemented
×
1972

1973
    x0 = SurfaceCoefficient('x0')
11✔
1974
    y0 = SurfaceCoefficient('y0')
11✔
1975
    z0 = SurfaceCoefficient('z0')
11✔
1976
    r2 = SurfaceCoefficient('r2')
11✔
1977
    dx = SurfaceCoefficient('dx')
11✔
1978
    dy = SurfaceCoefficient('dy')
11✔
1979
    dz = SurfaceCoefficient('dz')
11✔
1980

1981
    def _get_base_coeffs(self):
11✔
1982
        # The equation for a general cone with vertex at point p = (x0, y0, z0)
1983
        # and axis specified by the unit vector d = (dx, dy, dz) and opening
1984
        # half angle theta can be described by the equation
1985
        #
1986
        # (d*(r - p))^2 - (r - p)*(r - p)cos^2(theta) = 0
1987
        #
1988
        # where * is the dot product and the vector r is the evaluation point
1989
        # r = (x, y, z)
1990
        #
1991
        # The argument r2 for cones is actually tan^2(theta) so that
1992
        # cos^2(theta) = 1 / (1 + r2)
1993

1994
        x0, y0, z0 = self._origin
11✔
1995
        dx, dy, dz = self._axis
11✔
1996
        cos2 = 1 / (1 + self.r2)
11✔
1997

1998
        a = cos2 - dx*dx
11✔
1999
        b = cos2 - dy*dy
11✔
2000
        c = cos2 - dz*dz
11✔
2001
        d = -2*dx*dy
11✔
2002
        e = -2*dy*dz
11✔
2003
        f = -2*dx*dz
11✔
2004
        g = 2*(dx*(dy*y0 + dz*z0) - a*x0)
11✔
2005
        h = 2*(dy*(dx*x0 + dz*z0) - b*y0)
11✔
2006
        j = 2*(dz*(dx*x0 + dy*y0) - c*z0)
11✔
2007
        k = a*x0*x0 + b*y0*y0 + c*z0*z0 - 2*(dx*dy*x0*y0 + dy*dz*y0*z0 +
11✔
2008
                                             dx*dz*x0*z0)
2009

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

2012
    def to_xml_element(self):
11✔
2013
        """Return XML representation of the surface
2014

2015
        Returns
2016
        -------
2017
        element : lxml.etree._Element
2018
            XML element containing source data
2019

2020
        """
2021
        # This method overrides Surface.to_xml_element to generate a Quadric
2022
        # since the C++ layer doesn't support Cones right now
2023
        with catch_warnings():
×
2024
            simplefilter('ignore', IDWarning)
×
2025
            kwargs = {'boundary_type': self.boundary_type,
×
2026
                      'albedo': self.albedo,
2027
                      'name': self.name,
2028
                      'surface_id': self.id,
2029
                      'transformation': self.transformation}
2030
            quad_rep = Quadric(*self._get_base_coeffs(), **kwargs)
×
2031
        return quad_rep.to_xml_element()
×
2032

2033

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

2038
    .. Note::
2039
        This creates a double cone, which is two one-sided cones that meet at their apex.
2040
        For a one-sided cone see :class:`~openmc.model.XConeOneSided`.
2041

2042
    Parameters
2043
    ----------
2044
    x0 : float, optional
2045
        x-coordinate of the apex in [cm].
2046
    y0 : float, optional
2047
        y-coordinate of the apex in [cm].
2048
    z0 : float, optional
2049
        z-coordinate of the apex in [cm].
2050
    r2 : float, optional
2051
        The square of the slope of the cone. It is defined as
2052
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
2053
        distance :math:`h` from the apex. An easy way to define this quantity is
2054
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
2055
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
2056
        Boundary condition that defines the behavior for particles hitting the
2057
        surface. Defaults to transmissive boundary condition where particles
2058
        freely pass through the surface.
2059
    albedo : float, optional
2060
        Albedo of the surfaces as a ratio of particle weight after interaction
2061
        with the surface to the initial weight. Values must be positive. Only
2062
        applicable if the boundary type is 'reflective', 'periodic', 'white',
2063
        or 'transformation'.
2064
    name : str, optional
2065
        Name of the cone. If not specified, the name will be the empty string.
2066
    surface_id : int, optional
2067
        Unique identifier for the surface. If not specified, an identifier will
2068
        automatically be assigned.
2069

2070
    Attributes
2071
    ----------
2072
    x0 : float
2073
        x-coordinate of the apex in [cm]
2074
    y0 : float
2075
        y-coordinate of the apex in [cm]
2076
    z0 : float
2077
        z-coordinate of the apex in [cm]
2078
    r2 : float
2079
        Parameter related to the aperture
2080
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
2081
        Boundary condition that defines the behavior for particles hitting the
2082
        surface.
2083
    albedo : float
2084
        Boundary albedo as a positive multiplier of particle weight
2085
    coefficients : dict
2086
        Dictionary of surface coefficients
2087
    id : int
2088
        Unique identifier for the surface
2089
    name : str
2090
        Name of the surface
2091
    type : str
2092
        Type of the surface
2093

2094
    """
2095

2096
    _type = 'x-cone'
11✔
2097
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
11✔
2098

2099
    def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
11✔
2100
        R2 = kwargs.pop('R2', None)
11✔
2101
        if R2 is not None:
11✔
2102
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
2103
                 FutureWarning)
2104
            r2 = R2
×
2105
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
2106
        super().__init__(**kwargs)
11✔
2107

2108
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
11✔
2109
            setattr(self, key, val)
11✔
2110

2111
    x0 = SurfaceCoefficient('x0')
11✔
2112
    y0 = SurfaceCoefficient('y0')
11✔
2113
    z0 = SurfaceCoefficient('z0')
11✔
2114
    r2 = SurfaceCoefficient('r2')
11✔
2115
    dx = SurfaceCoefficient(1.)
11✔
2116
    dy = SurfaceCoefficient(0.)
11✔
2117
    dz = SurfaceCoefficient(0.)
11✔
2118

2119
    def _get_base_coeffs(self):
11✔
2120
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2121

2122
        a = -r2
×
2123
        b = c = 1.
×
2124
        d = e = f = 0.
×
2125
        g, h, j = 2*x0*r2, -2*y0, -2*z0
×
2126
        k = y0*y0 + z0*z0 - r2*x0*x0
×
2127

2128
        return (a, b, c, d, e, f, g, h, j, k)
×
2129

2130
    def evaluate(self, point):
11✔
2131
        x = point[0] - self.x0
11✔
2132
        y = point[1] - self.y0
11✔
2133
        z = point[2] - self.z0
11✔
2134
        return y*y + z*z - self.r2*x*x
11✔
2135

2136

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

2141
    .. Note::
2142
        This creates a double cone, which is two one-sided cones that meet at their apex.
2143
        For a one-sided cone see :class:`~openmc.model.YConeOneSided`.
2144

2145
    Parameters
2146
    ----------
2147
    x0 : float, optional
2148
        x-coordinate of the apex in [cm].
2149
    y0 : float, optional
2150
        y-coordinate of the apex in [cm].
2151
    z0 : float, optional
2152
        z-coordinate of the apex in [cm].
2153
    r2 : float, optional
2154
        The square of the slope of the cone. It is defined as
2155
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
2156
        distance :math:`h` from the apex. An easy way to define this quantity is
2157
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
2158
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
2159
        Boundary condition that defines the behavior for particles hitting the
2160
        surface. Defaults to transmissive boundary condition where particles
2161
        freely pass through the surface.
2162
    albedo : float, optional
2163
        Albedo of the surfaces as a ratio of particle weight after interaction
2164
        with the surface to the initial weight. Values must be positive. Only
2165
        applicable if the boundary type is 'reflective', 'periodic', 'white',
2166
        or 'transformation'.
2167
    name : str, optional
2168
        Name of the cone. If not specified, the name will be the empty string.
2169
    surface_id : int, optional
2170
        Unique identifier for the surface. If not specified, an identifier will
2171
        automatically be assigned.
2172

2173
    Attributes
2174
    ----------
2175
    x0 : float
2176
        x-coordinate of the apex in [cm]
2177
    y0 : float
2178
        y-coordinate of the apex in [cm]
2179
    z0 : float
2180
        z-coordinate of the apex in [cm]
2181
    r2 : float
2182
        Parameter related to the aperture
2183
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
2184
        Boundary condition that defines the behavior for particles hitting the
2185
        surface.
2186
    albedo : float
2187
        Boundary albedo as a positive multiplier of particle weight
2188
    coefficients : dict
2189
        Dictionary of surface coefficients
2190
    id : int
2191
        Unique identifier for the surface
2192
    name : str
2193
        Name of the surface
2194
    type : str
2195
        Type of the surface
2196

2197
    """
2198

2199
    _type = 'y-cone'
11✔
2200
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
11✔
2201

2202
    def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
11✔
2203
        R2 = kwargs.pop('R2', None)
11✔
2204
        if R2 is not None:
11✔
2205
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
2206
                 FutureWarning)
2207
            r2 = R2
×
2208
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
2209
        super().__init__(**kwargs)
11✔
2210

2211
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
11✔
2212
            setattr(self, key, val)
11✔
2213

2214
    x0 = SurfaceCoefficient('x0')
11✔
2215
    y0 = SurfaceCoefficient('y0')
11✔
2216
    z0 = SurfaceCoefficient('z0')
11✔
2217
    r2 = SurfaceCoefficient('r2')
11✔
2218
    dx = SurfaceCoefficient(0.)
11✔
2219
    dy = SurfaceCoefficient(1.)
11✔
2220
    dz = SurfaceCoefficient(0.)
11✔
2221

2222
    def _get_base_coeffs(self):
11✔
2223
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2224

2225
        b = -r2
×
2226
        a = c = 1.
×
2227
        d = e = f = 0.
×
2228
        g, h, j = -2*x0, 2*y0*r2, -2*z0
×
2229
        k = x0*x0 + z0*z0 - r2*y0*y0
×
2230

2231
        return (a, b, c, d, e, f, g, h, j, k)
×
2232

2233
    def evaluate(self, point):
11✔
2234
        x = point[0] - self.x0
11✔
2235
        y = point[1] - self.y0
11✔
2236
        z = point[2] - self.z0
11✔
2237
        return x*x + z*z - self.r2*y*y
11✔
2238

2239

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

2244
    .. Note::
2245
        This creates a double cone, which is two one-sided cones that meet at their apex.
2246
        For a one-sided cone see :class:`~openmc.model.ZConeOneSided`.
2247

2248
    Parameters
2249
    ----------
2250
    x0 : float, optional
2251
        x-coordinate of the apex in [cm].
2252
    y0 : float, optional
2253
        y-coordinate of the apex in [cm].
2254
    z0 : float, optional
2255
        z-coordinate of the apex in [cm].
2256
    r2 : float, optional
2257
        The square of the slope of the cone. It is defined as
2258
        :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial
2259
        distance :math:`h` from the apex. An easy way to define this quantity is
2260
        to take the square of the radius of the cone (in cm) 1 cm from the apex.
2261
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
2262
        Boundary condition that defines the behavior for particles hitting the
2263
        surface. Defaults to transmissive boundary condition where particles
2264
        freely pass through the surface.
2265
    albedo : float, optional
2266
        Albedo of the surfaces as a ratio of particle weight after interaction
2267
        with the surface to the initial weight. Values must be positive. Only
2268
        applicable if the boundary type is 'reflective', 'periodic', 'white',
2269
        or 'transformation'.
2270
    name : str, optional
2271
        Name of the cone. If not specified, the name will be the empty string.
2272
    surface_id : int, optional
2273
        Unique identifier for the surface. If not specified, an identifier will
2274
        automatically be assigned.
2275

2276
    Attributes
2277
    ----------
2278
    x0 : float
2279
        x-coordinate of the apex in [cm]
2280
    y0 : float
2281
        y-coordinate of the apex in [cm]
2282
    z0 : float
2283
        z-coordinate of the apex in [cm]
2284
    r2 : float
2285
        Parameter related to the aperture.
2286
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
2287
        Boundary condition that defines the behavior for particles hitting the
2288
        surface.
2289
    albedo : float
2290
        Boundary albedo as a positive multiplier of particle weight
2291
    coefficients : dict
2292
        Dictionary of surface coefficients
2293
    id : int
2294
        Unique identifier for the surface
2295
    name : str
2296
        Name of the surface
2297
    type : str
2298
        Type of the surface
2299

2300
    """
2301

2302
    _type = 'z-cone'
11✔
2303
    _coeff_keys = ('x0', 'y0', 'z0', 'r2')
11✔
2304

2305
    def __init__(self, x0=0., y0=0., z0=0., r2=1., *args, **kwargs):
11✔
2306
        R2 = kwargs.pop('R2', None)
11✔
2307
        if R2 is not None:
11✔
2308
            warn(_WARNING_UPPER.format(type(self).__name__, 'r2', 'R2'),
×
2309
                 FutureWarning)
2310
            r2 = R2
×
2311
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
2312
        super().__init__(**kwargs)
11✔
2313

2314
        for key, val in zip(self._coeff_keys, (x0, y0, z0, r2)):
11✔
2315
            setattr(self, key, val)
11✔
2316

2317
    x0 = SurfaceCoefficient('x0')
11✔
2318
    y0 = SurfaceCoefficient('y0')
11✔
2319
    z0 = SurfaceCoefficient('z0')
11✔
2320
    r2 = SurfaceCoefficient('r2')
11✔
2321
    dx = SurfaceCoefficient(0.)
11✔
2322
    dy = SurfaceCoefficient(0.)
11✔
2323
    dz = SurfaceCoefficient(1.)
11✔
2324

2325
    def _get_base_coeffs(self):
11✔
2326
        x0, y0, z0, r2 = self.x0, self.y0, self.z0, self.r2
×
2327

2328
        c = -r2
×
2329
        a = b = 1.
×
2330
        d = e = f = 0.
×
2331
        g, h, j = -2*x0, -2*y0, 2*z0*r2
×
2332
        k = x0*x0 + y0*y0 - r2*z0*z0
×
2333

2334
        return (a, b, c, d, e, f, g, h, j, k)
×
2335

2336
    def evaluate(self, point):
11✔
2337
        x = point[0] - self.x0
11✔
2338
        y = point[1] - self.y0
11✔
2339
        z = point[2] - self.z0
11✔
2340
        return x*x + y*y - self.r2*z*z
11✔
2341

2342

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

2347
    Parameters
2348
    ----------
2349
    a, b, c, d, e, f, g, h, j, k : float, optional
2350
        coefficients for the surface. All default to 0.
2351
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}, optional
2352
        Boundary condition that defines the behavior for particles hitting the
2353
        surface. Defaults to transmissive boundary condition where particles
2354
        freely pass through the surface.
2355
    albedo : float, optional
2356
        Albedo of the surfaces as a ratio of particle weight after interaction
2357
        with the surface to the initial weight. Values must be positive. Only
2358
        applicable if the boundary type is 'reflective', 'periodic', 'white',
2359
        or 'transformation'.
2360
    name : str, optional
2361
        Name of the surface. If not specified, the name will be the empty string.
2362
    surface_id : int, optional
2363
        Unique identifier for the surface. If not specified, an identifier will
2364
        automatically be assigned.
2365

2366
    Attributes
2367
    ----------
2368
    a, b, c, d, e, f, g, h, j, k : float
2369
        coefficients for the surface
2370
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
2371
        Boundary condition that defines the behavior for particles hitting the
2372
        surface.
2373
    albedo : float
2374
        Boundary albedo as a positive multiplier of particle weight
2375
    coefficients : dict
2376
        Dictionary of surface coefficients
2377
    id : int
2378
        Unique identifier for the surface
2379
    name : str
2380
        Name of the surface
2381
    type : str
2382
        Type of the surface
2383

2384
    """
2385

2386
    _type = 'quadric'
11✔
2387
    _coeff_keys = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k')
11✔
2388

2389
    def __init__(self, a=0., b=0., c=0., d=0., e=0., f=0., g=0., h=0., j=0.,
11✔
2390
                 k=0., *args, **kwargs):
2391
        kwargs = _future_kwargs_warning_helper(type(self), *args, **kwargs)
11✔
2392
        super().__init__(**kwargs)
11✔
2393

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

2397
    a = SurfaceCoefficient('a')
11✔
2398
    b = SurfaceCoefficient('b')
11✔
2399
    c = SurfaceCoefficient('c')
11✔
2400
    d = SurfaceCoefficient('d')
11✔
2401
    e = SurfaceCoefficient('e')
11✔
2402
    f = SurfaceCoefficient('f')
11✔
2403
    g = SurfaceCoefficient('g')
11✔
2404
    h = SurfaceCoefficient('h')
11✔
2405
    j = SurfaceCoefficient('j')
11✔
2406
    k = SurfaceCoefficient('k')
11✔
2407

2408
    def _get_base_coeffs(self):
11✔
2409
        return tuple(getattr(self, c) for c in self._coeff_keys)
11✔
2410

2411

2412
class TorusMixin:
11✔
2413
    """A Mixin class implementing common functionality for torus surfaces"""
2414
    _coeff_keys = ('x0', 'y0', 'z0', 'a', 'b', 'c')
11✔
2415

2416
    def __init__(self, x0=0., y0=0., z0=0., a=0., b=0., c=0., **kwargs):
11✔
2417
        super().__init__(**kwargs)
11✔
2418
        for key, val in zip(self._coeff_keys, (x0, y0, z0, a, b, c)):
11✔
2419
            setattr(self, key, val)
11✔
2420

2421
    x0 = SurfaceCoefficient('x0')
11✔
2422
    y0 = SurfaceCoefficient('y0')
11✔
2423
    z0 = SurfaceCoefficient('z0')
11✔
2424
    a = SurfaceCoefficient('a')
11✔
2425
    b = SurfaceCoefficient('b')
11✔
2426
    c = SurfaceCoefficient('c')
11✔
2427

2428
    def translate(self, vector, inplace=False):
11✔
2429
        surf = self if inplace else self.clone()
11✔
2430
        surf.x0 += vector[0]
11✔
2431
        surf.y0 += vector[1]
11✔
2432
        surf.z0 += vector[2]
11✔
2433
        return surf
11✔
2434

2435
    def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False):
11✔
2436
        pivot = np.asarray(pivot)
11✔
2437
        rotation = np.asarray(rotation, dtype=float)
11✔
2438

2439
        # Allow rotation matrix to be passed in directly, otherwise build it
2440
        if rotation.ndim == 2:
11✔
2441
            check_length('surface rotation', rotation.ravel(), 9)
×
2442
            Rmat = rotation
×
2443
        else:
2444
            Rmat = get_rotation_matrix(rotation, order=order)
11✔
2445

2446
        # Only can handle trivial rotation matrices
2447
        close = np.isclose
11✔
2448
        if not np.all(close(Rmat, -1.0) | close(Rmat, 0.0) | close(Rmat, 1.0)):
11✔
2449
            raise NotImplementedError('Torus surfaces cannot handle generic rotations')
11✔
2450

2451
        # Translate surface to pivot
2452
        surf = self.translate(-pivot, inplace=inplace)
11✔
2453

2454
        # Determine "center" of torus and a point above it (along main axis)
2455
        center = [surf.x0, surf.y0, surf.z0]
11✔
2456
        above_center = center.copy()
11✔
2457
        index = ['x-torus', 'y-torus', 'z-torus'].index(surf._type)
11✔
2458
        above_center[index] += 1
11✔
2459

2460
        # Compute new rotated torus center
2461
        center = Rmat @ center
11✔
2462

2463
        # Figure out which axis should be used after rotation
2464
        above_center = Rmat @ above_center
11✔
2465
        new_index = np.where(np.isclose(np.abs(above_center - center), 1.0))[0][0]
11✔
2466
        cls = [XTorus, YTorus, ZTorus][new_index]
11✔
2467

2468
        # Create rotated torus
2469
        kwargs = {
11✔
2470
            'boundary_type': surf.boundary_type,
2471
            'albedo': surf.albedo,
2472
            'name': surf.name,
2473
            'a': surf.a, 'b': surf.b, 'c': surf.c,
2474
            'transformation': surf.transformation
2475
        }
2476
        if inplace:
11✔
2477
            kwargs['surface_id'] = surf.id
×
2478
        surf = cls(x0=center[0], y0=center[1], z0=center[2], **kwargs)
11✔
2479

2480
        return surf.translate(pivot, inplace=inplace)
11✔
2481

2482
    def _get_base_coeffs(self):
11✔
2483
        raise NotImplementedError
×
2484

2485

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

2490
    .. versionadded:: 0.13.0
2491

2492
    Parameters
2493
    ----------
2494
    x0 : float
2495
        x-coordinate of the center of the axis of revolution in [cm]
2496
    y0 : float
2497
        y-coordinate of the center of the axis of revolution in [cm]
2498
    z0 : float
2499
        z-coordinate of the center of the axis of revolution in [cm]
2500
    a : float
2501
        Major radius of the torus in [cm]
2502
    b : float
2503
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2504
    c : float
2505
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2506
    kwargs : dict
2507
        Keyword arguments passed to the :class:`Surface` constructor
2508

2509
    Attributes
2510
    ----------
2511
    x0 : float
2512
        x-coordinate of the center of the axis of revolution in [cm]
2513
    y0 : float
2514
        y-coordinate of the center of the axis of revolution in [cm]
2515
    z0 : float
2516
        z-coordinate of the center of the axis of revolution in [cm]
2517
    a : float
2518
        Major radius of the torus in [cm]
2519
    b : float
2520
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2521
    c : float
2522
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2523
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
2524
        Boundary condition that defines the behavior for particles hitting the
2525
        surface.
2526
    albedo : float
2527
        Boundary albedo as a positive multiplier of particle weight
2528
    coefficients : dict
2529
        Dictionary of surface coefficients
2530
    id : int
2531
        Unique identifier for the surface
2532
    name : str
2533
        Name of the surface
2534
    type : str
2535
        Type of the surface
2536

2537
    """
2538
    _type = 'x-torus'
11✔
2539

2540
    def evaluate(self, point):
11✔
2541
        x = point[0] - self.x0
11✔
2542
        y = point[1] - self.y0
11✔
2543
        z = point[2] - self.z0
11✔
2544
        a = self.a
11✔
2545
        b = self.b
11✔
2546
        c = self.c
11✔
2547
        return (x*x)/(b*b) + (math.sqrt(y*y + z*z) - a)**2/(c*c) - 1
11✔
2548

2549
    def bounding_box(self, side):
11✔
2550
        x0, y0, z0 = self.x0, self.y0, self.z0
11✔
2551
        a, b, c = self.a, self.b, self.c
11✔
2552
        if side == '-':
11✔
2553
            return BoundingBox(
11✔
2554
                np.array([x0 - b, y0 - a - c, z0 - a - c]),
2555
                np.array([x0 + b, y0 + a + c, z0 + a + c])
2556
            )
2557
        elif side == '+':
11✔
2558
            return BoundingBox.infinite()
11✔
2559

2560

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

2565
    .. versionadded:: 0.13.0
2566

2567
    Parameters
2568
    ----------
2569
    x0 : float
2570
        x-coordinate of the center of the axis of revolution in [cm]
2571
    y0 : float
2572
        y-coordinate of the center of the axis of revolution in [cm]
2573
    z0 : float
2574
        z-coordinate of the center of the axis of revolution in [cm]
2575
    a : float
2576
        Major radius of the torus in [cm]
2577
    b : float
2578
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2579
    c : float
2580
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2581
    kwargs : dict
2582
        Keyword arguments passed to the :class:`Surface` constructor
2583

2584
    Attributes
2585
    ----------
2586
    x0 : float
2587
        x-coordinate of the center of the axis of revolution in [cm]
2588
    y0 : float
2589
        y-coordinate of the center of the axis of revolution in [cm]
2590
    z0 : float
2591
        z-coordinate of the center of the axis of revolution in [cm]
2592
    a : float
2593
        Major radius of the torus in [cm]
2594
    b : float
2595
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2596
    c : float
2597
        Minor radius of the torus (perpendicular to axis of revolution)
2598
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
2599
        Boundary condition that defines the behavior for particles hitting the
2600
        surface.
2601
    albedo : float
2602
        Boundary albedo as a positive multiplier of particle weight
2603
    coefficients : dict
2604
        Dictionary of surface coefficients
2605
    id : int
2606
        Unique identifier for the surface
2607
    name : str
2608
        Name of the surface
2609
    type : str
2610
        Type of the surface
2611

2612
    """
2613
    _type = 'y-torus'
11✔
2614

2615
    def evaluate(self, point):
11✔
2616
        x = point[0] - self.x0
11✔
2617
        y = point[1] - self.y0
11✔
2618
        z = point[2] - self.z0
11✔
2619
        a = self.a
11✔
2620
        b = self.b
11✔
2621
        c = self.c
11✔
2622
        return (y*y)/(b*b) + (math.sqrt(x*x + z*z) - a)**2/(c*c) - 1
11✔
2623

2624
    def bounding_box(self, side):
11✔
2625
        x0, y0, z0 = self.x0, self.y0, self.z0
11✔
2626
        a, b, c = self.a, self.b, self.c
11✔
2627
        if side == '-':
11✔
2628
            return BoundingBox(
11✔
2629
                np.array([x0 - a - c, y0 - b, z0 - a - c]),
2630
                np.array([x0 + a + c, y0 + b, z0 + a + c])
2631
            )
2632
        elif side == '+':
11✔
2633
            return BoundingBox.infinite()
11✔
2634

2635

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

2640
    .. versionadded:: 0.13.0
2641

2642
    Parameters
2643
    ----------
2644
    x0 : float
2645
        x-coordinate of the center of the axis of revolution in [cm]
2646
    y0 : float
2647
        y-coordinate of the center of the axis of revolution in [cm]
2648
    z0 : float
2649
        z-coordinate of the center of the axis of revolution in [cm]
2650
    a : float
2651
        Major radius of the torus in [cm]
2652
    b : float
2653
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2654
    c : float
2655
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2656
    kwargs : dict
2657
        Keyword arguments passed to the :class:`Surface` constructor
2658

2659
    Attributes
2660
    ----------
2661
    x0 : float
2662
        x-coordinate of the center of the axis of revolution in [cm]
2663
    y0 : float
2664
        y-coordinate of the center of the axis of revolution in [cm]
2665
    z0 : float
2666
        z-coordinate of the center of the axis of revolution in [cm]
2667
    a : float
2668
        Major radius of the torus in [cm]
2669
    b : float
2670
        Minor radius of the torus in [cm] (parallel to axis of revolution)
2671
    c : float
2672
        Minor radius of the torus in [cm] (perpendicular to axis of revolution)
2673
    boundary_type : {'transmission', 'vacuum', 'reflective', 'white', 'transformation'}
2674
        Boundary condition that defines the behavior for particles hitting the
2675
        surface.
2676
    albedo : float
2677
        Boundary albedo as a positive multiplier of particle weight
2678
    coefficients : dict
2679
        Dictionary of surface coefficients
2680
    id : int
2681
        Unique identifier for the surface
2682
    name : str
2683
        Name of the surface
2684
    type : str
2685
        Type of the surface
2686
    """
2687

2688
    _type = 'z-torus'
11✔
2689

2690
    def evaluate(self, point):
11✔
2691
        x = point[0] - self.x0
11✔
2692
        y = point[1] - self.y0
11✔
2693
        z = point[2] - self.z0
11✔
2694
        a = self.a
11✔
2695
        b = self.b
11✔
2696
        c = self.c
11✔
2697
        return (z*z)/(b*b) + (math.sqrt(x*x + y*y) - a)**2/(c*c) - 1
11✔
2698

2699
    def bounding_box(self, side):
11✔
2700
        x0, y0, z0 = self.x0, self.y0, self.z0
11✔
2701
        a, b, c = self.a, self.b, self.c
11✔
2702
        if side == '-':
11✔
2703
            return BoundingBox(
11✔
2704
                np.array([x0 - a - c, y0 - a - c, z0 - b]),
2705
                np.array([x0 + a + c, y0 + a + c, z0 + b])
2706
            )
2707
        elif side == '+':
11✔
2708
            return BoundingBox.infinite()
11✔
2709

2710

2711
class Halfspace(Region):
11✔
2712
    """A positive or negative half-space region.
2713

2714
    A half-space is either of the two parts into which a two-dimension surface
2715
    divides the three-dimensional Euclidean space. If the equation of the
2716
    surface is :math:`f(x,y,z) = 0`, the region for which :math:`f(x,y,z) < 0`
2717
    is referred to as the negative half-space and the region for which
2718
    :math:`f(x,y,z) > 0` is referred to as the positive half-space.
2719

2720
    Instances of Halfspace are generally not instantiated directly. Rather, they
2721
    can be created from an existing Surface through the __neg__ and __pos__
2722
    operators, as the following example demonstrates:
2723

2724
    >>> sphere = openmc.Sphere(surface_id=1, r=10.0)
2725
    >>> inside_sphere = -sphere
2726
    >>> outside_sphere = +sphere
2727
    >>> type(inside_sphere)
2728
    <class 'openmc.surface.Halfspace'>
2729

2730
    Parameters
2731
    ----------
2732
    surface : openmc.Surface
2733
        Surface which divides Euclidean space.
2734
    side : {'+', '-'}
2735
        Indicates whether the positive or negative half-space is used.
2736

2737
    Attributes
2738
    ----------
2739
    surface : openmc.Surface
2740
        Surface which divides Euclidean space.
2741
    side : {'+', '-'}
2742
        Indicates whether the positive or negative half-space is used.
2743
    bounding_box : openmc.BoundingBox
2744
        Lower-left and upper-right coordinates of an axis-aligned bounding box
2745

2746
    """
2747

2748
    def __init__(self, surface, side):
11✔
2749
        self.surface = surface
11✔
2750
        self.side = side
11✔
2751

2752
    def __and__(self, other):
11✔
2753
        if isinstance(other, Intersection):
11✔
2754
            return Intersection([self] + other[:])
11✔
2755
        else:
2756
            return Intersection((self, other))
11✔
2757

2758
    def __or__(self, other):
11✔
2759
        if isinstance(other, Union):
11✔
2760
            return Union([self] + other[:])
×
2761
        else:
2762
            return Union((self, other))
11✔
2763

2764
    def __invert__(self) -> Halfspace:
11✔
2765
        return -self.surface if self.side == '+' else +self.surface
11✔
2766

2767
    def __contains__(self, point):
11✔
2768
        """Check whether a point is contained in the half-space.
2769

2770
        Parameters
2771
        ----------
2772
        point : 3-tuple of float
2773
            Cartesian coordinates, :math:`(x',y',z')`, of the point
2774

2775
        Returns
2776
        -------
2777
        bool
2778
            Whether the point is in the half-space
2779

2780
        """
2781

2782
        val = self.surface.evaluate(point)
11✔
2783
        return val >= 0. if self.side == '+' else val < 0.
11✔
2784

2785
    @property
11✔
2786
    def surface(self):
11✔
2787
        return self._surface
11✔
2788

2789
    @surface.setter
11✔
2790
    def surface(self, surface):
11✔
2791
        check_type('surface', surface, Surface)
11✔
2792
        self._surface = surface
11✔
2793

2794
    @property
11✔
2795
    def side(self):
11✔
2796
        return self._side
11✔
2797

2798
    @side.setter
11✔
2799
    def side(self, side):
11✔
2800
        check_value('side', side, ('+', '-'))
11✔
2801
        self._side = side
11✔
2802

2803
    @property
11✔
2804
    def bounding_box(self):
11✔
2805
        return self.surface.bounding_box(self.side)
11✔
2806

2807
    def __str__(self):
11✔
2808
        return '-' + str(self.surface.id) if self.side == '-' \
11✔
2809
            else str(self.surface.id)
2810

2811
    def get_surfaces(self, surfaces=None):
11✔
2812
        """
2813
        Returns the surface that this is a halfspace of.
2814

2815
        Parameters
2816
        ----------
2817
        surfaces : dict, optional
2818
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
2819

2820
        Returns
2821
        -------
2822
        surfaces : dict
2823
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
2824

2825
        """
2826
        if surfaces is None:
11✔
2827
            surfaces = {}
11✔
2828

2829
        surfaces[self.surface.id] = self.surface
11✔
2830
        return surfaces
11✔
2831

2832
    def remove_redundant_surfaces(self, redundant_surfaces):
11✔
2833
        """Recursively remove all redundant surfaces referenced by this region
2834

2835
        Parameters
2836
        ----------
2837
        redundant_surfaces : dict
2838
            Dictionary mapping redundant surface IDs to surface IDs for the
2839
            :class:`openmc.Surface` instances that should replace them.
2840

2841
        """
2842

2843
        surf = redundant_surfaces.get(self.surface.id)
11✔
2844
        if surf is not None:
11✔
2845
            self.surface = surf
11✔
2846

2847
    def clone(self, memo=None):
11✔
2848
        """Create a copy of this halfspace, with a cloned surface with a
2849
        unique ID.
2850

2851
        Parameters
2852
        ----------
2853
        memo : dict or None
2854
            A nested dictionary of previously cloned objects. This parameter
2855
            is used internally and should not be specified by the user.
2856

2857
        Returns
2858
        -------
2859
        clone : openmc.Halfspace
2860
            The clone of this halfspace
2861

2862
        """
2863

2864
        if memo is None:
11✔
2865
            memo = dict
×
2866

2867
        clone = deepcopy(self)
11✔
2868
        clone.surface = self.surface.clone(memo)
11✔
2869
        return clone
11✔
2870

2871
    def translate(self, vector, inplace=False, memo=None):
11✔
2872
        """Translate half-space in given direction
2873

2874
        Parameters
2875
        ----------
2876
        vector : iterable of float
2877
            Direction in which region should be translated
2878
        memo : dict or None
2879
            Dictionary used for memoization
2880

2881
        Returns
2882
        -------
2883
        openmc.Halfspace
2884
            Translated half-space
2885

2886
        """
2887
        if memo is None:
11✔
2888
            memo = {}
×
2889

2890
        # If translated surface not in memo, add it
2891
        key = (self.surface, tuple(vector))
11✔
2892
        if key not in memo:
11✔
2893
            memo[key] = self.surface.translate(vector, inplace)
11✔
2894

2895
        # Return translated half-space
2896
        return type(self)(memo[key], self.side)
11✔
2897

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

2902
        .. versionadded:: 0.12
2903

2904
        Parameters
2905
        ----------
2906
        rotation : 3-tuple of float, or 3x3 iterable
2907
            A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where
2908
            the first element is the rotation about the x-axis in the fixed
2909
            laboratory frame, the second element is the rotation about the
2910
            y-axis in the fixed laboratory frame, and the third element is the
2911
            rotation about the z-axis in the fixed laboratory frame. The
2912
            rotations are active rotations. Additionally a 3x3 rotation matrix
2913
            can be specified directly either as a nested iterable or array.
2914
        pivot : iterable of float, optional
2915
            (x, y, z) coordinates for the point to rotate about. Defaults to
2916
            (0., 0., 0.)
2917
        order : str, optional
2918
            A string of 'x', 'y', and 'z' in some order specifying which
2919
            rotation to perform first, second, and third. Defaults to 'xyz'
2920
            which means, the rotation by angle :math:`\phi` about x will be
2921
            applied first, followed by :math:`\theta` about y and then
2922
            :math:`\psi` about z. This corresponds to an x-y-z extrinsic
2923
            rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan
2924
            angles :math:`(\phi, \theta, \psi)`.
2925
        inplace : bool
2926
            Whether or not to return a new instance of Surface or to modify the
2927
            coefficients of this Surface in place. Defaults to False.
2928
        memo : dict or None
2929
            Dictionary used for memoization
2930

2931
        Returns
2932
        -------
2933
        openmc.Halfspace
2934
            Translated half-space
2935

2936
        """
2937
        if memo is None:
11✔
2938
            memo = {}
×
2939

2940
        # If rotated surface not in memo, add it
2941
        key = (self.surface, tuple(np.ravel(rotation)), tuple(pivot), order, inplace)
11✔
2942
        if key not in memo:
11✔
2943
            memo[key] = self.surface.rotate(rotation, pivot=pivot, order=order,
11✔
2944
                                            inplace=inplace)
2945

2946
        # Return rotated half-space
2947
        return type(self)(memo[key], self.side)
11✔
2948

2949

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

2952

2953
# Set virtual base classes for "casting" up the hierarchy
2954
Plane._virtual_base = Plane
11✔
2955
XPlane._virtual_base = Plane
11✔
2956
YPlane._virtual_base = Plane
11✔
2957
ZPlane._virtual_base = Plane
11✔
2958
Cylinder._virtual_base = Cylinder
11✔
2959
XCylinder._virtual_base = Cylinder
11✔
2960
YCylinder._virtual_base = Cylinder
11✔
2961
ZCylinder._virtual_base = Cylinder
11✔
2962
Cone._virtual_base = Cone
11✔
2963
XCone._virtual_base = Cone
11✔
2964
YCone._virtual_base = Cone
11✔
2965
ZCone._virtual_base = Cone
11✔
2966
Sphere._virtual_base = Sphere
11✔
2967
Quadric._virtual_base = Quadric
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc