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

openmc-dev / openmc / 22007957997

14 Feb 2026 12:50AM UTC coverage: 81.717% (-0.05%) from 81.763%
22007957997

Pull #3765

github

web-flow
Merge 6111ffe2e into 19c0aafdc
Pull Request #3765: Store atomic mass in ParticleType.

17526 of 24626 branches covered (71.17%)

Branch coverage included in aggregate %.

6 of 7 new or added lines in 2 files covered. (85.71%)

1820 existing lines in 34 files now uncovered.

56937 of 66497 relevant lines covered (85.62%)

44324210.02 hits per line

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

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

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

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

18

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

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

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

57
        .. math::
58

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

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

98
        .. versionadded:: 0.12
99
    bounding_box : openmc.BoundingBox
100
        Axis-aligned bounding box of the cell
101

102
    """
103

104
    next_id = 1
10✔
105
    used_ids = set()
10✔
106

107
    def __init__(self, cell_id=None, name='', fill=None, region=None):
10✔
108
        # Initialize Cell class attributes
109
        self.id = cell_id
10✔
110
        self.name = name
10✔
111
        self.fill = fill
10✔
112
        self.region = region
10✔
113
        self._rotation = None
10✔
114
        self._rotation_matrix = None
10✔
115
        self._temperature = None
10✔
116
        self._density = None
10✔
117
        self._translation = None
10✔
118
        self._paths = None
10✔
119
        self._num_instances = None
10✔
120
        self._volume = None
10✔
121
        self._atoms = None
10✔
122

123
    def __contains__(self, point):
10✔
124
        if self.region is None:
10✔
125
            return True
10✔
126
        else:
127
            return point in self.region
10✔
128

129
    def __repr__(self):
10✔
130
        string = 'Cell\n'
10✔
131
        string += '{: <16}=\t{}\n'.format('\tID', self.id)
10✔
132
        string += '{: <16}=\t{}\n'.format('\tName', self.name)
10✔
133

134
        if self.fill_type == 'material':
10✔
135
            string += '{: <16}=\tMaterial {}\n'.format('\tFill', self.fill.id)
10✔
136
        elif self.fill_type == 'void':
10✔
137
            string += '{: <16}=\tNone\n'.format('\tFill')
10✔
138
        elif self.fill_type == 'distribmat':
10✔
139
            string += '{: <16}=\t{}\n'.format('\tFill', list(map(
10✔
140
                lambda m: m if m is None else m.id, self.fill)))
141
        else:
142
            string += '{: <16}=\t{}\n'.format('\tFill', self.fill.id)
10✔
143

144
        string += '{: <16}=\t{}\n'.format('\tRegion', self.region)
10✔
145
        string += '{: <16}=\t{}\n'.format('\tRotation', self.rotation)
10✔
146
        if self.fill_type == 'material':
10✔
147
            string += '\t{0: <15}=\t{1}\n'.format('Temperature',
10✔
148
                                                  self.temperature)
149
            string += '\t{0: <15}=\t{1}\n'.format('Density', self.density)
10✔
150
        string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation)
10✔
151
        string += '{: <16}=\t{}\n'.format('\tVolume', self.volume)
10✔
152

153
        return string
10✔
154

155
    @property
10✔
156
    def name(self):
10✔
157
        return self._name
10✔
158

159
    @name.setter
10✔
160
    def name(self, name):
10✔
161
        if name is not None:
10✔
162
            cv.check_type('cell name', name, str)
10✔
163
            self._name = name
10✔
164
        else:
165
            self._name = ''
10✔
166

167
    @property
10✔
168
    def fill(self):
10✔
169
        return self._fill
10✔
170

171
    @fill.setter
10✔
172
    def fill(self, fill):
10✔
173
        if fill is not None:
10✔
174
            if isinstance(fill, Iterable):
10✔
175
                for i, f in enumerate(fill):
10✔
176
                    if f is not None:
10✔
177
                        cv.check_type('cell.fill[i]', f, openmc.Material)
10✔
178

179
            elif not isinstance(fill, (openmc.Material, openmc.Lattice,
10✔
180
                                       openmc.UniverseBase)):
181
                msg = (f'Unable to set Cell ID="{self._id}" to use a '
×
182
                       f'non-Material or Universe fill "{fill}"')
183
                raise ValueError(msg)
×
184
        self._fill = fill
10✔
185

186
        # Info about atom content can now be invalid
187
        # (since fill has just changed)
188
        self._atoms = None
10✔
189

190
    @property
10✔
191
    def fill_type(self):
10✔
192
        if isinstance(self.fill, openmc.Material):
10✔
193
            return 'material'
10✔
194
        elif isinstance(self.fill, openmc.UniverseBase):
10✔
195
            return 'universe'
10✔
196
        elif isinstance(self.fill, openmc.Lattice):
10✔
197
            return 'lattice'
10✔
198
        elif isinstance(self.fill, Iterable):
10✔
199
            return 'distribmat'
10✔
200
        else:
201
            return 'void'
10✔
202

203
    @property
10✔
204
    def region(self):
10✔
205
        return self._region
10✔
206

207
    @region.setter
10✔
208
    def region(self, region):
10✔
209
        if region is not None:
10✔
210
            cv.check_type('cell region', region, Region)
10✔
211
        self._region = region
10✔
212

213
    @property
10✔
214
    def rotation(self):
10✔
215
        return self._rotation
10✔
216

217
    @rotation.setter
10✔
218
    def rotation(self, rotation):
10✔
219
        cv.check_length('cell rotation', rotation, 3)
10✔
220
        self._rotation = np.asarray(rotation)
10✔
221

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

238
    @property
10✔
239
    def rotation_matrix(self):
10✔
240
        return self._rotation_matrix
10✔
241

242
    @property
10✔
243
    def temperature(self):
10✔
244
        return self._temperature
10✔
245

246
    @temperature.setter
10✔
247
    def temperature(self, temperature):
10✔
248
        # Make sure temperatures are positive
249
        cv.check_type('cell temperature', temperature, (Iterable, Real), none_ok=True)
10✔
250
        if isinstance(temperature, Iterable):
10✔
251
            cv.check_type('cell temperature', temperature, Iterable, Real)
10✔
252
            for T in temperature:
10✔
253
                cv.check_greater_than('cell temperature', T, 0.0, True)
10✔
254
        elif isinstance(temperature, Real):
10✔
255
            cv.check_greater_than('cell temperature', temperature, 0.0, True)
10✔
256

257
        # If this cell is filled with a universe or lattice, propagate
258
        # temperatures to all cells contained. Otherwise, simply assign it.
259
        if self.fill_type in ('universe', 'lattice'):
10✔
260
            for c in self.get_all_cells().values():
10✔
261
                if c.fill_type == 'material':
10✔
262
                    c._temperature = temperature
10✔
263
        else:
264
            self._temperature = temperature
10✔
265

266
    @property
10✔
267
    def density(self):
10✔
268
        return self._density
10✔
269

270
    @density.setter
10✔
271
    def density(self, density):
10✔
272
        # Make sure densities are greater than zero
273
        cv.check_type('cell density', density, (Iterable, Real), none_ok=True)
10✔
274
        if isinstance(density, Iterable):
10✔
275
            cv.check_type('cell density', density, Iterable, Real)
10✔
276
            for rho in density:
10✔
277
                cv.check_greater_than('cell density', rho, 0.0, True)
10✔
278
        elif isinstance(density, Real):
10✔
279
            cv.check_greater_than('cell density', density, 0.0, True)
10✔
280

281
        # If this cell is filled with a universe or lattice, propagate
282
        # densities to all cells contained. Otherwise, simply assign it.
283
        if self.fill_type in ('universe', 'lattice'):
10✔
284
            for c in self.get_all_cells().values():
10✔
285
                if c.fill_type == 'material':
10✔
286
                    c._density = density
10✔
287
        else:
288
            self._density = density
10✔
289

290
    @property
10✔
291
    def translation(self):
10✔
292
        return self._translation
10✔
293

294
    @translation.setter
10✔
295
    def translation(self, translation):
10✔
296
        cv.check_type('cell translation', translation, Iterable, Real)
10✔
297
        cv.check_length('cell translation', translation, 3)
10✔
298
        self._translation = np.asarray(translation)
10✔
299

300
    @property
10✔
301
    def volume(self):
10✔
302
        return self._volume
10✔
303

304
    @volume.setter
10✔
305
    def volume(self, volume):
10✔
306
        if volume is not None:
10✔
307
            cv.check_type('cell volume', volume, (Real, UFloat))
10✔
308
            cv.check_greater_than('cell volume', volume, 0.0, equality=True)
10✔
309

310
        self._volume = volume
10✔
311

312
        # Info about atom content can now be invalid
313
        # (since volume has just changed)
314
        self._atoms = None
10✔
315

316
    @property
10✔
317
    def atoms(self):
10✔
318
        if self._atoms is None:
10✔
319
            if self._volume is None:
10✔
320
                msg = ('Cannot calculate atom content because no volume '
10✔
321
                       'is set. Use Cell.volume to provide it or perform '
322
                       'a stochastic volume calculation.')
323
                raise ValueError(msg)
10✔
324

325
            elif self.fill_type == 'void':
10✔
326
                msg = ('Cell is filled with void. It contains no atoms. '
10✔
327
                       'Material must be set to calculate atom content.')
328
                raise ValueError(msg)
10✔
329

330
            elif self.fill_type in ['lattice', 'universe']:
10✔
331
                msg = ('Universe and Lattice cells can contain multiple '
10✔
332
                       'materials in diffrent proportions. Atom content must '
333
                       'be calculated with stochastic volume calculation.')
334
                raise ValueError(msg)
10✔
335

336
            elif self.fill_type == 'material':
10✔
337
                # Get atomic densities
338
                self._atoms = self._fill.get_nuclide_atom_densities()
10✔
339

340
                # Convert to total number of atoms
341
                for key, atom_per_bcm in self._atoms.items():
10✔
342
                    atom = atom_per_bcm * self._volume * 1.0e+24
10✔
343
                    self._atoms[key] = atom
10✔
344

345
            elif self.fill_type == 'distribmat':
10✔
346
                # Assumes that volume is total volume of all instances
347
                # Also assumes that all instances have the same volume
348
                partial_volume = self.volume / len(self.fill)
10✔
349
                self._atoms = {}
10✔
350
                for mat in self.fill:
10✔
351
                    for key, atom_per_bcm in mat.get_nuclide_atom_densities().items():
10✔
352
                        # To account for overlap of nuclides between distribmat
353
                        # we need to append new atoms to any existing value
354
                        # hence it is necessary to ask for default.
355
                        atom = self._atoms.setdefault(key, 0)
10✔
356
                        atom += atom_per_bcm * partial_volume * 1.0e+24
10✔
357
                        self._atoms[key] = atom
10✔
358

359
            else:
360
                msg = f'Unrecognized fill_type: {self.fill_type}'
×
361
                raise ValueError(msg)
×
362

363
        return self._atoms
10✔
364

365
    @property
10✔
366
    def paths(self):
10✔
367
        if self._paths is None:
10✔
368
            raise ValueError('Cell instance paths have not been determined. '
×
369
                             'Call the Geometry.determine_paths() method.')
370
        return self._paths
10✔
371

372
    @property
10✔
373
    def bounding_box(self):
10✔
374
        if self.region is not None:
10✔
375
            return self.region.bounding_box
10✔
376
        else:
377
            return BoundingBox.infinite()
10✔
378

379
    @property
10✔
380
    def num_instances(self):
10✔
381
        if self._num_instances is None:
10✔
382
            raise ValueError(
×
383
                'Number of cell instances have not been determined. Call the '
384
                'Geometry.determine_paths() method.')
385
        return self._num_instances
10✔
386

387
    def add_volume_information(self, volume_calc):
10✔
388
        """Add volume information to a cell.
389

390
        Parameters
391
        ----------
392
        volume_calc : openmc.VolumeCalculation
393
            Results from a stochastic volume calculation
394

395
        """
396
        if volume_calc.domain_type == 'cell':
10✔
397
            if self.id in volume_calc.volumes:
10✔
398
                self._volume = volume_calc.volumes[self.id].n
10✔
399
                self._atoms = volume_calc.atoms[self.id]
10✔
400
            else:
401
                raise ValueError('No volume information found for this cell.')
×
402
        else:
403
            raise ValueError('No volume information found for this cell.')
×
404

405
    def get_nuclides(self):
10✔
406
        """Returns all nuclides in the cell
407

408
        Returns
409
        -------
410
        nuclides : list of str
411
            List of nuclide names
412

413
        """
414
        return self.fill.get_nuclides() if self.fill_type != 'void' else []
10✔
415

416
    def get_nuclide_densities(self):
10✔
417
        """Return all nuclides contained in the cell and their densities
418

419
        Returns
420
        -------
421
        nuclides : dict
422
            Dictionary whose keys are nuclide names and values are 2-tuples of
423
            (nuclide, density)
424

425
        """
426

427
        nuclides = {}
10✔
428

429
        if self.fill_type == 'material':
10✔
430
            nuclides.update(self.fill.get_nuclide_densities())
10✔
431
        elif self.fill_type == 'void':
10✔
432
            pass
10✔
433
        else:
434
            if self._atoms is not None:
×
435
                volume = self.volume
×
436
                for name, atoms in self._atoms.items():
×
437
                    density = 1.0e-24 * atoms.n/volume  # density in atoms/b-cm
×
438
                    nuclides[name] = (name, density)
×
439
            else:
440
                raise RuntimeError(
×
441
                    'Volume information is needed to calculate microscopic '
442
                    f'cross sections for cell {self.id}. This can be done by '
443
                    'running a stochastic volume calculation via the '
444
                    'openmc.VolumeCalculation object')
445

446
        return nuclides
10✔
447

448
    def get_all_cells(self, memo=None):
10✔
449
        """Return all cells that are contained within this one if it is filled with a
450
        universe or lattice
451

452
        Returns
453
        -------
454
        cells : dict
455
            Dictionary whose keys are cell IDs and values are :class:`Cell`
456
            instances
457

458
        """
459
        if memo is None:
10✔
460
            memo = set()
10✔
461
        elif self in memo:
10✔
462
            return {}
10✔
463
        memo.add(self)
10✔
464

465
        cells = {}
10✔
466
        if self.fill_type in ('universe', 'lattice'):
10✔
467
            cells.update(self.fill.get_all_cells(memo))
10✔
468

469
        return cells
10✔
470

471
    def get_all_materials(self, memo=None):
10✔
472
        """Return all materials that are contained within the cell
473

474
        Returns
475
        -------
476
        materials : dict
477
            Dictionary whose keys are material IDs and values are
478
            :class:`Material` instances
479

480
        """
481
        materials = {}
10✔
482
        if self.fill_type == 'material':
10✔
483
            materials[self.fill.id] = self.fill
10✔
484
        elif self.fill_type == 'distribmat':
10✔
485
            for m in self.fill:
10✔
486
                if m is not None:
10✔
487
                    materials[m.id] = m
10✔
488
        else:
489
            # Append all Cells in each Cell in the Universe to the dictionary
490
            cells = self.get_all_cells(memo)
10✔
491
            for cell in cells.values():
10✔
492
                materials.update(cell.get_all_materials(memo))
10✔
493

494
        return materials
10✔
495

496
    def get_all_universes(self, memo=None):
10✔
497
        """Return all universes that are contained within this one if any of
498
        its cells are filled with a universe or lattice.
499

500
        Returns
501
        -------
502
        universes : dict
503
            Dictionary whose keys are universe IDs and values are
504
            :class:`Universe` instances
505

506
        """
507
        if memo is None:
10✔
508
            memo = set()
10✔
509
        if self in memo:
10✔
510
            return {}
10✔
511
        memo.add(self)
10✔
512

513
        universes = {}
10✔
514
        if self.fill_type == 'universe':
10✔
515
            universes[self.fill.id] = self.fill
10✔
516
            universes.update(self.fill.get_all_universes(memo))
10✔
517
        elif self.fill_type == 'lattice':
10✔
518
            universes.update(self.fill.get_all_universes(memo))
10✔
519

520
        return universes
10✔
521

522
    def clone(self, clone_materials=True, clone_regions=True, memo=None):
10✔
523
        """Create a copy of this cell with a new unique ID, and clones
524
        the cell's region and fill.
525

526
        Parameters
527
        ----------
528
        clone_materials : bool
529
            Whether to create separate copies of the materials filling cells
530
            contained in this cell, or the material filling this cell.
531
        clone_regions : bool
532
            Whether to create separate copies of the regions bounding cells
533
            contained in this cell, and the region bounding this cell.
534
        memo : dict or None
535
            A nested dictionary of previously cloned objects. This parameter
536
            is used internally and should not be specified by the user.
537

538
        Returns
539
        -------
540
        clone : openmc.Cell
541
            The clone of this cell
542

543
        """
544

545
        if memo is None:
10✔
546
            memo = {}
10✔
547

548
        # If no memoize'd clone exists, instantiate one
549
        if self not in memo:
10✔
550
            # Temporarily remove paths
551
            paths = self._paths
10✔
552
            self._paths = None
10✔
553

554
            clone = openmc.Cell(name=self.name)
10✔
555
            clone.volume = self.volume
10✔
556
            if self.temperature is not None:
10✔
557
                clone.temperature = self.temperature
10✔
558
            if self.density is not None:
10✔
559
                clone.density = self.density
×
560
            if self.translation is not None:
10✔
561
                clone.translation = self.translation
10✔
562
            if self.rotation is not None:
10✔
563
                clone.rotation = self.rotation
10✔
564
            clone._num_instances = None
10✔
565

566
            # Restore paths on original instance
567
            self._paths = paths
10✔
568

569
            if self.region is not None:
10✔
570
                if clone_regions:
10✔
571
                    clone.region = self.region.clone(memo)
10✔
572
                else:
573
                    clone.region = self.region
10✔
574
            if self.fill is not None:
10✔
575
                if self.fill_type == 'distribmat':
10✔
576
                    if not clone_materials:
×
577
                        clone.fill = self.fill
×
578
                    else:
579
                        clone.fill = [fill.clone(memo) if fill is not None else
×
580
                                      None for fill in self.fill]
581
                elif self.fill_type == 'material':
10✔
582
                    if not clone_materials:
10✔
583
                        clone.fill = self.fill
10✔
584
                    else:
585
                        clone.fill = self.fill.clone(memo)
10✔
586
                else:
587
                    clone.fill = self.fill.clone(clone_materials,
×
588
                         clone_regions, memo)
589

590
            # Memoize the clone
591
            memo[self] = clone
10✔
592

593
        return memo[self]
10✔
594

595
    @add_plot_params
10✔
596
    def plot(self, *args, **kwargs):
10✔
597
        """Display a slice plot of the cell.
598

599
        .. versionadded:: 0.14.0
600
        """
601
        # Create dummy universe but preserve used_ids
602
        next_id = openmc.UniverseBase.next_id
10✔
603
        u = openmc.Universe(cells=[self])
10✔
604
        openmc.UniverseBase.used_ids.remove(u.id)
10✔
605
        openmc.UniverseBase.next_id = next_id
10✔
606
        return u.plot(*args, **kwargs)
10✔
607

608
    def create_xml_subelement(self, xml_element, memo=None):
10✔
609
        """Add the cell's xml representation to an incoming xml element
610

611
        Parameters
612
        ----------
613
        xml_element : lxml.etree._Element
614
            XML element to be added to
615

616
        memo : set or None
617
            A set of object IDs representing geometry entities already
618
            written to ``xml_element``. This parameter is used internally
619
            and should not be specified by users.
620

621
        Returns
622
        -------
623
        None
624

625
        """
626
        element = ET.Element("cell")
10✔
627
        element.set("id", str(self.id))
10✔
628

629
        if len(self._name) > 0:
10✔
630
            element.set("name", str(self.name))
10✔
631

632
        if self.fill_type == 'void':
10✔
633
            element.set("material", "void")
10✔
634

635
        elif self.fill_type == 'material':
10✔
636
            element.set("material", str(self.fill.id))
10✔
637

638
        elif self.fill_type == 'distribmat':
10✔
639
            material_subelement= ET.SubElement(element, "material")
10✔
640
            matlist_str = " ".join(
10✔
641
                ["void" if m is None else str(m.id) for m in self.fill]
642
            )
643
            material_subelement.text = matlist_str
10✔
644

645
        elif self.fill_type in ('universe', 'lattice'):
10✔
646
            element.set("fill", str(self.fill.id))
10✔
647
            self.fill.create_xml_subelement(xml_element, memo)
10✔
648

649
        if self.region is not None:
10✔
650
            # Set the region attribute with the region specification
651
            region = str(self.region)
10✔
652
            if region.startswith('('):
10✔
653
                region = region[1:-1]
10✔
654
            if len(region) > 0:
10✔
655
                element.set("region", region)
10✔
656

657
            # Only surfaces that appear in a region are added to the geometry
658
            # file, so the appropriate check is performed here. First we create
659
            # a function which is called recursively to navigate through the CSG
660
            # tree. When it reaches a leaf (a Halfspace), it creates a <surface>
661
            # element for the corresponding surface if none has been created
662
            # thus far.
663
            def create_surface_elements(node, element, memo=None):
10✔
664
                if isinstance(node, Halfspace):
10✔
665
                    if memo is None:
10✔
666
                        memo = set()
10✔
667
                    elif node.surface in memo:
10✔
668
                        return
10✔
669
                    memo.add(node.surface)
10✔
670
                    xml_element.append(node.surface.to_xml_element())
10✔
671

672
                elif isinstance(node, Complement):
10✔
UNCOV
673
                    create_surface_elements(node.node, element, memo)
×
674
                else:
675
                    for subnode in node:
10✔
676
                        create_surface_elements(subnode, element, memo)
10✔
677

678
            # Call the recursive function from the top node
679
            create_surface_elements(self.region, xml_element, memo)
10✔
680

681
        if self.temperature is not None:
10✔
682
            if isinstance(self.temperature, Iterable):
10✔
683
                temperature_subelement= ET.SubElement(element, "temperature")
10✔
684
                temperature_subelement.text = ' '.join(str(t) for t in self.temperature)
10✔
685
            else:
686
                element.set("temperature", str(self.temperature))
10✔
687

688
        if self.density is not None:
10✔
689
            if isinstance(self.density, Iterable):
10✔
690
                density_subelement= ET.SubElement(element, "density")
10✔
691
                density_subelement.text =  ' '.join(str(d) for d in self.density)
10✔
692
            else:
693
                element.set("density", str(self.density))
10✔
694

695
        if self.translation is not None:
10✔
696
            element.set("translation", ' '.join(map(str, self.translation)))
10✔
697

698
        if self.rotation is not None:
10✔
699
            element.set("rotation", ' '.join(map(str, self.rotation.ravel())))
10✔
700

701
        if self.volume is not None:
10✔
702
            element.set("volume", str(self.volume))
10✔
703

704
        return element
10✔
705

706
    @classmethod
10✔
707
    def from_xml_element(cls, elem, surfaces, materials, get_universe):
10✔
708
        """Generate cell from XML element
709

710
        Parameters
711
        ----------
712
        elem : lxml.etree._Element
713
            `<cell>` element
714
        surfaces : dict
715
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
716
        materials : dict
717
            Dictionary mapping material ID strings to :class:`openmc.Material`
718
            instances (defined in :meth:`openmc.Geometry.from_xml`)
719
        get_universe : function
720
            Function returning universe (defined in
721
            :meth:`openmc.Geometry.from_xml`)
722

723
        Returns
724
        -------
725
        openmc.Cell
726
            Cell instance
727

728
        """
729
        cell_id = int(get_text(elem, 'id'))
10✔
730
        name = get_text(elem, 'name')
10✔
731
        c = cls(cell_id, name)
10✔
732

733
        # Assign material/distributed materials or fill
734
        mat_ids = get_elem_list(elem, 'material', str)
10✔
735
        if mat_ids is not None:
10✔
736
            if len(mat_ids) > 1:
10✔
737
                c.fill = [materials[i] for i in mat_ids]
10✔
738
            else:
739
                c.fill = materials[mat_ids[0]]
10✔
740
        else:
741
            fill_id = int(get_text(elem, 'fill'))
10✔
742
            c.fill = get_universe(fill_id)
10✔
743

744
        # Assign region
745
        region = get_text(elem, 'region')
10✔
746
        if region is not None:
10✔
747
            c.region = Region.from_expression(region, surfaces)
10✔
748

749
        # Check for other attributes
750
        temperature = get_elem_list(elem, 'temperature', float)
10✔
751
        if temperature is not None:
10✔
752
            if len(temperature) > 1:
10✔
UNCOV
753
                c.temperature = temperature
×
754
            else:
755
                c.temperature = temperature[0]
10✔
756
        density = get_elem_list(elem, 'density', float)
10✔
757
        if density is not None:
10✔
758
            c.density = density if len(density) > 1 else density[0]
10✔
759
        v = get_text(elem, 'volume')
10✔
760
        if v is not None:
10✔
UNCOV
761
            c.volume = float(v)
×
762
        for key in ('temperature', 'density', 'rotation', 'translation'):
10✔
763
            values = get_elem_list(elem, key, float)
10✔
764
            if values is not None:
10✔
765
                if key == 'rotation' and len(values) == 9:
10✔
766
                    values = np.array(values).reshape(3, 3)
10✔
767
                setattr(c, key, values)
10✔
768

769
        # Add this cell to appropriate universe
770
        univ_id = int(get_text(elem, 'universe', 0))
10✔
771
        get_universe(univ_id).add_cell(c)
10✔
772
        return c
10✔
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