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

openmc-dev / openmc / 21621898565

03 Feb 2026 07:57AM UTC coverage: 81.278% (-0.5%) from 81.763%
21621898565

Pull #3474

github

web-flow
Merge 7486f3ff0 into b41e22f68
Pull Request #3474: Cylindrical IndependentSource enhancements

17285 of 24269 branches covered (71.22%)

Branch coverage included in aggregate %.

56 of 64 new or added lines in 2 files covered. (87.5%)

402 existing lines in 9 files now uncovered.

55547 of 65340 relevant lines covered (85.01%)

34579169.11 hits per line

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

92.71
/openmc/stats/multivariate.py
1
from __future__ import annotations
7✔
2
from abc import ABC, abstractmethod
7✔
3
from collections.abc import Iterable, Sequence
7✔
4
from math import cos, pi
7✔
5
from numbers import Real
7✔
6
from warnings import warn
7✔
7

8
import lxml.etree as ET
7✔
9
import numpy as np
7✔
10

11
import openmc
7✔
12
import openmc.checkvalue as cv
7✔
13
from .._xml import get_elem_list, get_text
7✔
14
from ..mesh import MeshBase
7✔
15
from .univariate import PowerLaw, Uniform, Univariate, delta_function
7✔
16

17

18
class UnitSphere(ABC):
7✔
19
    """Distribution of points on the unit sphere.
20

21
    This abstract class is used for angular distributions, since a direction is
22
    represented as a unit vector (i.e., vector on the unit sphere).
23

24
    Parameters
25
    ----------
26
    reference_uvw : Iterable of float
27
        Direction from which polar angle is measured
28

29
    Attributes
30
    ----------
31
    reference_uvw : Iterable of float
32
        Direction from which polar angle is measured
33

34
    """
35
    def __init__(self, reference_uvw=None):
7✔
36
        self._reference_uvw = None
7✔
37
        if reference_uvw is not None:
7✔
38
            self.reference_uvw = reference_uvw
7✔
39

40
    @property
7✔
41
    def reference_uvw(self):
7✔
42
        return self._reference_uvw
7✔
43

44
    @reference_uvw.setter
7✔
45
    def reference_uvw(self, uvw):
7✔
46
        cv.check_type('reference direction', uvw, Iterable, Real)
7✔
47
        uvw = np.asarray(uvw)
7✔
48
        self._reference_uvw = uvw/np.linalg.norm(uvw)
7✔
49

50
    @abstractmethod
7✔
51
    def to_xml_element(self):
7✔
52
        return ''
×
53

54
    @classmethod
7✔
55
    @abstractmethod
7✔
56
    def from_xml_element(cls, elem):
7✔
57
        distribution = get_text(elem, 'type')
7✔
58
        if distribution == 'mu-phi':
7✔
59
            return PolarAzimuthal.from_xml_element(elem)
7✔
60
        elif distribution == 'isotropic':
7✔
61
            return Isotropic.from_xml_element(elem)
7✔
62
        elif distribution == 'monodirectional':
7✔
63
            return Monodirectional.from_xml_element(elem)
7✔
64

65

66
class PolarAzimuthal(UnitSphere):
7✔
67
    """Angular distribution represented by polar and azimuthal angles
68

69
    This distribution allows one to specify the distribution of the cosine of
70
    the polar angle and the azimuthal angle independently of one another. The
71
    polar angle is measured relative to the reference angle.
72

73
    Parameters
74
    ----------
75
    mu : openmc.stats.Univariate
76
        Distribution of the cosine of the polar angle
77
    phi : openmc.stats.Univariate
78
        Distribution of the azimuthal angle in radians
79
    reference_uvw : Iterable of float
80
        Direction from which polar angle is measured. Defaults to the positive
81
        z-direction.
82
    reference_vwu : Iterable of float
83
        Direction from which azimuthal angle is measured. Defaults to the positive
84
        x-direction.
85

86
    Attributes
87
    ----------
88
    mu : openmc.stats.Univariate
89
        Distribution of the cosine of the polar angle
90
    phi : openmc.stats.Univariate
91
        Distribution of the azimuthal angle in radians
92

93
    """
94

95
    def __init__(self, mu=None, phi=None, reference_uvw=(0., 0., 1.), reference_vwu=(1., 0., 0.)):
7✔
96
        super().__init__(reference_uvw)
7✔
97
        self.reference_vwu = reference_vwu
7✔
98
        if mu is not None:
7✔
99
            self.mu = mu
7✔
100
        else:
101
            self.mu = Uniform(-1., 1.)
7✔
102

103
        if phi is not None:
7✔
104
            self.phi = phi
7✔
105
        else:
106
            self.phi = Uniform(0., 2*pi)
7✔
107

108
    @property
7✔
109
    def reference_vwu(self):
7✔
110
        return self._reference_vwu
7✔
111

112
    @reference_vwu.setter
7✔
113
    def reference_vwu(self, vwu):
7✔
114
        cv.check_type('reference v direction', vwu, Iterable, Real)
7✔
115
        vwu = np.asarray(vwu)
7✔
116
        uvw = self.reference_uvw
7✔
117
        cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu))
7✔
118
        vwu -= vwu.dot(uvw)*uvw
7✔
119
        cv.check_less_than('reference v direction must be orthogonal to reference u direction', np.abs(vwu.dot(uvw)), 1e-6)
7✔
120
        self._reference_vwu = vwu/np.linalg.norm(vwu)
7✔
121

122
    @property
7✔
123
    def mu(self):
7✔
124
        return self._mu
7✔
125

126
    @mu.setter
7✔
127
    def mu(self, mu):
7✔
128
        cv.check_type('cosine of polar angle', mu, Univariate)
7✔
129
        self._mu = mu
7✔
130

131
    @property
7✔
132
    def phi(self):
7✔
133
        return self._phi
7✔
134

135
    @phi.setter
7✔
136
    def phi(self, phi):
7✔
137
        cv.check_type('azimuthal angle', phi, Univariate)
7✔
138
        self._phi = phi
7✔
139

140
    def to_xml_element(self, element_name: str = None):
7✔
141
        """Return XML representation of the angular distribution
142

143
        Parameters
144
        ----------
145
        element_name : str, optional
146
            XML element name
147

148
        Returns
149
        -------
150
        element : lxml.etree._Element
151
            XML element containing angular distribution data
152

153
        """
154
        if element_name is not None:
7✔
155
            element = ET.Element(element_name)
7✔
156
        else:
157
            element = ET.Element('angle')
7✔
158

159
        element.set("type", "mu-phi")
7✔
160
        if self.reference_uvw is not None:
7✔
161
            element.set("reference_uvw", ' '.join(map(str, self.reference_uvw)))
7✔
162
        if self.reference_vwu is not None:
7✔
163
            element.set("reference_vwu", ' '.join(map(str, self.reference_vwu)))
7✔
164
        element.append(self.mu.to_xml_element('mu'))
7✔
165
        element.append(self.phi.to_xml_element('phi'))
7✔
166
        return element
7✔
167

168
    @classmethod
7✔
169
    def from_xml_element(cls, elem):
7✔
170
        """Generate angular distribution from an XML element
171

172
        Parameters
173
        ----------
174
        elem : lxml.etree._Element
175
            XML element
176

177
        Returns
178
        -------
179
        openmc.stats.PolarAzimuthal
180
            Angular distribution generated from XML element
181

182
        """
183
        mu_phi = cls()
7✔
184
        uvw = get_elem_list(elem, "reference_uvw", float)
7✔
185
        if uvw is not None:
7✔
186
            mu_phi.reference_uvw = uvw
7✔
187
        vwu = get_elem_list(elem, "reference_vwu", float)
7✔
188
        if vwu is not None:
7✔
189
            mu_phi.reference_vwu = vwu
7✔
190
        mu_phi.mu = Univariate.from_xml_element(elem.find('mu'))
7✔
191
        mu_phi.phi = Univariate.from_xml_element(elem.find('phi'))
7✔
192
        return mu_phi
7✔
193

194

195
class Isotropic(UnitSphere):
7✔
196
    """Isotropic angular distribution.
197

198
    Parameters
199
    ----------
200
    bias : openmc.stats.PolarAzimuthal, optional
201
        Distribution for biased sampling.
202

203
    Attributes
204
    ----------
205
    bias : openmc.stats.PolarAzimuthal or None
206
        Distribution for biased sampling
207

208
    """
209

210
    def __init__(self, bias: PolarAzimuthal | None = None):
7✔
211
        super().__init__()
7✔
212
        self.bias = bias
7✔
213

214
    @property
7✔
215
    def bias(self):
7✔
216
        return self._bias
7✔
217

218
    @bias.setter
7✔
219
    def bias(self, bias):
7✔
220
        cv.check_type('Biasing distribution', bias, PolarAzimuthal, none_ok=True)
7✔
221
        if bias is not None:
7✔
222
            if (bias.mu.bias is not None) or (bias.phi.bias is not None):
7✔
223
                raise RuntimeError('Biasing distributions should not have their own bias.')
×
224
            elif (bias.mu.support != (-1., 1.)
7✔
225
                or not np.all(np.isclose(bias.phi.support, (0., 2*np.pi)))):
226
                raise ValueError("Biasing distribution for an isotropic "
×
227
                                 "distribution should be supported on "
228
                                 "mu=(-1.0,1.0) and phi=(0.0,2*pi).")
229

230
        self._bias = bias
7✔
231

232
    def to_xml_element(self):
7✔
233
        """Return XML representation of the isotropic distribution
234

235
        Returns
236
        -------
237
        element : lxml.etree._Element
238
            XML element containing isotropic distribution data
239

240
        """
241
        element = ET.Element('angle')
7✔
242
        element.set("type", "isotropic")
7✔
243

244
        if self.bias is not None:
7✔
245
            bias_dist = self.bias
7✔
246
            if (bias_dist.mu.bias is not None) or (bias_dist.phi.bias is not None):
7✔
247
                raise RuntimeError('Biasing distributions should not have their own bias!')
×
248
            else:
249
                bias_elem = self.bias.to_xml_element("bias")
7✔
250
                element.append(bias_elem)
7✔
251

252
        return element
7✔
253

254
    @classmethod
7✔
255
    def from_xml_element(cls, elem: ET.Element):
7✔
256
        """Generate isotropic distribution from an XML element
257

258
        Parameters
259
        ----------
260
        elem : lxml.etree._Element
261
            XML element
262

263
        Returns
264
        -------
265
        openmc.stats.Isotropic
266
            Isotropic distribution generated from XML element
267

268
        """
269
        bias_elem = elem.find('bias')
7✔
270
        if bias_elem is not None:
7✔
271
            bias_dist = PolarAzimuthal.from_xml_element(bias_elem)
7✔
272
            return cls(bias=bias_dist)
7✔
273
        else:
274
            return cls()
7✔
275

276

277

278
class Monodirectional(UnitSphere):
7✔
279
    """Monodirectional angular distribution.
280

281
    A monodirectional angular distribution is one for which the polar and
282
    azimuthal angles are always the same. It is completely specified by the
283
    reference direction vector.
284

285
    Parameters
286
    ----------
287
    reference_uvw : Iterable of float
288
        Direction from which polar angle is measured. Defaults to the positive
289
        x-direction.
290

291
    """
292

293
    def __init__(self, reference_uvw: Sequence[float] = [1., 0., 0.]):
7✔
294
        super().__init__(reference_uvw)
7✔
295

296
    def to_xml_element(self):
7✔
297
        """Return XML representation of the monodirectional distribution
298

299
        Returns
300
        -------
301
        element : lxml.etree._Element
302
            XML element containing monodirectional distribution data
303

304
        """
305
        element = ET.Element('angle')
7✔
306
        element.set("type", "monodirectional")
7✔
307
        if self.reference_uvw is not None:
7✔
308
            element.set("reference_uvw", ' '.join(map(str, self.reference_uvw)))
7✔
309
        return element
7✔
310

311
    @classmethod
7✔
312
    def from_xml_element(cls, elem: ET.Element):
7✔
313
        """Generate monodirectional distribution from an XML element
314

315
        Parameters
316
        ----------
317
        elem : lxml.etree._Element
318
            XML element
319

320
        Returns
321
        -------
322
        openmc.stats.Monodirectional
323
            Monodirectional distribution generated from XML element
324

325
        """
326
        monodirectional = cls()
7✔
327
        uvw = get_elem_list(elem, "reference_uvw", float)
7✔
328
        if uvw is not None:
7✔
329
            monodirectional.reference_uvw = uvw
7✔
330
        return monodirectional
7✔
331

332

333
class Spatial(ABC):
7✔
334
    """Distribution of locations in three-dimensional Euclidean space.
335

336
    Classes derived from this abstract class can be used for spatial
337
    distributions of source sites.
338

339
    """
340
    @abstractmethod
7✔
341
    def to_xml_element(self):
7✔
342
        return ''
×
343

344
    @classmethod
7✔
345
    @abstractmethod
7✔
346
    def from_xml_element(cls, elem, meshes=None):
7✔
347
        distribution = get_text(elem, 'type')
7✔
348
        if distribution == 'cartesian':
7✔
349
            return CartesianIndependent.from_xml_element(elem)
7✔
350
        elif distribution == 'cylindrical':
7✔
351
            return CylindricalIndependent.from_xml_element(elem)
×
352
        elif distribution == 'spherical':
7✔
353
            return SphericalIndependent.from_xml_element(elem)
×
354
        elif distribution == 'box' or distribution == 'fission':
7✔
355
            return Box.from_xml_element(elem)
7✔
356
        elif distribution == 'point':
7✔
357
            return Point.from_xml_element(elem)
7✔
358
        elif distribution == 'mesh':
7✔
359
            return MeshSpatial.from_xml_element(elem, meshes)
3✔
360
        elif distribution == 'cloud':
7✔
361
            return PointCloud.from_xml_element(elem)
7✔
362

363

364
class CartesianIndependent(Spatial):
7✔
365
    """Spatial distribution with independent x, y, and z distributions.
366

367
    This distribution allows one to specify coordinates whose x-, y-, and z-
368
    components are sampled independently from one another.
369

370
    Parameters
371
    ----------
372
    x : openmc.stats.Univariate
373
        Distribution of x-coordinates
374
    y : openmc.stats.Univariate
375
        Distribution of y-coordinates
376
    z : openmc.stats.Univariate
377
        Distribution of z-coordinates
378

379
    Attributes
380
    ----------
381
    x : openmc.stats.Univariate
382
        Distribution of x-coordinates
383
    y : openmc.stats.Univariate
384
        Distribution of y-coordinates
385
    z : openmc.stats.Univariate
386
        Distribution of z-coordinates
387

388
    """
389

390
    def __init__(
7✔
391
        self,
392
        x: openmc.stats.Univariate,
393
        y: openmc.stats.Univariate,
394
        z: openmc.stats.Univariate
395
    ):
396
        self.x = x
7✔
397
        self.y = y
7✔
398
        self.z = z
7✔
399

400
    @property
7✔
401
    def x(self):
7✔
402
        return self._x
7✔
403

404
    @x.setter
7✔
405
    def x(self, x):
7✔
406
        cv.check_type('x coordinate', x, Univariate)
7✔
407
        self._x = x
7✔
408

409
    @property
7✔
410
    def y(self):
7✔
411
        return self._y
7✔
412

413
    @y.setter
7✔
414
    def y(self, y):
7✔
415
        cv.check_type('y coordinate', y, Univariate)
7✔
416
        self._y = y
7✔
417

418
    @property
7✔
419
    def z(self):
7✔
420
        return self._z
7✔
421

422
    @z.setter
7✔
423
    def z(self, z):
7✔
424
        cv.check_type('z coordinate', z, Univariate)
7✔
425
        self._z = z
7✔
426

427
    def to_xml_element(self):
7✔
428
        """Return XML representation of the spatial distribution
429

430
        Returns
431
        -------
432
        element : lxml.etree._Element
433
            XML element containing spatial distribution data
434

435
        """
436
        element = ET.Element('space')
7✔
437
        element.set('type', 'cartesian')
7✔
438
        element.append(self.x.to_xml_element('x'))
7✔
439
        element.append(self.y.to_xml_element('y'))
7✔
440
        element.append(self.z.to_xml_element('z'))
7✔
441
        return element
7✔
442

443
    @classmethod
7✔
444
    def from_xml_element(cls, elem: ET.Element):
7✔
445
        """Generate spatial distribution from an XML element
446

447
        Parameters
448
        ----------
449
        elem : lxml.etree._Element
450
            XML element
451

452
        Returns
453
        -------
454
        openmc.stats.CartesianIndependent
455
            Spatial distribution generated from XML element
456

457
        """
458
        x = Univariate.from_xml_element(elem.find('x'))
7✔
459
        y = Univariate.from_xml_element(elem.find('y'))
7✔
460
        z = Univariate.from_xml_element(elem.find('z'))
7✔
461
        return cls(x, y, z)
7✔
462

463

464
class SphericalIndependent(Spatial):
7✔
465
    r"""Spatial distribution represented in spherical coordinates.
466

467
    This distribution allows one to specify coordinates whose :math:`r`,
468
    :math:`\theta`, and :math:`\phi` components are sampled independently
469
    from one another and centered on the coordinates (x0, y0, z0).
470

471
    .. versionadded:: 0.12
472

473
    .. versionchanged:: 0.13.1
474
        Accepts ``cos_theta`` instead of ``theta``
475

476
    Parameters
477
    ----------
478
    r : openmc.stats.Univariate
479
        Distribution of r-coordinates in a reference frame specified by
480
        the origin parameter
481
    cos_theta : openmc.stats.Univariate
482
        Distribution of the cosine of the theta-coordinates (angle relative to
483
        the z-axis) in a reference frame specified by the origin parameter
484
    phi : openmc.stats.Univariate
485
        Distribution of phi-coordinates (azimuthal angle) in a reference frame
486
        specified by the origin parameter
487
    origin: Iterable of float, optional
488
        coordinates (x0, y0, z0) of the center of the spherical reference frame
489
        for the source. Defaults to (0.0, 0.0, 0.0)
490

491
    Attributes
492
    ----------
493
    r : openmc.stats.Univariate
494
        Distribution of r-coordinates in the local reference frame
495
    cos_theta : openmc.stats.Univariate
496
        Distribution of the cosine of the theta-coordinates (angle relative to
497
        the z-axis) in the local reference frame
498
    phi : openmc.stats.Univariate
499
        Distribution of phi-coordinates (azimuthal angle) in the local
500
        reference frame
501
    origin: Iterable of float, optional
502
        coordinates (x0, y0, z0) of the center of the spherical reference
503
        frame. Defaults to (0.0, 0.0, 0.0)
504

505
    """
506

507
    def __init__(self, r, cos_theta, phi, origin=(0.0, 0.0, 0.0)):
7✔
508
        self.r = r
7✔
509
        self.cos_theta = cos_theta
7✔
510
        self.phi = phi
7✔
511
        self.origin = origin
7✔
512

513
    @property
7✔
514
    def r(self):
7✔
515
        return self._r
7✔
516

517
    @r.setter
7✔
518
    def r(self, r):
7✔
519
        cv.check_type('r coordinate', r, Univariate)
7✔
520
        self._r = r
7✔
521

522
    @property
7✔
523
    def cos_theta(self):
7✔
524
        return self._cos_theta
7✔
525

526
    @cos_theta.setter
7✔
527
    def cos_theta(self, cos_theta):
7✔
528
        cv.check_type('cos_theta coordinate', cos_theta, Univariate)
7✔
529
        self._cos_theta = cos_theta
7✔
530

531
    @property
7✔
532
    def phi(self):
7✔
533
        return self._phi
7✔
534

535
    @phi.setter
7✔
536
    def phi(self, phi):
7✔
537
        cv.check_type('phi coordinate', phi, Univariate)
7✔
538
        self._phi = phi
7✔
539

540
    @property
7✔
541
    def origin(self):
7✔
542
        return self._origin
7✔
543

544
    @origin.setter
7✔
545
    def origin(self, origin):
7✔
546
        cv.check_type('origin coordinates', origin, Iterable, Real)
7✔
547
        origin = np.asarray(origin)
7✔
548
        self._origin = origin
7✔
549

550
    def to_xml_element(self):
7✔
551
        """Return XML representation of the spatial distribution
552

553
        Returns
554
        -------
555
        element : lxml.etree._Element
556
            XML element containing spatial distribution data
557

558
        """
559
        element = ET.Element('space')
7✔
560
        element.set('type', 'spherical')
7✔
561
        element.append(self.r.to_xml_element('r'))
7✔
562
        element.append(self.cos_theta.to_xml_element('cos_theta'))
7✔
563
        element.append(self.phi.to_xml_element('phi'))
7✔
564
        element.set("origin", ' '.join(map(str, self.origin)))
7✔
565
        return element
7✔
566

567
    @classmethod
7✔
568
    def from_xml_element(cls, elem: ET.Element):
7✔
569
        """Generate spatial distribution from an XML element
570

571
        Parameters
572
        ----------
573
        elem : lxml.etree._Element
574
            XML element
575

576
        Returns
577
        -------
578
        openmc.stats.SphericalIndependent
579
            Spatial distribution generated from XML element
580

581
        """
582
        r = Univariate.from_xml_element(elem.find('r'))
×
583
        cos_theta = Univariate.from_xml_element(elem.find('cos_theta'))
×
584
        phi = Univariate.from_xml_element(elem.find('phi'))
×
585
        origin = get_elem_list(elem, "origin", float)
×
586
        return cls(r, cos_theta, phi, origin=origin)
×
587

588

589
class CylindricalIndependent(Spatial):
7✔
590
    r"""Spatial distribution represented in cylindrical coordinates.
591

592
    This distribution allows one to specify coordinates whose :math:`r`,
593
    :math:`\phi`, and :math:`z` components are sampled independently from
594
    one another and in a reference frame whose origin is specified by the
595
    coordinates (x0, y0, z0).
596

597
    .. versionadded:: 0.12
598

599
    Parameters
600
    ----------
601
    r : openmc.stats.Univariate
602
        Distribution of r-coordinates in a reference frame specified by the
603
        origin parameter
604
    phi : openmc.stats.Univariate
605
        Distribution of phi-coordinates (azimuthal angle) in a reference frame
606
        specified by the origin parameter
607
    z : openmc.stats.Univariate
608
        Distribution of z-coordinates in a reference frame specified by the
609
        origin parameter
610
    origin: Iterable of float, optional
611
        coordinates (x0, y0, z0) of the center of the cylindrical reference
612
        frame. Defaults to (0.0, 0.0, 0.0)
613
    r_dir : Iterable of float, optional
614
        Unit vector of the cylinder r axis at phi=0. Defaults to (1.0, 0.0, 0.0).        
615
    z_dir : Iterable of float, optional
616
        Unit vector of the cylinder z axis direction. Defaults to (0.0, 0.0, 1.0).    
617

618
    Attributes
619
    ----------
620
    r : openmc.stats.Univariate
621
        Distribution of r-coordinates in the local reference frame
622
    phi : openmc.stats.Univariate
623
        Distribution of phi-coordinates (azimuthal angle) in the local
624
        reference frame
625
    z : openmc.stats.Univariate
626
        Distribution of z-coordinates in the local reference frame
627
    origin: Iterable of float, optional
628
        coordinates (x0, y0, z0) of the center of the cylindrical reference
629
        frame. Defaults to (0.0, 0.0, 0.0)
630
    r_dir : Iterable of float, optional
631
        Unit vector of the cylinder r axis at phi=0. Defaults to (1.0, 0.0, 0.0).        
632
    z_dir : Iterable of float, optional
633
        Unit vector of the cylinder z axis direction. Defaults to (0.0, 0.0, 1.0).    
634

635
    """
636

637
    def __init__(self, r, phi, z, origin=(0.0, 0.0, 0.0), r_dir=(1.0, 0.0, 0.0), z_dir=(0.0, 0.0, 1.0)):
7✔
638
        self.r = r
7✔
639
        self.phi = phi
7✔
640
        self.z = z
7✔
641
        self.origin = origin
7✔
642
        self.z_dir = z_dir
7✔
643
        self.r_dir = r_dir
7✔
644

645
    @property
7✔
646
    def r(self):
7✔
647
        return self._r
7✔
648

649
    @r.setter
7✔
650
    def r(self, r):
7✔
651
        cv.check_type('r coordinate', r, Univariate)
7✔
652
        self._r = r
7✔
653

654
    @property
7✔
655
    def phi(self):
7✔
656
        return self._phi
7✔
657

658
    @phi.setter
7✔
659
    def phi(self, phi):
7✔
660
        cv.check_type('phi coordinate', phi, Univariate)
7✔
661
        self._phi = phi
7✔
662

663
    @property
7✔
664
    def z(self):
7✔
665
        return self._z
7✔
666

667
    @z.setter
7✔
668
    def z(self, z):
7✔
669
        cv.check_type('z coordinate', z, Univariate)
7✔
670
        self._z = z
7✔
671

672
    @property
7✔
673
    def origin(self):
7✔
674
        return self._origin
7✔
675

676
    @origin.setter
7✔
677
    def origin(self, origin):
7✔
678
        cv.check_type('origin coordinates', origin, Iterable, Real)
7✔
679
        origin = np.asarray(origin)
7✔
680
        self._origin = origin
7✔
681
        
682
    @property
7✔
683
    def z_dir(self):
7✔
684
        return self._z_dir
7✔
685

686
    @z_dir.setter
7✔
687
    def z_dir(self, z_dir):
7✔
688
        cv.check_type('z-axis direction', z_dir, Iterable, Real)
7✔
689
        z_dir = np.asarray(z_dir)
7✔
690
        norm2 = np.dot(z_dir,z_dir)
7✔
691
        cv.check_greater_than('z-axis direction magnitude', norm2, 0.0)
7✔
692
        z_dir /= np.sqrt(norm2)
7✔
693
        self._z_dir = z_dir
7✔
694

695
    @property
7✔
696
    def r_dir(self):
7✔
697
        return self._r_dir
7✔
698

699
    @r_dir.setter
7✔
700
    def r_dir(self, r_dir):
7✔
701
        cv.check_type('r-axis direction', r_dir, Iterable, Real)
7✔
702
        r_dir = np.asarray(r_dir)
7✔
703
        r_dir -= np.dot(r_dir,self.z_dir)*self.z_dir
7✔
704
        norm2 = np.dot(r_dir,r_dir)
7✔
705
        cv.check_greater_than('r-axis direction magnitude', norm2,0.0)
7✔
706
        r_dir /= np.sqrt(norm2)
7✔
707
        self._r_dir = r_dir        
7✔
708

709
    def to_xml_element(self):
7✔
710
        """Return XML representation of the spatial distribution
711

712
        Returns
713
        -------
714
        element : lxml.etree._Element
715
            XML element containing spatial distribution data
716

717
        """
718
        element = ET.Element('space')
7✔
719
        element.set('type', 'cylindrical')
7✔
720
        element.append(self.r.to_xml_element('r'))
7✔
721
        element.append(self.phi.to_xml_element('phi'))
7✔
722
        element.append(self.z.to_xml_element('z'))
7✔
723
        element.set("origin", ' '.join(map(str, self.origin)))
7✔
724
        element.set("r_dir", ' '.join(map(str, self.r_dir)))
7✔
725
        element.set("z_dir", ' '.join(map(str, self.z_dir)))
7✔
726
        return element
7✔
727

728
    @classmethod
7✔
729
    def from_xml_element(cls, elem: ET.Element):
7✔
730
        """Generate spatial distribution from an XML element
731

732
        Parameters
733
        ----------
734
        elem : lxml.etree._Element
735
            XML element
736

737
        Returns
738
        -------
739
        openmc.stats.CylindricalIndependent
740
            Spatial distribution generated from XML element
741

742
        """
743
        r = Univariate.from_xml_element(elem.find('r'))
×
744
        phi = Univariate.from_xml_element(elem.find('phi'))
×
745
        z = Univariate.from_xml_element(elem.find('z'))
×
746
        origin = get_elem_list(elem, "origin", float)
×
NEW
747
        r_dir = get_elem_list(elem, "r_dir", float) or [1.0, 0.0, 0.0]
×
NEW
748
        z_dir = get_elem_list(elem, "z_dir", float) or [0.0, 0.0, 1.0]
×
NEW
749
        return cls(r, phi, z, origin=origin, r_dir=r_dir, z_dir=z_dir)
×
750

751

752
class MeshSpatial(Spatial):
7✔
753
    """Spatial distribution for a mesh.
754

755
    This distribution specifies a mesh to sample over with source strengths
756
    specified for each mesh element.
757

758
    .. versionadded:: 0.13.3
759

760
    Parameters
761
    ----------
762
    mesh : openmc.MeshBase
763
        The mesh instance used for sampling
764
    strengths : iterable of float, optional
765
        An iterable of values that represents the weights of each element. If no
766
        source strengths are specified, they will be equal for all mesh
767
        elements.
768
    volume_normalized : bool, optional
769
        Whether or not the strengths will be multiplied by element volumes at
770
        runtime. Default is True.
771
    bias : iterable of float, optional
772
        An iterable of values giving the selection weights assigned to each
773
        element during biased sampling.
774

775
    Attributes
776
    ----------
777
    mesh : openmc.MeshBase
778
        The mesh instance used for sampling
779
    strengths : numpy.ndarray or None
780
        An array of source strengths for each mesh element
781
    volume_normalized : bool
782
        Whether or not the strengths will be multiplied by element volumes at
783
        runtime.
784
    bias : numpy.ndarray or None
785
        Distribution for biased sampling
786
    """
787

788
    def __init__(self, mesh, strengths=None, volume_normalized=True,
7✔
789
                 bias: Sequence[float] | None = None):
790
        self.mesh = mesh
7✔
791
        self.strengths = strengths
7✔
792
        self.volume_normalized = volume_normalized
7✔
793
        self.bias = bias
7✔
794

795
    @property
7✔
796
    def mesh(self):
7✔
797
        return self._mesh
7✔
798

799
    @mesh.setter
7✔
800
    def mesh(self, mesh):
7✔
801
        if mesh is not None:
7✔
802
            cv.check_type('mesh instance', mesh, MeshBase)
7✔
803
        self._mesh = mesh
7✔
804

805
    @property
7✔
806
    def volume_normalized(self):
7✔
807
        return self._volume_normalized
7✔
808

809
    @volume_normalized.setter
7✔
810
    def volume_normalized(self, volume_normalized):
7✔
811
        cv.check_type('Multiply strengths by element volumes', volume_normalized, bool)
7✔
812
        self._volume_normalized = volume_normalized
7✔
813

814
    @property
7✔
815
    def strengths(self):
7✔
816
        return self._strengths
7✔
817

818
    @strengths.setter
7✔
819
    def strengths(self, given_strengths):
7✔
820
        if given_strengths is not None:
7✔
821
            cv.check_type('strengths array passed in', given_strengths, Iterable, Real)
7✔
822
            self._strengths = np.asarray(given_strengths, dtype=float).flatten()
7✔
823
        else:
824
            self._strengths = None
3✔
825

826
    @property
7✔
827
    def bias(self):
7✔
828
        return self._bias
7✔
829

830
    @bias.setter
7✔
831
    def bias(self, given_bias):
7✔
832
        if given_bias is not None:
7✔
833
            cv.check_type('Biasing strengths array', given_bias, Iterable, Real)
×
834
            bias_array = np.asarray(given_bias, dtype=float).flatten()
×
835
            if bias_array.size != self.strengths.size:
×
836
                raise ValueError(
×
837
                    'Bias strengths array must have same size as strengths array.')
838
            else:
839
                self._bias = bias_array
×
840
        else:
841
            self._bias = None
7✔
842

843
    @property
7✔
844
    def num_strength_bins(self):
7✔
845
        if self.strengths is None:
×
846
            raise ValueError('Strengths are not set')
×
847
        return self.strengths.size
×
848

849
    def to_xml_element(self):
7✔
850
        """Return XML representation of the spatial distribution
851

852
        Returns
853
        -------
854
        element : lxml.etree._Element
855
            XML element containing spatial distribution data
856

857
        """
858
        element = ET.Element('space')
7✔
859

860
        element.set('type', 'mesh')
7✔
861
        element.set("mesh_id", str(self.mesh.id))
7✔
862
        element.set("volume_normalized", str(self.volume_normalized))
7✔
863

864
        if self.strengths is not None:
7✔
865
            subelement = ET.SubElement(element, 'strengths')
7✔
866
            subelement.text = ' '.join(str(e) for e in self.strengths)
7✔
867

868
        if self.bias is not None:
7✔
869
            Univariate._append_array_bias_to_xml(self, element)
×
870

871
        return element
7✔
872

873
    @classmethod
7✔
874
    def from_xml_element(cls, elem, meshes):
7✔
875
        """Generate spatial distribution from an XML element
876

877
        Parameters
878
        ----------
879
        elem : lxml.etree._Element
880
            XML element
881
        meshes : dict
882
            A dictionary with mesh IDs as keys and openmc.MeshBase instances as
883
            values
884

885
        Returns
886
        -------
887
        openmc.stats.MeshSpatial
888
            Spatial distribution generated from XML element
889

890
        """
891

892
        mesh_id = int(get_text(elem, "mesh_id"))
3✔
893

894
        # check if this mesh has been read in from another location already
895
        if mesh_id not in meshes:
3✔
896
            raise ValueError(f'Could not locate mesh with ID "{mesh_id}"')
×
897

898
        volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true'
3✔
899
        strengths = get_elem_list(elem, 'strengths', float)
3✔
900
        bias_strengths = Univariate._read_array_bias_from_xml(elem)
3✔
901
        return cls(meshes[mesh_id], strengths, volume_normalized, bias=bias_strengths)
3✔
902

903

904
class PointCloud(Spatial):
7✔
905
    """Spatial distribution from a point cloud.
906

907
    This distribution specifies a discrete list of points, with corresponding
908
    relative probabilities.
909

910
    .. versionadded:: 0.15.1
911

912
    Parameters
913
    ----------
914
    positions : iterable of 3-tuples
915
        The points in space to be sampled
916
    strengths : iterable of float, optional
917
        An iterable of values that represents the relative probabilty of each
918
        point.
919
    bias : iterable of float, optional
920
        An iterable of values representing the relative probability of each
921
        point under biased sampling.
922

923
    Attributes
924
    ----------
925
    positions : numpy.ndarray
926
        The points in space to be sampled with shape (N, 3)
927
    strengths : numpy.ndarray or None
928
        An array of relative probabilities for each mesh point
929
    bias : numpy.ndarray or None
930
        An array of relative probabilities for biased sampling of mesh points
931
    """
932

933
    def __init__(
7✔
934
        self,
935
        positions: Sequence[Sequence[float]],
936
        strengths: Sequence[float] | None = None,
937
        bias: Sequence[float] | None = None
938
    ):
939
        self.positions = positions
7✔
940
        self.strengths = strengths
7✔
941
        self.bias = bias
7✔
942

943
    @property
7✔
944
    def positions(self) -> np.ndarray:
7✔
945
        return self._positions
7✔
946

947
    @positions.setter
7✔
948
    def positions(self, positions):
7✔
949
        positions = np.array(positions, dtype=float)
7✔
950
        if positions.ndim != 2:
7✔
951
            raise ValueError('positions must be a 2D array')
7✔
952
        elif positions.shape[1] != 3:
7✔
953
            raise ValueError('Each position must have 3 values')
7✔
954
        self._positions = positions
7✔
955

956
    @property
7✔
957
    def strengths(self) -> np.ndarray:
7✔
958
        return self._strengths
7✔
959

960
    @strengths.setter
7✔
961
    def strengths(self, strengths):
7✔
962
        if strengths is not None:
7✔
963
            strengths = np.array(strengths, dtype=float)
7✔
964
            if strengths.ndim != 1:
7✔
965
                raise ValueError('strengths must be a 1D array')
7✔
966
            elif strengths.size != self.positions.shape[0]:
7✔
967
                raise ValueError('strengths must have the same length as positions')
7✔
968
        self._strengths = strengths
7✔
969

970
    @property
7✔
971
    def bias(self):
7✔
972
        return self._bias
7✔
973

974
    @bias.setter
7✔
975
    def bias(self, given_bias):
7✔
976
        if given_bias is not None:
7✔
977
            cv.check_type('Biasing strengths array', given_bias, Iterable, Real)
×
978
            bias_array = np.asarray(given_bias, dtype=float).flatten()
×
979
            if bias_array.size != self.strengths.size:
×
980
                raise ValueError(
×
981
                    'Bias strengths array must have same size as strengths array.')
982
            else:
983
                self._bias = bias_array
×
984
        else:
985
            self._bias = None
7✔
986

987
    @property
7✔
988
    def num_strength_bins(self) -> int:
7✔
989
        if self.strengths is None:
×
990
            raise ValueError('Strengths are not set')
×
991
        return self.strengths.size
×
992

993
    def to_xml_element(self) -> ET.Element:
7✔
994
        """Return XML representation of the spatial distribution
995

996
        Returns
997
        -------
998
        element : lxml.etree._Element
999
            XML element containing spatial distribution data
1000

1001
        """
1002
        element = ET.Element('space')
7✔
1003
        element.set('type', 'cloud')
7✔
1004

1005
        subelement = ET.SubElement(element, 'coords')
7✔
1006
        subelement.text = ' '.join(str(e) for e in self.positions.flatten())
7✔
1007

1008
        if self.strengths is not None:
7✔
1009
            subelement = ET.SubElement(element, 'strengths')
7✔
1010
            subelement.text = ' '.join(str(e) for e in self.strengths)
7✔
1011

1012
        if self.bias is not None:
7✔
1013
            Univariate._append_array_bias_to_xml(self, element)
×
1014

1015
        return element
7✔
1016

1017
    @classmethod
7✔
1018
    def from_xml_element(cls, elem: ET.Element) -> PointCloud:
7✔
1019
        """Generate spatial distribution from an XML element
1020

1021
        Parameters
1022
        ----------
1023
        elem : lxml.etree._Element
1024
            XML element
1025

1026
        Returns
1027
        -------
1028
        openmc.stats.PointCloud
1029
            Spatial distribution generated from XML element
1030

1031

1032
        """
1033
        coord_data = get_elem_list(elem, 'coords', float)
7✔
1034
        positions = np.array(coord_data).reshape((-1, 3))
7✔
1035

1036
        strengths = get_elem_list(elem, 'strengths', float)
7✔
1037
        bias_strengths = Univariate._read_array_bias_from_xml(elem)
7✔
1038
        return cls(positions, strengths, bias=bias_strengths)
7✔
1039

1040

1041
class Box(Spatial):
7✔
1042
    """Uniform distribution of coordinates in a rectangular cuboid.
1043

1044
    Parameters
1045
    ----------
1046
    lower_left : Iterable of float
1047
        Lower-left coordinates of cuboid
1048
    upper_right : Iterable of float
1049
        Upper-right coordinates of cuboid
1050
    only_fissionable : bool, optional
1051
        Whether spatial sites should only be accepted if they occur in
1052
        fissionable materials
1053

1054
        .. deprecated:: 0.15.0
1055
            Use the `constraints` argument when defining a source object instead.
1056

1057
    Attributes
1058
    ----------
1059
    lower_left : Iterable of float
1060
        Lower-left coordinates of cuboid
1061
    upper_right : Iterable of float
1062
        Upper-right coordinates of cuboid
1063
    only_fissionable : bool, optional
1064
        Whether spatial sites should only be accepted if they occur in
1065
        fissionable materials
1066

1067
        .. deprecated:: 0.15.0
1068
            Use the `constraints` argument when defining a source object instead.
1069

1070
    """
1071

1072
    def __init__(
7✔
1073
        self,
1074
        lower_left: Sequence[float],
1075
        upper_right: Sequence[float],
1076
        only_fissionable: bool = False
1077
    ):
1078
        self.lower_left = lower_left
7✔
1079
        self.upper_right = upper_right
7✔
1080
        self.only_fissionable = only_fissionable
7✔
1081

1082
    @property
7✔
1083
    def lower_left(self):
7✔
1084
        return self._lower_left
7✔
1085

1086
    @lower_left.setter
7✔
1087
    def lower_left(self, lower_left):
7✔
1088
        cv.check_type('lower left coordinate', lower_left, Iterable, Real)
7✔
1089
        cv.check_length('lower left coordinate', lower_left, 3)
7✔
1090
        self._lower_left = lower_left
7✔
1091

1092
    @property
7✔
1093
    def upper_right(self):
7✔
1094
        return self._upper_right
7✔
1095

1096
    @upper_right.setter
7✔
1097
    def upper_right(self, upper_right):
7✔
1098
        cv.check_type('upper right coordinate', upper_right, Iterable, Real)
7✔
1099
        cv.check_length('upper right coordinate', upper_right, 3)
7✔
1100
        self._upper_right = upper_right
7✔
1101

1102
    @property
7✔
1103
    def only_fissionable(self):
7✔
1104
        return self._only_fissionable
7✔
1105

1106
    @only_fissionable.setter
7✔
1107
    def only_fissionable(self, only_fissionable):
7✔
1108
        cv.check_type('only fissionable', only_fissionable, bool)
7✔
1109
        self._only_fissionable = only_fissionable
7✔
1110
        if only_fissionable:
7✔
1111
            warn("The 'only_fissionable' has been deprecated. Use the "
×
1112
                 "'constraints' argument when defining a source instead.",
1113
                 FutureWarning)
1114

1115
    def to_xml_element(self):
7✔
1116
        """Return XML representation of the box distribution
1117

1118
        Returns
1119
        -------
1120
        element : lxml.etree._Element
1121
            XML element containing box distribution data
1122

1123
        """
1124
        element = ET.Element('space')
7✔
1125
        if self.only_fissionable:
7✔
1126
            element.set("type", "fission")
×
1127
        else:
1128
            element.set("type", "box")
7✔
1129
        params = ET.SubElement(element, "parameters")
7✔
1130
        params.text = ' '.join(map(str, self.lower_left)) + ' ' + \
7✔
1131
                      ' '.join(map(str, self.upper_right))
1132
        return element
7✔
1133

1134
    @classmethod
7✔
1135
    def from_xml_element(cls, elem: ET.Element):
7✔
1136
        """Generate box distribution from an XML element
1137

1138
        Parameters
1139
        ----------
1140
        elem : lxml.etree._Element
1141
            XML element
1142

1143
        Returns
1144
        -------
1145
        openmc.stats.Box
1146
            Box distribution generated from XML element
1147

1148
        """
1149
        only_fissionable = get_text(elem, 'type') == 'fission'
7✔
1150
        params = get_elem_list(elem, "parameters", float)
7✔
1151
        lower_left = params[:len(params)//2]
7✔
1152
        upper_right = params[len(params)//2:]
7✔
1153
        return cls(lower_left, upper_right, only_fissionable)
7✔
1154

1155

1156
class Point(Spatial):
7✔
1157
    """Delta function in three dimensions.
1158

1159
    This spatial distribution can be used for a point source where sites are
1160
    emitted at a specific location given by its Cartesian coordinates.
1161

1162
    Parameters
1163
    ----------
1164
    xyz : Iterable of float, optional
1165
        Cartesian coordinates of location. Defaults to (0., 0., 0.).
1166

1167
    Attributes
1168
    ----------
1169
    xyz : Iterable of float
1170
        Cartesian coordinates of location
1171

1172
    """
1173

1174
    def __init__(self, xyz: Sequence[float] = (0., 0., 0.)):
7✔
1175
        self.xyz = xyz
7✔
1176

1177
    @property
7✔
1178
    def xyz(self):
7✔
1179
        return self._xyz
7✔
1180

1181
    @xyz.setter
7✔
1182
    def xyz(self, xyz):
7✔
1183
        cv.check_type('coordinate', xyz, Iterable, Real)
7✔
1184
        cv.check_length('coordinate', xyz, 3)
7✔
1185
        self._xyz = xyz
7✔
1186

1187
    def to_xml_element(self):
7✔
1188
        """Return XML representation of the point distribution
1189

1190
        Returns
1191
        -------
1192
        element : lxml.etree._Element
1193
            XML element containing point distribution location
1194

1195
        """
1196
        element = ET.Element('space')
7✔
1197
        element.set("type", "point")
7✔
1198
        params = ET.SubElement(element, "parameters")
7✔
1199
        params.text = ' '.join(map(str, self.xyz))
7✔
1200
        return element
7✔
1201

1202
    @classmethod
7✔
1203
    def from_xml_element(cls, elem: ET.Element):
7✔
1204
        """Generate point distribution from an XML element
1205

1206
        Parameters
1207
        ----------
1208
        elem : lxml.etree._Element
1209
            XML element
1210

1211
        Returns
1212
        -------
1213
        openmc.stats.Point
1214
            Point distribution generated from XML element
1215

1216
        """
1217
        xyz = get_elem_list(elem, "parameters", float)
7✔
1218
        return cls(xyz)
7✔
1219

1220

1221
def spherical_uniform(
7✔
1222
        r_outer: float,
1223
        r_inner: float = 0.0,
1224
        thetas: Sequence[float] = (0., pi),
1225
        phis: Sequence[float] = (0., 2*pi),
1226
        origin: Sequence[float] = (0., 0., 0.)
1227
    ):
1228
    """Return a uniform spatial distribution over a spherical shell.
1229

1230
    This function provides a uniform spatial distribution over a spherical
1231
    shell between `r_inner` and `r_outer`. Optionally, the range of angles
1232
    can be restricted by the `thetas` and `phis` arguments.
1233

1234
    .. versionadded:: 0.13.1
1235

1236
    Parameters
1237
    ----------
1238
    r_outer : float
1239
        Outer radius of the spherical shell in [cm]
1240
    r_inner : float
1241
        Inner radius of the spherical shell in [cm]
1242
    thetas : iterable of float
1243
        Starting and ending theta coordinates (angle relative to
1244
        the z-axis) in radius in a reference frame centered at `origin`
1245
    phis : iterable of float
1246
        Starting and ending phi coordinates (azimuthal angle) in
1247
        radians in a reference frame centered at `origin`
1248
    origin: iterable of float
1249
        Coordinates (x0, y0, z0) of the center of the spherical
1250
        reference frame for the distribution.
1251

1252
    Returns
1253
    -------
1254
    openmc.stats.SphericalIndependent
1255
        Uniform distribution over the spherical shell
1256
    """
1257

1258
    r_dist = PowerLaw(r_inner, r_outer, 2)
7✔
1259
    cos_thetas_dist = Uniform(cos(thetas[1]), cos(thetas[0]))
7✔
1260
    phis_dist = Uniform(phis[0], phis[1])
7✔
1261

1262
    return SphericalIndependent(r_dist, cos_thetas_dist, phis_dist, origin)
7✔
1263

1264
    
1265
def cylindrical_uniform(
7✔
1266
        r_outer: float,
1267
        height: float,
1268
        r_inner: float = 0.0,
1269
        phis: Sequence[float] = (0., 2*pi),
1270
        origin: Sequence[float] = (0., 0., 0.),
1271
        r_dir: Sequence[float] = (1., 0., 0.),
1272
        z_dir: Sequence[float] = (0., 0., 1.),
1273
    ):
1274
    """Return a uniform spatial distribution over a cylindrical shell.
1275

1276
    This function provides a uniform spatial distribution over a cylindrical
1277
    shell between `r_inner` and `r_outer`. Optionally, the range of angles
1278
    can be restricted by the `phis` arguments.
1279

1280

1281
    Parameters
1282
    ----------
1283
    r_outer : float
1284
        Outer radius of the cylindrical shell in [cm]
1285
    height : float
1286
        Height of the cylindrical shell in [cm]        
1287
    r_inner : float
1288
        Inner radius of the cylindrical shell in [cm]
1289
    phis : iterable of float
1290
        Starting and ending phi coordinates (azimuthal angle) in
1291
        radians in a reference frame centered at `origin`        
1292
    origin: iterable of float
1293
        Coordinates (x0, y0, z0) of the center of the cylindrical
1294
        reference frame for the distribution. Defaults to (0.0, 0.0, 0.0)
1295
    r_dir : iterable of float
1296
        Direction of the r-axis at phi=0. Defaults to (1.0, 0.0, 0.0)
1297
    z_dir : 
1298
        Direction of the z-axis. Defaults to (0.0, 0.0, 1.0)
1299
    
1300
    Returns
1301
    -------
1302
    openmc.stats.CylindricalIndependent
1303
        Uniform distribution over the cylindrical shell
1304
    """
1305

1306
    r_dist = PowerLaw(r_inner, r_outer, 1)
7✔
1307
    phis_dist = Uniform(phis[0], phis[1])
7✔
1308
    z_dist = Uniform(-height/2, height/2)
7✔
1309
    
1310
    return CylindricalIndependent(r_dist, phis_dist, z_dist, origin, r_dir, z_dir) 
7✔
1311
    
1312
def ring_uniform(
7✔
1313
        r_outer: float,
1314
        r_inner: float = 0.0,
1315
        phis: Sequence[float] = (0., 2*pi),
1316
        origin: Sequence[float] = (0., 0., 0.),
1317
        r_dir: Sequence[float] = (1., 0., 0.),
1318
        z_dir: Sequence[float] = (0., 0., 1.),
1319
    ):
1320
    """Return a uniform spatial distribution over a ring.
1321

1322
    This function provides a uniform spatial distribution over a ring
1323
    shell between `r_inner` and `r_outer`. Optionally, the range of angles
1324
    can be restricted by the `phis` arguments.
1325

1326

1327
    Parameters
1328
    ----------
1329
    r_outer : float
1330
        Outer radius of the ring in [cm]       
1331
    r_inner : float
1332
        Inner radius of the ring in [cm]
1333
    phis : iterable of float
1334
        Starting and ending phi coordinates (azimuthal angle) in
1335
        radians in a reference frame centered at `origin`        
1336
    origin: iterable of float
1337
        Coordinates (x0, y0, z0) of the center of the cylindrical
1338
        reference frame for the distribution. Defaults to (0.0, 0.0, 0.0)
1339
    r_dir : iterable of float
1340
        Direction of the r-axis at phi=0. Defaults to (1.0, 0.0, 0.0)
1341
    z_dir : 
1342
        Direction of the z-axis. Defaults to (0.0, 0.0, 1.0)
1343
    
1344
    Returns
1345
    -------
1346
    openmc.stats.CylindricalIndependent
1347
        Uniform distribution over the ring
1348
    """
1349

1350
    r_dist = PowerLaw(r_inner, r_outer, 1)
7✔
1351
    phis_dist = Uniform(phis[0], phis[1])
7✔
1352
    z_dist = delta_function(np.dot(origin,z_dir))
7✔
1353
    
1354
    return CylindricalIndependent(r_dist, phis_dist, z_dist, origin, r_dir, z_dir)          
7✔
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