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

openmc-dev / openmc / 12472509828

23 Dec 2024 08:22PM UTC coverage: 84.847% (+0.02%) from 84.827%
12472509828

Pull #3056

github

web-flow
Merge 1f28a9e8e into 775c41512
Pull Request #3056: Differentiate materials in DAGMC universes

438 of 482 new or added lines in 13 files covered. (90.87%)

37 existing lines in 2 files now uncovered.

50041 of 58978 relevant lines covered (84.85%)

34126151.79 hits per line

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

92.58
/openmc/model/model.py
1
from __future__ import annotations
12✔
2
from collections.abc import Iterable
12✔
3
from functools import lru_cache
12✔
4
import os
12✔
5
from pathlib import Path
12✔
6
from numbers import Integral
12✔
7
from tempfile import NamedTemporaryFile
12✔
8
import warnings
12✔
9

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

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

23

24
class Model:
12✔
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,
12✔
68
                 tallies=None, plots=None):
69
        self.geometry = openmc.Geometry()
12✔
70
        self.materials = openmc.Materials()
12✔
71
        self.settings = openmc.Settings()
12✔
72
        self.tallies = openmc.Tallies()
12✔
73
        self.plots = openmc.Plots()
12✔
74

75
        if geometry is not None:
12✔
76
            self.geometry = geometry
12✔
77
        if materials is not None:
12✔
78
            self.materials = materials
12✔
79
        if settings is not None:
12✔
80
            self.settings = settings
12✔
81
        if tallies is not None:
12✔
82
            self.tallies = tallies
12✔
83
        if plots is not None:
12✔
84
            self.plots = plots
12✔
85

86
    @property
12✔
87
    def geometry(self) -> openmc.Geometry | None:
12✔
88
        return self._geometry
12✔
89

90
    @geometry.setter
12✔
91
    def geometry(self, geometry):
12✔
92
        check_type('geometry', geometry, openmc.Geometry)
12✔
93
        self._geometry = geometry
12✔
94

95
    @property
12✔
96
    def materials(self) -> openmc.Materials | None:
12✔
97
        return self._materials
12✔
98

99
    @materials.setter
12✔
100
    def materials(self, materials):
12✔
101
        check_type('materials', materials, Iterable, openmc.Material)
12✔
102
        if isinstance(materials, openmc.Materials):
12✔
103
            self._materials = materials
12✔
104
        else:
105
            del self._materials[:]
12✔
106
            for mat in materials:
12✔
107
                self._materials.append(mat)
12✔
108

109
    @property
12✔
110
    def settings(self) -> openmc.Settings | None:
12✔
111
        return self._settings
12✔
112

113
    @settings.setter
12✔
114
    def settings(self, settings):
12✔
115
        check_type('settings', settings, openmc.Settings)
12✔
116
        self._settings = settings
12✔
117

118
    @property
12✔
119
    def tallies(self) -> openmc.Tallies | None:
12✔
120
        return self._tallies
12✔
121

122
    @tallies.setter
12✔
123
    def tallies(self, tallies):
12✔
124
        check_type('tallies', tallies, Iterable, openmc.Tally)
12✔
125
        if isinstance(tallies, openmc.Tallies):
12✔
126
            self._tallies = tallies
12✔
127
        else:
128
            del self._tallies[:]
12✔
129
            for tally in tallies:
12✔
130
                self._tallies.append(tally)
12✔
131

132
    @property
12✔
133
    def plots(self) -> openmc.Plots | None:
12✔
134
        return self._plots
12✔
135

136
    @plots.setter
12✔
137
    def plots(self, plots):
12✔
138
        check_type('plots', plots, Iterable, openmc.Plot)
12✔
139
        if isinstance(plots, openmc.Plots):
12✔
140
            self._plots = plots
12✔
141
        else:
142
            del self._plots[:]
12✔
143
            for plot in plots:
12✔
144
                self._plots.append(plot)
12✔
145

146
    @property
12✔
147
    def is_initialized(self) -> bool:
12✔
148
        try:
12✔
149
            import openmc.lib
12✔
150
            return openmc.lib.is_initialized
12✔
151
        except ImportError:
×
UNCOV
152
            return False
×
153

154
    @property
12✔
155
    @lru_cache(maxsize=None)
12✔
156
    def _materials_by_id(self) -> dict:
12✔
157
        """Dictionary mapping material ID --> material"""
158
        if self.materials:
12✔
159
            mats = self.materials
12✔
160
        else:
161
            mats = self.geometry.get_all_materials().values()
12✔
162
        return {mat.id: mat for mat in mats}
12✔
163

164
    @property
12✔
165
    @lru_cache(maxsize=None)
12✔
166
    def _cells_by_id(self) -> dict:
12✔
167
        """Dictionary mapping cell ID --> cell"""
168
        cells = self.geometry.get_all_cells()
12✔
169
        return {cell.id: cell for cell in cells.values()}
12✔
170

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

184
    @property
12✔
185
    @lru_cache(maxsize=None)
12✔
186
    def _materials_by_name(self) -> dict[int, openmc.Material]:
12✔
187
        if self.materials is None:
12✔
UNCOV
188
            mats = self.geometry.get_all_materials().values()
×
189
        else:
190
            mats = self.materials
12✔
191
        result = {}
12✔
192
        for mat in mats:
12✔
193
            if mat.name not in result:
12✔
194
                result[mat.name] = set()
12✔
195
            result[mat.name].add(mat)
12✔
196
        return result
12✔
197

198
    @classmethod
12✔
199
    def from_xml(cls, geometry='geometry.xml', materials='materials.xml',
12✔
200
                 settings='settings.xml', tallies='tallies.xml',
201
                 plots='plots.xml') -> Model:
202
        """Create model from existing XML files
203

204
        Parameters
205
        ----------
206
        geometry : str
207
            Path to geometry.xml file
208
        materials : str
209
            Path to materials.xml file
210
        settings : str
211
            Path to settings.xml file
212
        tallies : str
213
            Path to tallies.xml file
214

215
            .. versionadded:: 0.13.0
216
        plots : str
217
            Path to plots.xml file
218

219
            .. versionadded:: 0.13.0
220

221
        Returns
222
        -------
223
        openmc.model.Model
224
            Model created from XML files
225

226
        """
227
        materials = openmc.Materials.from_xml(materials)
12✔
228
        geometry = openmc.Geometry.from_xml(geometry, materials)
12✔
229
        settings = openmc.Settings.from_xml(settings)
12✔
230
        tallies = openmc.Tallies.from_xml(tallies) if Path(tallies).exists() else None
12✔
231
        plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None
12✔
232
        return cls(geometry, materials, settings, tallies, plots)
12✔
233

234
    @classmethod
12✔
235
    def from_model_xml(cls, path='model.xml'):
12✔
236
        """Create model from single XML file
237

238
        .. versionadded:: 0.13.3
239

240
        Parameters
241
        ----------
242
        path : str or PathLike
243
            Path to model.xml file
244
        """
245
        parser = ET.XMLParser(huge_tree=True)
12✔
246
        tree = ET.parse(path, parser=parser)
12✔
247
        root = tree.getroot()
12✔
248

249
        model = cls()
12✔
250

251
        meshes = {}
12✔
252
        model.settings = openmc.Settings.from_xml_element(root.find('settings'), meshes)
12✔
253
        model.materials = openmc.Materials.from_xml_element(root.find('materials'))
12✔
254
        model.geometry = openmc.Geometry.from_xml_element(root.find('geometry'), model.materials)
12✔
255

256
        if root.find('tallies') is not None:
12✔
257
            model.tallies = openmc.Tallies.from_xml_element(root.find('tallies'), meshes)
12✔
258

259
        if root.find('plots') is not None:
12✔
260
            model.plots = openmc.Plots.from_xml_element(root.find('plots'))
12✔
261

262
        return model
12✔
263

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

268
        .. versionadded:: 0.13.0
269

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

295
        import openmc.lib
12✔
296

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

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

310
        self.finalize_lib()
12✔
311

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

319
        if self._intracomm.rank == 0:
12✔
320
            self.export_to_xml()
12✔
321
        self._intracomm.barrier()
12✔
322

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

328
    def sync_dagmc_universes(self):
12✔
329
        """
330
        Synchronize all DAGMC universes in the current geometry.
331
        This method iterates over all DAGMC universes in the geometry and
332
        synchronizes their cells with the current material assignments. Requires
333
        that the model has been initialized via :meth:`Model.init_lib`.
334

335
        .. versionadded:: 0.15.1-dev
336

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

350
    def finalize_lib(self):
12✔
351
        """Finalize simulation and free memory allocated for the C API
352

353
        .. versionadded:: 0.13.0
354

355
        """
356

357
        import openmc.lib
12✔
358

359
        openmc.lib.finalize()
12✔
360

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

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

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

393
        """
394

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

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

406
        # Store whether or not the library was initialized when we started
407
        started_initialized = self.is_initialized
12✔
408

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

414
            # Tell depletion_operator.finalize NOT to clear C API memory when
415
            # it is done
416
            depletion_operator.cleanup_when_done = False
12✔
417

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

425
            # Now perform the depletion
426
            with openmc.lib.quiet_dll(output):
12✔
427
                integrator.integrate(final_step)
12✔
428

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

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

446
    def export_to_xml(self, directory='.', remove_surfs=False):
12✔
447
        """Export model to separate XML files.
448

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

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

465
        self.settings.export_to_xml(d)
12✔
466
        self.geometry.export_to_xml(d, remove_surfs=remove_surfs)
12✔
467

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

478
        if self.tallies:
12✔
479
            self.tallies.export_to_xml(d)
12✔
480
        if self.plots:
12✔
481
            self.plots.export_to_xml(d)
12✔
482

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

486
        .. versionadded:: 0.13.3
487

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

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

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

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

523
        xml.clean_indentation(geometry_element, level=1)
12✔
524
        xml.clean_indentation(settings_element, level=1)
12✔
525

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

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

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

556
    def import_properties(self, filename):
12✔
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
12✔
573

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

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

580
            # Make sure number of cells matches
581
            n_cells = fh['geometry'].attrs['n_cells']
12✔
582
            if n_cells != len(cells):
12✔
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():
12✔
588
                cell_id = int(name.split()[1])
12✔
589
                cell = cells[cell_id]
12✔
590
                if cell.fill_type in ('material', 'distribmat'):
12✔
591
                    temperature = group['temperature'][()]
12✔
592
                    cell.temperature = temperature
12✔
593
                    if self.is_initialized:
12✔
594
                        lib_cell = openmc.lib.cells[cell_id]
12✔
595
                        if temperature.size > 1:
12✔
596
                            for i, T in enumerate(temperature):
×
UNCOV
597
                                lib_cell.set_temperature(T, i)
×
598
                        else:
599
                            lib_cell.set_temperature(temperature[0])
12✔
600

601
            # Make sure number of materials matches
602
            mats_group = fh['materials']
12✔
603
            n_cells = mats_group.attrs['n_materials']
12✔
604
            if n_cells != len(materials):
12✔
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():
12✔
610
                mat_id = int(name.split()[1])
12✔
611
                atom_density = group.attrs['atom_density']
12✔
612
                materials[mat_id].set_density('atom/b-cm', atom_density)
12✔
613
                if self.is_initialized:
12✔
614
                    C_mat = openmc.lib.materials[mat_id]
12✔
615
                    C_mat.set_density(atom_density, 'atom/b-cm')
12✔
616

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

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

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

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

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

669
            .. versionadded:: 0.13.3
670
        **export_kwargs
671
            Keyword arguments passed to either :meth:`Model.export_to_model_xml`
672
            or :meth:`Model.export_to_xml`.
673

674
        Returns
675
        -------
676
        Path
677
            Path to the last statepoint written by this run (None if no
678
            statepoint was written)
679

680
        """
681

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

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

704
                init_particles = openmc.lib.settings.particles
12✔
705
                if particles is not None:
12✔
706
                    if isinstance(particles, Integral) and particles > 0:
×
UNCOV
707
                        openmc.lib.settings.particles = particles
×
708

709
                init_event_based = openmc.lib.settings.event_based
12✔
710
                if event_based is not None:
12✔
UNCOV
711
                    openmc.lib.settings.event_based = event_based
×
712

713
                # Then run using the C API
714
                openmc.lib.run(output)
12✔
715

716
                # Reset changes for the openmc.run kwargs handling
717
                openmc.lib.settings.particles = init_particles
12✔
718
                openmc.lib.settings.event_based = init_event_based
12✔
719

720
            else:
721
                # Then run via the command line
722
                if export_model_xml:
12✔
723
                    self.export_to_model_xml(**export_kwargs)
12✔
724
                else:
725
                    self.export_to_xml(**export_kwargs)
12✔
726
                path_input = export_kwargs.get("path", None)
12✔
727
                openmc.run(particles, threads, geometry_debug, restart_file,
12✔
728
                           tracks, output, Path('.'), openmc_exec, mpi_args,
729
                           event_based, path_input)
730

731
            # Get output directory and return the last statepoint written
732
            if self.settings.output and 'path' in self.settings.output:
12✔
UNCOV
733
                output_dir = Path(self.settings.output['path'])
×
734
            else:
735
                output_dir = Path.cwd()
12✔
736
            for sp in output_dir.glob('statepoint.*.h5'):
12✔
737
                mtime = sp.stat().st_mtime
12✔
738
                if mtime >= tstart:  # >= allows for poor clock resolution
12✔
739
                    tstart = mtime
12✔
740
                    last_statepoint = sp
12✔
741
        return last_statepoint
12✔
742

743
    def calculate_volumes(self, threads=None, output=True, cwd='.',
12✔
744
                          openmc_exec='openmc', mpi_args=None,
745
                          apply_volumes=True, export_model_xml=True,
746
                          **export_kwargs):
747
        """Runs an OpenMC stochastic volume calculation and, if requested,
748
        applies volumes to the model
749

750
        .. versionadded:: 0.13.0
751

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

782
        """
783

784
        if len(self.settings.volume_calculations) == 0:
12✔
785
            # Then there is no volume calculation specified
786
            raise ValueError("The Settings.volume_calculations attribute must"
12✔
787
                             " be specified before executing this method!")
788

789
        with change_directory(cwd):
12✔
790
            if self.is_initialized:
12✔
791
                if threads is not None:
12✔
792
                    msg = "Threads must be set via Model.is_initialized(...)"
×
UNCOV
793
                    raise ValueError(msg)
×
794
                if mpi_args is not None:
12✔
UNCOV
795
                    msg = "The MPI environment must be set otherwise such as" \
×
796
                        "with the call to mpi4py"
UNCOV
797
                    raise ValueError(msg)
×
798

799
                # Compute the volumes
800
                openmc.lib.calculate_volumes(output)
12✔
801

802
            else:
803
                if export_model_xml:
12✔
804
                    self.export_to_model_xml(**export_kwargs)
12✔
805
                else:
UNCOV
806
                    self.export_to_xml(**export_kwargs)
×
807
                path_input = export_kwargs.get("path", None)
12✔
808
                openmc.calculate_volumes(
12✔
809
                    threads=threads, output=output, openmc_exec=openmc_exec,
810
                    mpi_args=mpi_args, path_input=path_input
811
                )
812

813
            # Now we apply the volumes
814
            if apply_volumes:
12✔
815
                # Load the results and add them to the model
816
                for i, vol_calc in enumerate(self.settings.volume_calculations):
12✔
817
                    vol_calc.load_results(f"volume_{i + 1}.h5")
12✔
818
                    # First add them to the Python side
819
                    if vol_calc.domain_type == "material" and self.materials:
12✔
820
                        for material in self.materials:
12✔
821
                            if material.id in vol_calc.volumes:
12✔
822
                                material.add_volume_information(vol_calc)
12✔
823
                    else:
824
                        self.geometry.add_volume_information(vol_calc)
12✔
825

826
                    # And now repeat for the C API
827
                    if self.is_initialized and vol_calc.domain_type == 'material':
12✔
828
                        # Then we can do this in the C API
829
                        for domain_id in vol_calc.ids:
12✔
830
                            openmc.lib.materials[domain_id].volume = \
12✔
831
                                vol_calc.volumes[domain_id].n
832

833
    def plot(
12✔
834
        self,
835
        n_samples: int | None = None,
836
        plane_tolerance: float = 1.,
837
        source_kwargs: dict | None = None,
838
        **kwargs,
839
    ):
840
        """Display a slice plot of the geometry.
841

842
        .. versionadded:: 0.15.1
843

844
        Parameters
845
        ----------
846
        n_samples : int, optional
847
            The number of source particles to sample and add to plot. Defaults
848
            to None which doesn't plot any particles on the plot.
849
        plane_tolerance: float
850
            When plotting a plane the source locations within the plane +/-
851
            the plane_tolerance will be included and those outside of the
852
            plane_tolerance will not be shown
853
        source_kwargs : dict, optional
854
            Keyword arguments passed to :func:`matplotlib.pyplot.scatter`.
855
        **kwargs
856
            Keyword arguments passed to :func:`openmc.Universe.plot`
857

858
        Returns
859
        -------
860
        matplotlib.axes.Axes
861
            Axes containing resulting image
862
        """
863

864
        check_type('n_samples', n_samples, int | None)
12✔
865
        check_type('plane_tolerance', plane_tolerance, float)
12✔
866
        if source_kwargs is None:
12✔
867
            source_kwargs = {}
12✔
868
        source_kwargs.setdefault('marker', 'x')
12✔
869

870
        ax = self.geometry.plot(**kwargs)
12✔
871
        if n_samples:
12✔
872
            # Sample external source particles
873
            particles = self.sample_external_source(n_samples)
12✔
874

875
            # Determine plotting parameters and bounding box of geometry
876
            bbox = self.geometry.bounding_box
12✔
877
            origin = kwargs.get('origin', None)
12✔
878
            basis = kwargs.get('basis', 'xy')
12✔
879
            indices = {'xy': (0, 1, 2), 'xz': (0, 2, 1), 'yz': (1, 2, 0)}[basis]
12✔
880

881
            # Infer origin if not provided
882
            if np.isinf(bbox.extent[basis]).any():
12✔
883
                if origin is None:
×
UNCOV
884
                    origin = (0, 0, 0)
×
885
            else:
886
                if origin is None:
12✔
887
                    # if nan values in the bbox.center they get replaced with 0.0
888
                    # this happens when the bounding_box contains inf values
889
                    with warnings.catch_warnings():
12✔
890
                        warnings.simplefilter("ignore", RuntimeWarning)
12✔
891
                        origin = np.nan_to_num(bbox.center)
12✔
892

893
            slice_index = indices[2]
12✔
894
            slice_value = origin[slice_index]
12✔
895

896
            xs = []
12✔
897
            ys = []
12✔
898
            tol = plane_tolerance
12✔
899
            for particle in particles:
12✔
900
                if (slice_value - tol < particle.r[slice_index] < slice_value + tol):
12✔
901
                    xs.append(particle.r[indices[0]])
12✔
902
                    ys.append(particle.r[indices[1]])
12✔
903
            ax.scatter(xs, ys, **source_kwargs)
12✔
904
        return ax
12✔
905

906
    def sample_external_source(
12✔
907
            self,
908
            n_samples: int = 1000,
909
            prn_seed: int | None = None,
910
            **init_kwargs
911
    ) -> openmc.ParticleList:
912
        """Sample external source and return source particles.
913

914
        .. versionadded:: 0.15.1
915

916
        Parameters
917
        ----------
918
        n_samples : int
919
            Number of samples
920
        prn_seed : int
921
            Pseudorandom number generator (PRNG) seed; if None, one will be
922
            generated randomly.
923
        **init_kwargs
924
            Keyword arguments passed to :func:`openmc.lib.init`
925

926
        Returns
927
        -------
928
        openmc.ParticleList
929
            List of samples source particles
930
        """
931
        import openmc.lib
12✔
932

933
        # Silence output by default. Also set arguments to start in volume
934
        # calculation mode to avoid loading cross sections
935
        init_kwargs.setdefault('output', False)
12✔
936
        init_kwargs.setdefault('args', ['-c'])
12✔
937

938
        with change_directory(tmpdir=True):
12✔
939
            # Export model within temporary directory
940
            self.export_to_model_xml()
12✔
941

942
            # Sample external source sites
943
            with openmc.lib.run_in_memory(**init_kwargs):
12✔
944
                return openmc.lib.sample_external_source(
12✔
945
                    n_samples=n_samples, prn_seed=prn_seed
946
                )
947

948
    def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc',
12✔
949
                      export_model_xml=True, **export_kwargs):
950
        """Creates plot images as specified by the Model.plots attribute
951

952
        .. versionadded:: 0.13.0
953

954
        Parameters
955
        ----------
956
        output : bool, optional
957
            Capture OpenMC output from standard out
958
        cwd : str, optional
959
            Path to working directory to run in. Defaults to the current
960
            working directory.
961
        openmc_exec : str, optional
962
            Path to OpenMC executable. Defaults to 'openmc'.
963
            This only applies to the case when not using the C API.
964
        export_model_xml : bool, optional
965
            Exports a single model.xml file rather than separate files. Defaults
966
            to True.
967
        **export_kwargs
968
            Keyword arguments passed to either :meth:`Model.export_to_model_xml`
969
            or :meth:`Model.export_to_xml`.
970

971
        """
972

973
        if len(self.plots) == 0:
12✔
974
            # Then there is no volume calculation specified
UNCOV
975
            raise ValueError("The Model.plots attribute must be specified "
×
976
                             "before executing this method!")
977

978
        with change_directory(cwd):
12✔
979
            if self.is_initialized:
12✔
980
                # Compute the volumes
981
                openmc.lib.plot_geometry(output)
12✔
982
            else:
983
                if export_model_xml:
12✔
984
                    self.export_to_model_xml(**export_kwargs)
12✔
985
                else:
UNCOV
986
                    self.export_to_xml(**export_kwargs)
×
987
                path_input = export_kwargs.get("path", None)
12✔
988
                openmc.plot_geometry(output=output, openmc_exec=openmc_exec,
12✔
989
                                     path_input=path_input)
990

991
    def _change_py_lib_attribs(self, names_or_ids, value, obj_type,
12✔
992
                               attrib_name, density_units='atom/b-cm'):
993
        # Method to do the same work whether it is a cell or material and
994
        # a temperature or volume
995
        check_type('names_or_ids', names_or_ids, Iterable, (Integral, str))
12✔
996
        check_type('obj_type', obj_type, str)
12✔
997
        obj_type = obj_type.lower()
12✔
998
        check_value('obj_type', obj_type, ('material', 'cell'))
12✔
999
        check_value('attrib_name', attrib_name,
12✔
1000
                    ('temperature', 'volume', 'density', 'rotation',
1001
                     'translation'))
1002
        # The C API only allows setting density units of atom/b-cm and g/cm3
1003
        check_value('density_units', density_units, ('atom/b-cm', 'g/cm3'))
12✔
1004
        # The C API has no way to set cell volume or material temperature
1005
        # so lets raise exceptions as needed
1006
        if obj_type == 'cell' and attrib_name == 'volume':
12✔
UNCOV
1007
            raise NotImplementedError(
×
1008
                'Setting a Cell volume is not supported!')
1009
        if obj_type == 'material' and attrib_name == 'temperature':
12✔
UNCOV
1010
            raise NotImplementedError(
×
1011
                'Setting a material temperature is not supported!')
1012

1013
        # And some items just dont make sense
1014
        if obj_type == 'cell' and attrib_name == 'density':
12✔
UNCOV
1015
            raise ValueError('Cannot set a Cell density!')
×
1016
        if obj_type == 'material' and attrib_name in ('rotation',
12✔
1017
                                                      'translation'):
UNCOV
1018
            raise ValueError('Cannot set a material rotation/translation!')
×
1019

1020
        # Set the
1021
        if obj_type == 'cell':
12✔
1022
            by_name = self._cells_by_name
12✔
1023
            by_id = self._cells_by_id
12✔
1024
            if self.is_initialized:
12✔
1025
                obj_by_id = openmc.lib.cells
12✔
1026
        else:
1027
            by_name = self._materials_by_name
12✔
1028
            by_id = self._materials_by_id
12✔
1029
            if self.is_initialized:
12✔
1030
                obj_by_id = openmc.lib.materials
12✔
1031
        # Get the list of ids to use if converting from names and accepting
1032
        # only values that have actual ids
1033
        ids = []
12✔
1034
        for name_or_id in names_or_ids:
12✔
1035
            if isinstance(name_or_id, Integral):
12✔
1036
                if name_or_id in by_id:
12✔
1037
                    ids.append(int(name_or_id))
12✔
1038
                else:
1039
                    cap_obj = obj_type.capitalize()
12✔
1040
                    msg = f'{cap_obj} ID {name_or_id} " \
12✔
1041
                        "is not present in the model!'
1042
                    raise InvalidIDError(msg)
12✔
1043
            elif isinstance(name_or_id, str):
12✔
1044
                if name_or_id in by_name:
12✔
1045
                    # Then by_name[name_or_id] is a list so we need to add all
1046
                    # entries
1047
                    ids.extend([obj.id for obj in by_name[name_or_id]])
12✔
1048
                else:
1049
                    cap_obj = obj_type.capitalize()
12✔
1050
                    msg = f'{cap_obj} {name_or_id} " \
12✔
1051
                        "is not present in the model!'
1052
                    raise InvalidIDError(msg)
12✔
1053

1054
        # Now perform the change to both python and C API
1055
        for id_ in ids:
12✔
1056
            obj = by_id[id_]
12✔
1057
            if attrib_name == 'density':
12✔
1058
                obj.set_density(density_units, value)
12✔
1059
            else:
1060
                setattr(obj, attrib_name, value)
12✔
1061
            # Next lets keep what is in C API memory up to date as well
1062
            if self.is_initialized:
12✔
1063
                lib_obj = obj_by_id[id_]
12✔
1064
                if attrib_name == 'density':
12✔
1065
                    lib_obj.set_density(value, density_units)
12✔
1066
                elif attrib_name == 'temperature':
12✔
1067
                    lib_obj.set_temperature(value)
12✔
1068
                else:
1069
                    setattr(lib_obj, attrib_name, value)
12✔
1070

1071
    def rotate_cells(self, names_or_ids, vector):
12✔
1072
        """Rotate the identified cell(s) by the specified rotation vector.
1073
        The rotation is only applied to cells filled with a universe.
1074

1075
        .. note:: If applying this change to a name that is not unique, then
1076
                  the change will be applied to all objects of that name.
1077

1078
        .. versionadded:: 0.13.0
1079

1080
        Parameters
1081
        ----------
1082
        names_or_ids : Iterable of str or int
1083
            The cell names (if str) or id (if int) that are to be translated
1084
            or rotated. This parameter can include a mix of names and ids.
1085
        vector : Iterable of float
1086
            The rotation vector of length 3 to apply. This array specifies the
1087
            angles in degrees about the x, y, and z axes, respectively.
1088

1089
        """
1090

1091
        self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'rotation')
12✔
1092

1093
    def translate_cells(self, names_or_ids, vector):
12✔
1094
        """Translate the identified cell(s) by the specified translation vector.
1095
        The translation is only applied to cells filled with a universe.
1096

1097
        .. note:: If applying this change to a name that is not unique, then
1098
                  the change will be applied to all objects of that name.
1099

1100
        .. versionadded:: 0.13.0
1101

1102
        Parameters
1103
        ----------
1104
        names_or_ids : Iterable of str or int
1105
            The cell names (if str) or id (if int) that are to be translated
1106
            or rotated. This parameter can include a mix of names and ids.
1107
        vector : Iterable of float
1108
            The translation vector of length 3 to apply. This array specifies
1109
            the x, y, and z dimensions of the translation.
1110

1111
        """
1112

1113
        self._change_py_lib_attribs(names_or_ids, vector, 'cell',
12✔
1114
                                    'translation')
1115

1116
    def update_densities(self, names_or_ids, density, density_units='atom/b-cm'):
12✔
1117
        """Update the density of a given set of materials to a new value
1118

1119
        .. note:: If applying this change to a name that is not unique, then
1120
                  the change will be applied to all objects of that name.
1121

1122
        .. versionadded:: 0.13.0
1123

1124
        Parameters
1125
        ----------
1126
        names_or_ids : Iterable of str or int
1127
            The material names (if str) or id (if int) that are to be updated.
1128
            This parameter can include a mix of names and ids.
1129
        density : float
1130
            The density to apply in the units specified by `density_units`
1131
        density_units : {'atom/b-cm', 'g/cm3'}, optional
1132
            Units for `density`. Defaults to 'atom/b-cm'
1133

1134
        """
1135

1136
        self._change_py_lib_attribs(names_or_ids, density, 'material',
12✔
1137
                                    'density', density_units)
1138

1139
    def update_cell_temperatures(self, names_or_ids, temperature):
12✔
1140
        """Update the temperature of a set of cells to the given value
1141

1142
        .. note:: If applying this change to a name that is not unique, then
1143
                  the change will be applied to all objects of that name.
1144

1145
        .. versionadded:: 0.13.0
1146

1147
        Parameters
1148
        ----------
1149
        names_or_ids : Iterable of str or int
1150
            The cell names (if str) or id (if int) that are to be updated.
1151
            This parameter can include a mix of names and ids.
1152
        temperature : float
1153
            The temperature to apply in units of Kelvin
1154

1155
        """
1156

1157
        self._change_py_lib_attribs(names_or_ids, temperature, 'cell',
12✔
1158
                                    'temperature')
1159

1160
    def update_material_volumes(self, names_or_ids, volume):
12✔
1161
        """Update the volume of a set of materials to the given value
1162

1163
        .. note:: If applying this change to a name that is not unique, then
1164
                  the change will be applied to all objects of that name.
1165

1166
        .. versionadded:: 0.13.0
1167

1168
        Parameters
1169
        ----------
1170
        names_or_ids : Iterable of str or int
1171
            The material names (if str) or id (if int) that are to be updated.
1172
            This parameter can include a mix of names and ids.
1173
        volume : float
1174
            The volume to apply in units of cm^3
1175

1176
        """
1177

1178
        self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume')
12✔
1179

1180
    def differentiate_depletable_mats(self, diff_volume_method : str = None):
12✔
1181
        """Assign distribmats for each depletable material
1182

1183
        .. versionadded:: 0.14.0
1184

1185
        .. version added:: 0.15.1-dev
1186
            diff_volume_method default is None, do not apply volume to the new
1187
            materials. Is now a convenience method for
1188
            differentiate_mats(diff_volume_method, depletable_only=True)
1189

1190
        Parameters
1191
        ----------
1192
        diff_volume_method : str
1193
            Specifies how the volumes of the new materials should be found.
1194
            Default is 'None', do not apply volume to the new materials,
1195
            'divide equally' which divides the original material
1196
            volume equally between the new materials,
1197
            'match cell' sets the volume of the material to volume of the cell
1198
            they fill.
1199
        """
1200
        self.differentiate_mats(diff_volume_method, depletable_only=True)
12✔
1201

1202
    def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bool = True):
12✔
1203
        """Assign distribmats for each material
1204

1205
        .. versionadded:: 0.15.1-dev
1206

1207
        Parameters
1208
        ----------
1209
        diff_volume_method : str
1210
            Specifies how the volumes of the new materials should be found.
1211
            - None: Do not assign volumes to the new materials (Default)
1212
            - 'divide_equally': Divide the original material volume equally between the new materials
1213
            - 'match cell': Set the volume of the material to the volume of the cell they fill
1214
        depletable_only : bool
1215
            Default is True, only depletable materials will be differentiated. If False, all materials will be
1216
            differentiated.
1217
        """
1218
        check_value('volume differentiation method', diff_volume_method, ("divide equally", "match cell", None))
12✔
1219

1220
        # Count the number of instances for each cell and material
1221
        self.geometry.determine_paths(instances_only=True)
12✔
1222

1223
        # Find all or depletable_only materials which have multiple instance
1224
        distribmats = set()
12✔
1225
        for mat in self.materials:
12✔
1226
            # Differentiate all materials with multiple instances
1227
            diff_mat = mat.num_instances > 1
12✔
1228
            # If depletable_only is True, differentiate only depletable materials
1229
            if depletable_only:
12✔
1230
                diff_mat = diff_mat and mat.depletable
12✔
1231
            if diff_mat:
12✔
1232
                # Assign volumes to the materials according to requirements
1233
                if diff_volume_method == "divide equally":
12✔
1234
                    if mat.volume is None:
12✔
NEW
1235
                        raise RuntimeError(
×
1236
                            "Volume not specified for "
1237
                            f"material with ID={mat.id}.")
1238
                    else:
1239
                        mat.volume /= mat.num_instances
12✔
1240
                elif diff_volume_method == "match cell":
12✔
1241
                    for cell in self.geometry.get_all_material_cells().values():
12✔
1242
                        if cell.fill == mat:
12✔
1243
                            if not cell.volume:
12✔
1244
                                raise ValueError(
×
1245
                                    f"Volume of cell ID={cell.id} not specified. "
1246
                                    "Set volumes of cells prior to using "
1247
                                    "diff_volume_method='match cell'.")
1248
                distribmats.add(mat)
12✔
1249

1250
        if not distribmats:
12✔
NEW
1251
            return
×
1252

1253
        # Assign distribmats to cells
1254
        for cell in self.geometry.get_all_material_cells().values():
12✔
1255
            if cell.fill in distribmats:
12✔
1256
                mat = cell.fill
12✔
1257
                if diff_volume_method != 'match cell':
12✔
1258
                    cell.fill = [mat.clone() for _ in range(cell.num_instances)]
12✔
1259
                elif diff_volume_method == 'match cell':
12✔
1260
                    cell.fill = mat.clone()
12✔
1261
                    cell.fill.volume = cell.volume
12✔
1262

1263
        if self.materials is not None:
12✔
1264
            self.materials = openmc.Materials(
12✔
1265
                self.geometry.get_all_materials().values()
1266
            )
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