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

openmc-dev / openmc / 12954288577

24 Jan 2025 05:13PM UTC coverage: 84.833% (-0.1%) from 84.928%
12954288577

Pull #2671

github

web-flow
Merge d2ca87df5 into 560bd22bc
Pull Request #2671: Adding methods to automatically apply results to existing Tally objects.

53 of 65 new or added lines in 7 files covered. (81.54%)

59 existing lines in 20 files now uncovered.

50089 of 59044 relevant lines covered (84.83%)

34992387.74 hits per line

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

94.2
/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
                    density = 1.0e-24 * atoms.n/volume  # density in atoms/b-cm
×
NEW
407
                    nuclides[name] = (name, density)
×
408
            else:
409
                raise RuntimeError(
×
410
                    'Volume information is needed to calculate microscopic '
411
                    f'cross sections for cell {self.id}. This can be done by '
412
                    'running a stochastic volume calculation via the '
413
                    'openmc.VolumeCalculation object')
414

415
        return nuclides
12✔
416

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

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

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

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

438
        return cells
12✔
439

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

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

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

463
        return materials
12✔
464

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

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

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

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

489
        return universes
12✔
490

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

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

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

512
        """
513

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

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

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

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

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

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

560
        return memo[self]
12✔
561

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

565
        .. versionadded:: 0.14.0
566

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

593
            .. code-block:: python
594

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

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

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

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

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

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

641
        Returns
642
        -------
643
        None
644

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

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

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

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

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

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

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

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

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

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

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

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

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

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

714
        return element
12✔
715

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc