From dc80b799ac518ed6fb7c3ba97691ca5788f69cfc Mon Sep 17 00:00:00 2001 From: agnelson Date: Wed, 29 Sep 2021 18:51:00 -0500 Subject: [PATCH] Resolving comments from @paulromano and updating tests accordingly --- openmc/deplete/integrators.py | 46 +--- openmc/deplete/operator.py | 6 +- openmc/executor.py | 122 ++++++---- openmc/lib/__init__.py | 3 +- openmc/lib/core.py | 6 +- openmc/model/model.py | 419 +++++++++++++++++---------------- tests/unit_tests/conftest.py | 4 +- tests/unit_tests/test_model.py | 152 +++++------- 8 files changed, 373 insertions(+), 385 deletions(-) diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index 3055561be7..585deabba4 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -583,40 +583,12 @@ class SILEQIIntegrator(SIIntegrator): return proc_time, [eos_conc, inter_conc], [res_bar] -def integrator_factory(method): - """This method is a factor for the integrator sub-classes - - Params - ------ - method : str - The type of integrator method to use. Valid values are: 'cecm', - 'predictor', 'cf4', 'epc_rk4', 'si_celi', 'si_leqi', 'celi', and 'leqi' - - Returns - ------- - integrator : Integrator - The type of integrator - - """ - - if method == 'cecm': - integrator = CECMIntegrator - elif method == 'predictor': - integrator = PredictorIntegrator - elif method == 'cf4': - integrator = CF4Integrator - elif method == 'epc_rk4': - integrator = EPCRK4Integrator - elif method == 'si_celi': - integrator = SICELIIntegrator - elif method == 'si_leqi': - integrator = SILEQIIntegrator - elif method == 'celi': - integrator = CELIIntegrator - elif method == 'leqi': - integrator = LEQIIntegrator - else: - msg = "Invalid integrator method: {}!".format(method) - raise ValueError(msg) - - return integrator +integrator_by_name = { + 'cecm': CECMIntegrator, + 'predictor': PredictorIntegrator, + 'cf4': CF4Integrator, + 'epc_rk4': EPCRK4Integrator, + 'si_celi': SICELIIntegrator, + 'si_leqi': SILEQIIntegrator, + 'celi': CELIIntegrator, + 'leqi': LEQIIntegrator} diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 8560f89e94..5b000a3891 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -202,7 +202,7 @@ class Operator(TransportOperator): self.settings = settings self.geometry = geometry self.diff_burnable_mats = diff_burnable_mats - self.cleanup_when_done = True + self._cleanup_when_done = True # Reduce the chain before we create more materials if reduce_chain: @@ -537,7 +537,7 @@ class Operator(TransportOperator): # Initialize OpenMC library comm.barrier() - if not openmc.lib.LIB_INIT: + if not openmc.lib.is_initialized: openmc.lib.init(intracomm=comm) # Generate tallies in memory @@ -556,7 +556,7 @@ class Operator(TransportOperator): def finalize(self): """Finalize a depletion simulation and release resources.""" - if self.cleanup_when_done: + if self._cleanup_when_done: openmc.lib.finalize() def _update_materials(self): diff --git a/openmc/executor.py b/openmc/executor.py index de5604c9da..fa55e13667 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -1,23 +1,79 @@ from collections.abc import Iterable from numbers import Integral import subprocess -from contextlib import contextmanager -from pathlib import Path -import os import openmc -@contextmanager -def change_directory(working_dir): - """A context manager for executing in a provided working directory""" - start_dir = Path().absolute() - try: - Path.mkdir(working_dir, exist_ok=True) - os.chdir(working_dir) - yield - finally: - os.chdir(start_dir) +def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, + plot=False, restart_file=None, threads=None, + tracks=False, event_based=False, + openmc_exec='openmc', mpi_args=None): + """Run an OpenMC simulation. + + Parameters + ---------- + volume : bool, optional + Run in stochastic volume calculation mode. Defaults to False. + geometry_debug : bool, optional + Turn on geometry debugging during simulation. Defaults to False. + particles : int, optional + Number of particles to simulate per generation. + plot : bool, optional + Run in plotting mode. Defaults to False. + restart_file : str, optional + Path to restart file to use + threads : int, optional + Number of OpenMP threads. If OpenMC is compiled with OpenMP threading + enabled, the default is implementation-dependent but is usually equal + to the number of hardware threads available (or a value set by the + :envvar:`OMP_NUM_THREADS` environment variable). + tracks : bool, optional + Write tracks for all particles. Defaults to False. + event_based : bool, optional + Turns on event-based parallelism, instead of default history-based + openmc_exec : str, optional + Path to OpenMC executable. Defaults to 'openmc'. + mpi_args : list of str, optional + MPI execute command and any additional MPI arguments to pass, + e.g. ['mpiexec', '-n', '8']. + + .. versionadded:: 0.13.0 + + Returns + ------- + args : Iterable of str + The runtime flags converted to CLI arguments of the OpenMC executable + + """ + + args = [openmc_exec] + + if volume: + args += ['--volume'] + + if isinstance(particles, Integral) and particles > 0: + args += ['-n', str(particles)] + + if isinstance(threads, Integral) and threads > 0: + args += ['-s', str(threads)] + + if geometry_debug: + args.append('-g') + + if event_based: + args.append('-e') + + if isinstance(restart_file, str): + args += ['-r', restart_file] + + if tracks: + args.append('-t') + + if mpi_args is not None: + args = mpi_args + args + + return args def _run(args, output, cwd): @@ -141,8 +197,8 @@ def calculate_volumes(threads=None, output=True, cwd='.', ---------- threads : int, optional Number of OpenMP threads. If OpenMC is compiled with OpenMP threading - enabled, the default is implementation-dependent but is usually equal to - the number of hardware threads available (or a value set by the + enabled, the default is implementation-dependent but is usually equal + to the number of hardware threads available (or a value set by the :envvar:`OMP_NUM_THREADS` environment variable). output : bool, optional Capture OpenMC output from standard out @@ -165,12 +221,9 @@ def calculate_volumes(threads=None, output=True, cwd='.', openmc.VolumeCalculation """ - args = [openmc_exec, '--volume'] - if isinstance(threads, Integral) and threads > 0: - args += ['-s', str(threads)] - if mpi_args is not None: - args = mpi_args + args + args = process_CLI_arguments(volume=True, threads=threads, + openmc_exec=openmc_exec, mpi_args=mpi_args) _run(args, output, cwd) @@ -186,8 +239,8 @@ def run(particles=None, threads=None, geometry_debug=False, Number of particles to simulate per generation. threads : int, optional Number of OpenMP threads. If OpenMC is compiled with OpenMP threading - enabled, the default is implementation-dependent but is usually equal to - the number of hardware threads available (or a value set by the + enabled, the default is implementation-dependent but is usually equal + to the number of hardware threads available (or a value set by the :envvar:`OMP_NUM_THREADS` environment variable). geometry_debug : bool, optional Turn on geometry debugging during simulation. Defaults to False. @@ -216,27 +269,10 @@ def run(particles=None, threads=None, geometry_debug=False, If the `openmc` executable returns a non-zero status """ - args = [openmc_exec] - if isinstance(particles, Integral) and particles > 0: - args += ['-n', str(particles)] - - if isinstance(threads, Integral) and threads > 0: - args += ['-s', str(threads)] - - if geometry_debug: - args.append('-g') - - if event_based: - args.append('-e') - - if isinstance(restart_file, str): - args += ['-r', restart_file] - - if tracks: - args.append('-t') - - if mpi_args is not None: - args = mpi_args + args + args = process_CLI_arguments( + volume=False, geometry_debug=geometry_debug, particles=particles, + restart_file=restart_file, threads=threads, tracks=tracks, + event_based=event_based, openmc_exec=openmc_exec, mpi_args=mpi_args) _run(args, output, cwd) diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 7e766a93b2..f1b9c291ff 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -60,4 +60,5 @@ from .settings import settings from .math import * from .plot import * -LIB_INIT = False +# Flag to denote whether or not openmc.lib.init has been called +is_initialized = False diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 03c6c41fcb..6b72dde555 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -148,7 +148,7 @@ def export_properties(filename=None): def finalize(): """Finalize simulation and free memory""" _dll.openmc_finalize() - openmc.lib.LIB_INIT = False + openmc.lib.is_initialized = False def find_cell(xyz): @@ -215,7 +215,7 @@ def import_properties(filename): See Also -------- openmc.lib.export_properties -mat + """ _dll.openmc_properties_import(filename.encode()) @@ -255,7 +255,7 @@ def init(args=None, intracomm=None): intracomm = c_void_p(address) _dll.openmc_init(argc, argv, intracomm) - openmc.lib.LIB_INIT = True + openmc.lib.is_initialized = True def is_statepoint_batch(): diff --git a/openmc/model/model.py b/openmc/model/model.py index 5e0c1a2bc0..e560ef9fb5 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,20 +1,30 @@ from collections.abc import Iterable +import operator import os from pathlib import Path from numbers import Integral import time import warnings import subprocess +from contextlib import contextmanager import h5py -import numpy as np import openmc -from openmc.checkvalue import check_type, check_value, check_iterable_type, \ - check_length -import openmc.deplete as dep -from openmc.data.library import DataLibrary -from openmc.exceptions import DataError, InvalidIDError, SetupError +from openmc.checkvalue import check_type, check_value +from openmc.exceptions import InvalidIDError + + +@contextmanager +def _change_directory(working_dir): + """A context manager for executing in a provided working directory""" + start_dir = Path().cwd() + Path.mkdir(working_dir, exist_ok=True) + os.chdir(working_dir) + try: + yield + finally: + os.chdir(start_dir) class Model: @@ -28,6 +38,8 @@ class Model: not set, it will attempt to create a ``materials.xml`` file based on all materials appearing in the geometry. + .. versionchanged:: 0.13.0 + Parameters ---------- geometry : openmc.Geometry, optional @@ -40,14 +52,8 @@ class Model: Tallies information plots : openmc.Plots, optional Plot information - chain_file : str or Path, optional - Path to the depletion chain XML file. Defaults to the chain - found under the ``depletion_chain`` in the - :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. If a - str is provided it will be converted to a Path object. - fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. - If not given, values will be pulled from the ``chain_file``. + intracomm : mpi4py.MPI.Intracomm or None, optional + MPI intracommunicator Attributes ---------- @@ -61,19 +67,13 @@ class Model: Tallies information plots : openmc.Plots Plot information - chain_file : str or Path - Path to the depletion chain XML file. Defaults to the chain - found under the ``depletion_chain`` in the - :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. If a - str is provided it will be converted to a Path object. - fission_q : dict - Dictionary of nuclides and their fission Q values [eV]. - If not given, values will be pulled from the ``chain_file``. + intracomm : mpi4py.MPI.Intracomm or None + MPI intracommunicator """ def __init__(self, geometry=None, materials=None, settings=None, - tallies=None, plots=None, chain_file=None, fission_q=None): + tallies=None, plots=None, intracomm=None): self.geometry = openmc.Geometry() self.materials = openmc.Materials() self.settings = openmc.Settings() @@ -91,18 +91,33 @@ class Model: if plots is not None: self.plots = plots - self.chain_file = chain_file - self.fission_q = fission_q + self.intracomm = intracomm + # Store dictionaries to the materials and cells by ID and names if materials is None: mats = self.geometry.get_all_materials().values() else: mats = self.materials - self._materials_by_id = {mat.id: mat for mat in mats} - self._materials_by_name = {mat.name: mat for mat in mats} cells = self.geometry.get_all_cells() + # Get the ID maps + self._materials_by_id = {mat.id: mat for mat in mats} self._cells_by_id = {cell.id: cell for cell in cells.values()} - self._cells_by_name = {cell.name: cell for cell in cells.values()} + + # Get the names maps, but since names are not unique, store a list for + # each name key. In this way when the user requests a change by a name, + # the change will be applied to all of the same name. + self._cells_by_name = {} + for cell in cells.values(): + if cell.name not in self._cells_by_name: + self._cells_by_name[cell.name] = [cell] + else: + self._cells_by_name[cell.name].append(cell) + self._materials_by_name = {} + for mat in mats: + if mat.name not in self._materials_by_name: + self._materials_by_name[mat.name] = [mat] + else: + self._materials_by_name[mat.name].append(mat) @property def geometry(self): @@ -125,16 +140,14 @@ class Model: return self._plots @property - def chain_file(self): - return self._chain_file + def intracomm(self): + return self._intracomm @property - def fission_q(self): - return self._fission_q - - @property - def C_init(self): - return openmc.lib.LIB_INIT + def is_initialized(self): + # TODO: Replace openmc.lib.is_initialized with a direct ctypes access + # to simulation::initialized + return openmc.lib.is_initialized @geometry.setter def geometry(self, geometry): @@ -176,20 +189,20 @@ class Model: for plot in plots: self._plots.append(plot) - @chain_file.setter - def chain_file(self, chain_file): - check_type('chain_file', chain_file, (type(None), str, Path)) - if isinstance(chain_file, str): - self._chain_file = Path(chain_file).resolve() - elif isinstance(chain_file, Path): - self._chain_file = chain_file.resolve() + @intracomm.setter + def intracomm(self, intracomm): + try: + from mpi4py import MPI + mpi_avail = True + except ImportError: + mpi_avail = False + if intracomm is None or not mpi_avail: + # TODO: move dummy_comm from openmc.deplete to openmc + from openmc.deplete.dummy_comm import DummyCommunicator + self._intracomm = DummyCommunicator() else: - self._chain_file = None - - @fission_q.setter - def fission_q(self, fission_q): - check_type('fission_q', fission_q, (type(None), dict)) - self._fission_q = fission_q + check_type('intracomm', intracomm, MPI.Comm) + self._intracomm = intracomm @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', @@ -218,17 +231,20 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) - def init_C_api(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=False): - """Initializes the model in memory via the C-API + def init_lib(self, threads=None, geometry_debug=False, restart_file=None, + tracks=False, output=True, event_based=False): + """Initializes the model in memory via the C API + + .. versionadded:: 0.13.0 Parameters ---------- threads : int, optional - Number of OpenMP threads. If OpenMC is compiled with OpenMP threading - enabled, the default is implementation-dependent but is usually equal to - the number of hardware threads available (or a value set by the - :envvar:`OMP_NUM_THREADS` environment variable). + Number of OpenMP threads. If OpenMC is compiled with OpenMP + threading enabled, the default is implementation-dependent but is + usually equal to the number of hardware threads available + (or a value set by the :envvar:`OMP_NUM_THREADS` environment + variable). geometry_debug : bool, optional Turn on geometry debugging during simulation. Defaults to False. restart_file : str, optional @@ -242,7 +258,7 @@ class Model: """ # TODO: right now the only way to set most of the above parameters via - # the C-API are at initialization time despite use-cases existing to + # the C API are at initialization time despite use-cases existing to # set them for individual runs. For now this functionality is exposed # where it exists (here in init), but in the future the functionality # should be exposed so that it can be accessed via model.run(...) @@ -267,42 +283,46 @@ class Model: if tracks: args.append('-t') - self.clear_C_api() + self.finalize_lib() - if dep.comm.rank == 0: + if self.intracomm.rank == 0: self.export_to_xml() - dep.comm.barrier() + self.intracomm.barrier() - openmc.lib.init(args=args, intracomm=dep.comm) + openmc.lib.init(args=args, intracomm=self.intracomm) + + def finalize_lib(self): + """Finalize simulation and free memory allocated for the C API + + .. versionadded:: 0.13.0 + + """ - def clear_C_api(self): - """Finalize simulation and free memory allocated for the C-API""" openmc.lib.finalize() - def deplete(self, timesteps, chain_file=None, method='cecm', - fission_q=None, final_step=True, directory='.', - **kwargs): + def deplete(self, timesteps, method='cecm', final_step=True, + operator_kwargs=None, integrator_kwargs=None, directory='.'): """Deplete model using specified timesteps/power + .. versionchanged:: 0.13.0 + Parameters ---------- timesteps : iterable of float Array of timesteps in units of [s]. Note that values are not cumulative. - chain_file : str, optional - Path to the depletion chain XML file. Defaults to the chain - found under the ``depletion_chain`` in the - :envvar:`OPENMC_CROSS_SECTIONS` environment variable if it exists. method : str, optional Integration method used for depletion (e.g., 'cecm', 'predictor'). Defaults to 'cecm'. - fission_q : dict, optional - Dictionary of nuclides and their fission Q values [eV]. - If not given, values will be pulled from the ``chain_file``. - Defaults to pulling from the ``chain_file``. final_step : bool, optional Indicate whether or not a transport solve should be run at the end of the last timestep. Defaults to running this transport solve. + operator_kwargs : dict + Keyword arguments passed to the depletion Operator initializer + (e.g., :func:`openmc.deplete.Operator`) + integrator_kwargs : dict + Keyword arguments passed to the depletion Operator initializer + (e.g., :func:`openmc.deplete.integrator.cecm`) directory : str, optional Directory to write XML files to. If it doesn't exist already, it will be created. Defaults to the current working directory @@ -312,69 +332,43 @@ class Model: """ - # To keep Model.deplete(...) API compatibility, we will allow the - # chain_file and fission_q params to be set if provided while we set - # the depletion operator - if chain_file is not None: - this_chain_file = Path(chain_file).resolve() - warnings.warn("The chain_file argument of Model.deplete(...) " - "has been deprecated and may be removed in a " - "future version. The Model.chain_file should be" - "used instead.", DeprecationWarning) - else: - this_chain_file = self.chain_file - if fission_q is not None: - warnings.warn("The fission_q argument of Model.deplete(...) " - "has been deprecated and may be removed in a " - "future version. The Model.fission_q should be" - "used instead.", DeprecationWarning) - this_fission_q = fission_q - else: - this_fission_q = fission_q + # Import openmc.deplete here so the Model can be used even if the + # shared library is unavailable. + import openmc.deplete as dep - # Create directory if required - d = Path(directory) - if not d.is_dir(): - d.mkdir(parents=True) - start_dir = Path.cwd() - os.chdir(d) + with _change_directory(Path(directory)): + depletion_operator = \ + dep.Operator(self.geometry, self.settings, **operator_kwargs) + # Tell depletion_operator.finalize NOT to clear C API memory when + # it is done + depletion_operator.cleanup_when_done = False - depletion_operator = \ - dep.Operator(self.geometry, self.settings, - str(this_chain_file.absolute()), - fission_q=this_fission_q) - # Tell depletion_operator.finalize NOT to clear C-API memory when it is - # done - depletion_operator.cleanup_when_done = False + # Set up the integrator + check_value('method', method, + dep.integrators.integrator_by_name.keys()) + integrator_class = dep.integrators.integrator_by_name[method] + integrator = integrator_class(depletion_operator, timesteps, + **integrator_kwargs) - # Set up the integrator - integrator_class = dep.integrators.integrator_factory(method) - integrator = integrator_class(depletion_operator, - timesteps, **kwargs) + # Now perform the depletion + integrator.integrate(final_step) - # Now perform the depletion - integrator.integrate(final_step) + # If we did not perform a transport calculation on the final step, + # then make the code update the C API material inventory + if not final_step: + depletion_operator._update_materials() - # If we did not perform a transport calculation on the final step, then - # make the code update the C-API material inventory - if not final_step: - depletion_operator._update_materials() - - # Now make the python Materials match the C-API material data - for mat_id, mat in self._materials_by_id.items(): - if mat.depletable: - # Get the C data - c_mat = openmc.lib.materials[mat_id] - nuclides, densities = c_mat._get_densities() - # And now we can remove isotopes and add these ones in - atom_density = 0. - for nuc, density in zip(nuclides, densities): - mat.remove_nuclide(nuc) # Replace if it's there - mat.add_nuclide(nuc, density) - atom_density += density - mat.set_density('atom/b-cm', atom_density) - - os.chdir(start_dir) + # Now make the python Materials match the C API material data + for mat_id, mat in self._materials_by_id.items(): + if mat.depletable: + # Get the C data + c_mat = openmc.lib.materials[mat_id] + nuclides, densities = c_mat._get_densities() + # And now we can remove isotopes and add these ones in + mat.nuclides.clear() + for nuc, density in zip(nuclides, densities): + mat.add_nuclide(nuc, density) + mat.set_density('atom/b-cm', sum(densities)) def export_to_xml(self, directory='.'): """Export model to XML files. @@ -412,8 +406,8 @@ class Model: def import_properties(self, filename): """Import physical properties - .. versionchanged:: 0.12.3 - This method now updates values as loaded in memory with the C-API + .. versionchanged:: 0.13.0 + This method now updates values as loaded in memory with the C API Parameters ---------- @@ -443,9 +437,9 @@ class Model: cell = cells[cell_id] if cell.fill_type in ('material', 'distribmat'): cell.temperature = group['temperature'][()] - if self.C_init: - C_cell = openmc.lib.cells[cell_id] - C_cell.set_temperature(group['temperature'][()]) + if self.is_initialized: + lib_cell = openmc.lib.cells[cell_id] + lib_cell.set_temperature(group['temperature'][()]) # Make sure number of materials matches mats_group = fh['materials'] @@ -459,14 +453,14 @@ class Model: mat_id = int(name.split()[1]) atom_density = group.attrs['atom_density'] materials[mat_id].set_density('atom/b-cm', atom_density) - if self.C_init: + if self.is_initialized: C_mat = openmc.lib.materials[mat_id] C_mat.set_density(atom_density, 'atom/b-cm') def run(self, particles=None, threads=None, geometry_debug=False, restart_file=None, tracks=False, output=True, cwd='.', openmc_exec='openmc', mpi_args=None, event_based=False): - """Runs OpenMC. If the C-API has been initialized, then the C-API is + """Runs OpenMC. If the C API has been initialized, then the C API is used, otherwise, this method creates the XML files and runs OpenMC via a system call. In both cases this method returns the path to the last statepoint file generated. @@ -475,8 +469,8 @@ class Model: Instead of returning the final k-effective value, this function now returns the path to the final statepoint written. - .. versionchanged:: 0.12.3 - This method can utilize the C-API for execution + .. versionchanged:: 0.13.0 + This method can utilize the C API for execution Parameters ---------- @@ -520,8 +514,8 @@ class Model: last_statepoint = None # Operate in the provided working directory - with openmc.change_directory(Path(cwd)): - if self.C_init: + with _change_directory(Path(cwd)): + if self.is_initialized: # Handle the run options as applicable # First dont allow ones that must be set via init for arg_name, arg, default in zip( @@ -529,8 +523,7 @@ class Model: [threads, geometry_debug, restart_file, tracks], [None, False, None, False]): if arg != default: - msg = "{} must be set via Model.c_init(...)".format( - arg_name) + msg = f"{arg_name} must be set via Model.is_initialized(...)" raise ValueError(msg) if particles is not None: @@ -546,9 +539,9 @@ class Model: # Event-based can be set at init-time or on a case-basis. # Handle the argument here. - # TODO This will be dealt with in a future change to the C-API + # TODO This will be dealt with in a future change to the C API - # Then run using the C-API + # Then run using the C API openmc.lib.run() # Reset changes for the openmc.run kwargs handling @@ -577,12 +570,14 @@ class Model: last_statepoint = sp return last_statepoint - def calculate_volume(self, threads=None, output=True, cwd='.', - openmc_exec='openmc', mpi_args=None, - apply_volumes=True): + def calculate_volumes(self, threads=None, output=True, cwd='.', + openmc_exec='openmc', mpi_args=None, + apply_volumes=True): """Runs an OpenMC stochastic volume calculation and, if requested, applies volumes to the model + .. versionadded:: 0.13.0 + Parameters ---------- threads : int, optional @@ -590,16 +585,16 @@ class Model: threading enabled, the default is implementation-dependent but is usually equal to the number of hardware threads available (or a value set by the :envvar:`OMP_NUM_THREADS` environment variable). - This currenty only applies to the case when not using the C-API. + This currenty only applies to the case when not using the C API. output : bool, optional Capture OpenMC output from standard out openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. - This only applies to the case when not using the C-API. + This only applies to the case when not using the C API. mpi_args : list of str, optional MPI execute command and any additional MPI arguments to pass, e.g. ['mpiexec', '-n', '8']. - This only applies to the case when not using the C-API. + This only applies to the case when not using the C API. cwd : str, optional Path to working directory to run in. Defaults to the current working directory. @@ -613,10 +608,10 @@ class Model: raise ValueError("The Settings.volume_calculation attribute must" " be specified before executing this method!") - with openmc.change_directory(Path(cwd)): - if self.C_init: + with _change_directory(Path(cwd)): + if self.is_initialized: if threads is not None: - msg = "Threads must be set via Model.c_init(...)" + msg = "Threads must be set via Model.is_initialized(...)" raise ValueError(msg) if mpi_args is not None: msg = "The MPI environment must be set otherwise such as" \ @@ -645,7 +640,7 @@ class Model: if apply_volumes: # Load the results f_names = \ - ["volume_{}.h5".format(i + 1) + [f"volume_{i + 1}.h5" for i in range(len(self.settings.volume_calculations))] vol_calcs = [openmc.VolumeCalculation.load_results(f_name) for f_name in f_names] @@ -654,9 +649,9 @@ class Model: # First add them to the Python side self.geometry.add_volume_information(vol_calc) - # And now repeat for the C-API + # And now repeat for the C API if vol_calc.domain == 'material': - # Then we can do this in the C-API + # Then we can do this in the C API for domain_id in vol_calc.domains: self.update_material_volumes( [domain_id], vol_calc.volumes[domain_id]) @@ -670,6 +665,8 @@ class Model: `ImageMagick `_ which includes a `convert` command. + .. versionadded:: 0.13.0 + Parameters ---------- output : bool, optional @@ -679,7 +676,7 @@ class Model: working directory. openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. - This only applies to the case when not using the C-API. + This only applies to the case when not using the C API. convert : bool, optional Whether or not to attempt to convert from PPM to PNG convert_exec : str, optional @@ -691,14 +688,14 @@ class Model: raise ValueError("The Model.plots attribute must be specified " "before executing this method!") - with openmc.change_directory(Path(cwd)): - # TODO: openmc_init doesnt read plots.xml unless it is in plot mode + with _change_directory(Path(cwd)): + # TODO: openmis_initialized doesnt read plots.xml unless it is in plot mode # so the following will not work. Commented out for now and - # replacing with non-C-API code + # replacing with non-C API code self.export_to_xml() openmc.plot_geometry(output=output, openmc_exec=openmc_exec) - # if self.C_init: + # if self.is_initialized: # # Apply the output settings # if not output: # init_verbosity = openmc.lib.settings.verbosity @@ -735,13 +732,13 @@ class Model: check_value('attrib_name', attrib_name, ('temperature', 'volume', 'density', 'rotation', 'translation')) - # The C-API only allows setting density units of atom/b-cm and g/cm3 + # The C API only allows setting density units of atom/b-cm and g/cm3 check_value('density_units', density_units, ('atom/b-cm', 'g/cm3')) - # The C-API has no way to set cell volume so lets raise an exception + # The C API has no way to set cell volume so lets raise an exception if obj_type == 'cell' and attrib_name == 'volume': raise NotImplementedError( 'Setting a Cell volume is not supported!') - # Same with setting temperatures, TODO: update C-API for this + # Same with setting temperatures, TODO: update C API for this if obj_type == 'material' and attrib_name == 'temperature': raise NotImplementedError( 'Setting a Material temperature is not yet supported!') @@ -756,62 +753,61 @@ class Model: if obj_type == 'cell': by_name = self._cells_by_name by_id = self._cells_by_id - C_by_id = openmc.lib.cells + obj_by_id = openmc.lib.cells else: by_name = self._materials_by_name by_id = self._materials_by_id - C_by_id = openmc.lib.materials + obj_by_id = openmc.lib.materials # Get the list of ids to use if converting from names and accepting # only values that have actual ids - ids = [None] * len(names_or_ids) - for i, name_or_id in enumerate(names_or_ids): + ids = [] + for name_or_id in names_or_ids: if isinstance(name_or_id, Integral): if name_or_id in by_id: - ids[i] = int(name_or_id) + ids.append(int(name_or_id)) else: - msg = '{} ID {} is not present in the model!'.format( - obj_type.capitalize(), name_or_id) + cap_obj = obj_type.capitalize() + msg = f'{cap_obj} ID {name_or_id} " \ + "is not present in the model!' raise InvalidIDError(msg) elif isinstance(name_or_id, str): if name_or_id in by_name: - ids[i] = by_name[name_or_id].id + # Then by_name[name_or_id] is a list so we need to add all + # entries + ids.extend([obj.id for obj in by_name[name_or_id]]) else: - msg = '{} {} is not present in the model!'.format( - obj_type.capitalize(), name_or_id) + cap_obj = obj_type.capitalize() + msg = f'{cap_obj} {name_or_id} " \ + "is not present in the model!' raise InvalidIDError(msg) - # Now perform the change to both python and C-API + # Now perform the change to both python and C API for id_ in ids: obj = by_id[id_] - if attrib_name == 'rotation': - obj.rotation = value - elif attrib_name == 'translation': - obj.translation = value - elif attrib_name == 'volume': - obj.volume = value - elif attrib_name == 'temperature': - obj.temperature = value - elif attrib_name == 'density': + if attrib_name == 'density': obj.set_density(density_units, value) - # Next lets keep what is in C-API memory up to date as well - if self.C_init: - C_obj = C_by_id[id_] - if attrib_name == 'rotation': - C_obj.rotation = value - elif attrib_name == 'translation': - C_obj.translation = value - elif attrib_name == 'volume': - C_obj.volume = value - elif attrib_name == 'temperature': - C_obj.set_temperature(value) + else: + setattr(obj, attrib_name, value) + # Next lets keep what is in C API memory up to date as well + if self.is_initialized: + lib_obj = obj_by_id[id_] + if attrib_name == 'temperature': + lib_obj.set_temperature(value) elif attrib_name == 'density': - C_obj.set_density(value, density_units) + lib_obj.set_density(value, density_units) + else: + setattr(lib_obj, attrib_name, value) def rotate_cells(self, names_or_ids, vector): """Rotate the identified cell(s) by the specified rotation vector. The rotation is only applied to cells filled with a universe. + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -829,6 +825,11 @@ class Model: """Translate the identified cell(s) by the specified translation vector. The translation is only applied to cells filled with a universe. + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -845,6 +846,11 @@ class Model: def update_densities(self, names_or_ids, density, density_units='atom/b-cm'): """Update the density of a given set of materials to a new value + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -863,6 +869,11 @@ class Model: def update_cell_temperatures(self, names_or_ids, temperature): """Update the temperature of a set of cells to the given value + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -879,6 +890,11 @@ class Model: def update_material_temperatures(self, names_or_ids, temperature): """Update the temperature of a set of materials to the given value + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int @@ -895,6 +911,11 @@ class Model: def update_material_volumes(self, names_or_ids, volume): """Update the volume of a set of materials to the given value + .. note:: If applying this change to a name that is not unique, then + the change will be applied to all objects of that name. + + .. versionadded:: 0.13.0 + Parameters ---------- names_or_ids : Iterable of str or int diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index ff5340249b..7ddca6dd73 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -129,9 +129,9 @@ def pin_model_attributes(): """ + operator_kwargs = {'chain_file': chain, 'fission_q': fission_q} - return (mats, geom, settings, tals, plots, chain, fission_q, - chain_file_xml) + return (mats, geom, settings, tals, plots, operator_kwargs, chain_file_xml) @pytest.fixture(scope='module') def mpi_intracomm(): diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index dd6242fe1f..6dc135056b 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -4,10 +4,11 @@ from pathlib import Path from shutil import which import openmc import openmc.lib +from openmc.deplete.dummy_comm import DummyCommunicator -def test_init(run_in_tmpdir, pin_model_attributes): - mats, geom, settings, tals, plots, chain, fission_q, _ = \ +def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, _, _ = \ pin_model_attributes openmc.reset_auto_ids() @@ -22,29 +23,25 @@ def test_init(run_in_tmpdir, pin_model_attributes): assert test_model.settings.__dict__[ref_k] == ref_v assert len(test_model.tallies) == 0 assert len(test_model.plots) == 0 - assert test_model.chain_file is None - assert test_model.fission_q is None assert test_model._materials_by_id == {} assert test_model._materials_by_name == {} assert test_model._cells_by_id == {} assert test_model._cells_by_name == {} - assert test_model.C_init is False + assert test_model.is_initialized is False + assert isinstance(test_model.intracomm, DummyCommunicator) # Now check proper init of an actual model. Assume no interference between # parameters and so we can apply them all at once instead of testing one # parameter initialization at a time - test_model = openmc.Model(geom, mats, settings, tals, plots, chain, - fission_q) + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) assert test_model.geometry is geom assert test_model.materials is mats assert test_model.settings is settings assert test_model.tallies is tals assert test_model.plots is plots - assert test_model.chain_file == Path(chain).resolve() - assert test_model.fission_q is fission_q assert test_model._materials_by_id == {1: mats[0], 2: mats[1], 3: mats[2]} - assert test_model._materials_by_name == {'UO2': mats[0], 'Zirc': mats[1], - 'Borated water': mats[2]} + assert test_model._materials_by_name == { + 'UO2': [mats[0]], 'Zirc': [mats[1]], 'Borated water': [mats[2]]} # The last cell is the one that contains the infinite fuel assert test_model._cells_by_id == \ {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], @@ -53,13 +50,21 @@ def test_init(run_in_tmpdir, pin_model_attributes): # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': geom.root_universe.cells[2], '': geom.root_universe.cells[4], - 'inf fuel': geom.root_universe.cells[2].fill.cells[1]} - assert test_model.C_init is False + 'fuel': [geom.root_universe.cells[2]], + '': [geom.root_universe.cells[3], geom.root_universe.cells[4]], + 'inf fuel': [geom.root_universe.cells[2].fill.cells[1]]} + assert test_model.is_initialized is False + if mpi_intracomm is None: + assert isinstance(test_model.intracomm, DummyCommunicator) + else: + assert test_model.intracomm == mpi_intracomm # Finally test the parameter type checking by passing bad types and # obtaining the right exception types - def_params = [geom, mats, settings, tals, plots, chain, fission_q] + if mpi_intracomm is not None: + def_params = [geom, mats, settings, tals, plots, mpi_intracomm] + else: + def_params = [geom, mats, settings, tals, plots] for i in range(len(def_params)): args = def_params.copy() # Try an integer, as that is a bad type for all arguments @@ -69,7 +74,7 @@ def test_init(run_in_tmpdir, pin_model_attributes): def test_from_xml(run_in_tmpdir, pin_model_attributes): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes + mats, geom, settings, tals, plots, _, _ = pin_model_attributes # This test will write the individual files to xml and then init that way # and run the same sort of test as in test_init @@ -99,14 +104,12 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): assert test_model.settings.__dict__[ref_k] == settings.__dict__[ref_k] assert len(test_model.tallies) == 0 assert len(test_model.plots) == 0 - assert test_model.chain_file is None - assert test_model.fission_q is None assert test_model._materials_by_id == \ {1: test_model.materials[0], 2: test_model.materials[1], 3: test_model.materials[2]} assert test_model._materials_by_name == { - 'UO2': test_model.materials[0], 'Zirc': test_model.materials[1], - 'Borated water': test_model.materials[2]} + 'UO2': [test_model.materials[0]], 'Zirc': [test_model.materials[1]], + 'Borated water': [test_model.materials[2]]} assert test_model._cells_by_id == { 2: test_model.geometry.root_universe.cells[2], 3: test_model.geometry.root_universe.cells[3], @@ -115,28 +118,23 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): # No cell name for 2 and 3, so we expect a blank name to be assigned to # cell 3 due to overwriting assert test_model._cells_by_name == { - 'fuel': test_model.geometry.root_universe.cells[2], - '': test_model.geometry.root_universe.cells[4], - 'inf fuel': test_model.geometry.root_universe.cells[2].fill.cells[1]} - assert test_model.C_init is False + 'fuel': [test_model.geometry.root_universe.cells[2]], + '': [test_model.geometry.root_universe.cells[3], + test_model.geometry.root_universe.cells[4]], + 'inf fuel': [test_model.geometry.root_universe.cells[2].fill.cells[1]]} + assert test_model.is_initialized is False + assert isinstance(test_model.intracomm, DummyCommunicator) -def test_init_clear_C_api(run_in_tmpdir, pin_model_attributes, mpi_intracomm): +def test_init_finalize_lib(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # We are going to init and then make sure data is loaded - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) - # Set the depletion module to use the test intracomm as the depletion mods - # intracomm is the one that init uses. We will not perturb system state - # outside of this test by re-setting the openmc.deplete.comm to what it was - # before when we exist - if mpi_intracomm is not None: - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm - test_model.init_C_api() + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model.init_lib() # First check that the API is advertised as initialized - assert openmc.lib.LIB_INIT is True - assert test_model.C_init is True + assert openmc.lib.is_initialized is True + assert test_model.is_initialized is True # Now make sure it actually is initialized by making a call to the lib c_mat = openmc.lib.find_material((0.6, 0., 0.)) # This should be Borated water @@ -144,20 +142,14 @@ def test_init_clear_C_api(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert c_mat.id == 3 # Ok, now lets test that we can clear the data and check that it is cleared - test_model.clear_C_api() + test_model.finalize_lib() # First check that the API is advertised as initialized - assert openmc.lib.LIB_INIT is False - assert test_model.C_init is False + assert openmc.lib.is_initialized is False + assert test_model.is_initialized is False # Note we cant actually test that a sys call fails because we should get a # seg fault - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm - - # TODO: in above test, tests are necessary for all the arguments of init - def test_import_properties(run_in_tmpdir, mpi_intracomm): """Test importing properties on the Model class """ @@ -165,7 +157,7 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # Create PWR pin cell model and write XML files openmc.reset_auto_ids() model = openmc.examples.pwr_pin_cell() - model.init_C_api() + model.init_lib() # Change fuel temperature and density and export properties cell = openmc.lib.cells[1] @@ -185,7 +177,7 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): assert openmc.lib.cells[1].get_temperature() == 600. assert openmc.lib.materials[1].get_density('g/cm3') == pytest.approx(5.0) - # Clear the C-API + # Clear the C API openmc.lib.finalize() # Verify the attributes survived by exporting to XML and re-creating @@ -203,24 +195,17 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) # This case will run by getting the k-eff and tallies for command-line and - # C-API execution modes and ensuring they give the same result. + # C API execution modes and ensuring they give the same result. sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: cli_keff = sp.k_combined cli_flux = sp.get_tally(id=1).get_values()[0, 0, 0] - if mpi_intracomm is not None: - # Set the depletion module to use the test intracomm as the depletion - # module's intracomm is the one that init uses. We will not perturb - # system state outside of this test by re-setting the - # openmc.deplete.comm to what it was before when we exist - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm - test_model.init_C_api() + test_model.init_lib() sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: C_keff = sp.k_combined @@ -241,24 +226,12 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): with pytest.raises(ValueError): test_model.run(tracks=True) - test_model.clear_C_api() - - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm + test_model.finalize_lib() def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) - - if mpi_intracomm is not None: - # Set the depletion module to use the test intracomm as the depletion - # module's intracomm is the one that init uses. We will not perturb - # system state outside of this test by re-setting the - # openmc.deplete.comm to what it was before when we exist - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) # This test cannot check the correctness of the plot, but it can # check that a plot was made and that the expected ppm and png files are @@ -272,10 +245,10 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): else: exts = ['ppm'] - # We will run the test twice, the first time without C-API, the second with + # We will run the test twice, the first time without C API, the second with for i in range(2): if i == 1: - test_model.init_C_api() + test_model.init_lib() test_model.plot_geometry(output=True, convert=convert) # Now look for the files, expect to find test.ppm, plot_2.ppm, and if @@ -286,25 +259,14 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_file.exists() test_file.unlink() - test_model.clear_C_api() - - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm + test_model.finalize_lib() def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): - mats, geom, settings, tals, plots, _, _, _ = pin_model_attributes - test_model = openmc.Model(geom, mats, settings, tals, plots) + mats, geom, settings, tals, plots, _, _ = pin_model_attributes + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) - if mpi_intracomm is not None: - # Set the depletion module to use the test intracomm as the depletion - # module's intracomm is the one that init uses. We will not perturb - # system state outside of this test by re-setting the - # openmc.deplete.comm to what it was before when we exist - orig_comm = openmc.deplete.comm - openmc.deplete.comm = mpi_intracomm - test_model.init_C_api() + test_model.init_lib() # Now we can call rotate_cells, translate_cells, update_densities, # update_cell_temperatures, and update_material_temperatures and make sure @@ -376,7 +338,7 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Check initial conditions with pytest.raises(NotImplementedError): test_model.update_material_temperatures(['UO2'], 600.) - # TODO: When C-API material.set_temperature is implemented, uncomment below + # TODO: When C API material.set_temperature is implemented, uncomment below # assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 # # The temperature on the material will be None because its just the # # default assert test_model.materials[0].temperature is None @@ -395,8 +357,4 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert abs(openmc.lib.materials[1].volume - 2.) < 1e-13 assert abs(test_model.materials[0].volume - 2.) < 1e-13 - test_model.clear_C_api() - - # And before done, reset the deplete communicator - if mpi_intracomm is not None: - openmc.deplete.comm = orig_comm + test_model.finalize_lib()