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

openmc-dev / openmc / 15574113465

11 Jun 2025 01:45AM UTC coverage: 85.159% (+0.001%) from 85.158%
15574113465

Pull #3437

github

web-flow
Merge 743d7818a into f796fa04e
Pull Request #3437: Automatically generate valid object IDs after loading from XML

1 of 1 new or added line in 1 file covered. (100.0%)

12 existing lines in 1 file now uncovered.

52353 of 61477 relevant lines covered (85.16%)

36760978.23 hits per line

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

95.05
/openmc/geometry.py
1
from __future__ import annotations
11✔
2
import os
11✔
3
from collections import defaultdict
11✔
4
from copy import deepcopy
11✔
5
from collections.abc import Iterable
11✔
6
from pathlib import Path
11✔
7
import warnings
11✔
8
import lxml.etree as ET
11✔
9

10
import openmc
11✔
11
import openmc._xml as xml
11✔
12
from .plots import add_plot_params
11✔
13
from .checkvalue import check_type, check_less_than, check_greater_than, PathLike
11✔
14
from .mixin import update_auto_ids
11✔
15

16

17
class Geometry:
11✔
18
    """Geometry representing a collection of surfaces, cells, and universes.
19

20
    Parameters
21
    ----------
22
    root : openmc.UniverseBase or Iterable of openmc.Cell, optional
23
        Root universe which contains all others, or an iterable of cells that
24
        should be used to create a root universe.
25

26
    Attributes
27
    ----------
28
    root_universe : openmc.UniverseBase
29
        Root universe which contains all others
30
    bounding_box : openmc.BoundingBox
31
        Lower-left and upper-right coordinates of an axis-aligned bounding box
32
        of the universe.
33
    merge_surfaces : bool
34
        Whether to remove redundant surfaces when the geometry is exported.
35
    surface_precision : int
36
        Number of decimal places to round to for comparing the coefficients of
37
        surfaces for considering them topologically equivalent.
38

39
    """
40

41
    def __init__(
11✔
42
        self,
43
        root: openmc.UniverseBase | Iterable[openmc.Cell] | None = None,
44
        merge_surfaces: bool = False,
45
        surface_precision: int = 10
46
    ):
47
        self._root_universe = None
11✔
48
        self._offsets = {}
11✔
49
        self.merge_surfaces = merge_surfaces
11✔
50
        self.surface_precision = surface_precision
11✔
51
        if root is not None:
11✔
52
            if isinstance(root, openmc.UniverseBase):
11✔
53
                self.root_universe = root
11✔
54
            else:
55
                univ = openmc.Universe()
11✔
56
                for cell in root:
11✔
57
                    univ.add_cell(cell)
11✔
58
                self._root_universe = univ
11✔
59

60
    @property
11✔
61
    def root_universe(self) -> openmc.UniverseBase:
11✔
62
        return self._root_universe
11✔
63

64
    @root_universe.setter
11✔
65
    def root_universe(self, root_universe):
11✔
66
        check_type('root universe', root_universe, openmc.UniverseBase)
11✔
67
        self._root_universe = root_universe
11✔
68

69
    @property
11✔
70
    def bounding_box(self) -> openmc.BoundingBox:
11✔
71
        return self.root_universe.bounding_box
11✔
72

73
    @property
11✔
74
    def merge_surfaces(self) -> bool:
11✔
75
        return self._merge_surfaces
11✔
76

77
    @merge_surfaces.setter
11✔
78
    def merge_surfaces(self, merge_surfaces):
11✔
79
        check_type('merge surfaces', merge_surfaces, bool)
11✔
80
        self._merge_surfaces = merge_surfaces
11✔
81

82
    @property
11✔
83
    def surface_precision(self) -> int:
11✔
84
        return self._surface_precision
11✔
85

86
    @surface_precision.setter
11✔
87
    def surface_precision(self, surface_precision):
11✔
88
        check_type('surface precision', surface_precision, int)
11✔
89
        check_less_than('surface_precision', surface_precision, 16)
11✔
90
        check_greater_than('surface_precision', surface_precision, 0)
11✔
91
        self._surface_precision = surface_precision
11✔
92

93
    def add_volume_information(self, volume_calc):
11✔
94
        """Add volume information from a stochastic volume calculation.
95

96
        Parameters
97
        ----------
98
        volume_calc : openmc.VolumeCalculation
99
            Results from a stochastic volume calculation
100

101
        """
102
        if volume_calc.domain_type == 'cell':
11✔
103
            for cell in self.get_all_cells().values():
11✔
104
                if cell.id in volume_calc.volumes:
11✔
105
                    cell.add_volume_information(volume_calc)
11✔
106
        elif volume_calc.domain_type == 'material':
11✔
107
            for material in self.get_all_materials().values():
11✔
108
                if material.id in volume_calc.volumes:
11✔
109
                    material.add_volume_information(volume_calc)
11✔
110
        elif volume_calc.domain_type == 'universe':
11✔
111
            for universe in self.get_all_universes().values():
11✔
112
                if universe.id in volume_calc.volumes:
11✔
113
                    universe.add_volume_information(volume_calc)
11✔
114

115
    def to_xml_element(self, remove_surfs=False) -> ET.Element:
11✔
116
        """Creates a 'geometry' element to be written to an XML file.
117

118
        Parameters
119
        ----------
120
        remove_surfs : bool
121
            Whether or not to remove redundant surfaces from the geometry when
122
            exporting
123

124
        """
125
        # Find and remove redundant surfaces from the geometry
126
        if remove_surfs:
11✔
UNCOV
127
            warnings.warn("remove_surfs kwarg will be deprecated soon, please "
×
128
                          "set the Geometry.merge_surfaces attribute instead.")
UNCOV
129
            self.merge_surfaces = True
×
130

131
        if self.merge_surfaces:
11✔
UNCOV
132
            self.remove_redundant_surfaces()
×
133

134
        # Create XML representation
135
        element = ET.Element("geometry")
11✔
136
        self.root_universe.create_xml_subelement(element)
11✔
137

138
        # Sort the elements in the file
139
        element[:] = sorted(element, key=lambda x: (
11✔
140
            x.tag, int(x.get('id'))))
141

142
        # Clean the indentation in the file to be user-readable
143
        xml.clean_indentation(element)
11✔
144
        xml.reorder_attributes(element)  # TODO: Remove when support is Python 3.8+
11✔
145

146
        return element
11✔
147

148
    def export_to_xml(self, path='geometry.xml', remove_surfs=False):
11✔
149
        """Export geometry to an XML file.
150

151
        Parameters
152
        ----------
153
        path : str
154
            Path to file to write. Defaults to 'geometry.xml'.
155
        remove_surfs : bool
156
            Whether or not to remove redundant surfaces from the geometry when
157
            exporting
158

159
            .. versionadded:: 0.12
160

161
        """
162
        root_element = self.to_xml_element(remove_surfs)
11✔
163

164
        # Check if path is a directory
165
        p = Path(path)
11✔
166
        if p.is_dir():
11✔
167
            p /= 'geometry.xml'
11✔
168

169
        # Write the XML Tree to the geometry.xml file
170
        tree = ET.ElementTree(root_element)
11✔
171
        tree.write(str(p), xml_declaration=True, encoding='utf-8')
11✔
172

173
    @classmethod
11✔
174
    def from_xml_element(cls, elem, materials=None) -> Geometry:
11✔
175
        """Generate geometry from an XML element
176

177
        Parameters
178
        ----------
179
        elem : lxml.etree._Element
180
            XML element
181
        materials : openmc.Materials or None
182
            Materials used to assign to cells. If None, an attempt is made to
183
            generate it from the materials.xml file.
184

185
        Returns
186
        -------
187
        openmc.Geometry
188
            Geometry object
189

190
        """
191
        mats = dict()
11✔
192
        if materials is not None:
11✔
193
            mats.update({str(m.id): m for m in materials})
11✔
194
        mats['void'] = None
11✔
195

196
        # Helper function for keeping a cache of Universe instances
197
        universes = {}
11✔
198
        def get_universe(univ_id):
11✔
199
            if univ_id not in universes:
11✔
200
                univ = openmc.Universe(univ_id)
11✔
201
                universes[univ_id] = univ
11✔
202
            return universes[univ_id]
11✔
203

204
        # Get surfaces
205
        surfaces = {}
11✔
206
        periodic = {}
11✔
207
        for surface in elem.findall('surface'):
11✔
208
            s = openmc.Surface.from_xml_element(surface)
11✔
209
            surfaces[s.id] = s
11✔
210

211
            # Check for periodic surface
212
            other_id = xml.get_text(surface, 'periodic_surface_id')
11✔
213
            if other_id is not None:
11✔
214
                periodic[s.id] = int(other_id)
11✔
215

216
        # Apply periodic surfaces
217
        for s1, s2 in periodic.items():
11✔
218
            surfaces[s1].periodic_surface = surfaces[s2]
11✔
219

220
        # Add any DAGMC universes
221
        for e in elem.findall('dagmc_universe'):
11✔
222
            dag_univ = openmc.DAGMCUniverse.from_xml_element(e, mats)
1✔
223
            universes[dag_univ.id] = dag_univ
1✔
224

225
        # Dictionary that maps each universe to a list of cells/lattices that
226
        # contain it (needed to determine which universe is the elem)
227
        child_of = defaultdict(list)
11✔
228

229
        for e in elem.findall('lattice'):
11✔
230
            lat = openmc.RectLattice.from_xml_element(e, get_universe)
11✔
231
            universes[lat.id] = lat
11✔
232
            if lat.outer is not None:
11✔
233
                child_of[lat.outer].append(lat)
11✔
234
            for u in lat.universes.ravel():
11✔
235
                child_of[u].append(lat)
11✔
236

237
        for e in elem.findall('hex_lattice'):
11✔
238
            lat = openmc.HexLattice.from_xml_element(e, get_universe)
11✔
239
            universes[lat.id] = lat
11✔
240
            if lat.outer is not None:
11✔
241
                child_of[lat.outer].append(lat)
11✔
242
            if lat.ndim == 2:
11✔
243
                for ring in lat.universes:
×
244
                    for u in ring:
×
UNCOV
245
                        child_of[u].append(lat)
×
246
            else:
247
                for axial_slice in lat.universes:
11✔
248
                    for ring in axial_slice:
11✔
249
                        for u in ring:
11✔
250
                            child_of[u].append(lat)
11✔
251

252
        for e in elem.findall('cell'):
11✔
253
            c = openmc.Cell.from_xml_element(e, surfaces, mats, get_universe)
11✔
254
            if c.fill_type in ('universe', 'lattice'):
11✔
255
                child_of[c.fill].append(c)
11✔
256

257
        # Update auto ID counters to prevent conflicts with all loaded objects
258
        update_auto_ids()
11✔
259

260
        # Determine which universe is the root by finding one which is not a
261
        # child of any other object
262
        for u in universes.values():
11✔
263
            if not child_of[u]:
11✔
264
                return cls(u)
11✔
265
        else:
UNCOV
266
            raise ValueError('Error determining root universe.')
×
267

268
    @classmethod
11✔
269
    def from_xml(
11✔
270
        cls,
271
        path: PathLike = 'geometry.xml',
272
        materials: PathLike | 'openmc.Materials' | None = 'materials.xml'
273
    ) -> Geometry:
274
        """Generate geometry from XML file
275

276
        Parameters
277
        ----------
278
        path : PathLike, optional
279
            Path to geometry XML file
280
        materials : openmc.Materials or PathLike
281
            Materials used to assign to cells. If PathLike, an attempt is made
282
            to generate materials from the provided xml file.
283

284
        Returns
285
        -------
286
        openmc.Geometry
287
            Geometry object
288

289
        """
290

291
        # Using str and os.PathLike here to avoid error when using just the imported PathLike
292
        # TypeError: Subscripted generics cannot be used with class and instance checks
293
        check_type('materials', materials, (str, os.PathLike, openmc.Materials))
11✔
294

295
        if isinstance(materials, (str, os.PathLike)):
11✔
296
            materials = openmc.Materials.from_xml(materials)
11✔
297

298
        parser = ET.XMLParser(huge_tree=True)
11✔
299
        tree = ET.parse(path, parser=parser)
11✔
300
        root = tree.getroot()
11✔
301

302
        return cls.from_xml_element(root, materials)
11✔
303

304
    def find(self, point) -> list:
11✔
305
        """Find cells/universes/lattices which contain a given point
306

307
        Parameters
308
        ----------
309
        point : 3-tuple of float
310
            Cartesian coordinates of the point
311

312
        Returns
313
        -------
314
        list
315
            Sequence of universes, cells, and lattices which are traversed to
316
            find the given point
317

318
        """
319
        return self.root_universe.find(point)
11✔
320

321
    def get_instances(self, paths) -> int | list[int]:
11✔
322
        """Return the instance number(s) for a cell/material in a geometry path.
323

324
        The instance numbers are used as indices into distributed
325
        material/temperature arrays and tally distribcell filter arrays.
326

327
        Parameters
328
        ----------
329
        paths : str or iterable of str
330
            The path traversed through the CSG tree to reach a cell or material
331
            instance. For example, 'u0->c10->l20(2,2,1)->u5->c5' would indicate
332
            the cell instance whose first level is universe 0 and cell 10,
333
            second level is lattice 20 position (2,2,1), and third level is
334
            universe 5 and cell 5.
335

336
        Returns
337
        -------
338
        int or list of int
339
            Instance number(s) for the given path(s)
340

341
        """
342
        # Make sure we are working with an iterable
343
        return_list = (isinstance(paths, Iterable) and
11✔
344
                       not isinstance(paths, str))
345
        path_list = paths if return_list else [paths]
11✔
346

347
        indices = []
11✔
348
        for p in path_list:
11✔
349
            # Extract the cell id from the path
350
            last_index = p.rfind('>')
11✔
351
            last_path = p[last_index+1:]
11✔
352
            uid = int(last_path[1:])
11✔
353

354
            # Get corresponding cell/material
355
            if last_path[0] == 'c':
11✔
356
                obj = self.get_all_cells()[uid]
11✔
357
            elif last_path[0] == 'm':
11✔
358
                obj = self.get_all_materials()[uid]
11✔
359

360
            # Determine index in paths array
361
            try:
11✔
362
                indices.append(obj.paths.index(p))
11✔
UNCOV
363
            except ValueError:
×
UNCOV
364
                indices.append(None)
×
365

366
        return indices if return_list else indices[0]
11✔
367

368
    def get_all_cells(self) -> dict[int, openmc.Cell]:
11✔
369
        """Return all cells in the geometry.
370

371
        Returns
372
        -------
373
        dict
374
            Dictionary mapping cell IDs to :class:`openmc.Cell` instances
375

376
        """
377
        if self.root_universe is not None:
11✔
378
            return self.root_universe.get_all_cells()
11✔
379
        else:
380
            return {}
11✔
381

382
    def get_all_universes(self) -> dict[int, openmc.Universe]:
11✔
383
        """Return all universes in the geometry.
384

385
        Returns
386
        -------
387
        dict
388
            Dictionary mapping universe IDs to :class:`openmc.Universe`
389
            instances
390

391
        """
392
        universes = {}
11✔
393
        universes[self.root_universe.id] = self.root_universe
11✔
394
        universes.update(self.root_universe.get_all_universes())
11✔
395
        return universes
11✔
396

397
    def get_all_nuclides(self) -> list[str]:
11✔
398
        """Return all nuclides within the geometry.
399

400
        Returns
401
        -------
402
        list
403
            Sorted list of all nuclides in materials appearing in the geometry
404

405
        """
406
        all_nuclides = set()
11✔
407
        for material in self.get_all_materials().values():
11✔
408
            all_nuclides |= set(material.get_nuclides())
11✔
409
        return sorted(all_nuclides)
11✔
410

411
    def get_all_materials(self) -> dict[int, openmc.Material]:
11✔
412
        """Return all materials within the geometry.
413

414
        Returns
415
        -------
416
        dict
417
            Dictionary mapping material IDs to :class:`openmc.Material`
418
            instances
419

420
        """
421
        if self.root_universe is not None:
11✔
422
            return self.root_universe.get_all_materials()
11✔
423
        else:
424
            return {}
11✔
425

426
    def get_all_material_cells(self) -> dict[int, openmc.Cell]:
11✔
427
        """Return all cells filled by a material
428

429
        Returns
430
        -------
431
        dict
432
            Dictionary mapping cell IDs to :class:`openmc.Cell` instances that
433
            are filled with materials or distributed materials.
434

435
        """
436
        material_cells = {}
11✔
437

438
        for cell in self.get_all_cells().values():
11✔
439
            if cell.fill_type in ('material', 'distribmat'):
11✔
440
                if cell not in material_cells:
11✔
441
                    material_cells[cell.id] = cell
11✔
442

443
        return material_cells
11✔
444

445
    def get_all_material_universes(self) -> dict[int, openmc.Universe]:
11✔
446
        """Return all universes having at least one material-filled cell.
447

448
        This method can be used to find universes that have at least one cell
449
        that is filled with a material or is void.
450

451
        Returns
452
        -------
453
        dict
454
            Dictionary mapping universe IDs to :class:`openmc.Universe`
455
            instances with at least one material-filled cell
456

457
        """
458
        material_universes = {}
11✔
459

460
        for universe in self.get_all_universes().values():
11✔
461
            for cell in universe.cells.values():
11✔
462
                if cell.fill_type in ('material', 'distribmat', 'void'):
11✔
463
                    if universe not in material_universes:
11✔
464
                        material_universes[universe.id] = universe
11✔
465

466
        return material_universes
11✔
467

468
    def get_all_lattices(self) -> dict[int, openmc.Lattice]:
11✔
469
        """Return all lattices defined
470

471
        Returns
472
        -------
473
        dict
474
            Dictionary mapping lattice IDs to :class:`openmc.Lattice` instances
475

476
        """
477
        lattices = {}
11✔
478

479
        for cell in self.get_all_cells().values():
11✔
480
            if cell.fill_type == 'lattice':
11✔
481
                if cell.fill.id not in lattices:
11✔
482
                    lattices[cell.fill.id] = cell.fill
11✔
483

484
        return lattices
11✔
485

486
    def get_all_surfaces(self) -> dict[int, openmc.Surface]:
11✔
487
        """
488
        Return all surfaces used in the geometry
489

490
        Returns
491
        -------
492
        dict
493
            Dictionary mapping surface IDs to :class:`openmc.Surface` instances
494

495
        """
496
        surfaces = {}
11✔
497

498
        for cell in self.get_all_cells().values():
11✔
499
            if cell.region is not None:
11✔
500
                surfaces = cell.region.get_surfaces(surfaces)
11✔
501
        return surfaces
11✔
502

503
    def _get_domains_by_name(self, name, case_sensitive, matching, domain_type) -> list:
11✔
504
        if not case_sensitive:
11✔
505
            name = name.lower()
11✔
506

507
        domains = []
11✔
508

509
        func = getattr(self, f'get_all_{domain_type}s')
11✔
510
        for domain in func().values():
11✔
511
            domain_name = domain.name if case_sensitive else domain.name.lower()
11✔
512
            if domain_name == name:
11✔
513
                domains.append(domain)
11✔
514
            elif not matching and name in domain_name:
11✔
515
                domains.append(domain)
11✔
516

517
        domains.sort(key=lambda x: x.id)
11✔
518
        return domains
11✔
519

520
    def get_materials_by_name(
11✔
521
        self, name, case_sensitive=False, matching=False
522
    ) -> list[openmc.Material]:
523
        """Return a list of materials with matching names.
524

525
        Parameters
526
        ----------
527
        name : str
528
            The name to match
529
        case_sensitive : bool
530
            Whether to distinguish upper and lower case letters in each
531
            material's name (default is False)
532
        matching : bool
533
            Whether the names must match completely (default is False)
534

535
        Returns
536
        -------
537
        list of openmc.Material
538
            Materials matching the queried name
539

540
        """
541
        return self._get_domains_by_name(name, case_sensitive, matching, 'material')
11✔
542

543
    def get_cells_by_name(
11✔
544
        self, name, case_sensitive=False, matching=False
545
    ) -> list[openmc.Cell]:
546
        """Return a list of cells with matching names.
547

548
        Parameters
549
        ----------
550
        name : str
551
            The name to search match
552
        case_sensitive : bool
553
            Whether to distinguish upper and lower case letters in each
554
            cell's name (default is False)
555
        matching : bool
556
            Whether the names must match completely (default is False)
557

558
        Returns
559
        -------
560
        list of openmc.Cell
561
            Cells matching the queried name
562

563
        """
564
        return self._get_domains_by_name(name, case_sensitive, matching, 'cell')
11✔
565

566
    def get_surfaces_by_name(
11✔
567
        self, name, case_sensitive=False, matching=False
568
    ) -> list[openmc.Surface]:
569
        """Return a list of surfaces with matching names.
570

571
        .. versionadded:: 0.13.3
572

573
        Parameters
574
        ----------
575
        name : str
576
            The name to search match
577
        case_sensitive : bool
578
            Whether to distinguish upper and lower case letters in each
579
            surface's name (default is False)
580
        matching : bool
581
            Whether the names must match completely (default is False)
582

583
        Returns
584
        -------
585
        list of openmc.Surface
586
            Surfaces matching the queried name
587

588
        """
589
        return self._get_domains_by_name(name, case_sensitive, matching, 'surface')
11✔
590

591
    def get_cells_by_fill_name(
11✔
592
        self, name, case_sensitive=False, matching=False
593
    ) -> list[openmc.Cell]:
594
        """Return a list of cells with fills with matching names.
595

596
        Parameters
597
        ----------
598
        name : str
599
            The name to match
600
        case_sensitive : bool
601
            Whether to distinguish upper and lower case letters in each
602
            cell's name (default is False)
603
        matching : bool
604
            Whether the names must match completely (default is False)
605

606
        Returns
607
        -------
608
        list of openmc.Cell
609
            Cells with fills matching the queried name
610

611
        """
612

613
        if not case_sensitive:
11✔
614
            name = name.lower()
11✔
615

616
        cells = set()
11✔
617

618
        for cell in self.get_all_cells().values():
11✔
619
            names = []
11✔
620
            if cell.fill_type in ('material', 'universe', 'lattice'):
11✔
621
                names.append(cell.fill.name)
11✔
UNCOV
622
            elif cell.fill_type == 'distribmat':
×
UNCOV
623
                for mat in cell.fill:
×
UNCOV
624
                    if mat is not None:
×
UNCOV
625
                        names.append(mat.name)
×
626

627
            for fill_name in names:
11✔
628
                if not case_sensitive:
11✔
629
                    fill_name = fill_name.lower()
11✔
630

631
                if fill_name == name:
11✔
632
                    cells.add(cell)
11✔
633
                elif not matching and name in fill_name:
11✔
634
                    cells.add(cell)
11✔
635

636
        return sorted(cells, key=lambda x: x.id)
11✔
637

638
    def get_universes_by_name(
11✔
639
        self, name, case_sensitive=False, matching=False
640
    ) -> list[openmc.Universe]:
641
        """Return a list of universes with matching names.
642

643
        Parameters
644
        ----------
645
        name : str
646
            The name to match
647
        case_sensitive : bool
648
            Whether to distinguish upper and lower case letters in each
649
            universe's name (default is False)
650
        matching : bool
651
            Whether the names must match completely (default is False)
652

653
        Returns
654
        -------
655
        list of openmc.Universe
656
            Universes matching the queried name
657

658
        """
659
        return self._get_domains_by_name(name, case_sensitive, matching, 'universe')
11✔
660

661
    def get_lattices_by_name(
11✔
662
        self, name, case_sensitive=False, matching=False
663
    ) -> list[openmc.Lattice]:
664
        """Return a list of lattices with matching names.
665

666
        Parameters
667
        ----------
668
        name : str
669
            The name to match
670
        case_sensitive : bool
671
            Whether to distinguish upper and lower case letters in each
672
            lattice's name (default is False)
673
        matching : bool
674
            Whether the names must match completely (default is False)
675

676
        Returns
677
        -------
678
        list of openmc.Lattice
679
            Lattices matching the queried name
680

681
        """
682
        return self._get_domains_by_name(name, case_sensitive, matching, 'lattice')
11✔
683

684
    def remove_redundant_surfaces(self) -> dict[int, openmc.Surface]:
11✔
685
        """Remove and return all of the redundant surfaces.
686

687
        Uses surface_precision attribute of Geometry instance for rounding and
688
        comparing surface coefficients.
689

690
        .. versionadded:: 0.12
691

692
        Returns
693
        -------
694
        redundant_surfaces
695
            Dictionary whose keys are the ID of a redundant surface and whose
696
            values are the topologically equivalent :class:`openmc.Surface`
697
            that should replace it.
698

699
        """
700
        # Get redundant surfaces
701
        redundancies = defaultdict(list)
11✔
702
        for surf in self.get_all_surfaces().values():
11✔
703
            coeffs = tuple(round(surf._coefficients[k],
11✔
704
                                 self.surface_precision)
705
                           for k in surf._coeff_keys)
706
            key = (surf._type, surf._boundary_type) + coeffs
11✔
707
            redundancies[key].append(surf)
11✔
708

709
        redundant_surfaces = {replace.id: keep
11✔
710
                              for keep, *redundant in redundancies.values()
711
                              for replace in redundant}
712

713
        if redundant_surfaces:
11✔
714
            # Iterate through all cells contained in the geometry
715
            for cell in self.get_all_cells().values():
11✔
716
                # Recursively remove redundant surfaces from regions
717
                if cell.region:
11✔
718
                    cell.region.remove_redundant_surfaces(redundant_surfaces)
11✔
719

720
        return redundant_surfaces
11✔
721

722
    def determine_paths(self, instances_only=False):
11✔
723
        """Determine paths through CSG tree for cells and materials.
724

725
        This method recursively traverses the CSG tree to determine each unique
726
        path that reaches every cell and material. The paths are stored in the
727
        :attr:`Cell.paths` and :attr:`Material.paths` attributes.
728

729
        Parameters
730
        ----------
731
        instances_only : bool, optional
732
            If true, this method will only determine the number of instances of
733
            each cell and material.
734

735
        """
736
        # (Re-)initialize all cell instances to 0
737
        for cell in self.get_all_cells().values():
11✔
738
            cell._paths = []
11✔
739
            cell._num_instances = 0
11✔
740
        for material in self.get_all_materials().values():
11✔
741
            material._paths = []
11✔
742
            material._num_instances = 0
11✔
743

744
        # Recursively traverse the CSG tree to count all cell instances
745
        self.root_universe._determine_paths(instances_only=instances_only)
11✔
746

747
    def clone(self) -> Geometry:
11✔
748
        """Create a copy of this geometry with new unique IDs for all of its
749
        enclosed materials, surfaces, cells, universes and lattices."""
750

751
        clone = deepcopy(self)
11✔
752
        clone.root_universe = self.root_universe.clone()
11✔
753
        return clone
11✔
754

755
    @add_plot_params
11✔
756
    def plot(self, *args, **kwargs):
11✔
757
        """Display a slice plot of the geometry.
758

759
        .. versionadded:: 0.14.0
760
        """
UNCOV
761
        return self.root_universe.plot(*args, **kwargs)
×
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