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

openmc-dev / openmc / 12776996362

14 Jan 2025 09:49PM UTC coverage: 84.938% (+0.2%) from 84.729%
12776996362

Pull #3133

github

web-flow
Merge 0495246d9 into 549cc0973
Pull Request #3133: Kinetics parameters using Iterated Fission Probability

318 of 330 new or added lines in 10 files covered. (96.36%)

1658 existing lines in 66 files now uncovered.

50402 of 59340 relevant lines covered (84.94%)

33987813.96 hits per line

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

93.94
/openmc/cell.py
1
from collections.abc import Iterable
12✔
2
from math import cos, sin, pi
12✔
3
from numbers import Real
12✔
4

5
import lxml.etree as ET
12✔
6
import numpy as np
12✔
7
from uncertainties import UFloat
12✔
8

9
import openmc
12✔
10
import openmc.checkvalue as cv
12✔
11
from ._xml import get_text
12✔
12
from .mixin import IDManagerMixin
12✔
13
from .region import Region, Complement
12✔
14
from .surface import Halfspace
12✔
15
from .bounding_box import BoundingBox
12✔
16

17

18
class Cell(IDManagerMixin):
12✔
19
    r"""A region of space defined as the intersection of half-space created by
20
    quadric surfaces.
21

22
    Parameters
23
    ----------
24
    cell_id : int, optional
25
        Unique identifier for the cell. If not specified, an identifier will
26
        automatically be assigned.
27
    name : str, optional
28
        Name of the cell. If not specified, the name is the empty string.
29
    fill : openmc.Material or openmc.UniverseBase or openmc.Lattice or None or iterable of openmc.Material, optional
30
        Indicates what the region of space is filled with
31
    region : openmc.Region, optional
32
        Region of space that is assigned to the cell.
33

34
    Attributes
35
    ----------
36
    id : int
37
        Unique identifier for the cell
38
    name : str
39
        Name of the cell
40
    fill : openmc.Material or openmc.UniverseBase or openmc.Lattice or None or iterable of openmc.Material
41
        Indicates what the region of space is filled with. If None, the cell is
42
        treated as a void. An iterable of materials is used to fill repeated
43
        instances of a cell with different materials.
44
    fill_type : {'material', 'universe', 'lattice', 'distribmat', 'void'}
45
        Indicates what the cell is filled with.
46
    region : openmc.Region or None
47
        Region of space that is assigned to the cell.
48
    rotation : Iterable of float
49
        If the cell is filled with a universe, this array specifies the angles
50
        in degrees about the x, y, and z axes that the filled universe should be
51
        rotated. The rotation applied is an intrinsic rotation with specified
52
        Tait-Bryan angles. That is to say, if the angles are :math:`(\phi,
53
        \theta, \psi)`, then the rotation matrix applied is :math:`R_z(\psi)
54
        R_y(\theta) R_x(\phi)` or
55

56
        .. math::
57

58
           \left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\phi \sin\psi
59
           + \sin\phi \sin\theta \cos\psi & \sin\phi \sin\psi + \cos\phi
60
           \sin\theta \cos\psi \\ \cos\theta \sin\psi & \cos\phi \cos\psi +
61
           \sin\phi \sin\theta \sin\psi & -\sin\phi \cos\psi + \cos\phi
62
           \sin\theta \sin\psi \\ -\sin\theta & \sin\phi \cos\theta & \cos\phi
63
           \cos\theta \end{array} \right ]
64

65
        A rotation matrix can also be specified directly by setting this
66
        attribute to a nested list (or 2D numpy array) that specifies each
67
        element of the matrix.
68
    rotation_matrix : numpy.ndarray
69
        The rotation matrix defined by the angles specified in the
70
        :attr:`Cell.rotation` property.
71
    temperature : float or iterable of float
72
        Temperature of the cell in Kelvin.  Multiple temperatures can be given
73
        to give each distributed cell instance a unique temperature.
74
    translation : Iterable of float
75
        If the cell is filled with a universe, this array specifies a vector
76
        that is used to translate (shift) the universe.
77
    paths : list of str
78
        The paths traversed through the CSG tree to reach each cell
79
        instance. This property is initialized by calling the
80
        :meth:`Geometry.determine_paths` method.
81
    num_instances : int
82
        The number of instances of this cell throughout the geometry.
83
    volume : float
84
        Volume of the cell in cm^3. This can either be set manually or
85
        calculated in a stochastic volume calculation and added via the
86
        :meth:`Cell.add_volume_information` method. For 'distribmat' cells
87
        it is the total volume of all instances.
88
    atoms : dict
89
        Mapping of nuclides to the total number of atoms for each nuclide
90
        present in the cell, or in all of its instances for a 'distribmat'
91
        fill. For example, {'U235': 1.0e22, 'U238': 5.0e22, ...}.
92

93
        .. versionadded:: 0.12
94
    bounding_box : openmc.BoundingBox
95
        Axis-aligned bounding box of the cell
96

97
    """
98

99
    next_id = 1
12✔
100
    used_ids = set()
12✔
101

102
    def __init__(self, cell_id=None, name='', fill=None, region=None):
12✔
103
        # Initialize Cell class attributes
104
        self.id = cell_id
12✔
105
        self.name = name
12✔
106
        self.fill = fill
12✔
107
        self.region = region
12✔
108
        self._rotation = None
12✔
109
        self._rotation_matrix = None
12✔
110
        self._temperature = None
12✔
111
        self._translation = None
12✔
112
        self._paths = None
12✔
113
        self._num_instances = None
12✔
114
        self._volume = None
12✔
115
        self._atoms = None
12✔
116

117
    def __contains__(self, point):
12✔
118
        if self.region is None:
12✔
119
            return True
12✔
120
        else:
121
            return point in self.region
12✔
122

123
    def __repr__(self):
12✔
124
        string = 'Cell\n'
12✔
125
        string += '{: <16}=\t{}\n'.format('\tID', self.id)
12✔
126
        string += '{: <16}=\t{}\n'.format('\tName', self.name)
12✔
127

128
        if self.fill_type == 'material':
12✔
129
            string += '{: <16}=\tMaterial {}\n'.format('\tFill', self.fill.id)
12✔
130
        elif self.fill_type == 'void':
12✔
131
            string += '{: <16}=\tNone\n'.format('\tFill')
12✔
132
        elif self.fill_type == 'distribmat':
12✔
133
            string += '{: <16}=\t{}\n'.format('\tFill', list(map(
12✔
134
                lambda m: m if m is None else m.id, self.fill)))
135
        else:
136
            string += '{: <16}=\t{}\n'.format('\tFill', self.fill.id)
12✔
137

138
        string += '{: <16}=\t{}\n'.format('\tRegion', self.region)
12✔
139
        string += '{: <16}=\t{}\n'.format('\tRotation', self.rotation)
12✔
140
        if self.fill_type == 'material':
12✔
141
            string += '\t{0: <15}=\t{1}\n'.format('Temperature',
12✔
142
                                                  self.temperature)
143
        string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation)
12✔
144
        string += '{: <16}=\t{}\n'.format('\tVolume', self.volume)
12✔
145

146
        return string
12✔
147

148
    @property
12✔
149
    def name(self):
12✔
150
        return self._name
12✔
151

152
    @name.setter
12✔
153
    def name(self, name):
12✔
154
        if name is not None:
12✔
155
            cv.check_type('cell name', name, str)
12✔
156
            self._name = name
12✔
157
        else:
158
            self._name = ''
12✔
159

160
    @property
12✔
161
    def fill(self):
12✔
162
        return self._fill
12✔
163

164
    @fill.setter
12✔
165
    def fill(self, fill):
12✔
166
        if fill is not None:
12✔
167
            if isinstance(fill, Iterable):
12✔
168
                for i, f in enumerate(fill):
12✔
169
                    if f is not None:
12✔
170
                        cv.check_type('cell.fill[i]', f, openmc.Material)
12✔
171

172
            elif not isinstance(fill, (openmc.Material, openmc.Lattice,
12✔
173
                                       openmc.UniverseBase)):
174
                msg = (f'Unable to set Cell ID="{self._id}" to use a '
×
175
                       f'non-Material or Universe fill "{fill}"')
176
                raise ValueError(msg)
×
177
        self._fill = fill
12✔
178

179
        # Info about atom content can now be invalid
180
        # (since fill has just changed)
181
        self._atoms = None
12✔
182

183
    @property
12✔
184
    def fill_type(self):
12✔
185
        if isinstance(self.fill, openmc.Material):
12✔
186
            return 'material'
12✔
187
        elif isinstance(self.fill, openmc.UniverseBase):
12✔
188
            return 'universe'
12✔
189
        elif isinstance(self.fill, openmc.Lattice):
12✔
190
            return 'lattice'
12✔
191
        elif isinstance(self.fill, Iterable):
12✔
192
            return 'distribmat'
12✔
193
        else:
194
            return 'void'
12✔
195

196
    @property
12✔
197
    def region(self):
12✔
198
        return self._region
12✔
199

200
    @region.setter
12✔
201
    def region(self, region):
12✔
202
        if region is not None:
12✔
203
            cv.check_type('cell region', region, Region)
12✔
204
        self._region = region
12✔
205

206
    @property
12✔
207
    def rotation(self):
12✔
208
        return self._rotation
12✔
209

210
    @rotation.setter
12✔
211
    def rotation(self, rotation):
12✔
212
        cv.check_length('cell rotation', rotation, 3)
12✔
213
        self._rotation = np.asarray(rotation)
12✔
214

215
        # Save rotation matrix -- the reason we do this instead of having it be
216
        # automatically calculated when the rotation_matrix property is accessed
217
        # is so that plotting on a rotated geometry can be done faster.
218
        if self._rotation.ndim == 2:
12✔
219
            # User specified rotation matrix directly
220
            self._rotation_matrix = self._rotation
12✔
221
        else:
222
            phi, theta, psi = self.rotation*(-pi/180.)
12✔
223
            c3, s3 = cos(phi), sin(phi)
12✔
224
            c2, s2 = cos(theta), sin(theta)
12✔
225
            c1, s1 = cos(psi), sin(psi)
12✔
226
            self._rotation_matrix = np.array([
12✔
227
                [c1*c2, c1*s2*s3 - c3*s1, s1*s3 + c1*c3*s2],
228
                [c2*s1, c1*c3 + s1*s2*s3, c3*s1*s2 - c1*s3],
229
                [-s2, c2*s3, c2*c3]])
230

231
    @property
12✔
232
    def rotation_matrix(self):
12✔
233
        return self._rotation_matrix
12✔
234

235
    @property
12✔
236
    def temperature(self):
12✔
237
        return self._temperature
12✔
238

239
    @temperature.setter
12✔
240
    def temperature(self, temperature):
12✔
241
        # Make sure temperatures are positive
242
        cv.check_type('cell temperature', temperature, (Iterable, Real), none_ok=True)
12✔
243
        if isinstance(temperature, Iterable):
12✔
244
            cv.check_type('cell temperature', temperature, Iterable, Real)
12✔
245
            for T in temperature:
12✔
246
                cv.check_greater_than('cell temperature', T, 0.0, True)
12✔
247
        elif isinstance(temperature, Real):
12✔
248
            cv.check_greater_than('cell temperature', temperature, 0.0, True)
12✔
249

250
        # If this cell is filled with a universe or lattice, propagate
251
        # temperatures to all cells contained. Otherwise, simply assign it.
252
        if self.fill_type in ('universe', 'lattice'):
12✔
253
            for c in self.get_all_cells().values():
12✔
254
                if c.fill_type == 'material':
12✔
255
                    c._temperature = temperature
12✔
256
        else:
257
            self._temperature = temperature
12✔
258

259
    @property
12✔
260
    def translation(self):
12✔
261
        return self._translation
12✔
262

263
    @translation.setter
12✔
264
    def translation(self, translation):
12✔
265
        cv.check_type('cell translation', translation, Iterable, Real)
12✔
266
        cv.check_length('cell translation', translation, 3)
12✔
267
        self._translation = np.asarray(translation)
12✔
268

269
    @property
12✔
270
    def volume(self):
12✔
271
        return self._volume
12✔
272

273
    @volume.setter
12✔
274
    def volume(self, volume):
12✔
275
        if volume is not None:
12✔
276
            cv.check_type('cell volume', volume, (Real, UFloat))
12✔
277
            cv.check_greater_than('cell volume', volume, 0.0, equality=True)
12✔
278

279
        self._volume = volume
12✔
280

281
        # Info about atom content can now be invalid
282
        # (since volume has just changed)
283
        self._atoms = None
12✔
284

285
    @property
12✔
286
    def atoms(self):
12✔
287
        if self._atoms is None:
12✔
288
            if self._volume is None:
12✔
289
                msg = ('Cannot calculate atom content because no volume '
12✔
290
                       'is set. Use Cell.volume to provide it or perform '
291
                       'a stochastic volume calculation.')
292
                raise ValueError(msg)
12✔
293

294
            elif self.fill_type == 'void':
12✔
295
                msg = ('Cell is filled with void. It contains no atoms. '
12✔
296
                       'Material must be set to calculate atom content.')
297
                raise ValueError(msg)
12✔
298

299
            elif self.fill_type in ['lattice', 'universe']:
12✔
300
                msg = ('Universe and Lattice cells can contain multiple '
12✔
301
                       'materials in diffrent proportions. Atom content must '
302
                       'be calculated with stochastic volume calculation.')
303
                raise ValueError(msg)
12✔
304

305
            elif self.fill_type == 'material':
12✔
306
                # Get atomic densities
307
                self._atoms = self._fill.get_nuclide_atom_densities()
12✔
308

309
                # Convert to total number of atoms
310
                for key, atom_per_bcm in self._atoms.items():
12✔
311
                    atom = atom_per_bcm * self._volume * 1.0e+24
12✔
312
                    self._atoms[key] = atom
12✔
313

314
            elif self.fill_type == 'distribmat':
12✔
315
                # Assumes that volume is total volume of all instances
316
                # Also assumes that all instances have the same volume
317
                partial_volume = self.volume / len(self.fill)
12✔
318
                self._atoms = {}
12✔
319
                for mat in self.fill:
12✔
320
                    for key, atom_per_bcm in mat.get_nuclide_atom_densities().items():
12✔
321
                        # To account for overlap of nuclides between distribmat
322
                        # we need to append new atoms to any existing value
323
                        # hence it is necessary to ask for default.
324
                        atom = self._atoms.setdefault(key, 0)
12✔
325
                        atom += atom_per_bcm * partial_volume * 1.0e+24
12✔
326
                        self._atoms[key] = atom
12✔
327

328
            else:
329
                msg = f'Unrecognized fill_type: {self.fill_type}'
×
330
                raise ValueError(msg)
×
331

332
        return self._atoms
12✔
333

334
    @property
12✔
335
    def paths(self):
12✔
336
        if self._paths is None:
12✔
337
            raise ValueError('Cell instance paths have not been determined. '
×
338
                             'Call the Geometry.determine_paths() method.')
339
        return self._paths
12✔
340

341
    @property
12✔
342
    def bounding_box(self):
12✔
343
        if self.region is not None:
12✔
344
            return self.region.bounding_box
12✔
345
        else:
346
            return BoundingBox.infinite()
12✔
347

348
    @property
12✔
349
    def num_instances(self):
12✔
350
        if self._num_instances is None:
12✔
351
            raise ValueError(
×
352
                'Number of cell instances have not been determined. Call the '
353
                'Geometry.determine_paths() method.')
354
        return self._num_instances
12✔
355

356
    def add_volume_information(self, volume_calc):
12✔
357
        """Add volume information to a cell.
358

359
        Parameters
360
        ----------
361
        volume_calc : openmc.VolumeCalculation
362
            Results from a stochastic volume calculation
363

364
        """
365
        if volume_calc.domain_type == 'cell':
12✔
366
            if self.id in volume_calc.volumes:
12✔
367
                self._volume = volume_calc.volumes[self.id].n
12✔
368
                self._atoms = volume_calc.atoms[self.id]
12✔
369
            else:
370
                raise ValueError('No volume information found for this cell.')
×
371
        else:
372
            raise ValueError('No volume information found for this cell.')
×
373

374
    def get_nuclides(self):
12✔
375
        """Returns all nuclides in the cell
376

377
        Returns
378
        -------
379
        nuclides : list of str
380
            List of nuclide names
381

382
        """
383
        return self.fill.get_nuclides() if self.fill_type != 'void' else []
12✔
384

385
    def get_nuclide_densities(self):
12✔
386
        """Return all nuclides contained in the cell and their densities
387

388
        Returns
389
        -------
390
        nuclides : dict
391
            Dictionary whose keys are nuclide names and values are 2-tuples of
392
            (nuclide, density)
393

394
        """
395

396
        nuclides = {}
12✔
397

398
        if self.fill_type == 'material':
12✔
399
            nuclides.update(self.fill.get_nuclide_densities())
12✔
400
        elif self.fill_type == 'void':
12✔
401
            pass
12✔
402
        else:
403
            if self._atoms is not None:
×
404
                volume = self.volume
×
405
                for name, atoms in self._atoms.items():
×
406
                    nuclide = openmc.Nuclide(name)
×
407
                    density = 1.0e-24 * atoms.n/volume  # density in atoms/b-cm
×
408
                    nuclides[name] = (nuclide, density)
×
409
            else:
410
                raise RuntimeError(
×
411
                    'Volume information is needed to calculate microscopic '
412
                    f'cross sections for cell {self.id}. This can be done by '
413
                    'running a stochastic volume calculation via the '
414
                    'openmc.VolumeCalculation object')
415

416
        return nuclides
12✔
417

418
    def get_all_cells(self, memo=None):
12✔
419
        """Return all cells that are contained within this one if it is filled with a
420
        universe or lattice
421

422
        Returns
423
        -------
424
        cells : dict
425
            Dictionary whose keys are cell IDs and values are :class:`Cell`
426
            instances
427

428
        """
429
        if memo is None:
12✔
430
            memo = set()
12✔
431
        elif self in memo:
12✔
432
            return {}
12✔
433
        memo.add(self)
12✔
434

435
        cells = {}
12✔
436
        if self.fill_type in ('universe', 'lattice'):
12✔
437
            cells.update(self.fill.get_all_cells(memo))
12✔
438

439
        return cells
12✔
440

441
    def get_all_materials(self, memo=None):
12✔
442
        """Return all materials that are contained within the cell
443

444
        Returns
445
        -------
446
        materials : dict
447
            Dictionary whose keys are material IDs and values are
448
            :class:`Material` instances
449

450
        """
451
        materials = {}
12✔
452
        if self.fill_type == 'material':
12✔
453
            materials[self.fill.id] = self.fill
12✔
454
        elif self.fill_type == 'distribmat':
12✔
455
            for m in self.fill:
12✔
456
                if m is not None:
12✔
457
                    materials[m.id] = m
12✔
458
        else:
459
            # Append all Cells in each Cell in the Universe to the dictionary
460
            cells = self.get_all_cells(memo)
12✔
461
            for cell in cells.values():
12✔
462
                materials.update(cell.get_all_materials(memo))
12✔
463

464
        return materials
12✔
465

466
    def get_all_universes(self, memo=None):
12✔
467
        """Return all universes that are contained within this one if any of
468
        its cells are filled with a universe or lattice.
469

470
        Returns
471
        -------
472
        universes : dict
473
            Dictionary whose keys are universe IDs and values are
474
            :class:`Universe` instances
475

476
        """
477
        if memo is None:
12✔
478
            memo = set()
12✔
479
        if self in memo:
12✔
480
            return {}
12✔
481
        memo.add(self)
12✔
482

483
        universes = {}
12✔
484
        if self.fill_type == 'universe':
12✔
485
            universes[self.fill.id] = self.fill
12✔
486
            universes.update(self.fill.get_all_universes(memo))
12✔
487
        elif self.fill_type == 'lattice':
12✔
488
            universes.update(self.fill.get_all_universes(memo))
12✔
489

490
        return universes
12✔
491

492
    def clone(self, clone_materials=True, clone_regions=True, memo=None):
12✔
493
        """Create a copy of this cell with a new unique ID, and clones
494
        the cell's region and fill.
495

496
        Parameters
497
        ----------
498
        clone_materials : bool
499
            Whether to create separate copies of the materials filling cells
500
            contained in this cell, or the material filling this cell.
501
        clone_regions : bool
502
            Whether to create separate copies of the regions bounding cells
503
            contained in this cell, and the region bounding this cell.
504
        memo : dict or None
505
            A nested dictionary of previously cloned objects. This parameter
506
            is used internally and should not be specified by the user.
507

508
        Returns
509
        -------
510
        clone : openmc.Cell
511
            The clone of this cell
512

513
        """
514

515
        if memo is None:
12✔
516
            memo = {}
12✔
517

518
        # If no memoize'd clone exists, instantiate one
519
        if self not in memo:
12✔
520
            # Temporarily remove paths
521
            paths = self._paths
12✔
522
            self._paths = None
12✔
523

524
            clone = openmc.Cell(name=self.name)
12✔
525
            clone.volume = self.volume
12✔
526
            if self.temperature is not None:
12✔
527
                clone.temperature = self.temperature
12✔
528
            if self.translation is not None:
12✔
529
                clone.translation = self.translation
12✔
530
            if self.rotation is not None:
12✔
531
                clone.rotation = self.rotation
12✔
532
            clone._num_instances = None
12✔
533

534
            # Restore paths on original instance
535
            self._paths = paths
12✔
536

537
            if self.region is not None:
12✔
538
                if clone_regions:
12✔
539
                    clone.region = self.region.clone(memo)
12✔
540
                else:
541
                    clone.region = self.region
12✔
542
            if self.fill is not None:
12✔
543
                if self.fill_type == 'distribmat':
12✔
544
                    if not clone_materials:
×
545
                        clone.fill = self.fill
×
546
                    else:
547
                        clone.fill = [fill.clone(memo) if fill is not None else
×
548
                                      None for fill in self.fill]
549
                elif self.fill_type == 'material':
12✔
550
                    if not clone_materials:
12✔
551
                        clone.fill = self.fill
12✔
552
                    else:
553
                        clone.fill = self.fill.clone(memo)
12✔
554
                else:
555
                    clone.fill = self.fill.clone(clone_materials,
×
556
                         clone_regions, memo)
557

558
            # Memoize the clone
559
            memo[self] = clone
12✔
560

561
        return memo[self]
12✔
562

563
    def plot(self, *args, **kwargs):
12✔
564
        """Display a slice plot of the cell.
565

566
        .. versionadded:: 0.14.0
567

568
        Parameters
569
        ----------
570
        origin : iterable of float
571
            Coordinates at the origin of the plot. If left as None then the
572
            bounding box center will be used to attempt to ascertain the origin.
573
            Defaults to (0, 0, 0) if the bounding box is not finite
574
        width : iterable of float
575
            Width of the plot in each basis direction. If left as none then the
576
            bounding box width will be used to attempt to ascertain the plot
577
            width. Defaults to (10, 10) if the bounding box is not finite
578
        pixels : Iterable of int or int
579
            If iterable of ints provided, then this directly sets the number of
580
            pixels to use in each basis direction. If int provided, then this
581
            sets the total number of pixels in the plot and the number of pixels
582
            in each basis direction is calculated from this total and the image
583
            aspect ratio.
584
        basis : {'xy', 'xz', 'yz'}
585
            The basis directions for the plot
586
        color_by : {'cell', 'material'}
587
            Indicate whether the plot should be colored by cell or by material
588
        colors : dict
589
            Assigns colors to specific materials or cells. Keys are instances of
590
            :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA
591
            4-tuples, or strings indicating SVG color names. Red, green, blue,
592
            and alpha should all be floats in the range [0.0, 1.0], for example:
593

594
            .. code-block:: python
595

596
               # Make water blue
597
               water = openmc.Cell(fill=h2o)
598
               water.plot(colors={water: (0., 0., 1.)})
599
        seed : int
600
            Seed for the random number generator
601
        openmc_exec : str
602
            Path to OpenMC executable.
603
        axes : matplotlib.Axes
604
            Axes to draw to
605
        legend : bool
606
            Whether a legend showing material or cell names should be drawn
607
        legend_kwargs : dict
608
            Keyword arguments passed to :func:`matplotlib.pyplot.legend`.
609
        outline : bool
610
            Whether outlines between color boundaries should be drawn
611
        axis_units : {'km', 'm', 'cm', 'mm'}
612
            Units used on the plot axis
613
        **kwargs
614
            Keyword arguments passed to :func:`matplotlib.pyplot.imshow`
615

616
        Returns
617
        -------
618
        matplotlib.axes.Axes
619
            Axes containing resulting image
620

621
        """
622
        # Create dummy universe but preserve used_ids
623
        next_id = openmc.Universe.next_id
12✔
624
        u = openmc.Universe(cells=[self])
12✔
625
        openmc.Universe.used_ids.remove(u.id)
12✔
626
        openmc.Universe.next_id = next_id
12✔
627
        return u.plot(*args, **kwargs)
12✔
628

629
    def create_xml_subelement(self, xml_element, memo=None):
12✔
630
        """Add the cell's xml representation to an incoming xml element
631

632
        Parameters
633
        ----------
634
        xml_element : lxml.etree._Element
635
            XML element to be added to
636

637
        memo : set or None
638
            A set of object IDs representing geometry entities already
639
            written to ``xml_element``. This parameter is used internally
640
            and should not be specified by users.
641

642
        Returns
643
        -------
644
        None
645

646
        """
647
        element = ET.Element("cell")
12✔
648
        element.set("id", str(self.id))
12✔
649

650
        if len(self._name) > 0:
12✔
651
            element.set("name", str(self.name))
12✔
652

653
        if self.fill_type == 'void':
12✔
654
            element.set("material", "void")
12✔
655

656
        elif self.fill_type == 'material':
12✔
657
            element.set("material", str(self.fill.id))
12✔
658

659
        elif self.fill_type == 'distribmat':
12✔
660
            element.set("material", ' '.join(['void' if m is None else str(m.id)
12✔
661
                                              for m in self.fill]))
662

663
        elif self.fill_type in ('universe', 'lattice'):
12✔
664
            element.set("fill", str(self.fill.id))
12✔
665
            self.fill.create_xml_subelement(xml_element, memo)
12✔
666

667
        if self.region is not None:
12✔
668
            # Set the region attribute with the region specification
669
            region = str(self.region)
12✔
670
            if region.startswith('('):
12✔
671
                region = region[1:-1]
12✔
672
            if len(region) > 0:
12✔
673
                element.set("region", region)
12✔
674

675
            # Only surfaces that appear in a region are added to the geometry
676
            # file, so the appropriate check is performed here. First we create
677
            # a function which is called recursively to navigate through the CSG
678
            # tree. When it reaches a leaf (a Halfspace), it creates a <surface>
679
            # element for the corresponding surface if none has been created
680
            # thus far.
681
            def create_surface_elements(node, element, memo=None):
12✔
682
                if isinstance(node, Halfspace):
12✔
683
                    if memo is None:
12✔
684
                        memo = set()
12✔
685
                    elif node.surface in memo:
12✔
686
                        return
12✔
687
                    memo.add(node.surface)
12✔
688
                    xml_element.append(node.surface.to_xml_element())
12✔
689

690
                elif isinstance(node, Complement):
12✔
UNCOV
691
                    create_surface_elements(node.node, element, memo)
×
692
                else:
693
                    for subnode in node:
12✔
694
                        create_surface_elements(subnode, element, memo)
12✔
695

696
            # Call the recursive function from the top node
697
            create_surface_elements(self.region, xml_element, memo)
12✔
698

699
        if self.temperature is not None:
12✔
700
            if isinstance(self.temperature, Iterable):
12✔
701
                element.set("temperature", ' '.join(
12✔
702
                    str(t) for t in self.temperature))
703
            else:
704
                element.set("temperature", str(self.temperature))
12✔
705

706
        if self.translation is not None:
12✔
707
            element.set("translation", ' '.join(map(str, self.translation)))
12✔
708

709
        if self.rotation is not None:
12✔
710
            element.set("rotation", ' '.join(map(str, self.rotation.ravel())))
12✔
711

712
        if self.volume is not None:
12✔
713
            element.set("volume", str(self.volume))
12✔
714

715
        return element
12✔
716

717
    @classmethod
12✔
718
    def from_xml_element(cls, elem, surfaces, materials, get_universe):
12✔
719
        """Generate cell from XML element
720

721
        Parameters
722
        ----------
723
        elem : lxml.etree._Element
724
            `<cell>` element
725
        surfaces : dict
726
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
727
        materials : dict
728
            Dictionary mapping material ID strings to :class:`openmc.Material`
729
            instances (defined in :meth:`openmc.Geometry.from_xml`)
730
        get_universe : function
731
            Function returning universe (defined in
732
            :meth:`openmc.Geometry.from_xml`)
733

734
        Returns
735
        -------
736
        openmc.Cell
737
            Cell instance
738

739
        """
740
        cell_id = int(get_text(elem, 'id'))
12✔
741
        name = get_text(elem, 'name')
12✔
742
        c = cls(cell_id, name)
12✔
743

744
        # Assign material/distributed materials or fill
745
        mat_text = get_text(elem, 'material')
12✔
746
        if mat_text is not None:
12✔
747
            mat_ids = mat_text.split()
12✔
748
            if len(mat_ids) > 1:
12✔
749
                c.fill = [materials[i] for i in mat_ids]
12✔
750
            else:
751
                c.fill = materials[mat_ids[0]]
12✔
752
        else:
753
            fill_id = int(get_text(elem, 'fill'))
12✔
754
            c.fill = get_universe(fill_id)
12✔
755

756
        # Assign region
757
        region = get_text(elem, 'region')
12✔
758
        if region is not None:
12✔
759
            c.region = Region.from_expression(region, surfaces)
12✔
760

761
        # Check for other attributes
762
        t = get_text(elem, 'temperature')
12✔
763
        if t is not None:
12✔
764
            if ' ' in t:
12✔
765
                c.temperature = [float(t_i) for t_i in t.split()]
×
766
            else:
767
                c.temperature = float(t)
12✔
768
        v = get_text(elem, 'volume')
12✔
769
        if v is not None:
12✔
770
            c.volume = float(v)
×
771
        for key in ('temperature', 'rotation', 'translation'):
12✔
772
            value = get_text(elem, key)
12✔
773
            if value is not None:
12✔
774
                values = [float(x) for x in value.split()]
12✔
775
                if key == 'rotation' and len(values) == 9:
12✔
776
                    values = np.array(values).reshape(3, 3)
12✔
777
                setattr(c, key, values)
12✔
778

779
        # Add this cell to appropriate universe
780
        univ_id = int(get_text(elem, 'universe', 0))
12✔
781
        get_universe(univ_id).add_cell(c)
12✔
782
        return c
12✔
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

© 2025 Coveralls, Inc