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

openmc-dev / openmc / 13399118471

18 Feb 2025 08:03PM UTC coverage: 85.031% (+0.06%) from 84.969%
13399118471

Pull #3299

github

web-flow
Merge f8e8fbd3c into 81b738862
Pull Request #3299: Random Ray Explicit Void Treatment

132 of 159 new or added lines in 5 files covered. (83.02%)

272 existing lines in 9 files now uncovered.

50569 of 59471 relevant lines covered (85.03%)

34980588.94 hits per line

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

92.64
/openmc/model/model.py
1
from __future__ import annotations
11✔
2
from collections.abc import Iterable, Sequence
11✔
3
from functools import lru_cache
11✔
4
from pathlib import Path
11✔
5
import math
11✔
6
from numbers import Integral, Real
11✔
7
from tempfile import NamedTemporaryFile, TemporaryDirectory
11✔
8
import warnings
11✔
9

10
import h5py
11✔
11
import lxml.etree as ET
11✔
12
import numpy as np
11✔
13

14
import openmc
11✔
15
import openmc._xml as xml
11✔
16
from openmc.dummy_comm import DummyCommunicator
11✔
17
from openmc.executor import _process_CLI_arguments
11✔
18
from openmc.checkvalue import check_type, check_value, PathLike
11✔
19
from openmc.exceptions import InvalidIDError
11✔
20
from openmc.plots import add_plot_params
11✔
21
from openmc.utility_funcs import change_directory
11✔
22

23

24
class Model:
11✔
25
    """Model container.
26

27
    This class can be used to store instances of :class:`openmc.Geometry`,
28
    :class:`openmc.Materials`, :class:`openmc.Settings`,
29
    :class:`openmc.Tallies`, and :class:`openmc.Plots`, thus making a complete
30
    model. The :meth:`Model.export_to_xml` method will export XML files for all
31
    attributes that have been set. If the :attr:`Model.materials` attribute is
32
    not set, it will attempt to create a ``materials.xml`` file based on all
33
    materials appearing in the geometry.
34

35
    .. versionchanged:: 0.13.0
36
        The model information can now be loaded in to OpenMC directly via
37
        openmc.lib
38

39
    Parameters
40
    ----------
41
    geometry : openmc.Geometry, optional
42
        Geometry information
43
    materials : openmc.Materials, optional
44
        Materials information
45
    settings : openmc.Settings, optional
46
        Settings information
47
    tallies : openmc.Tallies, optional
48
        Tallies information
49
    plots : openmc.Plots, optional
50
        Plot information
51

52
    Attributes
53
    ----------
54
    geometry : openmc.Geometry
55
        Geometry information
56
    materials : openmc.Materials
57
        Materials information
58
    settings : openmc.Settings
59
        Settings information
60
    tallies : openmc.Tallies
61
        Tallies information
62
    plots : openmc.Plots
63
        Plot information
64

65
    """
66

67
    def __init__(self, geometry=None, materials=None, settings=None,
11✔
68
                 tallies=None, plots=None):
69
        self.geometry = openmc.Geometry() if geometry is None else geometry
11✔
70
        self.materials = openmc.Materials() if materials is None else materials
11✔
71
        self.settings = openmc.Settings() if settings is None else settings
11✔
72
        self.tallies = openmc.Tallies() if tallies is None else tallies
11✔
73
        self.plots = openmc.Plots() if plots is None else plots
11✔
74

75
    @property
11✔
76
    def geometry(self) -> openmc.Geometry | None:
11✔
77
        return self._geometry
11✔
78

79
    @geometry.setter
11✔
80
    def geometry(self, geometry):
11✔
81
        check_type('geometry', geometry, openmc.Geometry)
11✔
82
        self._geometry = geometry
11✔
83

84
    @property
11✔
85
    def materials(self) -> openmc.Materials | None:
11✔
86
        return self._materials
11✔
87

88
    @materials.setter
11✔
89
    def materials(self, materials):
11✔
90
        check_type('materials', materials, Iterable, openmc.Material)
11✔
91
        if isinstance(materials, openmc.Materials):
11✔
92
            self._materials = materials
11✔
93
        else:
94
            del self._materials[:]
11✔
95
            for mat in materials:
11✔
96
                self._materials.append(mat)
11✔
97

98
    @property
11✔
99
    def settings(self) -> openmc.Settings | None:
11✔
100
        return self._settings
11✔
101

102
    @settings.setter
11✔
103
    def settings(self, settings):
11✔
104
        check_type('settings', settings, openmc.Settings)
11✔
105
        self._settings = settings
11✔
106

107
    @property
11✔
108
    def tallies(self) -> openmc.Tallies | None:
11✔
109
        return self._tallies
11✔
110

111
    @tallies.setter
11✔
112
    def tallies(self, tallies):
11✔
113
        check_type('tallies', tallies, Iterable, openmc.Tally)
11✔
114
        if isinstance(tallies, openmc.Tallies):
11✔
115
            self._tallies = tallies
11✔
116
        else:
117
            del self._tallies[:]
11✔
118
            for tally in tallies:
11✔
119
                self._tallies.append(tally)
11✔
120

121
    @property
11✔
122
    def plots(self) -> openmc.Plots | None:
11✔
123
        return self._plots
11✔
124

125
    @plots.setter
11✔
126
    def plots(self, plots):
11✔
127
        check_type('plots', plots, Iterable, openmc.PlotBase)
11✔
128
        if isinstance(plots, openmc.Plots):
11✔
129
            self._plots = plots
11✔
130
        else:
131
            del self._plots[:]
11✔
132
            for plot in plots:
11✔
133
                self._plots.append(plot)
11✔
134

135
    @property
11✔
136
    def bounding_box(self) -> openmc.BoundingBox:
11✔
137
        return self.geometry.bounding_box
11✔
138

139
    @property
11✔
140
    def is_initialized(self) -> bool:
11✔
141
        try:
11✔
142
            import openmc.lib
11✔
143
            return openmc.lib.is_initialized
11✔
144
        except ImportError:
×
145
            return False
×
146

147
    @property
11✔
148
    @lru_cache(maxsize=None)
11✔
149
    def _materials_by_id(self) -> dict:
11✔
150
        """Dictionary mapping material ID --> material"""
151
        if self.materials:
11✔
152
            mats = self.materials
11✔
153
        else:
154
            mats = self.geometry.get_all_materials().values()
11✔
155
        return {mat.id: mat for mat in mats}
11✔
156

157
    @property
11✔
158
    @lru_cache(maxsize=None)
11✔
159
    def _cells_by_id(self) -> dict:
11✔
160
        """Dictionary mapping cell ID --> cell"""
161
        cells = self.geometry.get_all_cells()
11✔
162
        return {cell.id: cell for cell in cells.values()}
11✔
163

164
    @property
11✔
165
    @lru_cache(maxsize=None)
11✔
166
    def _cells_by_name(self) -> dict[int, openmc.Cell]:
11✔
167
        # Get the names maps, but since names are not unique, store a set for
168
        # each name key. In this way when the user requests a change by a name,
169
        # the change will be applied to all of the same name.
170
        result = {}
11✔
171
        for cell in self.geometry.get_all_cells().values():
11✔
172
            if cell.name not in result:
11✔
173
                result[cell.name] = set()
11✔
174
            result[cell.name].add(cell)
11✔
175
        return result
11✔
176

177
    @property
11✔
178
    @lru_cache(maxsize=None)
11✔
179
    def _materials_by_name(self) -> dict[int, openmc.Material]:
11✔
180
        if self.materials is None:
11✔
181
            mats = self.geometry.get_all_materials().values()
×
182
        else:
183
            mats = self.materials
11✔
184
        result = {}
11✔
185
        for mat in mats:
11✔
186
            if mat.name not in result:
11✔
187
                result[mat.name] = set()
11✔
188
            result[mat.name].add(mat)
11✔
189
        return result
11✔
190

191
    @classmethod
11✔
192
    def from_xml(cls, geometry='geometry.xml', materials='materials.xml',
11✔
193
                 settings='settings.xml', tallies='tallies.xml',
194
                 plots='plots.xml') -> Model:
195
        """Create model from existing XML files
196

197
        Parameters
198
        ----------
199
        geometry : str
200
            Path to geometry.xml file
201
        materials : str
202
            Path to materials.xml file
203
        settings : str
204
            Path to settings.xml file
205
        tallies : str
206
            Path to tallies.xml file
207

208
            .. versionadded:: 0.13.0
209
        plots : str
210
            Path to plots.xml file
211

212
            .. versionadded:: 0.13.0
213

214
        Returns
215
        -------
216
        openmc.model.Model
217
            Model created from XML files
218

219
        """
220
        materials = openmc.Materials.from_xml(materials)
11✔
221
        geometry = openmc.Geometry.from_xml(geometry, materials)
11✔
222
        settings = openmc.Settings.from_xml(settings)
11✔
223
        tallies = openmc.Tallies.from_xml(
11✔
224
            tallies) if Path(tallies).exists() else None
225
        plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None
11✔
226
        return cls(geometry, materials, settings, tallies, plots)
11✔
227

228
    @classmethod
11✔
229
    def from_model_xml(cls, path='model.xml'):
11✔
230
        """Create model from single XML file
231

232
        .. versionadded:: 0.13.3
233

234
        Parameters
235
        ----------
236
        path : str or PathLike
237
            Path to model.xml file
238
        """
239
        parser = ET.XMLParser(huge_tree=True)
11✔
240
        tree = ET.parse(path, parser=parser)
11✔
241
        root = tree.getroot()
11✔
242

243
        model = cls()
11✔
244

245
        meshes = {}
11✔
246
        model.settings = openmc.Settings.from_xml_element(
11✔
247
            root.find('settings'), meshes)
248
        model.materials = openmc.Materials.from_xml_element(
11✔
249
            root.find('materials'))
250
        model.geometry = openmc.Geometry.from_xml_element(
11✔
251
            root.find('geometry'), model.materials)
252

253
        if root.find('tallies') is not None:
11✔
254
            model.tallies = openmc.Tallies.from_xml_element(
11✔
255
                root.find('tallies'), meshes)
256

257
        if root.find('plots') is not None:
11✔
258
            model.plots = openmc.Plots.from_xml_element(root.find('plots'))
11✔
259

260
        return model
11✔
261

262
    def init_lib(self, threads=None, geometry_debug=False, restart_file=None,
11✔
263
                 tracks=False, output=True, event_based=None, intracomm=None):
264
        """Initializes the model in memory via the C API
265

266
        .. versionadded:: 0.13.0
267

268
        Parameters
269
        ----------
270
        threads : int, optional
271
            Number of OpenMP threads. If OpenMC is compiled with OpenMP
272
            threading enabled, the default is implementation-dependent but is
273
            usually equal to the number of hardware threads available
274
            (or a value set by the :envvar:`OMP_NUM_THREADS` environment
275
            variable).
276
        geometry_debug : bool, optional
277
            Turn on geometry debugging during simulation. Defaults to False.
278
        restart_file : str, optional
279
            Path to restart file to use
280
        tracks : bool, optional
281
            Enables the writing of particles tracks. The number of particle
282
            tracks written to tracks.h5 is limited to 1000 unless
283
            Settings.max_tracks is set. Defaults to False.
284
        output : bool
285
            Capture OpenMC output from standard out
286
        event_based : None or bool, optional
287
            Turns on event-based parallelism if True. If None, the value in
288
            the Settings will be used.
289
        intracomm : mpi4py.MPI.Intracomm or None, optional
290
            MPI intracommunicator
291
        """
292

293
        import openmc.lib
11✔
294

295
        # TODO: right now the only way to set most of the above parameters via
296
        # the C API are at initialization time despite use-cases existing to
297
        # set them for individual runs. For now this functionality is exposed
298
        # where it exists (here in init), but in the future the functionality
299
        # should be exposed so that it can be accessed via model.run(...)
300

301
        args = _process_CLI_arguments(
11✔
302
            volume=False, geometry_debug=geometry_debug,
303
            restart_file=restart_file, threads=threads, tracks=tracks,
304
            event_based=event_based)
305
        # Args adds the openmc_exec command in the first entry; remove it
306
        args = args[1:]
11✔
307

308
        self.finalize_lib()
11✔
309

310
        # The Model object needs to be aware of the communicator so it can
311
        # use it in certain cases, therefore lets store the communicator
312
        if intracomm is not None:
11✔
313
            self._intracomm = intracomm
5✔
314
        else:
315
            self._intracomm = DummyCommunicator()
11✔
316

317
        if self._intracomm.rank == 0:
11✔
318
            self.export_to_xml()
11✔
319
        self._intracomm.barrier()
11✔
320

321
        # We cannot pass DummyCommunicator to openmc.lib.init so pass instead
322
        # the user-provided intracomm which will either be None or an mpi4py
323
        # communicator
324
        openmc.lib.init(args=args, intracomm=intracomm, output=output)
11✔
325

326
    def sync_dagmc_universes(self):
11✔
327
        """Synchronize all DAGMC universes in the current geometry.
328

329
        This method iterates over all DAGMC universes in the geometry and
330
        synchronizes their cells with the current material assignments. Requires
331
        that the model has been initialized via :meth:`Model.init_lib`.
332

333
        .. versionadded:: 0.15.1
334

335
        """
336
        if self.is_initialized:
1✔
337
            if self.materials:
1✔
338
                materials = self.materials
1✔
339
            else:
340
                materials = list(self.geometry.get_all_materials().values())
×
341
            for univ in self.geometry.get_all_universes().values():
1✔
342
                if isinstance(univ, openmc.DAGMCUniverse):
1✔
343
                    univ.sync_dagmc_cells(materials)
1✔
344
        else:
UNCOV
345
            raise ValueError("The model must be initialized before calling "
×
346
                             "this method")
347

348
    def finalize_lib(self):
11✔
349
        """Finalize simulation and free memory allocated for the C API
350

351
        .. versionadded:: 0.13.0
352

353
        """
354

355
        import openmc.lib
11✔
356

357
        openmc.lib.finalize()
11✔
358

359
    def deplete(self, timesteps, method='cecm', final_step=True,
11✔
360
                operator_kwargs=None, directory='.', output=True,
361
                **integrator_kwargs):
362
        """Deplete model using specified timesteps/power
363

364
        .. versionchanged:: 0.13.0
365
            The *final_step*, *operator_kwargs*, *directory*, and *output*
366
            arguments were added.
367

368
        Parameters
369
        ----------
370
        timesteps : iterable of float
371
            Array of timesteps in units of [s]. Note that values are not
372
            cumulative.
373
        method : str, optional
374
             Integration method used for depletion (e.g., 'cecm', 'predictor').
375
             Defaults to 'cecm'.
376
        final_step : bool, optional
377
            Indicate whether or not a transport solve should be run at the end
378
            of the last timestep. Defaults to running this transport solve.
379
        operator_kwargs : dict
380
            Keyword arguments passed to the depletion operator initializer
381
            (e.g., :func:`openmc.deplete.Operator`)
382
        directory : str, optional
383
            Directory to write XML files to. If it doesn't exist already, it
384
            will be created. Defaults to the current working directory
385
        output : bool
386
            Capture OpenMC output from standard out
387
        integrator_kwargs : dict
388
            Remaining keyword arguments passed to the depletion Integrator
389
            initializer (e.g., :func:`openmc.deplete.integrator.cecm`).
390

391
        """
392

393
        if operator_kwargs is None:
11✔
UNCOV
394
            op_kwargs = {}
×
395
        elif isinstance(operator_kwargs, dict):
11✔
396
            op_kwargs = operator_kwargs
11✔
397
        else:
UNCOV
398
            raise ValueError("operator_kwargs must be a dict or None")
×
399

400
        # Import openmc.deplete here so the Model can be used even if the
401
        # shared library is unavailable.
402
        import openmc.deplete as dep
11✔
403

404
        # Store whether or not the library was initialized when we started
405
        started_initialized = self.is_initialized
11✔
406

407
        with change_directory(directory):
11✔
408
            with openmc.lib.quiet_dll(output):
11✔
409
                # TODO: Support use of IndependentOperator too
410
                depletion_operator = dep.CoupledOperator(self, **op_kwargs)
11✔
411

412
            # Tell depletion_operator.finalize NOT to clear C API memory when
413
            # it is done
414
            depletion_operator.cleanup_when_done = False
11✔
415

416
            # Set up the integrator
417
            check_value('method', method,
11✔
418
                        dep.integrators.integrator_by_name.keys())
419
            integrator_class = dep.integrators.integrator_by_name[method]
11✔
420
            integrator = integrator_class(depletion_operator, timesteps,
11✔
421
                                          **integrator_kwargs)
422

423
            # Now perform the depletion
424
            with openmc.lib.quiet_dll(output):
11✔
425
                integrator.integrate(final_step)
11✔
426

427
            # Now make the python Materials match the C API material data
428
            for mat_id, mat in self._materials_by_id.items():
11✔
429
                if mat.depletable:
11✔
430
                    # Get the C data
431
                    c_mat = openmc.lib.materials[mat_id]
11✔
432
                    nuclides, densities = c_mat._get_densities()
11✔
433
                    # And now we can remove isotopes and add these ones in
434
                    mat.nuclides.clear()
11✔
435
                    for nuc, density in zip(nuclides, densities):
11✔
436
                        mat.add_nuclide(nuc, density)
11✔
437
                    mat.set_density('atom/b-cm', sum(densities))
11✔
438

439
            # If we didnt start intialized, we should cleanup after ourselves
440
            if not started_initialized:
11✔
441
                depletion_operator.cleanup_when_done = True
11✔
442
                depletion_operator.finalize()
11✔
443

444
    def export_to_xml(self, directory='.', remove_surfs=False):
11✔
445
        """Export model to separate XML files.
446

447
        Parameters
448
        ----------
449
        directory : str
450
            Directory to write XML files to. If it doesn't exist already, it
451
            will be created.
452
        remove_surfs : bool
453
            Whether or not to remove redundant surfaces from the geometry when
454
            exporting.
455

456
            .. versionadded:: 0.13.1
457
        """
458
        # Create directory if required
459
        d = Path(directory)
11✔
460
        if not d.is_dir():
11✔
461
            d.mkdir(parents=True, exist_ok=True)
11✔
462

463
        self.settings.export_to_xml(d)
11✔
464
        self.geometry.export_to_xml(d, remove_surfs=remove_surfs)
11✔
465

466
        # If a materials collection was specified, export it. Otherwise, look
467
        # for all materials in the geometry and use that to automatically build
468
        # a collection.
469
        if self.materials:
11✔
470
            self.materials.export_to_xml(d)
11✔
471
        else:
472
            materials = openmc.Materials(self.geometry.get_all_materials()
11✔
473
                                         .values())
474
            materials.export_to_xml(d)
11✔
475

476
        if self.tallies:
11✔
477
            self.tallies.export_to_xml(d)
11✔
478
        if self.plots:
11✔
479
            self.plots.export_to_xml(d)
11✔
480

481
    def export_to_model_xml(self, path='model.xml', remove_surfs=False):
11✔
482
        """Export model to a single XML file.
483

484
        .. versionadded:: 0.13.3
485

486
        Parameters
487
        ----------
488
        path : str or PathLike
489
            Location of the XML file to write (default is 'model.xml'). Can be a
490
            directory or file path.
491
        remove_surfs : bool
492
            Whether or not to remove redundant surfaces from the geometry when
493
            exporting.
494

495
        """
496
        xml_path = Path(path)
11✔
497
        # if the provided path doesn't end with the XML extension, assume the
498
        # input path is meant to be a directory. If the directory does not
499
        # exist, create it and place a 'model.xml' file there.
500
        if not str(xml_path).endswith('.xml'):
11✔
501
            if not xml_path.exists():
11✔
UNCOV
502
                xml_path.mkdir(parents=True, exist_ok=True)
×
503
            elif not xml_path.is_dir():
11✔
504
                raise FileExistsError(f"File exists and is not a directory: '{xml_path}'")
×
505
            xml_path /= 'model.xml'
11✔
506
        # if this is an XML file location and the file's parent directory does
507
        # not exist, create it before continuing
508
        elif not xml_path.parent.exists():
11✔
509
            xml_path.parent.mkdir(parents=True, exist_ok=True)
×
510

511
        if remove_surfs:
11✔
UNCOV
512
            warnings.warn("remove_surfs kwarg will be deprecated soon, please "
×
513
                          "set the Geometry.merge_surfaces attribute instead.")
UNCOV
514
            self.geometry.merge_surfaces = True
×
515

516
        # provide a memo to track which meshes have been written
517
        mesh_memo = set()
11✔
518
        settings_element = self.settings.to_xml_element(mesh_memo)
11✔
519
        geometry_element = self.geometry.to_xml_element()
11✔
520

521
        xml.clean_indentation(geometry_element, level=1)
11✔
522
        xml.clean_indentation(settings_element, level=1)
11✔
523

524
        # If a materials collection was specified, export it. Otherwise, look
525
        # for all materials in the geometry and use that to automatically build
526
        # a collection.
527
        if self.materials:
11✔
528
            materials = self.materials
11✔
529
        else:
530
            materials = openmc.Materials(self.geometry.get_all_materials()
11✔
531
                                         .values())
532

533
        with open(xml_path, 'w', encoding='utf-8', errors='xmlcharrefreplace') as fh:
11✔
534
            # write the XML header
535
            fh.write("<?xml version='1.0' encoding='utf-8'?>\n")
11✔
536
            fh.write("<model>\n")
11✔
537
            # Write the materials collection to the open XML file first.
538
            # This will write the XML header also
539
            materials._write_xml(fh, False, level=1)
11✔
540
            # Write remaining elements as a tree
541
            fh.write(ET.tostring(geometry_element, encoding="unicode"))
11✔
542
            fh.write(ET.tostring(settings_element, encoding="unicode"))
11✔
543

544
            if self.tallies:
11✔
545
                tallies_element = self.tallies.to_xml_element(mesh_memo)
11✔
546
                xml.clean_indentation(
11✔
547
                    tallies_element, level=1, trailing_indent=self.plots)
548
                fh.write(ET.tostring(tallies_element, encoding="unicode"))
11✔
549
            if self.plots:
11✔
550
                plots_element = self.plots.to_xml_element()
11✔
551
                xml.clean_indentation(
11✔
552
                    plots_element, level=1, trailing_indent=False)
553
                fh.write(ET.tostring(plots_element, encoding="unicode"))
11✔
554
            fh.write("</model>\n")
11✔
555

556
    def import_properties(self, filename):
11✔
557
        """Import physical properties
558

559
        .. versionchanged:: 0.13.0
560
            This method now updates values as loaded in memory with the C API
561

562
        Parameters
563
        ----------
564
        filename : str
565
            Path to properties HDF5 file
566

567
        See Also
568
        --------
569
        openmc.lib.export_properties
570

571
        """
572
        import openmc.lib
11✔
573

574
        cells = self.geometry.get_all_cells()
11✔
575
        materials = self.geometry.get_all_materials()
11✔
576

577
        with h5py.File(filename, 'r') as fh:
11✔
578
            cells_group = fh['geometry/cells']
11✔
579

580
            # Make sure number of cells matches
581
            n_cells = fh['geometry'].attrs['n_cells']
11✔
582
            if n_cells != len(cells):
11✔
UNCOV
583
                raise ValueError("Number of cells in properties file doesn't "
×
584
                                 "match current model.")
585

586
            # Update temperatures for cells filled with materials
587
            for name, group in cells_group.items():
11✔
588
                cell_id = int(name.split()[1])
11✔
589
                cell = cells[cell_id]
11✔
590
                if cell.fill_type in ('material', 'distribmat'):
11✔
591
                    temperature = group['temperature'][()]
11✔
592
                    cell.temperature = temperature
11✔
593
                    if self.is_initialized:
11✔
594
                        lib_cell = openmc.lib.cells[cell_id]
11✔
595
                        if temperature.size > 1:
11✔
UNCOV
596
                            for i, T in enumerate(temperature):
×
UNCOV
597
                                lib_cell.set_temperature(T, i)
×
598
                        else:
599
                            lib_cell.set_temperature(temperature[0])
11✔
600

601
            # Make sure number of materials matches
602
            mats_group = fh['materials']
11✔
603
            n_cells = mats_group.attrs['n_materials']
11✔
604
            if n_cells != len(materials):
11✔
UNCOV
605
                raise ValueError("Number of materials in properties file "
×
606
                                 "doesn't match current model.")
607

608
            # Update material densities
609
            for name, group in mats_group.items():
11✔
610
                mat_id = int(name.split()[1])
11✔
611
                atom_density = group.attrs['atom_density']
11✔
612
                materials[mat_id].set_density('atom/b-cm', atom_density)
11✔
613
                if self.is_initialized:
11✔
614
                    C_mat = openmc.lib.materials[mat_id]
11✔
615
                    C_mat.set_density(atom_density, 'atom/b-cm')
11✔
616

617
    def run(self, particles=None, threads=None, geometry_debug=False,
11✔
618
            restart_file=None, tracks=False, output=True, cwd='.',
619
            openmc_exec='openmc', mpi_args=None, event_based=None,
620
            export_model_xml=True, apply_tally_results=False,
621
            **export_kwargs):
622
        """Run OpenMC
623

624
        If the C API has been initialized, then the C API is used, otherwise,
625
        this method creates the XML files and runs OpenMC via a system call. In
626
        both cases this method returns the path to the last statepoint file
627
        generated.
628

629
        .. versionchanged:: 0.12
630
            Instead of returning the final k-effective value, this function now
631
            returns the path to the final statepoint written.
632

633
        .. versionchanged:: 0.13.0
634
            This method can utilize the C API for execution
635

636
        Parameters
637
        ----------
638
        particles : int, optional
639
            Number of particles to simulate per generation.
640
        threads : int, optional
641
            Number of OpenMP threads. If OpenMC is compiled with OpenMP
642
            threading enabled, the default is implementation-dependent but is
643
            usually equal to the number of hardware threads available (or a
644
            value set by the :envvar:`OMP_NUM_THREADS` environment variable).
645
        geometry_debug : bool, optional
646
            Turn on geometry debugging during simulation. Defaults to False.
647
        restart_file : str or PathLike
648
            Path to restart file to use
649
        tracks : bool, optional
650
            Enables the writing of particles tracks. The number of particle
651
            tracks written to tracks.h5 is limited to 1000 unless
652
            Settings.max_tracks is set. Defaults to False.
653
        output : bool, optional
654
            Capture OpenMC output from standard out
655
        cwd : PathLike, optional
656
            Path to working directory to run in. Defaults to the current working
657
            directory.
658
        openmc_exec : str, optional
659
            Path to OpenMC executable. Defaults to 'openmc'.
660
        mpi_args : list of str, optional
661
            MPI execute command and any additional MPI arguments to pass, e.g.
662
            ['mpiexec', '-n', '8'].
663
        event_based : None or bool, optional
664
            Turns on event-based parallelism if True. If None, the value in the
665
            Settings will be used.
666
        export_model_xml : bool, optional
667
            Exports a single model.xml file rather than separate files. Defaults
668
            to True.
669

670
            .. versionadded:: 0.13.3
671
        apply_tally_results : bool
672
            Whether to apply results of the final statepoint file to the
673
            model's tally objects.
674

675
            .. versionadded:: 0.15.1
676
        **export_kwargs
677
            Keyword arguments passed to either :meth:`Model.export_to_model_xml`
678
            or :meth:`Model.export_to_xml`.
679

680
        Returns
681
        -------
682
        Path
683
            Path to the last statepoint written by this run (None if no
684
            statepoint was written)
685

686
        """
687

688
        # Setting tstart here ensures we don't pick up any pre-existing
689
        # statepoint files in the output directory -- just in case there are
690
        # differences between the system clock and the filesystem, we get the
691
        # time of a just-created temporary file
692
        with NamedTemporaryFile() as fp:
11✔
693
            tstart = Path(fp.name).stat().st_mtime
11✔
694
        last_statepoint = None
11✔
695

696
        # Operate in the provided working directory
697
        with change_directory(cwd):
11✔
698
            if self.is_initialized:
11✔
699
                # Handle the run options as applicable
700
                # First dont allow ones that must be set via init
701
                for arg_name, arg, default in zip(
11✔
702
                    ['threads', 'geometry_debug', 'restart_file', 'tracks'],
703
                    [threads, geometry_debug, restart_file, tracks],
704
                    [None, False, None, False]
705
                ):
706
                    if arg != default:
11✔
707
                        msg = f"{arg_name} must be set via Model.is_initialized(...)"
11✔
708
                        raise ValueError(msg)
11✔
709

710
                init_particles = openmc.lib.settings.particles
11✔
711
                if particles is not None:
11✔
UNCOV
712
                    if isinstance(particles, Integral) and particles > 0:
×
UNCOV
713
                        openmc.lib.settings.particles = particles
×
714

715
                init_event_based = openmc.lib.settings.event_based
11✔
716
                if event_based is not None:
11✔
UNCOV
717
                    openmc.lib.settings.event_based = event_based
×
718

719
                # Then run using the C API
720
                openmc.lib.run(output)
11✔
721

722
                # Reset changes for the openmc.run kwargs handling
723
                openmc.lib.settings.particles = init_particles
11✔
724
                openmc.lib.settings.event_based = init_event_based
11✔
725

726
            else:
727
                # Then run via the command line
728
                if export_model_xml:
11✔
729
                    self.export_to_model_xml(**export_kwargs)
11✔
730
                else:
731
                    self.export_to_xml(**export_kwargs)
11✔
732
                path_input = export_kwargs.get("path", None)
11✔
733
                openmc.run(particles, threads, geometry_debug, restart_file,
11✔
734
                           tracks, output, Path('.'), openmc_exec, mpi_args,
735
                           event_based, path_input)
736

737
            # Get output directory and return the last statepoint written
738
            if self.settings.output and 'path' in self.settings.output:
11✔
UNCOV
739
                output_dir = Path(self.settings.output['path'])
×
740
            else:
741
                output_dir = Path.cwd()
11✔
742
            for sp in output_dir.glob('statepoint.*.h5'):
11✔
743
                mtime = sp.stat().st_mtime
11✔
744
                if mtime >= tstart:  # >= allows for poor clock resolution
11✔
745
                    tstart = mtime
11✔
746
                    last_statepoint = sp
11✔
747

748
        if apply_tally_results:
11✔
749
            self.apply_tally_results(last_statepoint)
11✔
750

751
        return last_statepoint
11✔
752

753
    def calculate_volumes(self, threads=None, output=True, cwd='.',
11✔
754
                          openmc_exec='openmc', mpi_args=None,
755
                          apply_volumes=True, export_model_xml=True,
756
                          **export_kwargs):
757
        """Runs an OpenMC stochastic volume calculation and, if requested,
758
        applies volumes to the model
759

760
        .. versionadded:: 0.13.0
761

762
        Parameters
763
        ----------
764
        threads : int, optional
765
            Number of OpenMP threads. If OpenMC is compiled with OpenMP
766
            threading enabled, the default is implementation-dependent but is
767
            usually equal to the number of hardware threads available (or a
768
            value set by the :envvar:`OMP_NUM_THREADS` environment variable).
769
            This currenty only applies to the case when not using the C API.
770
        output : bool, optional
771
            Capture OpenMC output from standard out
772
        openmc_exec : str, optional
773
            Path to OpenMC executable. Defaults to 'openmc'.
774
            This only applies to the case when not using the C API.
775
        mpi_args : list of str, optional
776
            MPI execute command and any additional MPI arguments to pass,
777
            e.g. ['mpiexec', '-n', '8'].
778
            This only applies to the case when not using the C API.
779
        cwd : str, optional
780
            Path to working directory to run in. Defaults to the current
781
            working directory.
782
        apply_volumes : bool, optional
783
            Whether apply the volume calculation results from this calculation
784
            to the model. Defaults to applying the volumes.
785
        export_model_xml : bool, optional
786
            Exports a single model.xml file rather than separate files. Defaults
787
            to True.
788
        **export_kwargs
789
            Keyword arguments passed to either :meth:`Model.export_to_model_xml`
790
            or :meth:`Model.export_to_xml`.
791

792
        """
793

794
        if len(self.settings.volume_calculations) == 0:
11✔
795
            # Then there is no volume calculation specified
796
            raise ValueError("The Settings.volume_calculations attribute must"
11✔
797
                             " be specified before executing this method!")
798

799
        with change_directory(cwd):
11✔
800
            if self.is_initialized:
11✔
801
                if threads is not None:
11✔
UNCOV
802
                    msg = "Threads must be set via Model.is_initialized(...)"
×
UNCOV
803
                    raise ValueError(msg)
×
804
                if mpi_args is not None:
11✔
UNCOV
805
                    msg = "The MPI environment must be set otherwise such as" \
×
806
                        "with the call to mpi4py"
UNCOV
807
                    raise ValueError(msg)
×
808

809
                # Compute the volumes
810
                openmc.lib.calculate_volumes(output)
11✔
811

812
            else:
813
                if export_model_xml:
11✔
814
                    self.export_to_model_xml(**export_kwargs)
11✔
815
                else:
UNCOV
816
                    self.export_to_xml(**export_kwargs)
×
817
                path_input = export_kwargs.get("path", None)
11✔
818
                openmc.calculate_volumes(
11✔
819
                    threads=threads, output=output, openmc_exec=openmc_exec,
820
                    mpi_args=mpi_args, path_input=path_input
821
                )
822

823
            # Now we apply the volumes
824
            if apply_volumes:
11✔
825
                # Load the results and add them to the model
826
                for i, vol_calc in enumerate(self.settings.volume_calculations):
11✔
827
                    vol_calc.load_results(f"volume_{i + 1}.h5")
11✔
828
                    # First add them to the Python side
829
                    if vol_calc.domain_type == "material" and self.materials:
11✔
830
                        for material in self.materials:
11✔
831
                            if material.id in vol_calc.volumes:
11✔
832
                                material.add_volume_information(vol_calc)
11✔
833
                    else:
834
                        self.geometry.add_volume_information(vol_calc)
11✔
835

836
                    # And now repeat for the C API
837
                    if self.is_initialized and vol_calc.domain_type == 'material':
11✔
838
                        # Then we can do this in the C API
839
                        for domain_id in vol_calc.ids:
11✔
840
                            openmc.lib.materials[domain_id].volume = \
11✔
841
                                vol_calc.volumes[domain_id].n
842

843
    @add_plot_params
11✔
844
    def plot(
11✔
845
        self,
846
        origin: Sequence[float] | None = None,
847
        width: Sequence[float] | None = None,
848
        pixels: int | Sequence[int] = 40000,
849
        basis: str = 'xy',
850
        color_by: str = 'cell',
851
        colors: dict | None = None,
852
        seed: int | None = None,
853
        openmc_exec: PathLike = 'openmc',
854
        axes=None,
855
        legend: bool = False,
856
        axis_units: str = 'cm',
857
        outline: bool | str = False,
858
        n_samples: int | None = None,
859
        plane_tolerance: float = 1.,
860
        legend_kwargs: dict | None = None,
861
        source_kwargs: dict | None = None,
862
        contour_kwargs: dict | None = None,
863
        **kwargs,
864
    ):
865
        """Display a slice plot of the model.
866

867
        .. versionadded:: 0.15.1
868
        """
869
        import matplotlib.image as mpimg
11✔
870
        import matplotlib.patches as mpatches
11✔
871
        import matplotlib.pyplot as plt
11✔
872

873
        check_type('n_samples', n_samples, int | None)
11✔
874
        check_type('plane_tolerance', plane_tolerance, Real)
11✔
875
        if legend_kwargs is None:
11✔
876
            legend_kwargs = {}
11✔
877
        legend_kwargs.setdefault('bbox_to_anchor', (1.05, 1))
11✔
878
        legend_kwargs.setdefault('loc', 2)
11✔
879
        legend_kwargs.setdefault('borderaxespad', 0.0)
11✔
880
        if source_kwargs is None:
11✔
881
            source_kwargs = {}
11✔
882
        source_kwargs.setdefault('marker', 'x')
11✔
883

884
        # Determine extents of plot
885
        if basis == 'xy':
11✔
886
            x, y, z = 0, 1, 2
11✔
887
            xlabel, ylabel = f'x [{axis_units}]', f'y [{axis_units}]'
11✔
888
        elif basis == 'yz':
11✔
889
            x, y, z = 1, 2, 0
11✔
890
            xlabel, ylabel = f'y [{axis_units}]', f'z [{axis_units}]'
11✔
891
        elif basis == 'xz':
11✔
892
            x, y, z = 0, 2, 1
11✔
893
            xlabel, ylabel = f'x [{axis_units}]', f'z [{axis_units}]'
11✔
894

895
        bb = self.bounding_box
11✔
896
        # checks to see if bounding box contains -inf or inf values
897
        if np.isinf(bb.extent[basis]).any():
11✔
898
            if origin is None:
11✔
899
                origin = (0, 0, 0)
11✔
900
            if width is None:
11✔
901
                width = (10, 10)
11✔
902
        else:
903
            if origin is None:
11✔
904
                # if nan values in the bb.center they get replaced with 0.0
905
                # this happens when the bounding_box contains inf values
906
                with warnings.catch_warnings():
11✔
907
                    warnings.simplefilter("ignore", RuntimeWarning)
11✔
908
                    origin = np.nan_to_num(bb.center)
11✔
909
            if width is None:
11✔
910
                bb_width = bb.width
11✔
911
                width = (bb_width[x], bb_width[y])
11✔
912

913
        if isinstance(pixels, int):
11✔
914
            aspect_ratio = width[0] / width[1]
11✔
915
            pixels_y = math.sqrt(pixels / aspect_ratio)
11✔
916
            pixels = (int(pixels / pixels_y), int(pixels_y))
11✔
917

918
        axis_scaling_factor = {'km': 0.00001, 'm': 0.01, 'cm': 1, 'mm': 10}
11✔
919

920
        x_min = (origin[x] - 0.5*width[0]) * axis_scaling_factor[axis_units]
11✔
921
        x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units]
11✔
922
        y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units]
11✔
923
        y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units]
11✔
924

925
        with TemporaryDirectory() as tmpdir:
11✔
926
            if seed is not None:
11✔
UNCOV
927
                self.settings.plot_seed = seed
×
928

929
            # Create plot object matching passed arguments
930
            plot = openmc.Plot()
11✔
931
            plot.origin = origin
11✔
932
            plot.width = width
11✔
933
            plot.pixels = pixels
11✔
934
            plot.basis = basis
11✔
935
            plot.color_by = color_by
11✔
936
            if colors is not None:
11✔
937
                plot.colors = colors
11✔
938
            self.plots.append(plot)
11✔
939

940
            # Run OpenMC in geometry plotting mode
941
            self.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec)
11✔
942

943
            # Read image from file
944
            img_path = Path(tmpdir) / f'plot_{plot.id}.png'
11✔
945
            if not img_path.is_file():
11✔
UNCOV
946
                img_path = img_path.with_suffix('.ppm')
×
947
            img = mpimg.imread(str(img_path))
11✔
948

949
            # Create a figure sized such that the size of the axes within
950
            # exactly matches the number of pixels specified
951
            if axes is None:
11✔
952
                px = 1/plt.rcParams['figure.dpi']
11✔
953
                fig, axes = plt.subplots()
11✔
954
                axes.set_xlabel(xlabel)
11✔
955
                axes.set_ylabel(ylabel)
11✔
956
                params = fig.subplotpars
11✔
957
                width = pixels[0]*px/(params.right - params.left)
11✔
958
                height = pixels[1]*px/(params.top - params.bottom)
11✔
959
                fig.set_size_inches(width, height)
11✔
960

961
            if outline:
11✔
962
                # Combine R, G, B values into a single int
963
                rgb = (img * 256).astype(int)
11✔
964
                image_value = (rgb[..., 0] << 16) + \
11✔
965
                    (rgb[..., 1] << 8) + (rgb[..., 2])
966

967
                # Set default arguments for contour()
968
                if contour_kwargs is None:
11✔
969
                    contour_kwargs = {}
11✔
970
                contour_kwargs.setdefault('colors', 'k')
11✔
971
                contour_kwargs.setdefault('linestyles', 'solid')
11✔
972
                contour_kwargs.setdefault('algorithm', 'serial')
11✔
973

974
                axes.contour(
11✔
975
                    image_value,
976
                    origin="upper",
977
                    levels=np.unique(image_value),
978
                    extent=(x_min, x_max, y_min, y_max),
979
                    **contour_kwargs
980
                )
981

982
            # add legend showing which colors represent which material
983
            # or cell if that was requested
984
            if legend:
11✔
985
                if plot.colors == {}:
11✔
986
                    raise ValueError("Must pass 'colors' dictionary if you "
11✔
987
                                     "are adding a legend via legend=True.")
988

989
                if color_by == "cell":
11✔
UNCOV
990
                    expected_key_type = openmc.Cell
×
991
                else:
992
                    expected_key_type = openmc.Material
11✔
993

994
                patches = []
11✔
995
                for key, color in plot.colors.items():
11✔
996

997
                    if isinstance(key, int):
11✔
UNCOV
998
                        raise TypeError(
×
999
                            "Cannot use IDs in colors dict for auto legend.")
1000
                    elif not isinstance(key, expected_key_type):
11✔
UNCOV
1001
                        raise TypeError(
×
1002
                            "Color dict key type does not match color_by")
1003

1004
                    # this works whether we're doing cells or materials
1005
                    label = key.name if key.name != '' else key.id
11✔
1006

1007
                    # matplotlib takes RGB on 0-1 scale rather than 0-255. at
1008
                    # this point PlotBase has already checked that 3-tuple
1009
                    # based colors are already valid, so if the length is three
1010
                    # then we know it just needs to be converted to the 0-1
1011
                    # format.
1012
                    if len(color) == 3 and not isinstance(color, str):
11✔
1013
                        scaled_color = (
11✔
1014
                            color[0]/255, color[1]/255, color[2]/255)
1015
                    else:
1016
                        scaled_color = color
11✔
1017

1018
                    key_patch = mpatches.Patch(color=scaled_color, label=label)
11✔
1019
                    patches.append(key_patch)
11✔
1020

1021
                axes.legend(handles=patches, **legend_kwargs)
11✔
1022

1023
            # Plot image and return the axes
1024
            if outline != 'only':
11✔
1025
                axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs)
11✔
1026

1027

1028
        if n_samples:
11✔
1029
            # Sample external source particles
1030
            particles = self.sample_external_source(n_samples)
11✔
1031

1032
            # Get points within tolerance of the slice plane
1033
            slice_value = origin[z]
11✔
1034
            xs = []
11✔
1035
            ys = []
11✔
1036
            tol = plane_tolerance
11✔
1037
            for particle in particles:
11✔
1038
                if (slice_value - tol < particle.r[z] < slice_value + tol):
11✔
1039
                    xs.append(particle.r[x])
11✔
1040
                    ys.append(particle.r[y])
11✔
1041
            axes.scatter(xs, ys, **source_kwargs)
11✔
1042

1043
        return axes
11✔
1044

1045
    def sample_external_source(
11✔
1046
            self,
1047
            n_samples: int = 1000,
1048
            prn_seed: int | None = None,
1049
            **init_kwargs
1050
    ) -> openmc.ParticleList:
1051
        """Sample external source and return source particles.
1052

1053
        .. versionadded:: 0.15.1
1054

1055
        Parameters
1056
        ----------
1057
        n_samples : int
1058
            Number of samples
1059
        prn_seed : int
1060
            Pseudorandom number generator (PRNG) seed; if None, one will be
1061
            generated randomly.
1062
        **init_kwargs
1063
            Keyword arguments passed to :func:`openmc.lib.init`
1064

1065
        Returns
1066
        -------
1067
        openmc.ParticleList
1068
            List of samples source particles
1069
        """
1070
        import openmc.lib
11✔
1071

1072
        # Silence output by default. Also set arguments to start in volume
1073
        # calculation mode to avoid loading cross sections
1074
        init_kwargs.setdefault('output', False)
11✔
1075
        init_kwargs.setdefault('args', ['-c'])
11✔
1076

1077
        with change_directory(tmpdir=True):
11✔
1078
            # Export model within temporary directory
1079
            self.export_to_model_xml()
11✔
1080

1081
            # Sample external source sites
1082
            with openmc.lib.run_in_memory(**init_kwargs):
11✔
1083
                return openmc.lib.sample_external_source(
11✔
1084
                    n_samples=n_samples, prn_seed=prn_seed
1085
                )
1086

1087
    def apply_tally_results(self, statepoint: PathLike | openmc.StatePoint):
11✔
1088
        """Apply results from a statepoint to tally objects on the Model
1089

1090
        Parameters
1091
        ----------
1092
        statepoint : PathLike or openmc.StatePoint
1093
            Statepoint file used to update tally results
1094
        """
1095
        self.tallies.add_results(statepoint)
11✔
1096

1097
    def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc',
11✔
1098
                      export_model_xml=True, **export_kwargs):
1099
        """Creates plot images as specified by the Model.plots attribute
1100

1101
        .. versionadded:: 0.13.0
1102

1103
        Parameters
1104
        ----------
1105
        output : bool, optional
1106
            Capture OpenMC output from standard out
1107
        cwd : str, optional
1108
            Path to working directory to run in. Defaults to the current
1109
            working directory.
1110
        openmc_exec : str, optional
1111
            Path to OpenMC executable. Defaults to 'openmc'.
1112
            This only applies to the case when not using the C API.
1113
        export_model_xml : bool, optional
1114
            Exports a single model.xml file rather than separate files. Defaults
1115
            to True.
1116
        **export_kwargs
1117
            Keyword arguments passed to either :meth:`Model.export_to_model_xml`
1118
            or :meth:`Model.export_to_xml`.
1119

1120
        """
1121

1122
        if len(self.plots) == 0:
11✔
1123
            # Then there is no volume calculation specified
UNCOV
1124
            raise ValueError("The Model.plots attribute must be specified "
×
1125
                             "before executing this method!")
1126

1127
        with change_directory(cwd):
11✔
1128
            if self.is_initialized:
11✔
1129
                # Compute the volumes
1130
                openmc.lib.plot_geometry(output)
11✔
1131
            else:
1132
                if export_model_xml:
11✔
1133
                    self.export_to_model_xml(**export_kwargs)
11✔
1134
                else:
UNCOV
1135
                    self.export_to_xml(**export_kwargs)
×
1136
                path_input = export_kwargs.get("path", None)
11✔
1137
                openmc.plot_geometry(output=output, openmc_exec=openmc_exec,
11✔
1138
                                     path_input=path_input)
1139

1140
    def _change_py_lib_attribs(self, names_or_ids, value, obj_type,
11✔
1141
                               attrib_name, density_units='atom/b-cm'):
1142
        # Method to do the same work whether it is a cell or material and
1143
        # a temperature or volume
1144
        check_type('names_or_ids', names_or_ids, Iterable, (Integral, str))
11✔
1145
        check_type('obj_type', obj_type, str)
11✔
1146
        obj_type = obj_type.lower()
11✔
1147
        check_value('obj_type', obj_type, ('material', 'cell'))
11✔
1148
        check_value('attrib_name', attrib_name,
11✔
1149
                    ('temperature', 'volume', 'density', 'rotation',
1150
                     'translation'))
1151
        # The C API only allows setting density units of atom/b-cm and g/cm3
1152
        check_value('density_units', density_units, ('atom/b-cm', 'g/cm3'))
11✔
1153
        # The C API has no way to set cell volume or material temperature
1154
        # so lets raise exceptions as needed
1155
        if obj_type == 'cell' and attrib_name == 'volume':
11✔
UNCOV
1156
            raise NotImplementedError(
×
1157
                'Setting a Cell volume is not supported!')
1158
        if obj_type == 'material' and attrib_name == 'temperature':
11✔
UNCOV
1159
            raise NotImplementedError(
×
1160
                'Setting a material temperature is not supported!')
1161

1162
        # And some items just dont make sense
1163
        if obj_type == 'cell' and attrib_name == 'density':
11✔
UNCOV
1164
            raise ValueError('Cannot set a Cell density!')
×
1165
        if obj_type == 'material' and attrib_name in ('rotation',
11✔
1166
                                                      'translation'):
UNCOV
1167
            raise ValueError('Cannot set a material rotation/translation!')
×
1168

1169
        # Set the
1170
        if obj_type == 'cell':
11✔
1171
            by_name = self._cells_by_name
11✔
1172
            by_id = self._cells_by_id
11✔
1173
            if self.is_initialized:
11✔
1174
                obj_by_id = openmc.lib.cells
11✔
1175
        else:
1176
            by_name = self._materials_by_name
11✔
1177
            by_id = self._materials_by_id
11✔
1178
            if self.is_initialized:
11✔
1179
                obj_by_id = openmc.lib.materials
11✔
1180
        # Get the list of ids to use if converting from names and accepting
1181
        # only values that have actual ids
1182
        ids = []
11✔
1183
        for name_or_id in names_or_ids:
11✔
1184
            if isinstance(name_or_id, Integral):
11✔
1185
                if name_or_id in by_id:
11✔
1186
                    ids.append(int(name_or_id))
11✔
1187
                else:
1188
                    cap_obj = obj_type.capitalize()
11✔
1189
                    msg = f'{cap_obj} ID {name_or_id} " \
11✔
1190
                        "is not present in the model!'
1191
                    raise InvalidIDError(msg)
11✔
1192
            elif isinstance(name_or_id, str):
11✔
1193
                if name_or_id in by_name:
11✔
1194
                    # Then by_name[name_or_id] is a list so we need to add all
1195
                    # entries
1196
                    ids.extend([obj.id for obj in by_name[name_or_id]])
11✔
1197
                else:
1198
                    cap_obj = obj_type.capitalize()
11✔
1199
                    msg = f'{cap_obj} {name_or_id} " \
11✔
1200
                        "is not present in the model!'
1201
                    raise InvalidIDError(msg)
11✔
1202

1203
        # Now perform the change to both python and C API
1204
        for id_ in ids:
11✔
1205
            obj = by_id[id_]
11✔
1206
            if attrib_name == 'density':
11✔
1207
                obj.set_density(density_units, value)
11✔
1208
            else:
1209
                setattr(obj, attrib_name, value)
11✔
1210
            # Next lets keep what is in C API memory up to date as well
1211
            if self.is_initialized:
11✔
1212
                lib_obj = obj_by_id[id_]
11✔
1213
                if attrib_name == 'density':
11✔
1214
                    lib_obj.set_density(value, density_units)
11✔
1215
                elif attrib_name == 'temperature':
11✔
1216
                    lib_obj.set_temperature(value)
11✔
1217
                else:
1218
                    setattr(lib_obj, attrib_name, value)
11✔
1219

1220
    def rotate_cells(self, names_or_ids, vector):
11✔
1221
        """Rotate the identified cell(s) by the specified rotation vector.
1222
        The rotation is only applied to cells filled with a universe.
1223

1224
        .. note:: If applying this change to a name that is not unique, then
1225
                  the change will be applied to all objects of that name.
1226

1227
        .. versionadded:: 0.13.0
1228

1229
        Parameters
1230
        ----------
1231
        names_or_ids : Iterable of str or int
1232
            The cell names (if str) or id (if int) that are to be translated
1233
            or rotated. This parameter can include a mix of names and ids.
1234
        vector : Iterable of float
1235
            The rotation vector of length 3 to apply. This array specifies the
1236
            angles in degrees about the x, y, and z axes, respectively.
1237

1238
        """
1239

1240
        self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'rotation')
11✔
1241

1242
    def translate_cells(self, names_or_ids, vector):
11✔
1243
        """Translate the identified cell(s) by the specified translation vector.
1244
        The translation is only applied to cells filled with a universe.
1245

1246
        .. note:: If applying this change to a name that is not unique, then
1247
                  the change will be applied to all objects of that name.
1248

1249
        .. versionadded:: 0.13.0
1250

1251
        Parameters
1252
        ----------
1253
        names_or_ids : Iterable of str or int
1254
            The cell names (if str) or id (if int) that are to be translated
1255
            or rotated. This parameter can include a mix of names and ids.
1256
        vector : Iterable of float
1257
            The translation vector of length 3 to apply. This array specifies
1258
            the x, y, and z dimensions of the translation.
1259

1260
        """
1261

1262
        self._change_py_lib_attribs(names_or_ids, vector, 'cell',
11✔
1263
                                    'translation')
1264

1265
    def update_densities(self, names_or_ids, density, density_units='atom/b-cm'):
11✔
1266
        """Update the density of a given set of materials to a new value
1267

1268
        .. note:: If applying this change to a name that is not unique, then
1269
                  the change will be applied to all objects of that name.
1270

1271
        .. versionadded:: 0.13.0
1272

1273
        Parameters
1274
        ----------
1275
        names_or_ids : Iterable of str or int
1276
            The material names (if str) or id (if int) that are to be updated.
1277
            This parameter can include a mix of names and ids.
1278
        density : float
1279
            The density to apply in the units specified by `density_units`
1280
        density_units : {'atom/b-cm', 'g/cm3'}, optional
1281
            Units for `density`. Defaults to 'atom/b-cm'
1282

1283
        """
1284

1285
        self._change_py_lib_attribs(names_or_ids, density, 'material',
11✔
1286
                                    'density', density_units)
1287

1288
    def update_cell_temperatures(self, names_or_ids, temperature):
11✔
1289
        """Update the temperature of a set of cells to the given value
1290

1291
        .. note:: If applying this change to a name that is not unique, then
1292
                  the change will be applied to all objects of that name.
1293

1294
        .. versionadded:: 0.13.0
1295

1296
        Parameters
1297
        ----------
1298
        names_or_ids : Iterable of str or int
1299
            The cell names (if str) or id (if int) that are to be updated.
1300
            This parameter can include a mix of names and ids.
1301
        temperature : float
1302
            The temperature to apply in units of Kelvin
1303

1304
        """
1305

1306
        self._change_py_lib_attribs(names_or_ids, temperature, 'cell',
11✔
1307
                                    'temperature')
1308

1309
    def update_material_volumes(self, names_or_ids, volume):
11✔
1310
        """Update the volume of a set of materials to the given value
1311

1312
        .. note:: If applying this change to a name that is not unique, then
1313
                  the change will be applied to all objects of that name.
1314

1315
        .. versionadded:: 0.13.0
1316

1317
        Parameters
1318
        ----------
1319
        names_or_ids : Iterable of str or int
1320
            The material names (if str) or id (if int) that are to be updated.
1321
            This parameter can include a mix of names and ids.
1322
        volume : float
1323
            The volume to apply in units of cm^3
1324

1325
        """
1326

1327
        self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume')
11✔
1328

1329
    def differentiate_depletable_mats(self, diff_volume_method: str = None):
11✔
1330
        """Assign distribmats for each depletable material
1331

1332
        .. versionadded:: 0.14.0
1333

1334
        .. versionchanged:: 0.15.1
1335
            diff_volume_method default is None, do not set volumes on the new
1336
            material ovjects. Is now a convenience method for
1337
            differentiate_mats(diff_volume_method, depletable_only=True)
1338

1339
        Parameters
1340
        ----------
1341
        diff_volume_method : str
1342
            Specifies how the volumes of the new materials should be found.
1343
            - None: Do not assign volumes to the new materials (Default)
1344
            - 'divide equally': Divide the original material volume equally between the new materials
1345
            - 'match cell': Set the volume of the material to the volume of the cell they fill
1346
        """
1347
        self.differentiate_mats(diff_volume_method, depletable_only=True)
11✔
1348

1349
    def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bool = True):
11✔
1350
        """Assign distribmats for each material
1351

1352
        .. versionadded:: 0.15.1
1353

1354
        Parameters
1355
        ----------
1356
        diff_volume_method : str
1357
            Specifies how the volumes of the new materials should be found.
1358
            - None: Do not assign volumes to the new materials (Default)
1359
            - 'divide equally': Divide the original material volume equally between the new materials
1360
            - 'match cell': Set the volume of the material to the volume of the cell they fill
1361
        depletable_only : bool
1362
            Default is True, only depletable materials will be differentiated. If False, all materials will be
1363
            differentiated.
1364
        """
1365
        check_value('volume differentiation method', diff_volume_method, ("divide equally", "match cell", None))
11✔
1366

1367
        # Count the number of instances for each cell and material
1368
        self.geometry.determine_paths(instances_only=True)
11✔
1369

1370
        # Get list of materials
1371
        if self.materials:
11✔
1372
            materials = self.materials
11✔
1373
        else:
UNCOV
1374
            materials = list(self.geometry.get_all_materials().values())
×
1375

1376
        # Find all or depletable_only materials which have multiple instance
1377
        distribmats = set()
11✔
1378
        for mat in materials:
11✔
1379
            # Differentiate all materials with multiple instances
1380
            diff_mat = mat.num_instances > 1
11✔
1381
            # If depletable_only is True, differentiate only depletable materials
1382
            if depletable_only:
11✔
1383
                diff_mat = diff_mat and mat.depletable
11✔
1384
            if diff_mat:
11✔
1385
                # Assign volumes to the materials according to requirements
1386
                if diff_volume_method == "divide equally":
11✔
1387
                    if mat.volume is None:
11✔
UNCOV
1388
                        raise RuntimeError(
×
1389
                            "Volume not specified for "
1390
                            f"material with ID={mat.id}.")
1391
                    else:
1392
                        mat.volume /= mat.num_instances
11✔
1393
                elif diff_volume_method == "match cell":
11✔
1394
                    for cell in self.geometry.get_all_material_cells().values():
11✔
1395
                        if cell.fill == mat:
11✔
1396
                            if not cell.volume:
11✔
1397
                                raise ValueError(
×
1398
                                    f"Volume of cell ID={cell.id} not specified. "
1399
                                    "Set volumes of cells prior to using "
1400
                                    "diff_volume_method='match cell'.")
1401
                distribmats.add(mat)
11✔
1402

1403
        if not distribmats:
11✔
UNCOV
1404
            return
×
1405

1406
        # Assign distribmats to cells
1407
        for cell in self.geometry.get_all_material_cells().values():
11✔
1408
            if cell.fill in distribmats:
11✔
1409
                mat = cell.fill
11✔
1410

1411
                # Clone materials
1412
                if cell.num_instances > 1:
11✔
1413
                    cell.fill = [mat.clone() for _ in range(cell.num_instances)]
1✔
1414
                else:
1415
                    cell.fill = mat.clone()
11✔
1416

1417
                # For 'match cell', assign volumes based on the cells
1418
                if diff_volume_method == 'match cell':
11✔
1419
                    if cell.fill_type == 'distribmat':
11✔
UNCOV
1420
                        for clone_mat in cell.fill:
×
UNCOV
1421
                            clone_mat.volume = cell.volume
×
1422
                    else:
1423
                        cell.fill.volume = cell.volume
11✔
1424

1425
        if self.materials is not None:
11✔
1426
            self.materials = openmc.Materials(
11✔
1427
                self.geometry.get_all_materials().values()
1428
            )
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