From 3f24c3d4e19c3d8a132cbdbad8c0c79614299137 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 23 Sep 2021 16:22:42 -0500 Subject: [PATCH 01/19] Added a LIB_INIT module-level flag to openmc.lib. Began process of setting up model to be able to use the C-API instead of only working with XML files --- openmc/deplete/integrators.py | 39 +++++ openmc/deplete/operator.py | 3 +- openmc/lib/__init__.py | 2 + openmc/lib/core.py | 3 + openmc/lib/material.py | 1 - openmc/model/model.py | 280 +++++++++++++++++++++++++++++++--- 6 files changed, 303 insertions(+), 25 deletions(-) diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index fbf43a808..3055561be 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -581,3 +581,42 @@ class SILEQIIntegrator(SIIntegrator): proc_time += time1 + time2 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 diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 0e5cc9efa..9c67d3bec 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -536,7 +536,8 @@ class Operator(TransportOperator): # Initialize OpenMC library comm.barrier() - openmc.lib.init(intracomm=comm) + if not openmc.lib.LIB_INIT: + openmc.lib.init(intracomm=comm) # Generate tallies in memory materials = [openmc.lib.materials[int(i)] diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 82dc92ba4..7e766a93b 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -59,3 +59,5 @@ from .tally import * from .settings import settings from .math import * from .plot import * + +LIB_INIT = False diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 0da24aab8..e9601c332 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -109,6 +109,7 @@ def global_bounding_box(): return llc, urc + def calculate_volumes(): """Run stochastic volume calculation""" _dll.openmc_calculate_volumes() @@ -147,6 +148,7 @@ def export_properties(filename=None): def finalize(): """Finalize simulation and free memory""" _dll.openmc_finalize() + openmc.lib.LIB_INIT = False def find_cell(xyz): @@ -253,6 +255,7 @@ def init(args=None, intracomm=None): intracomm = c_void_p(address) _dll.openmc_init(argc, argv, intracomm) + openmc.lib.LIB_INIT = True def is_statepoint_batch(): diff --git a/openmc/lib/material.py b/openmc/lib/material.py index 6770721ab..fde197d3d 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -172,7 +172,6 @@ class Material(_FortranObjectWithID): @property def nuclides(self): return self._get_densities()[0] - return nuclides @property def densities(self): diff --git a/openmc/model/model.py b/openmc/model/model.py index d6c91e73e..7f58e9589 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,11 +1,17 @@ from collections.abc import Iterable from pathlib import Path import time +import warnings import h5py +import numpy as np import openmc -from openmc.checkvalue import check_type, check_value +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 class Model: @@ -31,6 +37,14 @@ 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``. Attributes ---------- @@ -44,11 +58,19 @@ 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``. """ def __init__(self, geometry=None, materials=None, settings=None, - tallies=None, plots=None): + tallies=None, plots=None, chain_file=None, fission_q=None): self.geometry = openmc.Geometry() self.materials = openmc.Materials() self.settings = openmc.Settings() @@ -66,6 +88,19 @@ class Model: if plots is not None: self.plots = plots + self.chain_file = chain_file + self.fission_q = fission_q + self.depletion_operator = None + + if self.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} + cells = self.geometry.get_all_cells() + self._cells_by_id = {cell.id: cell for cell in cells.values()} + self._cells_by_name = {cell.name: cell for cell in cells.values()} + @property def geometry(self): return self._geometry @@ -86,6 +121,22 @@ class Model: def plots(self): return self._plots + @property + def chain_file(self): + return self._chain_file + + @property + def fission_q(self): + return self._fission_q + + @property + def depletion_operator(self): + return self._depletion_operator + + @property + def C_init(self): + return openmc.lib.LIB_INIT + @geometry.setter def geometry(self, geometry): check_type('geometry', geometry, openmc.Geometry) @@ -126,6 +177,25 @@ 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) + else: + self._chain_file = chain_file + + @fission_q.setter + def fission_q(self, fission_q): + check_type('fission_q', fission_q, (type(None), dict)) + self._fission_q = fission_q + + @depletion_operator.setter + def depletion_operator(self, depletion_operator): + check_type('depletion_operator', depletion_operator, + (type(None), dep.Operator)) + self._depletion_operator = depletion_operator + @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): @@ -151,8 +221,39 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) + def init_C_api(self, use_depletion_operator=False): + """Initializes the model in memory via the C-API + + Parameters + ---------- + directory : str + Directory to write XML files to. If it doesn't exist already, it + will be created. + use_depletion_operator : bool, optional + If True, the model will be loaded using the depletion operator + including all isotopes necessary from fission. This parameter will + use the :attr:`Model.chain_file` and :attr:`Model.fission_q` + attributes. + """ + + if use_depletion_operator: + # Create OpenMC transport operator + self.depletion_operator = \ + dep.Operator(self.geometry, self.settings, + str(self.chain_file), fission_q=self.fission_q) + else: + openmc.lib.hard_reset() + if dep.comm.rank == 0: + self.export_to_xml() + dep.comm.barrier() + openmc.lib.init(intracomm=dep.comm) + + 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, **kwargs): + fission_q=None, final_step=True, **kwargs): """Deplete model using specified timesteps/power Parameters @@ -169,26 +270,75 @@ class Model: fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled 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. + + .. versionadded:: 0.12.3 **kwargs Keyword arguments passed to integration function (e.g., :func:`openmc.deplete.integrator.cecm`) """ - # Import the depletion module. This is done here rather than the module - # header to delay importing openmc.lib (through openmc.deplete) which - # can be tough to install properly. - import openmc.deplete as dep - # Create OpenMC transport operator - op = dep.Operator( - self.geometry, self.settings, chain_file, - fission_q=fission_q, - ) + if self.C_init and self.depletion_operator is not None: + # Then the user has properly initialized the information and we can + # just carry forward + pass + elif self.C_init and self.depletion_operator is None: + # Then the user has initialzed the C-API but without the depletion + # isotopes loaded. We would have to reset and reload data, but + # doing so could lose user information. Therefore let us just + # provide an error and quit. + msg = "Model.deplete(...) cannot be called after " \ + "Model.init_C_api(...) if the use_depletion_operator " \ + "argument to Model.init_C_api(...) is False." + raise SetupError(msg) + else: + # To get here, the C-API is not initialized. So we can do that now + # To keep Model.deplete(...) API compatibility, we will allow the + # chain_file and fission_q params to be set since we havent loaded + # the API anyways + if chain_file is not None: + self.chain_file = chain_file + 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) + 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) + self.fission_q = fission_q + self.init_C_api(use_depletion_operator=True) - # Perform depletion - check_value('method', method, ('cecm', 'predictor', 'cf4', 'epc_rk4', - 'si_celi', 'si_leqi', 'celi', 'leqi')) - getattr(dep.integrator, method)(op, timesteps, **kwargs) + # Set up the integrator + integrator_class = dep.integrators.integrator_factory(method) + integrator = integrator_class(self.depletion_operator, + timesteps, **kwargs) + + # 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: + self.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) def export_to_xml(self, directory='.'): """Export model to XML files. @@ -269,13 +419,18 @@ class Model: materials[mat_id].set_density('atom/b-cm', atom_density) def run(self, **kwargs): - """Creates the XML files, runs OpenMC, and returns the path to the last + """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 cal. In both cases this method returns the path to the last statepoint file generated. .. versionchanged:: 0.12 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 + Parameters ---------- **kwargs @@ -289,16 +444,20 @@ class Model: """ - self.export_to_xml() - - # Setting tstart here ensures we don't pick up any pre-existing statepoint - # files in the output directory + # Setting tstart here ensures we don't pick up any pre-existing + # statepoint files in the output directory tstart = time.time() last_statepoint = None - openmc.run(**kwargs) + if self.C_init: + # Then run using the C-API + openmc.lib.run() + else: + # Then run via the command line + self.export_to_xml() + openmc.run(**kwargs) - # Get output directory and return the last statepoint written by this run + # Get output directory and return the last statepoint written this run if self.settings.output and 'path' in self.settings.output: output_dir = Path(self.settings.output['path']) else: @@ -309,3 +468,78 @@ class Model: tstart = mtime last_statepoint = sp return last_statepoint + + def _move_cell(self, cell_names_or_ids, vector, attrib_name): + # Method to do the same work whether it is a rotation or translation + check_type('cell_names_or_ids', cell_names_or_ids, Iterable, + (np.int, int, str)) + check_type('vector', vector, Iterable, (np.float, float)) + check_length('vector', vector, 3) + check_value('attrib_name', attrib_name, ('rotation', 'translation')) + + # Get the list of cell ids to use y converting from names and accepting + # only values that have actual ids + cell_ids = [None] * len(cell_names_or_ids) + for c, cell_name_or_id in enumerate(cell_names_or_ids): + if isinstance(cell_name_or_id, (int, np.int)): + if cell_name_or_id in self._cells_by_id: + cell_ids[c] = int(cell_name_or_id) + msg = 'Cell ID {} is not present in the model!'.format( + cell_name_or_id) + raise InvalidIDError(msg) + elif isinstance(cell_name_or_id, str): + if cell_name_or_id in self._cells_by_name: + cell_ids[c] = self._cells_by_name[cell_name_or_id] + else: + msg = 'Cell {} is not present in the model!'.format( + cell_name_or_id) + raise InvalidIDError(msg) + + # Now perform the motion + for cell_id in cell_ids: + cell = self._cells_by_id[cell_id] + if attrib_name == 'rotation': + cell.rotation = vector + elif attrib_name == 'translation': + cell.translation = vector + # Next lets keep what is in C-API memory up to date as well + if self.C_init: + C_cell = openmc.lib.cells[cell_id] + if attrib_name == 'rotation': + C_cell.rotation = vector + elif attrib_name == 'translation': + C_cell.translation = vector + + def rotate_cells(self, cell_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. + + Parameters + ---------- + cell_names_or_ids : Iterable of str or int + The cell names (if str) or id (if int) that are to be translated + or rotated. This parameter can include a mix of names and ids. + vector : Iterable of float + The rotation vector of length 3 to apply. This array specifies the + angles in degrees about the x, y, and z axes, respectively. + + """ + + self._move_cell(cell_names_or_ids, vector, 'rotation') + + def translate_cells(self, cell_names_or_ids, vector): + """Translate the identified cell(s) by the specified translation vector. + The translation is only applied to cells filled with a universe. + + Parameters + ---------- + cell_names_or_ids : Iterable of str or int + The cell names (if str) or id (if int) that are to be translated + or rotated. This parameter can include a mix of names and ids. + vector : Iterable of float + The translation vector of length 3 to apply. This array specifies + the x, y, and z dimensions of the translation. + + """ + + self._move_cell(cell_names_or_ids, vector, 'translation') From c15564b8260f00ada383d90e654d085b9f26d077 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 24 Sep 2021 16:29:53 -0500 Subject: [PATCH 02/19] Update copyright to include UChicago Argonne, LLC --- LICENSE | 3 ++- docs/source/conf.py | 2 +- docs/source/license.rst | 3 ++- man/man1/openmc.1 | 4 ++-- src/output.cpp | 20 ++++++++++---------- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/LICENSE b/LICENSE index 848a23845..79f0c96f0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Copyright (c) 2011-2021 Massachusetts Institute of Technology and OpenMC contributors +Copyright (c) 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/docs/source/conf.py b/docs/source/conf.py index 52d872abf..33fb515a5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,7 @@ master_doc = 'index' # General information about the project. project = 'OpenMC' -copyright = '2011-2021, Massachusetts Institute of Technology and OpenMC contributors' +copyright = '2011-2021, Massachusetts Institute of Technology, UChicago Argonne, LLC, and OpenMC contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/source/license.rst b/docs/source/license.rst index 40ab106ad..5a3d40763 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,8 @@ License Agreement ================= -Copyright © 2011-2021 Massachusetts Institute of Technology and OpenMC contributors +Copyright © 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index 8a93af514..989e846e0 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -57,8 +57,8 @@ Indicates the default path to an HDF5 file that contains multi-group cross section libraries if the user has not specified the tag in .I materials.xml\fP. .SH LICENSE -Copyright \(co 2011-2021 Massachusetts Institute of Technology and OpenMC -contributors. +Copyright \(co 2011-2021 Massachusetts Institute of Technology, UChicago +Argonne, LLC, and OpenMC contributors. .PP Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/output.cpp b/src/output.cpp index 73f3a0e9a..b78314823 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -72,26 +72,26 @@ void title() // Write version information fmt::print( - " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2021 MIT and OpenMC contributors\n" - " License | https://docs.openmc.org/en/latest/license.html\n" - " Version | {}.{}.{}{}\n", + " | The OpenMC Monte Carlo Code\n" + " Copyright | 2011-2021 MIT, UChicago Argonne, LLC and contributors\n" + " License | https://docs.openmc.org/en/latest/license.html\n" + " Version | {}.{}.{}{}\n", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : ""); #ifdef GIT_SHA1 - fmt::print(" Git SHA1 | {}\n", GIT_SHA1); + fmt::print(" Git SHA1 | {}\n", GIT_SHA1); #endif // Write the date and time - fmt::print(" Date/Time | {}\n", time_stamp()); + fmt::print(" Date/Time | {}\n", time_stamp()); #ifdef OPENMC_MPI // Write number of processors - fmt::print(" MPI Processes | {}\n", mpi::n_procs); + fmt::print(" MPI Processes | {}\n", mpi::n_procs); #endif #ifdef _OPENMP // Write number of OpenMP threads - fmt::print(" OpenMP Threads | {}\n", omp_get_max_threads()); + fmt::print(" OpenMP Threads | {}\n", omp_get_max_threads()); #endif std::cout << std::endl; } @@ -335,8 +335,8 @@ void print_version() #ifdef GIT_SHA1 fmt::print("Git SHA1: {}\n", GIT_SHA1); #endif - fmt::print("Copyright (c) 2011-2021 Massachusetts Institute of " - "Technology and OpenMC contributors\nMIT/X license at " + fmt::print("Copyright (c) 2011-2021 MIT, UChicago Argonne, LLC, and " + "contributors\nMIT/X license at " "\n"); } } From b0bb2493b989cdaaaadfd218ff9ee0ba4eb859b0 Mon Sep 17 00:00:00 2001 From: agnelson Date: Fri, 24 Sep 2021 16:33:47 -0500 Subject: [PATCH 03/19] Added generic ability to update Models volumes, temperatures, rotations, translations, densities for cells and materials as applicable. Other various changes based on local testing to make sure Model.deplete works and that it works when called in separate instances while keeping the model in C-API memory --- openmc/deplete/operator.py | 4 +- openmc/model/model.py | 292 +++++++++++++++++++++++-------------- 2 files changed, 189 insertions(+), 107 deletions(-) diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 9c67d3bec..8560f89e9 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -202,6 +202,7 @@ class Operator(TransportOperator): self.settings = settings self.geometry = geometry self.diff_burnable_mats = diff_burnable_mats + self.cleanup_when_done = True # Reduce the chain before we create more materials if reduce_chain: @@ -555,7 +556,8 @@ class Operator(TransportOperator): def finalize(self): """Finalize a depletion simulation and release resources.""" - openmc.lib.finalize() + if self.cleanup_when_done: + openmc.lib.finalize() def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" diff --git a/openmc/model/model.py b/openmc/model/model.py index 7f58e9589..d93f60f27 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,4 +1,5 @@ from collections.abc import Iterable +import os from pathlib import Path import time import warnings @@ -90,13 +91,13 @@ class Model: self.chain_file = chain_file self.fission_q = fission_q - self.depletion_operator = None - if self.materials is None: + 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() self._cells_by_id = {cell.id: cell for cell in cells.values()} self._cells_by_name = {cell.name: cell for cell in cells.values()} @@ -129,10 +130,6 @@ class Model: def fission_q(self): return self._fission_q - @property - def depletion_operator(self): - return self._depletion_operator - @property def C_init(self): return openmc.lib.LIB_INIT @@ -181,21 +178,15 @@ class Model: 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) + self._chain_file = Path(chain_file).resolve() else: - self._chain_file = chain_file + self._chain_file = chain_file.resolve() @fission_q.setter def fission_q(self, fission_q): check_type('fission_q', fission_q, (type(None), dict)) self._fission_q = fission_q - @depletion_operator.setter - def depletion_operator(self, depletion_operator): - check_type('depletion_operator', depletion_operator, - (type(None), dep.Operator)) - self._depletion_operator = depletion_operator - @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): @@ -221,28 +212,11 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) - def init_C_api(self, use_depletion_operator=False): - """Initializes the model in memory via the C-API + def init_C_api(self): + """Initializes the model in memory via the C-API""" - Parameters - ---------- - directory : str - Directory to write XML files to. If it doesn't exist already, it - will be created. - use_depletion_operator : bool, optional - If True, the model will be loaded using the depletion operator - including all isotopes necessary from fission. This parameter will - use the :attr:`Model.chain_file` and :attr:`Model.fission_q` - attributes. - """ + self.clear_C_api() - if use_depletion_operator: - # Create OpenMC transport operator - self.depletion_operator = \ - dep.Operator(self.geometry, self.settings, - str(self.chain_file), fission_q=self.fission_q) - else: - openmc.lib.hard_reset() if dep.comm.rank == 0: self.export_to_xml() dep.comm.barrier() @@ -253,7 +227,8 @@ class Model: openmc.lib.finalize() def deplete(self, timesteps, chain_file=None, method='cecm', - fission_q=None, final_step=True, **kwargs): + fission_q=None, final_step=True, directory='.', + **kwargs): """Deplete model using specified timesteps/power Parameters @@ -265,57 +240,63 @@ class Model: 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 - Integration method used for depletion (e.g., 'cecm', 'predictor') + 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. - - .. versionadded:: 0.12.3 + of the last timestep. Defaults to running this transport solve. + 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 **kwargs Keyword arguments passed to integration function (e.g., :func:`openmc.deplete.integrator.cecm`) """ - if self.C_init and self.depletion_operator is not None: - # Then the user has properly initialized the information and we can - # just carry forward - pass - elif self.C_init and self.depletion_operator is None: - # Then the user has initialzed the C-API but without the depletion - # isotopes loaded. We would have to reset and reload data, but - # doing so could lose user information. Therefore let us just - # provide an error and quit. - msg = "Model.deplete(...) cannot be called after " \ - "Model.init_C_api(...) if the use_depletion_operator " \ - "argument to Model.init_C_api(...) is False." - raise SetupError(msg) + # 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: - # To get here, the C-API is not initialized. So we can do that now - # To keep Model.deplete(...) API compatibility, we will allow the - # chain_file and fission_q params to be set since we havent loaded - # the API anyways - if chain_file is not None: - self.chain_file = chain_file - 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) - 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) - self.fission_q = fission_q - self.init_C_api(use_depletion_operator=True) + 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 + + # Create directory if required + d = Path(directory) + if not d.is_dir(): + d.mkdir(parents=True) + start_dir = Path.cwd() + os.chdir(d) + + 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 integrator_class = dep.integrators.integrator_factory(method) - integrator = integrator_class(self.depletion_operator, + integrator = integrator_class(depletion_operator, timesteps, **kwargs) # Now perform the depletion @@ -324,7 +305,7 @@ class Model: # 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: - self.depletion_operator._update_materials() + 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(): @@ -340,6 +321,8 @@ class Model: atom_density += density mat.set_density('atom/b-cm', atom_density) + os.chdir(start_dir) + def export_to_xml(self, directory='.'): """Export model to XML files. @@ -376,6 +359,9 @@ 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 + Parameters ---------- filename : str @@ -404,6 +390,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'][()]) # Make sure number of materials matches mats_group = fh['materials'] @@ -417,6 +406,9 @@ 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: + C_mat = openmc.lib.materials[mat_id] + C_mat.set_density(atom_density, 'atom/b-cm') def run(self, **kwargs): """Runs OpenMC. If the C-API has been initialized, then the C-API is @@ -469,54 +461,92 @@ class Model: last_statepoint = sp return last_statepoint - def _move_cell(self, cell_names_or_ids, vector, attrib_name): - # Method to do the same work whether it is a rotation or translation - check_type('cell_names_or_ids', cell_names_or_ids, Iterable, - (np.int, int, str)) - check_type('vector', vector, Iterable, (np.float, float)) - check_length('vector', vector, 3) - check_value('attrib_name', attrib_name, ('rotation', 'translation')) + def _change_py_C_attribs(self, names_or_ids, value, obj_type, attrib_name, + density_units='atom/b-cm'): + # Method to do the same work whether it is a cell or material and + # a temperature or volume + check_type('names_or_ids', names_or_ids, Iterable, (np.int, int, str)) + check_type('obj_type', obj_type, str) + obj_type = obj_type.lower() + check_value('obj_type', obj_type, ('material', 'cell')) + 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 + 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 + if obj_type == 'cell' and attrib_name == 'volume': + raise NotImplementedError( + 'Setting a Cell volume is not yet supported!') + # And some items just dont make sense + if obj_type == 'cell' and attrib_name == 'density': + raise ValueError('Cannot set a Cell density!') + if obj_type == 'material' and attrib_name in ('rotation', + 'translation'): + raise ValueError('Cannot set a material rotation/translation!') - # Get the list of cell ids to use y converting from names and accepting + # Set the + if obj_type == 'cell': + by_name = self._cells_by_name + by_id = self._cells_by_id + C_by_id = openmc.lib.cells + else: + by_name = self._materials_by_name + by_id = self._materials_by_id + C_by_id = openmc.lib.materials + + # Get the list of ids to use if converting from names and accepting # only values that have actual ids - cell_ids = [None] * len(cell_names_or_ids) - for c, cell_name_or_id in enumerate(cell_names_or_ids): - if isinstance(cell_name_or_id, (int, np.int)): - if cell_name_or_id in self._cells_by_id: - cell_ids[c] = int(cell_name_or_id) - msg = 'Cell ID {} is not present in the model!'.format( - cell_name_or_id) + ids = [None] * len(names_or_ids) + for i, name_or_id in enumerate(names_or_ids): + if isinstance(name_or_id, (int, np.int)): + if name_or_id in by_id: + ids[i] = int(name_or_id) + msg = '{} ID {} is not present in the model!'.format( + obj_type.capitalize(), name_or_id) raise InvalidIDError(msg) - elif isinstance(cell_name_or_id, str): - if cell_name_or_id in self._cells_by_name: - cell_ids[c] = self._cells_by_name[cell_name_or_id] + elif isinstance(name_or_id, str): + if name_or_id in by_name: + ids[i] = by_name[name_or_id].id else: - msg = 'Cell {} is not present in the model!'.format( - cell_name_or_id) + msg = '{} {} is not present in the model!'.format( + obj_type.capitalize(), name_or_id) raise InvalidIDError(msg) - # Now perform the motion - for cell_id in cell_ids: - cell = self._cells_by_id[cell_id] + # Now perform the change to both python and C-API + for id_ in ids: + obj = by_id[id_] if attrib_name == 'rotation': - cell.rotation = vector + obj.rotation = value elif attrib_name == 'translation': - cell.translation = vector + obj.translation = value + elif attrib_name == 'volume': + obj.volume = value + elif attrib_name == 'temperature': + obj.temperature = value + elif attrib_name == 'density': + obj.set_density(value, density_units) # Next lets keep what is in C-API memory up to date as well if self.C_init: - C_cell = openmc.lib.cells[cell_id] + C_obj = C_by_id[id_] if attrib_name == 'rotation': - C_cell.rotation = vector + C_obj.rotation = value elif attrib_name == 'translation': - C_cell.translation = vector + C_obj.translation = value + elif attrib_name == 'volume': + C_obj.set_volume = value + elif attrib_name == 'temperature': + C_obj.set_temperature = value + elif attrib_name == 'density': + C_obj.set_density(value, density_units) - def rotate_cells(self, cell_names_or_ids, vector): + 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. Parameters ---------- - cell_names_or_ids : Iterable of str or int + names_or_ids : Iterable of str or int The cell names (if str) or id (if int) that are to be translated or rotated. This parameter can include a mix of names and ids. vector : Iterable of float @@ -525,15 +555,15 @@ class Model: """ - self._move_cell(cell_names_or_ids, vector, 'rotation') + self._change_py_C_attribs(names_or_ids, vector, 'cell', 'rotation') - def translate_cells(self, cell_names_or_ids, vector): + def translate_cells(self, names_or_ids, vector): """Translate the identified cell(s) by the specified translation vector. The translation is only applied to cells filled with a universe. Parameters ---------- - cell_names_or_ids : Iterable of str or int + names_or_ids : Iterable of str or int The cell names (if str) or id (if int) that are to be translated or rotated. This parameter can include a mix of names and ids. vector : Iterable of float @@ -542,4 +572,54 @@ class Model: """ - self._move_cell(cell_names_or_ids, vector, 'translation') + self._change_py_C_attribs(names_or_ids, vector, 'cell', 'translation') + + def update_densities(self, names_or_ids, density, density_units): + """Update the density of a given set of materials to a new value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The material names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + density : float + The density to apply in the units specified by `density_units` + density_units : {'atom/b-cm', 'g/cm3'} + Units for `density` + + """ + + self._change_py_C_attribs(names_or_ids, density, 'material', 'density', + density_units) + + def update_cell_temperatures(self, names_or_ids, temperature): + """Update the temperature of a set of cells to the given value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The cell names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + temperature : float + The temperature to apply in units of Kelvin + + """ + + self._change_py_C_attribs(names_or_ids, temperature, 'cell', + 'temperature') + + def update_material_temperatures(self, names_or_ids, temperature): + """Update the temperature of a set of materials to the given value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The material names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + temperature : float + The temperature to apply in units of Kelvin + + """ + + self._change_py_C_attribs(names_or_ids, temperature, 'material', + 'temperature') From 24bca0631099a594e44afeee620e7c093b60f38b Mon Sep 17 00:00:00 2001 From: agnelson Date: Sat, 25 Sep 2021 08:17:02 -0500 Subject: [PATCH 04/19] Correcting application of Path.resolve --- openmc/model/model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index d93f60f27..ae0936183 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -179,8 +179,10 @@ class Model: check_type('chain_file', chain_file, (type(None), str, Path)) if isinstance(chain_file, str): self._chain_file = Path(chain_file).resolve() - else: + elif isinstance(chain_file, Path): self._chain_file = chain_file.resolve() + else: + self._chain_file = None @fission_q.setter def fission_q(self, fission_q): From 0d2f923be804cde98b8184d1660ced09926bdc33 Mon Sep 17 00:00:00 2001 From: agnelson Date: Mon, 27 Sep 2021 08:18:54 -0500 Subject: [PATCH 05/19] Added tests of the openmc.Model class. Still need to do tests of deplete and the methods which change python and C attributes. Pushing this now to verify the CI shows passes for the MPI configs --- openmc/model/model.py | 6 +- tests/unit_tests/conftest.py | 120 +++++++++++++++++++++ tests/unit_tests/test_model.py | 185 ++++++++++++++++++++++++++++++++- 3 files changed, 308 insertions(+), 3 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index ae0936183..aa00d40b4 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -193,6 +193,8 @@ class Model: def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): """Create model from existing XML files + When initializing this way, the user must manually load plots, tallies, + the chain_file and fission_q attributes. Parameters ---------- @@ -428,7 +430,9 @@ class Model: Parameters ---------- **kwargs - Keyword arguments passed to :func:`openmc.run` + Keyword arguments passed to :func:`openmc.run`. Note that these are + ignored if running via the C-API. Instead the parameters should be + set via the Settings object. Returns ------- diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 51d1b19a3..a1c4a24da 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -1,9 +1,129 @@ +from math import pi import openmc import pytest from tests.regression_tests import config +@pytest.fixture(scope='module') +def pin_model_attributes(): + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.29769) + uo2.add_element('U', 1., enrichment=2.4) + uo2.add_element('O', 2.) + + zirc = openmc.Material(name='Zirc') + zirc.set_density('g/cm3', 6.55) + zirc.add_element('Zr', 1.) + + borated_water = openmc.Material(name='Borated water') + borated_water.set_density('g/cm3', 0.740582) + borated_water.add_element('B', 4.0e-5) + borated_water.add_element('H', 5.0e-2) + borated_water.add_element('O', 2.4e-2) + borated_water.add_s_alpha_beta('c_H_in_H2O') + + mats = openmc.Materials([uo2, zirc, borated_water]) + + pitch = 1.25984 + fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') + clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') + box = openmc.model.rectangular_prism(pitch, pitch, boundary_type='reflective') + + # Define cells + fuel = openmc.Cell(name='fuel', fill=uo2, region=-fuel_or) + clad = openmc.Cell(fill=zirc, region=+fuel_or & -clad_or) + water = openmc.Cell(fill=borated_water, region=+clad_or & box) + + # Define overall geometry + geom = openmc.Geometry([fuel, clad, water]) + uo2.volume = pi * fuel_or.r**2 + + settings = openmc.Settings() + settings.batches = 100 + settings.inactive = 10 + settings.particles = 1000 + + # Create an initial uniform spatial source distribution over fissionable zones + bounds = [-0.62992, -0.62992, -1, 0.62992, 0.62992, 1] + uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + settings.source = openmc.source.Source(space=uniform_dist) + + entropy_mesh = openmc.RegularMesh() + entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] + entropy_mesh.upper_right = [0.39218, 0.39218, 1.e50] + entropy_mesh.dimension = [10, 10, 1] + settings.entropy_mesh = entropy_mesh + + tals = openmc.Tallies() + tal = openmc.Tally(name='test') + tal.scores = ['flux'] + tals.append(tal) + + plot = openmc.Plot() + plot.origin = (0., 0., 0.) + plot.width = (pitch, pitch) + plot.pixels = (300, 300) + plot.color_by = 'material' + plots = openmc.Plots((plot,)) + + chain = './chain_simple.xml' + fission_q = {'U235': 200e6} + + chain_file_xml = """ + + + + + + + + + + + + + + + + + + + + + 2.53000e-02 + + Gd157 Gd156 I135 Xe135 Xe136 Cs135 + 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 4.356330e-05 + + + + + + + 2.53000e-02 + + Gd157 Gd156 I135 Xe135 Xe136 Cs135 + 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 4.9097e-6 + + + + + + + 2.53000e-02 + + Gd157 Gd156 I135 Xe135 Xe136 Cs135 + 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 3.7100E-07 + + + + +""" + + return (mats, geom, settings, tals, plots, chain, fission_q, + chain_file_xml) + @pytest.fixture(scope='module') def mpi_intracomm(): if config['mpi']: diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4b658d5ab..b616856b0 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,18 +1,162 @@ +from typing import Type import pytest +from pathlib import Path import openmc import openmc.lib +def test_init(run_in_tmpdir, pin_model_attributes): + mats, geom, settings, tals, plots, chain, fission_q, _ = \ + pin_model_attributes + + openmc.reset_auto_ids() + # Check blank initialization of a model + test_model = openmc.Model() + assert test_model.geometry.root_universe is None + assert len(test_model.materials) == 0 + ref_settings = openmc.Settings() + assert sorted(test_model.settings.__dict__.keys()) == \ + sorted(ref_settings.__dict__.keys()) + for ref_k, ref_v in ref_settings.__dict__.items(): + 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 + + # 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) + 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._cells_by_id == {1: geom.root_universe.cells[1], + 2: geom.root_universe.cells[2], + 3: geom.root_universe.cells[3]} + # 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[1], '': geom.root_universe.cells[3]} + assert test_model.C_init is False + + # 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] + for i in range(len(def_params)): + args = def_params.copy() + # Try an integer, as that is a bad type for all arguments + args[i] = i + with pytest.raises(TypeError): + test_model = openmc.Model(*args) + + +def test_from_xml(run_in_tmpdir, 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 + mats.export_to_xml() + geom.export_to_xml() + settings.export_to_xml() + tals.export_to_xml() + plots.export_to_xml() + + # This from_xml method cannot load chain and fission_q + test_model = openmc.Model.from_xml() + assert test_model.geometry.root_universe.cells.keys() == \ + geom.root_universe.cells.keys() + assert [c.fill.name for c in test_model.geometry.root_universe.cells.values()] == \ + [c.fill.name for c in geom.root_universe.cells.values()] + assert [mat.name for mat in test_model.materials] == [mat.name for mat in mats] + # We will assume the attributes of settings that are custom objects are + # OK if the others are so we dotn need to implement explicit comparisons + no_test = ['_source', '_entropy_mesh'] + assert sorted(k for k in test_model.settings.__dict__.keys() if k not in no_test) == \ + sorted(k for k in settings.__dict__.keys() if k not in no_test) + keys = sorted(k for k in settings.__dict__.keys() if k not in no_test) + for ref_k in keys: + 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]} + assert test_model._cells_by_id == {\ + 1: test_model.geometry.root_universe.cells[1], + 2: test_model.geometry.root_universe.cells[2], + 3: test_model.geometry.root_universe.cells[3]} + # 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[1], + '': test_model.geometry.root_universe.cells[3]} + assert test_model.C_init is False + + +def test_init_clear_C_api(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() + + # First check that the API is advertised as initialized + assert openmc.lib.LIB_INIT is True + assert test_model.C_init 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 + assert c_mat.name == 'Borated water' + 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() + + # First check that the API is advertised as initialized + assert openmc.lib.LIB_INIT is False + assert test_model.C_init 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 + + def test_import_properties(run_in_tmpdir, mpi_intracomm): """Test importing properties on the Model class """ # Create PWR pin cell model and write XML files openmc.reset_auto_ids() model = openmc.examples.pwr_pin_cell() - model.export_to_xml() + model.init_C_api() # Change fuel temperature and density and export properties - openmc.lib.init(intracomm=mpi_intracomm) cell = openmc.lib.cells[1] cell.set_temperature(600.0) cell.fill.set_density(5.0, 'g/cm3') @@ -32,3 +176,40 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): cell = model_with_properties.geometry.get_all_cells()[1] assert cell.temperature == [600.0] assert cell.fill.get_mass_density() == pytest.approx(5.0) + + +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) + # 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 + + # 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. + 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: + orig_comm = openmc.deplete.comm + openmc.deplete.comm = mpi_intracomm + test_model.settings.verbosity = 1 + test_model.init_C_api() + sp_path = test_model.run() + with openmc.StatePoint(sp_path) as sp: + C_keff = sp.k_combined + C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] + test_model.clear_C_api() + + # and lets compare results + assert (C_keff - cli_keff) < 1e-15 + assert (C_flux - cli_flux) < 1e-15 + + # And before done, reset the deplete communicator + if mpi_intracomm is not None: + openmc.deplete.comm = orig_comm + + From f03b98ac0f3429468efa7c7c40df35ae0fceb280 Mon Sep 17 00:00:00 2001 From: agnelson Date: Mon, 27 Sep 2021 12:32:25 -0500 Subject: [PATCH 06/19] Updating model.init_C_api and model.run to handle the kwargs that have been assigned to model.run for the CLI interface. Also updated tests --- openmc/model/model.py | 120 +++++++++++++++++++++++++++++++-- tests/unit_tests/test_model.py | 15 +++-- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index aa00d40b4..3b7a64da9 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,6 +1,7 @@ from collections.abc import Iterable import os from pathlib import Path +from numbers import Integral import time import warnings @@ -216,15 +217,62 @@ class Model: settings = openmc.Settings.from_xml(settings) return cls(geometry, materials, settings) - def init_C_api(self): - """Initializes the model in memory via the C-API""" + 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 + + 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). + geometry_debug : bool, optional + Turn on geometry debugging during simulation. Defaults to False. + restart_file : str, optional + Path to restart file to use + tracks : bool, optional + Write tracks for all particles. Defaults to False. + output : bool + Capture OpenMC output from standard out + event_based : bool, optional + Turns on event-based parallelism, instead of default history-based + """ + + # 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 + # 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(...) + + # TODO: the output flag is not yet implemented for the openmc.lib.init + # command. This will be the subject of future work. + + args = [] + + 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') self.clear_C_api() if dep.comm.rank == 0: self.export_to_xml() dep.comm.barrier() - openmc.lib.init(intracomm=dep.comm) + # TODO: Implement the output flag somewhere on the C++ side + openmc.lib.init(args=args, intracomm=dep.comm) def clear_C_api(self): """Finalize simulation and free memory allocated for the C-API""" @@ -429,10 +477,31 @@ class Model: Parameters ---------- - **kwargs - Keyword arguments passed to :func:`openmc.run`. Note that these are - ignored if running via the C-API. Instead the parameters should be - set via the Settings object. + particles : int, optional + 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 :envvar:`OMP_NUM_THREADS` environment variable). + geometry_debug : bool, optional + Turn on geometry debugging during simulation. Defaults to False. + restart_file : str, optional + Path to restart file to use + tracks : bool, optional + Write tracks for all particles. Defaults to False. + output : bool + Capture OpenMC output from standard out + cwd : str, optional + Path to working directory to run in. Defaults to the current + working directory. + 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']. + event_based : bool, optional + Turns on event-based parallelism, instead of default history-based Returns ------- @@ -448,8 +517,45 @@ class Model: last_statepoint = None if self.C_init: + # Handle the openmc.run kwargs + # First dont allow ones that must be set via init + via_args = ['threads', 'geometry_debug', 'restart_file', 'tracks', + 'cwd'] + provided_args = [k for k in kwargs.keys()] + if any([key in via_args for key in provided_args]): + msg = "Argument {} must be set via Model.c_init(...)" + raise ValueError(msg) + + if 'particles' in kwargs: + init_particles = openmc.lib.settings.particles + if isinstance(kwargs['particles'], Integral) and \ + kwargs['particles'] > 0: + openmc.lib.settings.particles = kwargs['particles'] + + # Now lets handle the ones we can handle + if 'output' in kwargs: + init_verbosity = openmc.lib.settings.verbosity + if not kwargs['output']: + openmc.lib.settings.verbosity = 1 + + # Event-based can be set at init-time or on a case-basis. Handle + # the case-basis here. + if 'event_based' in kwargs: + # TODO: However, the event based flag isnt exposed in the C-API + # yet. Support for actual handling will be added in future work + msg = "Setting event-based mode directly via the C-API is " \ + "not yet implemented" + raise NotImplementedError(msg) + # Then run using the C-API openmc.lib.run() + + # Reset changes for the openmc.run kwargs handling + if 'particles' in kwargs: + openmc.lib.settings.particles = init_particles + if 'output' in kwargs: + openmc.lib.settings.verbosity = init_verbosity + else: # Then run via the command line self.export_to_xml() diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index b616856b0..0c389ac62 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -147,6 +147,8 @@ def test_init_clear_C_api(run_in_tmpdir, pin_model_attributes, mpi_intracomm): 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 """ @@ -196,17 +198,22 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): if mpi_intracomm is not None: orig_comm = openmc.deplete.comm openmc.deplete.comm = mpi_intracomm - test_model.settings.verbosity = 1 test_model.init_C_api() - sp_path = test_model.run() + sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: C_keff = sp.k_combined C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] - test_model.clear_C_api() # and lets compare results assert (C_keff - cli_keff) < 1e-15 - assert (C_flux - cli_flux) < 1e-15 + assert (C_flux - cli_flux) < 1e-13 + + # Now we should make sure the event-based flag gives us our not implemented + # error + with pytest.raises(NotImplementedError): + test_model.run(output=False, event_based=True) + + test_model.clear_C_api() # And before done, reset the deplete communicator if mpi_intracomm is not None: From d45af5a4ef1c47c5a747c208e0dd85e998ef9081 Mon Sep 17 00:00:00 2001 From: agnelson Date: Mon, 27 Sep 2021 13:45:31 -0500 Subject: [PATCH 07/19] Changes to the model.run interface again for clarity and for test passing --- openmc/model/model.py | 47 +++++++++++++++++----------------- tests/unit_tests/test_model.py | 16 +++++++++--- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 3b7a64da9..84174eb5e 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -462,10 +462,12 @@ class Model: C_mat = openmc.lib.materials[mat_id] C_mat.set_density(atom_density, 'atom/b-cm') - def run(self, **kwargs): + 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 used, otherwise, this method creates the XML files and runs OpenMC via - a system cal. In both cases this method returns the path to the last + a system call. In both cases this method returns the path to the last statepoint file generated. .. versionchanged:: 0.12 @@ -490,7 +492,7 @@ class Model: Path to restart file to use tracks : bool, optional Write tracks for all particles. Defaults to False. - output : bool + output : bool, optional Capture OpenMC output from standard out cwd : str, optional Path to working directory to run in. Defaults to the current @@ -519,47 +521,44 @@ class Model: if self.C_init: # Handle the openmc.run kwargs # First dont allow ones that must be set via init - via_args = ['threads', 'geometry_debug', 'restart_file', 'tracks', - 'cwd'] - provided_args = [k for k in kwargs.keys()] - if any([key in via_args for key in provided_args]): - msg = "Argument {} must be set via Model.c_init(...)" - raise ValueError(msg) + for arg_name, arg, default in zip( + ['threads', 'geometry_debug', 'restart_file', 'tracks', 'cwd'], + [threads, geometry_debug, restart_file, tracks, cwd], + [None, False, None, False, '.']): + if arg != default: + msg = "{} must be set via Model.c_init(...)".format( + arg_name) + raise ValueError(msg) - if 'particles' in kwargs: + if particles is not None: init_particles = openmc.lib.settings.particles - if isinstance(kwargs['particles'], Integral) and \ - kwargs['particles'] > 0: - openmc.lib.settings.particles = kwargs['particles'] + if isinstance(particles, Integral) and particles > 0: + openmc.lib.settings.particles = particles # Now lets handle the ones we can handle - if 'output' in kwargs: + if not output: init_verbosity = openmc.lib.settings.verbosity - if not kwargs['output']: + if not output: openmc.lib.settings.verbosity = 1 # Event-based can be set at init-time or on a case-basis. Handle # the case-basis here. - if 'event_based' in kwargs: - # TODO: However, the event based flag isnt exposed in the C-API - # yet. Support for actual handling will be added in future work - msg = "Setting event-based mode directly via the C-API is " \ - "not yet implemented" - raise NotImplementedError(msg) + # TODO This will be dealt with in a future change to the C-API # Then run using the C-API openmc.lib.run() # Reset changes for the openmc.run kwargs handling - if 'particles' in kwargs: + if particles is not None: openmc.lib.settings.particles = init_particles - if 'output' in kwargs: + if output is not None: openmc.lib.settings.verbosity = init_verbosity else: # Then run via the command line self.export_to_xml() - openmc.run(**kwargs) + openmc.run(particles, threads, geometry_debug, restart_file, + tracks, output, cwd, openmc_exec, mpi_args, event_based) # Get output directory and return the last statepoint written this run if self.settings.output and 'path' in self.settings.output: diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 0c389ac62..a068ef781 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -208,10 +208,18 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert (C_keff - cli_keff) < 1e-15 assert (C_flux - cli_flux) < 1e-13 - # Now we should make sure the event-based flag gives us our not implemented - # error - with pytest.raises(NotImplementedError): - test_model.run(output=False, event_based=True) + # Now we should make sure that the flags for items which should be handled + # by init are properly set + with pytest.raises(ValueError): + test_model.run(threads=1) + with pytest.raises(ValueError): + test_model.run(geometry_debug=True) + with pytest.raises(ValueError): + test_model.run(restart_file='1.h5') + with pytest.raises(ValueError): + test_model.run(tracks=True) + with pytest.raises(ValueError): + test_model.run(cwd='hi') test_model.clear_C_api() From e62ba9b85af208d15cb959cd0c7d83b3724f15d4 Mon Sep 17 00:00:00 2001 From: agnelson Date: Tue, 28 Sep 2021 08:44:31 -0500 Subject: [PATCH 08/19] Upgrading tests of model, and adding better file management, Model.calculate_volumes method and Model.plot_geometry method. These last two still need to be tested as well as Model.deplete --- openmc/executor.py | 15 ++ openmc/lib/core.py | 2 +- openmc/model/model.py | 282 ++++++++++++++++++++++++++------- tests/unit_tests/conftest.py | 7 +- tests/unit_tests/test_model.py | 177 ++++++++++++++++++--- 5 files changed, 396 insertions(+), 87 deletions(-) diff --git a/openmc/executor.py b/openmc/executor.py index e8f0de2c7..de5604c9d 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -1,10 +1,25 @@ 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 _run(args, output, cwd): # Launch a subprocess p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, diff --git a/openmc/lib/core.py b/openmc/lib/core.py index e9601c332..03c6c41fc 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -215,7 +215,7 @@ def import_properties(filename): See Also -------- openmc.lib.export_properties - +mat """ _dll.openmc_properties_import(filename.encode()) diff --git a/openmc/model/model.py b/openmc/model/model.py index 84174eb5e..e32876a6c 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -4,6 +4,7 @@ from pathlib import Path from numbers import Integral import time import warnings +import subprocess import h5py import numpy as np @@ -450,8 +451,8 @@ class Model: mats_group = fh['materials'] n_cells = mats_group.attrs['n_materials'] if n_cells != len(materials): - raise ValueError("Number of materials in properties file doesn't " - "match current model.") + raise ValueError("Number of materials in properties file " + "doesn't match current model.") # Update material densities for name, group in mats_group.items(): @@ -518,65 +519,208 @@ class Model: tstart = time.time() last_statepoint = None - if self.C_init: - # Handle the openmc.run kwargs - # First dont allow ones that must be set via init - for arg_name, arg, default in zip( - ['threads', 'geometry_debug', 'restart_file', 'tracks', 'cwd'], - [threads, geometry_debug, restart_file, tracks, cwd], - [None, False, None, False, '.']): - if arg != default: - msg = "{} must be set via Model.c_init(...)".format( - arg_name) + # Operate in the provided working directory + with openmc.change_directory(Path(cwd)): + if self.C_init: + # Handle the run options as applicable + # First dont allow ones that must be set via init + for arg_name, arg, default in zip( + ['threads', 'geometry_debug', 'restart_file', 'tracks'], + [threads, geometry_debug, restart_file, tracks], + [None, False, None, False]): + if arg != default: + msg = "{} must be set via Model.c_init(...)".format( + arg_name) + raise ValueError(msg) + + if particles is not None: + init_particles = openmc.lib.settings.particles + if isinstance(particles, Integral) and particles > 0: + openmc.lib.settings.particles = particles + + # If we dont want output, make the verbosity quiet + if not output: + init_verbosity = openmc.lib.settings.verbosity + if not output: + openmc.lib.settings.verbosity = 1 + + # 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 + + # Then run using the C-API + openmc.lib.run() + + # Reset changes for the openmc.run kwargs handling + if particles is not None: + openmc.lib.settings.particles = init_particles + if not output: + # Then re-set the initial verbosity + openmc.lib.settings.verbosity = init_verbosity + + else: + # Then run via the command line + self.export_to_xml() + openmc.run(particles, threads, geometry_debug, restart_file, + tracks, output, Path('.'), openmc_exec, mpi_args, + event_based) + + # Get output directory and return the last statepoint written + if self.settings.output and 'path' in self.settings.output: + output_dir = Path(self.settings.output['path']) + else: + output_dir = Path.cwd() + for sp in output_dir.glob('statepoint.*.h5'): + mtime = sp.stat().st_mtime + if mtime >= tstart: # >= allows for poor clock resolution + tstart = mtime + last_statepoint = sp + return last_statepoint + + def calculate_volume(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 + + 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). + 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. + 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. + cwd : str, optional + Path to working directory to run in. Defaults to the current + working directory. + apply_volumes : bool, optional + Whether apply the volume calculation results from this calculation + to the model. Defaults to applying the volumes. + """ + + if len(self.settings.volume_calculation) == 0: + # Then there is no volume calculation specified + raise ValueError("The Settings.volume_calculation attribute must" + " be specified before executing this method!") + + with openmc.change_directory(Path(cwd)): + if self.C_init: + if threads is not None: + msg = "Threads must be set via Model.c_init(...)" + raise ValueError(msg) + if mpi_args is not None: + msg = "The MPI environment must be set otherwise such as" \ + "with the call to mpi4py" raise ValueError(msg) - if particles is not None: - init_particles = openmc.lib.settings.particles - if isinstance(particles, Integral) and particles > 0: - openmc.lib.settings.particles = particles - - # Now lets handle the ones we can handle - if not output: - init_verbosity = openmc.lib.settings.verbosity + # Apply the output settings if not output: - openmc.lib.settings.verbosity = 1 + init_verbosity = openmc.lib.settings.verbosity + if not output: + openmc.lib.settings.verbosity = 1 - # Event-based can be set at init-time or on a case-basis. Handle - # the case-basis here. - # TODO This will be dealt with in a future change to the C-API + # Compute the volumes + openmc.lib.calculate_volumes() - # Then run using the C-API - openmc.lib.run() + # Reset the output verbosity + if not output: + openmc.lib.settings.verbosity = init_verbosity + else: + openmc.calculate_volumes(threads=threads, output=output, + openmc_exec=openmc_exec, + mpi_args=mpi_args) - # Reset changes for the openmc.run kwargs handling - if particles is not None: - openmc.lib.settings.particles = init_particles - if output is not None: - openmc.lib.settings.verbosity = init_verbosity + # Now we apply the volumes + if apply_volumes: + # Load the results + f_names = \ + ["volume_{}.h5".format(i + 1) + for i in range(len(self.settings.volume_calculations))] + vol_calcs = [openmc.VolumeCalculation.load_results(f_name) + for f_name in f_names] + # And now we can add them to the model + for vol_calc in vol_calcs: + # First add them to the Python side + self.geometry.add_volume_information(vol_calc) - else: - # Then run via the command line - self.export_to_xml() - openmc.run(particles, threads, geometry_debug, restart_file, - tracks, output, cwd, openmc_exec, mpi_args, event_based) + # And now repeat for the C-API + if vol_calc.domain == 'material': + # 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]) - # Get output directory and return the last statepoint written this run - if self.settings.output and 'path' in self.settings.output: - output_dir = Path(self.settings.output['path']) - else: - output_dir = Path.cwd() - for sp in output_dir.glob('statepoint.*.h5'): - mtime = sp.stat().st_mtime - if mtime >= tstart: # >= allows for poor clock resolution - tstart = mtime - last_statepoint = sp - return last_statepoint + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', + convert=True, convert_exec='convert'): + """Creates plot images as specified by the Model.plots attribute + + If convert is True, this function requires that a program is installed + to convert PPM files to PNG files. Typically, that would be + `ImageMagick `_ which includes a + `convert` command. + + Parameters + ---------- + output : bool, optional + Capture OpenMC output from standard out + cwd : str, optional + Path to working directory to run in. Defaults to the current + 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. + convert : bool, optional + Whether or not to attempt to convert from PPM to PNG + convert_exec : str, optional + Command that can convert PPM files into PNG files + """ + + if len(self.plots) == 0: + # Then there is no volume calculation specified + raise ValueError("The Model.plots attribute must be specified " + "before executing this method!") + + with openmc.change_directory(Path(cwd)): + if self.C_init: + # Apply the output settings + if not output: + init_verbosity = openmc.lib.settings.verbosity + if not output: + openmc.lib.settings.verbosity = 1 + + # Compute the volumes + openmc.lib.plot_geometry() + + # Reset the output verbosity + if not output: + openmc.lib.settings.verbosity = init_verbosity + else: + openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + + if convert: + for p in self.plots: + if p.filename is not None: + ppm_file = f'{p.filename}.ppm' + else: + ppm_file = f'plot_{p.id}.ppm' + png_file = ppm_file.replace('.ppm', '.png') + subprocess.check_call([convert_exec, ppm_file, png_file]) def _change_py_C_attribs(self, names_or_ids, value, obj_type, attrib_name, density_units='atom/b-cm'): # Method to do the same work whether it is a cell or material and # a temperature or volume - check_type('names_or_ids', names_or_ids, Iterable, (np.int, int, str)) + check_type('names_or_ids', names_or_ids, Iterable, (Integral, str)) check_type('obj_type', obj_type, str) obj_type = obj_type.lower() check_value('obj_type', obj_type, ('material', 'cell')) @@ -588,7 +732,11 @@ class Model: # 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 yet supported!') + 'Setting a Cell volume is not supported!') + # 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!') # And some items just dont make sense if obj_type == 'cell' and attrib_name == 'density': raise ValueError('Cannot set a Cell density!') @@ -610,12 +758,13 @@ class Model: # only values that have actual ids ids = [None] * len(names_or_ids) for i, name_or_id in enumerate(names_or_ids): - if isinstance(name_or_id, (int, np.int)): + if isinstance(name_or_id, Integral): if name_or_id in by_id: ids[i] = int(name_or_id) - msg = '{} ID {} is not present in the model!'.format( - obj_type.capitalize(), name_or_id) - raise InvalidIDError(msg) + else: + msg = '{} ID {} is not present in the model!'.format( + obj_type.capitalize(), name_or_id) + raise InvalidIDError(msg) elif isinstance(name_or_id, str): if name_or_id in by_name: ids[i] = by_name[name_or_id].id @@ -636,7 +785,7 @@ class Model: elif attrib_name == 'temperature': obj.temperature = value elif attrib_name == 'density': - obj.set_density(value, density_units) + 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_] @@ -645,9 +794,9 @@ class Model: elif attrib_name == 'translation': C_obj.translation = value elif attrib_name == 'volume': - C_obj.set_volume = value + C_obj.volume = value elif attrib_name == 'temperature': - C_obj.set_temperature = value + C_obj.set_temperature(value) elif attrib_name == 'density': C_obj.set_density(value, density_units) @@ -685,7 +834,7 @@ class Model: self._change_py_C_attribs(names_or_ids, vector, 'cell', 'translation') - def update_densities(self, names_or_ids, density, density_units): + 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 Parameters @@ -695,8 +844,8 @@ class Model: This parameter can include a mix of names and ids. density : float The density to apply in the units specified by `density_units` - density_units : {'atom/b-cm', 'g/cm3'} - Units for `density` + density_units : {'atom/b-cm', 'g/cm3'}, optional + Units for `density`. Defaults to 'atom/b-cm' """ @@ -734,3 +883,18 @@ class Model: self._change_py_C_attribs(names_or_ids, temperature, 'material', 'temperature') + + def update_material_volumes(self, names_or_ids, volume): + """Update the volume of a set of materials to the given value + + Parameters + ---------- + names_or_ids : Iterable of str or int + The material names (if str) or id (if int) that are to be updated. + This parameter can include a mix of names and ids. + volume : float + The volume to apply in units of cm^3 + + """ + + self._change_py_C_attribs(names_or_ids, volume, 'material', 'volume') diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index a1c4a24da..d6f94e82e 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -28,10 +28,13 @@ def pin_model_attributes(): pitch = 1.25984 fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') - box = openmc.model.rectangular_prism(pitch, pitch, boundary_type='reflective') + box = openmc.model.rectangular_prism(pitch, pitch, + boundary_type='reflective') # Define cells - fuel = openmc.Cell(name='fuel', fill=uo2, region=-fuel_or) + fuel_inf_cell = openmc.Cell(name='inf fuel', fill=uo2) + fuel_inf_univ = openmc.Universe(cells=[fuel_inf_cell]) + fuel = openmc.Cell(name='fuel', fill=fuel_inf_univ, region=-fuel_or) clad = openmc.Cell(fill=zirc, region=+fuel_or & -clad_or) water = openmc.Cell(fill=borated_water, region=+clad_or & box) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index a068ef781..0ee8568e3 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,4 +1,4 @@ -from typing import Type +import numpy as np import pytest from pathlib import Path import openmc @@ -6,7 +6,7 @@ import openmc.lib def test_init(run_in_tmpdir, pin_model_attributes): - mats, geom, settings, tals, plots, chain, fission_q, _ = \ + mats, geom, settings, tals, plots, chain, fission_q, _ = \ pin_model_attributes openmc.reset_auto_ids() @@ -44,13 +44,16 @@ def test_init(run_in_tmpdir, pin_model_attributes): 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._cells_by_id == {1: geom.root_universe.cells[1], - 2: geom.root_universe.cells[2], - 3: geom.root_universe.cells[3]} + # 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], + 4: geom.root_universe.cells[4], + 1: geom.root_universe.cells[2].fill.cells[1]} # 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[1], '': geom.root_universe.cells[3]} + '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 # Finally test the parameter type checking by passing bad types and @@ -79,13 +82,16 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): test_model = openmc.Model.from_xml() assert test_model.geometry.root_universe.cells.keys() == \ geom.root_universe.cells.keys() - assert [c.fill.name for c in test_model.geometry.root_universe.cells.values()] == \ + assert [c.fill.name for c in + test_model.geometry.root_universe.cells.values()] == \ [c.fill.name for c in geom.root_universe.cells.values()] - assert [mat.name for mat in test_model.materials] == [mat.name for mat in mats] + assert [mat.name for mat in test_model.materials] == \ + [mat.name for mat in mats] # We will assume the attributes of settings that are custom objects are # OK if the others are so we dotn need to implement explicit comparisons no_test = ['_source', '_entropy_mesh'] - assert sorted(k for k in test_model.settings.__dict__.keys() if k not in no_test) == \ + assert sorted(k for k in test_model.settings.__dict__.keys() + if k not in no_test) == \ sorted(k for k in settings.__dict__.keys() if k not in no_test) keys = sorted(k for k in settings.__dict__.keys() if k not in no_test) for ref_k in keys: @@ -100,15 +106,17 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): assert test_model._materials_by_name == { 'UO2': test_model.materials[0], 'Zirc': test_model.materials[1], 'Borated water': test_model.materials[2]} - assert test_model._cells_by_id == {\ - 1: test_model.geometry.root_universe.cells[1], + assert test_model._cells_by_id == { 2: test_model.geometry.root_universe.cells[2], - 3: test_model.geometry.root_universe.cells[3]} + 3: test_model.geometry.root_universe.cells[3], + 4: test_model.geometry.root_universe.cells[4], + 1: test_model.geometry.root_universe.cells[2].fill.cells[1]} # 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[1], - '': test_model.geometry.root_universe.cells[3]} + '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 @@ -163,13 +171,26 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): cell.set_temperature(600.0) cell.fill.set_density(5.0, 'g/cm3') openmc.lib.export_properties() + + # Import properties to existing model + model.import_properties("properties.h5") + + # Check to see that values are assigned to the C and python representations + # First python + cell = model.geometry.get_all_cells()[1] + assert cell.temperature == [600.0] + assert cell.fill.get_mass_density() == pytest.approx(5.0) + # Now C + 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 openmc.lib.finalize() - # Import properties to existing model and re-export to new directory - model.import_properties("properties.h5") + # Verify the attributes survived by exporting to XML and re-creating model.export_to_xml("with_properties") - # Load model with properties and confirm temperature/density has been changed + # Load model with properties and confirm temperature/density changed model_with_properties = openmc.Model.from_xml( 'with_properties/geometry.xml', 'with_properties/materials.xml', @@ -183,10 +204,6 @@ 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) - # 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 # 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. @@ -196,6 +213,10 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): 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() @@ -205,8 +226,8 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] # and lets compare results - assert (C_keff - cli_keff) < 1e-15 - assert (C_flux - cli_flux) < 1e-13 + assert abs(C_keff - cli_keff) < 1e-15 + assert abs(C_flux - cli_flux) < 1e-13 # Now we should make sure that the flags for items which should be handled # by init are properly set @@ -218,8 +239,6 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.run(restart_file='1.h5') with pytest.raises(ValueError): test_model.run(tracks=True) - with pytest.raises(ValueError): - test_model.run(cwd='hi') test_model.clear_C_api() @@ -228,3 +247,111 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): openmc.deplete.comm = orig_comm +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) + + 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() + + # Now we can call rotate_cells, translate_cells, update_densities, + # update_cell_temperatures, and update_material_temperatures and make sure + # the changes have taken hold. + # For each we will first try bad inputs to make sure we get the right + # errors and then we do a good one which calls the material by name and + # then id to make sure it worked + + # The rotate_cells and translate_cells will work on the cell named fill, as + # it is filled with a universe and thus the operation will be valid + + # First rotate_cells + with pytest.raises(TypeError): + # Make sure it tells us we have a bad names_or_ids type + test_model.rotate_cells(None, (0, 0, 90)) + with pytest.raises(TypeError): + test_model.rotate_cells([None], (0, 0, 90)) + with pytest.raises(openmc.exceptions.InvalidIDError): + # Make sure it tells us we had a bad id + test_model.rotate_cells([7200], (0, 0, 90)) + with pytest.raises(openmc.exceptions.InvalidIDError): + # Make sure it tells us we had a bad id + test_model.rotate_cells(['bad_name'], (0, 0, 90)) + # Now a good one + assert np.all(openmc.lib.cells[2].rotation == (0., 0., 0.)) + test_model.rotate_cells([2], (0, 0, 90)) + assert np.all(openmc.lib.cells[2].rotation == (0., 0., 90.)) + + # And same thing by name + test_model.rotate_cells(['fuel'], (0, 0, 180)) + + # Now translate_cells. We dont need to re-check the TypeErrors/bad ids, + # because the other functions use the same hidden method as rotate_cells + assert np.all(openmc.lib.cells[2].translation == (0., 0., 0.)) + test_model.translate_cells([2], (0, 0, 10)) + assert np.all(openmc.lib.cells[2].translation == (0., 0., 10.)) + + # Now lets do the density updates. + # Check initial conditions + assert abs(openmc.lib.materials[1].get_density( + 'atom/b-cm') - 0.06891296988603757) < 1e-13 + mat_a_dens = np.sum( + [v[1] for v in test_model.materials[0]. + get_nuclide_atom_densities().values()]) + assert abs(mat_a_dens - 0.06891296988603757) < 1e-8 + # Change the density + test_model.update_densities(['UO2'], 2.) + assert abs(openmc.lib.materials[1].get_density('atom/b-cm') - 2.) < 1e-13 + mat_a_dens = np.sum( + [v[1] for v in test_model.materials[0]. + get_nuclide_atom_densities().values()]) + assert abs(mat_a_dens - 2.) < 1e-8 + + # Now lets do the cell temperature updates. + # Check initial conditions + + assert test_model._cells_by_id == \ + {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], + 4: geom.root_universe.cells[4], + 1: geom.root_universe.cells[2].fill.cells[1]} + assert abs(openmc.lib.cells[3].get_temperature() - 293.6) < 1e-13 + assert test_model.geometry.root_universe.cells[3].temperature is None + # Change the temperature + test_model.update_cell_temperatures([3], 600.) + assert abs(openmc.lib.cells[3].get_temperature() - 600.) < 1e-13 + assert abs(test_model.geometry.root_universe.cells[3].temperature - + 600.) < 1e-13 + + # Now lets do the material temperature updates. + # 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 + # 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 + # # Change the temperature + # test_model.update_material_temperatures(['UO2'], 600.) + # assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 + # assert abs(test_model.materials[0].temperature - 600.) < 1e-13 + + # And finally material volume + # import pdb; pdb.set_trace() + assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 + # The temperature on the material will be None because its just the default + assert abs(test_model.materials[0].volume - 0.4831931368640985) < 1e-13 + # Change the temperature + test_model.update_material_volumes(['UO2'], 2.) + 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 From d7bf830028d20bb91ac9178b4d462b7bda81b24b Mon Sep 17 00:00:00 2001 From: agnelson Date: Wed, 29 Sep 2021 08:22:16 -0500 Subject: [PATCH 09/19] Added test of Model.plot_geometry and made changes to reflect that openmc.lib.plot_geometry wont work if openmc.lib.init wasnt told that the run mode is to plot because then plots.xml isnt read in --- openmc/model/model.py | 36 +++++++++++++++---------- tests/unit_tests/conftest.py | 18 ++++++++----- tests/unit_tests/test_model.py | 49 ++++++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index e32876a6c..5e0c1a2bc 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -272,7 +272,7 @@ class Model: if dep.comm.rank == 0: self.export_to_xml() dep.comm.barrier() - # TODO: Implement the output flag somewhere on the C++ side + openmc.lib.init(args=args, intracomm=dep.comm) def clear_C_api(self): @@ -636,6 +636,7 @@ class Model: if not output: openmc.lib.settings.verbosity = init_verbosity else: + self.export_to_xml() openmc.calculate_volumes(threads=threads, output=output, openmc_exec=openmc_exec, mpi_args=mpi_args) @@ -691,21 +692,28 @@ class Model: "before executing this method!") with openmc.change_directory(Path(cwd)): - if self.C_init: - # Apply the output settings - if not output: - init_verbosity = openmc.lib.settings.verbosity - if not output: - openmc.lib.settings.verbosity = 1 + # TODO: openmc_init 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 + self.export_to_xml() + openmc.plot_geometry(output=output, openmc_exec=openmc_exec) - # Compute the volumes - openmc.lib.plot_geometry() + # if self.C_init: + # # Apply the output settings + # if not output: + # init_verbosity = openmc.lib.settings.verbosity + # if not output: + # openmc.lib.settings.verbosity = 1 - # Reset the output verbosity - if not output: - openmc.lib.settings.verbosity = init_verbosity - else: - openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + # # Compute the volumes + # openmc.lib.plot_geometry() + + # # Reset the output verbosity + # if not output: + # openmc.lib.settings.verbosity = init_verbosity + # else: + # self.export_to_xml() + # openmc.plot_geometry(output=output, openmc_exec=openmc_exec) if convert: for p in self.plots: diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index d6f94e82e..ff5340249 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -63,12 +63,18 @@ def pin_model_attributes(): tal.scores = ['flux'] tals.append(tal) - plot = openmc.Plot() - plot.origin = (0., 0., 0.) - plot.width = (pitch, pitch) - plot.pixels = (300, 300) - plot.color_by = 'material' - plots = openmc.Plots((plot,)) + plot1 = openmc.Plot() + plot1.origin = (0., 0., 0.) + plot1.width = (pitch, pitch) + plot1.pixels = (300, 300) + plot1.color_by = 'material' + plot1.filename = 'test' + plot2 = openmc.Plot() + plot2.origin = (0., 0., 0.) + plot2.width = (pitch, pitch) + plot2.pixels = (300, 300) + plot2.color_by = 'cell' + plots = openmc.Plots((plot1, plot2)) chain = './chain_simple.xml' fission_q = {'U235': 200e6} diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 0ee8568e3..dd6242fe1 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,6 +1,7 @@ import numpy as np import pytest from pathlib import Path +from shutil import which import openmc import openmc.lib @@ -226,7 +227,7 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] # and lets compare results - assert abs(C_keff - cli_keff) < 1e-15 + assert abs(C_keff - cli_keff) < 1e-13 assert abs(C_flux - cli_flux) < 1e-13 # Now we should make sure that the flags for items which should be handled @@ -247,6 +248,51 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): openmc.deplete.comm = orig_comm +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 + + # 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 + # there + + # We will only test convert if it is on the system, so as not to add an + # extra dependency just for tests + convert = which('convert') is not None + if convert: + exts = ['ppm', 'png'] + else: + exts = ['ppm'] + + # 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.plot_geometry(output=True, convert=convert) + + # Now look for the files, expect to find test.ppm, plot_2.ppm, and if + # convert is True, test.png, plot_2.png + for fname in ['test.', 'plot_2.']: + for ext in exts: + test_file = Path('./{}{}'.format(fname, ext)) + 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 + + 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) @@ -314,7 +360,6 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Now lets do the cell temperature updates. # Check initial conditions - assert test_model._cells_by_id == \ {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], 4: geom.root_universe.cells[4], From dc80b799ac518ed6fb7c3ba97691ca5788f69cfc Mon Sep 17 00:00:00 2001 From: agnelson Date: Wed, 29 Sep 2021 18:51:00 -0500 Subject: [PATCH 10/19] 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 3055561be..585deabba 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 8560f89e9..5b000a389 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 de5604c9d..fa55e1366 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 7e766a93b..f1b9c291f 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 03c6c41fc..6b72dde55 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 5e0c1a2bc..e560ef9fb 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 ff5340249..7ddca6dd7 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 dd6242fe1..6dc135056 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() From ad483b3fea16587d9a6cae35e684d0fcdb085825 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 11:18:35 -0500 Subject: [PATCH 11/19] Moved the dummy communicator to being within openmc vice openmc.deplete, added test of model.calculate_volumes and model.deplete, fixed issues identified from those tests, and added a DLL quieting method (still needs to be propagated to integrator.integrate --- openmc/__init__.py | 2 + openmc/deplete/__init__.py | 11 - openmc/deplete/abc.py | 2 +- openmc/deplete/helpers.py | 2 +- openmc/deplete/operator.py | 19 +- openmc/deplete/results.py | 2 +- openmc/lib/core.py | 103 +++++++-- openmc/model/model.py | 161 ++++++-------- openmc/{deplete/dummy_comm.py => mpi.py} | 8 + tests/unit_tests/conftest.py | 129 ----------- tests/unit_tests/test_deplete_chain.py | 3 +- tests/unit_tests/test_deplete_integrator.py | 8 +- tests/unit_tests/test_lib.py | 7 +- tests/unit_tests/test_model.py | 234 ++++++++++++++++++-- 14 files changed, 407 insertions(+), 284 deletions(-) rename openmc/{deplete/dummy_comm.py => mpi.py} (79%) diff --git a/openmc/__init__.py b/openmc/__init__.py index 8985099ba..7366e8383 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -30,8 +30,10 @@ from openmc.plotter import * from openmc.search import * from openmc.polynomial import * from . import examples +from openmc.mpi import DummyCommunicator, MPI, comm # Import a few names from the model module from openmc.model import rectangular_prism, hexagonal_prism, Model + __version__ = '0.13.0-dev' diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 00b29e319..b8c1cdfff 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -4,17 +4,6 @@ openmc.deplete A depletion front-end tool. """ -import sys -from unittest.mock import Mock - -from .dummy_comm import DummyCommunicator - -try: - from mpi4py import MPI - comm = MPI.COMM_WORLD -except ImportError: - MPI = Mock() - comm = DummyCommunicator() from .nuclide import * from .chain import * diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index ca16926ea..bf9f4da82 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -22,7 +22,7 @@ from uncertainties import ufloat from openmc.data import DataLibrary from openmc.lib import MaterialFilter, Tally from openmc.checkvalue import check_type, check_greater_than -from . import comm +from openmc import comm from .results import Results from .chain import Chain from .results_list import ResultsList diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index 6c3badc09..a8cf5212e 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -10,7 +10,7 @@ import sys from numpy import dot, zeros, newaxis, asarray -from . import comm +from openmc import comm from openmc.checkvalue import check_type, check_greater_than from openmc.data import JOULE_PER_EV, REACTION_MT from openmc.lib import ( diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 5b000a389..e4db13f95 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -20,7 +20,7 @@ from uncertainties import ufloat import openmc from openmc.checkvalue import check_value import openmc.lib -from . import comm +from openmc import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates @@ -176,6 +176,9 @@ class Operator(TransportOperator): results are to be used. diff_burnable_mats : bool Whether to differentiate burnable materials with multiple instances + cleanup_when_done : bool + Whether to finalize and clear the shared library memory when the + depletion operation is complete. Defaults to clearing the library. """ _fission_helpers = { "average": AveragedFissionYieldHelper, @@ -202,7 +205,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: @@ -318,6 +321,10 @@ class Operator(TransportOperator): # Reset results in OpenMC openmc.lib.reset() + # Update the number densities regardless of the source rate + self.number.set_density(vec) + self._update_materials() + # If the source rate is zero, return zero reaction rates without running # a transport solve if source_rate == 0.0: @@ -328,11 +335,7 @@ class Operator(TransportOperator): # Prevent OpenMC from complaining about re-creating tallies openmc.reset_auto_ids() - # Update status - self.number.set_density(vec) - - # Update material compositions and tally nuclides - self._update_materials() + # Update tally nuclides data in preparation for transport solve nuclides = self._get_tally_nuclides() self._rate_helper.nuclides = nuclides self._normalization_helper.nuclides = nuclides @@ -556,7 +559,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/deplete/results.py b/openmc/deplete/results.py index ce8331e82..05d78f629 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -9,7 +9,7 @@ import copy import h5py import numpy as np -from . import comm, MPI +from openmc import comm, MPI from .reaction_rates import ReactionRates VERSION_RESULTS = (1, 1) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 6b72dde55..6f08f76b2 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -2,6 +2,7 @@ from contextlib import contextmanager from ctypes import (c_bool, c_int, c_int32, c_int64, c_double, c_char_p, c_char, POINTER, Structure, c_void_p, create_string_buffer) import sys +import os import numpy as np from numpy.ctypeslib import as_array @@ -110,9 +111,14 @@ def global_bounding_box(): return llc, urc -def calculate_volumes(): +def calculate_volumes(output=True): """Run stochastic volume calculation""" - _dll.openmc_calculate_volumes() + + if output: + _dll.openmc_calculate_volumes() + else: + with quiet_dll(): + _dll.openmc_calculate_volumes() def current_batch(): @@ -127,13 +133,17 @@ def current_batch(): return c_int.in_dll(_dll, 'current_batch').value -def export_properties(filename=None): +def export_properties(filename=None, output=True): """Export physical properties. + .. versionchanged:: 0.13.0 + Parameters ---------- filename : str or None Filename to export properties to (defaults to "properties.h5") + output: bool, optional + Whether or not to show output. Defaults to showing output See Also -------- @@ -142,7 +152,11 @@ def export_properties(filename=None): """ if filename is not None: filename = c_char_p(filename.encode()) - _dll.openmc_properties_export(filename) + if output: + _dll.openmc_properties_export(filename) + else: + with quiet_dll(): + _dll.openmc_properties_export(filename) def finalize(): @@ -220,15 +234,19 @@ def import_properties(filename): _dll.openmc_properties_import(filename.encode()) -def init(args=None, intracomm=None): +def init(args=None, intracomm=None, output=True): """Initialize OpenMC + .. versionchanged:: 0.13.0 + Parameters ---------- - args : list of str + args : list of str, optional Command-line arguments - intracomm : mpi4py.MPI.Intracomm or None + intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator + output: bool, optional + Whether or not to show output. Defaults to showing output """ if args is not None: @@ -254,7 +272,11 @@ def init(args=None, intracomm=None): address = MPI._addressof(intracomm) intracomm = c_void_p(address) - _dll.openmc_init(argc, argv, intracomm) + if output: + _dll.openmc_init(argc, argv, intracomm) + else: + with quiet_dll(): + _dll.openmc_init(argc, argv, intracomm) openmc.lib.is_initialized = True @@ -345,9 +367,22 @@ def next_batch(): return status.value -def plot_geometry(): - """Plot geometry""" - _dll.openmc_plot_geometry() +def plot_geometry(output=True): + """Plot geometry + + .. versionchanged:: 0.13.0 + + Parameters + ---------- + output: bool, optional + Whether or not to show output. Defaults to showing output + """ + + if output: + _dll.openmc_plot_geometry() + else: + with quiet_dll(): + _dll.openmc_plot_geometry() def reset(): @@ -360,9 +395,22 @@ def reset_timers(): _dll.openmc_reset_timers() -def run(): - """Run simulation""" - _dll.openmc_run() +def run(output=True): + """Run simulation + + .. versionchanged:: 0.13.0 + + Parameters + ---------- + output: bool, optional + Whether or not to show output. Defaults to showing output + """ + + if output: + _dll.openmc_run() + else: + with quiet_dll(): + _dll.openmc_run() def simulation_init(): @@ -478,3 +526,30 @@ class _FortranObjectWithID(_FortranObject): # assigned. If the array index of the object is out of bounds, an # OutOfBoundsError will be raised here by virtue of referencing self.id self.id + + +@contextmanager +def quiet_dll(): + """This context manager allows us to suppress standard output from DLLs""" + sys.stdout.flush() + # Save the initial file descriptor states + initial_stdout = sys.stdout + initial_stdout_fno = os.dup(sys.stdout.fileno()) + # Get a garbage descriptor so we can throw away output + devnull = os.open(os.devnull, os.O_WRONLY) + + # Get the current stdout stream and make a duplicate of it + new_stdout = os.dup(1) + # Copy the garbage output to the stdout stream + os.dup2(devnull, 1) + os.close(devnull) + # Now point stdout to the re-defined stdout + sys.stdout = os.fdopen(new_stdout, 'w') + + try: + yield + finally: + # Now we just clean up after ourselves and reset the streams + sys.stdout = initial_stdout + sys.stdout.flush() + os.dup2(initial_stdout_fno, 1) diff --git a/openmc/model/model.py b/openmc/model/model.py index e560ef9fb..c65470e3e 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -191,17 +191,11 @@ class Model: @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() + if intracomm is None: + self._intracomm = openmc.comm else: - check_type('intracomm', intracomm, MPI.Comm) + check_type('intracomm', intracomm, + (openmc.MPI.Comm, openmc.DummyCommunicator)) self._intracomm = intracomm @classmethod @@ -263,25 +257,12 @@ class Model: # where it exists (here in init), but in the future the functionality # should be exposed so that it can be accessed via model.run(...) - # TODO: the output flag is not yet implemented for the openmc.lib.init - # command. This will be the subject of future work. - - args = [] - - 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') + args = openmc.process_CLI_arguments( + volume=False, geometry_debug=geometry_debug, + restart_file=restart_file, threads=threads, tracks=tracks, + event_based=event_based) + # Args adds the openmc_exec command in the first entry; remove it + args = args[1:] self.finalize_lib() @@ -289,7 +270,13 @@ class Model: self.export_to_xml() self.intracomm.barrier() - openmc.lib.init(args=args, intracomm=self.intracomm) + if isinstance(self.intracomm, openmc.DummyCommunicator): + # openmc.lib.init does not accept DummyCommunicator, and importing + # the DummyCommunicator class is overkill. Filter it here + intracomm = None + else: + intracomm = self.intracomm + openmc.lib.init(args=args, intracomm=intracomm, output=output) def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -301,7 +288,7 @@ class Model: openmc.lib.finalize() def deplete(self, timesteps, method='cecm', final_step=True, - operator_kwargs=None, integrator_kwargs=None, directory='.'): + operator_kwargs=None, directory='.', **integrator_kwargs): """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 @@ -320,25 +307,34 @@ class Model: 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 - **kwargs - Keyword arguments passed to integration function (e.g., - :func:`openmc.deplete.integrator.cecm`) + integrator_kwargs : dict + Remaining keyword arguments passed to the depletion Integrator + initializer (e.g., :func:`openmc.deplete.integrator.cecm`). """ + if operator_kwargs is None: + op_kwargs = {} + elif isinstance(operator_kwargs, dict): + op_kwargs = operator_kwargs + else: + msg = "operator_kwargs must be a dict or None" + raise ValueError(msg) + # Import openmc.deplete here so the Model can be used even if the # shared library is unavailable. import openmc.deplete as dep + # Store whether or not the library was initialized when we started + started_initialized = self.is_initialized + with _change_directory(Path(directory)): depletion_operator = \ - dep.Operator(self.geometry, self.settings, **operator_kwargs) + dep.Operator(self.geometry, self.settings, **op_kwargs) + # Tell depletion_operator.finalize NOT to clear C API memory when # it is done depletion_operator.cleanup_when_done = False @@ -351,13 +347,9 @@ class Model: **integrator_kwargs) # Now perform the depletion + # TODO: add output parameter to integrate 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() - # Now make the python Materials match the C API material data for mat_id, mat in self._materials_by_id.items(): if mat.depletable: @@ -370,6 +362,11 @@ class Model: mat.add_nuclide(nuc, density) mat.set_density('atom/b-cm', sum(densities)) + # If we didnt start intialized, we should cleanup after ourselves + if not started_initialized: + depletion_operator.cleanup_when_done = True + depletion_operator.finalize() + def export_to_xml(self, directory='.'): """Export model to XML files. @@ -531,25 +528,16 @@ class Model: if isinstance(particles, Integral) and particles > 0: openmc.lib.settings.particles = particles - # If we dont want output, make the verbosity quiet - if not output: - init_verbosity = openmc.lib.settings.verbosity - if not output: - openmc.lib.settings.verbosity = 1 - # 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 # Then run using the C API - openmc.lib.run() + openmc.lib.run(output) # Reset changes for the openmc.run kwargs handling if particles is not None: openmc.lib.settings.particles = init_particles - if not output: - # Then re-set the initial verbosity - openmc.lib.settings.verbosity = init_verbosity else: # Then run via the command line @@ -603,7 +591,7 @@ class Model: to the model. Defaults to applying the volumes. """ - if len(self.settings.volume_calculation) == 0: + if len(self.settings.volume_calculations) == 0: # Then there is no volume calculation specified raise ValueError("The Settings.volume_calculation attribute must" " be specified before executing this method!") @@ -618,18 +606,9 @@ class Model: "with the call to mpi4py" raise ValueError(msg) - # Apply the output settings - if not output: - init_verbosity = openmc.lib.settings.verbosity - if not output: - openmc.lib.settings.verbosity = 1 - # Compute the volumes - openmc.lib.calculate_volumes() + openmc.lib.calculate_volumes(output) - # Reset the output verbosity - if not output: - openmc.lib.settings.verbosity = init_verbosity else: self.export_to_xml() openmc.calculate_volumes(threads=threads, output=output, @@ -638,23 +617,19 @@ class Model: # Now we apply the volumes if apply_volumes: - # Load the results - f_names = \ - [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] - # And now we can add them to the model - for vol_calc in vol_calcs: + # Load the results and add them to the model + for i, vol_calc in enumerate(self.settings.volume_calculations): + f_name = f"volume_{i + 1}.h5" + vol_calc.load_results(f_name) # First add them to the Python side self.geometry.add_volume_information(vol_calc) # And now repeat for the C API - if vol_calc.domain == 'material': + if self.is_initialized and vol_calc.domain_type == 'material': # 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]) + for domain_id in vol_calc.ids: + openmc.lib.materials[domain_id].volume = \ + vol_calc.volumes[domain_id].n def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', convert=True, convert_exec='convert'): @@ -689,25 +664,15 @@ class Model: "before executing this method!") 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 + # TODO: openmc.is_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 self.export_to_xml() openmc.plot_geometry(output=output, openmc_exec=openmc_exec) # if self.is_initialized: - # # Apply the output settings - # if not output: - # init_verbosity = openmc.lib.settings.verbosity - # if not output: - # openmc.lib.settings.verbosity = 1 - # # Compute the volumes - # openmc.lib.plot_geometry() - - # # Reset the output verbosity - # if not output: - # openmc.lib.settings.verbosity = init_verbosity + # openmc.lib.plot_geometry(output) # else: # self.export_to_xml() # openmc.plot_geometry(output=output, openmc_exec=openmc_exec) @@ -721,8 +686,8 @@ class Model: png_file = ppm_file.replace('.ppm', '.png') subprocess.check_call([convert_exec, ppm_file, png_file]) - def _change_py_C_attribs(self, names_or_ids, value, obj_type, attrib_name, - density_units='atom/b-cm'): + def _change_py_lib_attribs(self, names_or_ids, value, obj_type, + attrib_name, density_units='atom/b-cm'): # Method to do the same work whether it is a cell or material and # a temperature or volume check_type('names_or_ids', names_or_ids, Iterable, (Integral, str)) @@ -819,7 +784,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, vector, 'cell', 'rotation') + self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'rotation') def translate_cells(self, names_or_ids, vector): """Translate the identified cell(s) by the specified translation vector. @@ -841,7 +806,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, vector, 'cell', 'translation') + self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'translation') 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 @@ -863,7 +828,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, density, 'material', 'density', + self._change_py_lib_attribs(names_or_ids, density, 'material', 'density', density_units) def update_cell_temperatures(self, names_or_ids, temperature): @@ -884,7 +849,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, temperature, 'cell', + self._change_py_lib_attribs(names_or_ids, temperature, 'cell', 'temperature') def update_material_temperatures(self, names_or_ids, temperature): @@ -905,7 +870,7 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, temperature, 'material', + self._change_py_lib_attribs(names_or_ids, temperature, 'material', 'temperature') def update_material_volumes(self, names_or_ids, volume): @@ -926,4 +891,4 @@ class Model: """ - self._change_py_C_attribs(names_or_ids, volume, 'material', 'volume') + self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume') diff --git a/openmc/deplete/dummy_comm.py b/openmc/mpi.py similarity index 79% rename from openmc/deplete/dummy_comm.py rename to openmc/mpi.py index 7ac9be6c3..b64a84bc6 100644 --- a/openmc/deplete/dummy_comm.py +++ b/openmc/mpi.py @@ -1,4 +1,5 @@ import sys +from unittest.mock import Mock class DummyCommunicator: @@ -31,3 +32,10 @@ class DummyCommunicator: def Abort(self, exit_code_or_msg): sys.exit(exit_code_or_msg) + +try: + from mpi4py import MPI + comm = MPI.COMM_WORLD +except ImportError: + MPI = Mock() + comm = DummyCommunicator() diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 7ddca6dd7..51d1b19a3 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -1,138 +1,9 @@ -from math import pi import openmc import pytest from tests.regression_tests import config -@pytest.fixture(scope='module') -def pin_model_attributes(): - uo2 = openmc.Material(name='UO2') - uo2.set_density('g/cm3', 10.29769) - uo2.add_element('U', 1., enrichment=2.4) - uo2.add_element('O', 2.) - - zirc = openmc.Material(name='Zirc') - zirc.set_density('g/cm3', 6.55) - zirc.add_element('Zr', 1.) - - borated_water = openmc.Material(name='Borated water') - borated_water.set_density('g/cm3', 0.740582) - borated_water.add_element('B', 4.0e-5) - borated_water.add_element('H', 5.0e-2) - borated_water.add_element('O', 2.4e-2) - borated_water.add_s_alpha_beta('c_H_in_H2O') - - mats = openmc.Materials([uo2, zirc, borated_water]) - - pitch = 1.25984 - fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') - clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') - box = openmc.model.rectangular_prism(pitch, pitch, - boundary_type='reflective') - - # Define cells - fuel_inf_cell = openmc.Cell(name='inf fuel', fill=uo2) - fuel_inf_univ = openmc.Universe(cells=[fuel_inf_cell]) - fuel = openmc.Cell(name='fuel', fill=fuel_inf_univ, region=-fuel_or) - clad = openmc.Cell(fill=zirc, region=+fuel_or & -clad_or) - water = openmc.Cell(fill=borated_water, region=+clad_or & box) - - # Define overall geometry - geom = openmc.Geometry([fuel, clad, water]) - uo2.volume = pi * fuel_or.r**2 - - settings = openmc.Settings() - settings.batches = 100 - settings.inactive = 10 - settings.particles = 1000 - - # Create an initial uniform spatial source distribution over fissionable zones - bounds = [-0.62992, -0.62992, -1, 0.62992, 0.62992, 1] - uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - settings.source = openmc.source.Source(space=uniform_dist) - - entropy_mesh = openmc.RegularMesh() - entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] - entropy_mesh.upper_right = [0.39218, 0.39218, 1.e50] - entropy_mesh.dimension = [10, 10, 1] - settings.entropy_mesh = entropy_mesh - - tals = openmc.Tallies() - tal = openmc.Tally(name='test') - tal.scores = ['flux'] - tals.append(tal) - - plot1 = openmc.Plot() - plot1.origin = (0., 0., 0.) - plot1.width = (pitch, pitch) - plot1.pixels = (300, 300) - plot1.color_by = 'material' - plot1.filename = 'test' - plot2 = openmc.Plot() - plot2.origin = (0., 0., 0.) - plot2.width = (pitch, pitch) - plot2.pixels = (300, 300) - plot2.color_by = 'cell' - plots = openmc.Plots((plot1, plot2)) - - chain = './chain_simple.xml' - fission_q = {'U235': 200e6} - - chain_file_xml = """ - - - - - - - - - - - - - - - - - - - - - 2.53000e-02 - - Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 4.356330e-05 - - - - - - - 2.53000e-02 - - Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 4.9097e-6 - - - - - - - 2.53000e-02 - - Gd157 Gd156 I135 Xe135 Xe136 Cs135 - 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 3.7100E-07 - - - - -""" - operator_kwargs = {'chain_file': chain, 'fission_q': fission_q} - - return (mats, geom, settings, tals, plots, operator_kwargs, chain_file_xml) - @pytest.fixture(scope='module') def mpi_intracomm(): if config['mpi']: diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index fa90b35f2..0904ff39a 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -7,7 +7,8 @@ import os from pathlib import Path import numpy as np -from openmc.deplete import comm, Chain, reaction_rates, nuclide, cram, pool +from openmc import comm +from openmc.deplete import Chain, reaction_rates, nuclide, cram, pool import pytest from tests import cdtemp diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 3195c5976..ec424b05b 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -14,11 +14,11 @@ import numpy as np from uncertainties import ufloat import pytest +from openmc import comm from openmc.deplete import ( - ReactionRates, Results, ResultsList, comm, OperatorResult, - PredictorIntegrator, CECMIntegrator, CF4Integrator, CELIIntegrator, - EPCRK4Integrator, LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, - cram) + ReactionRates, Results, ResultsList, OperatorResult, PredictorIntegrator, + CECMIntegrator, CF4Integrator, CELIIntegrator, EPCRK4Integrator, + LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram) from tests import dummy_operator diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 41613832f..079b4f9eb 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -713,7 +713,6 @@ def test_trigger_set_n_batches(uo2_trigger_model, mpi_intracomm): def test_cell_translation(pincell_model_w_univ, mpi_intracomm): openmc.lib.finalize() openmc.lib.init(intracomm=mpi_intracomm) - openmc.lib.simulation_init() # Cell 1 is filled with a material so it has a translation, but we can't # set it. cell = openmc.lib.cells[1] @@ -727,9 +726,12 @@ def test_cell_translation(pincell_model_w_univ, mpi_intracomm): # This time we *can* set it cell.translation = (1., 0., -1.) assert cell.translation == pytest.approx([1., 0., -1.]) + openmc.lib.finalize() -def test_cell_rotation(pincell_model_w_univ): +def test_cell_rotation(pincell_model_w_univ, mpi_intracomm): + openmc.lib.finalize() + openmc.lib.init(intracomm=mpi_intracomm) # Cell 1 is filled with a material so we cannot rotate it, but we can get # its rotation matrix (which will be the identity matrix) cell = openmc.lib.cells[1] @@ -742,3 +744,4 @@ def test_cell_rotation(pincell_model_w_univ): assert cell.rotation == pytest.approx([0., 0., 0.]) cell.rotation = (180., 0., 0.) assert cell.rotation == pytest.approx([180., 0., 0.]) + openmc.lib.finalize() diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 6dc135056..4ab4c7eb7 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,10 +1,110 @@ +from math import pi import numpy as np import pytest from pathlib import Path from shutil import which import openmc import openmc.lib -from openmc.deplete.dummy_comm import DummyCommunicator +from openmc.mpi import DummyCommunicator + + +@pytest.fixture(scope='function') +def pin_model_attributes(): + uo2 = openmc.Material(material_id=1, name='UO2') + uo2.set_density('g/cm3', 10.29769) + uo2.add_element('U', 1., enrichment=2.4) + uo2.add_element('O', 2.) + uo2.depletable = True + + zirc = openmc.Material(material_id=2, name='Zirc') + zirc.set_density('g/cm3', 6.55) + zirc.add_element('Zr', 1.) + zirc.depletable = False + + borated_water = openmc.Material(material_id=3, name='Borated water') + borated_water.set_density('g/cm3', 0.740582) + borated_water.add_element('B', 4.0e-5) + borated_water.add_element('H', 5.0e-2) + borated_water.add_element('O', 2.4e-2) + borated_water.add_s_alpha_beta('c_H_in_H2O') + borated_water.depletable = False + + mats = openmc.Materials([uo2, zirc, borated_water]) + + pitch = 1.25984 + fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') + clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') + box = openmc.model.rectangular_prism(pitch, pitch, + boundary_type='reflective') + + # Define cells + fuel_inf_cell = openmc.Cell(cell_id=1, name='inf fuel', fill=uo2) + fuel_inf_univ = openmc.Universe(universe_id=1, cells=[fuel_inf_cell]) + fuel = openmc.Cell(cell_id=2, name='fuel', + fill=fuel_inf_univ, region=-fuel_or) + clad = openmc.Cell(cell_id=3, fill=zirc, region=+fuel_or & -clad_or) + water = openmc.Cell(cell_id=4, fill=borated_water, region=+clad_or & box) + + # Define overall geometry + geom = openmc.Geometry([fuel, clad, water]) + uo2.volume = pi * fuel_or.r**2 + + settings = openmc.Settings() + settings.batches = 100 + settings.inactive = 10 + settings.particles = 1000 + + # Create a uniform spatial source distribution over fissionable zones + bounds = [-0.62992, -0.62992, -1, 0.62992, 0.62992, 1] + uniform_dist = openmc.stats.Box( + bounds[:3], bounds[3:], only_fissionable=True) + settings.source = openmc.source.Source(space=uniform_dist) + + entropy_mesh = openmc.RegularMesh() + entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] + entropy_mesh.upper_right = [0.39218, 0.39218, 1.e50] + entropy_mesh.dimension = [10, 10, 1] + settings.entropy_mesh = entropy_mesh + + tals = openmc.Tallies() + tal = openmc.Tally(tally_id=1, name='test') + tal.filters = [openmc.MaterialFilter(bins=[uo2])] + tal.scores = ['flux', 'fission'] + tals.append(tal) + + plot1 = openmc.Plot(plot_id=1) + plot1.origin = (0., 0., 0.) + plot1.width = (pitch, pitch) + plot1.pixels = (300, 300) + plot1.color_by = 'material' + plot1.filename = 'test' + plot2 = openmc.Plot(plot_id=2) + plot2.origin = (0., 0., 0.) + plot2.width = (pitch, pitch) + plot2.pixels = (300, 300) + plot2.color_by = 'cell' + plots = openmc.Plots((plot1, plot2)) + + chain = './test_chain.xml' + + chain_file_xml = """ + + + + + + 2.53000e-02 + + Xe136 + 1.0 + + + + +""" + operator_kwargs = {'chain_file': chain} + + return (mats, geom, settings, tals, plots, operator_kwargs, chain_file_xml) def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): @@ -130,7 +230,7 @@ 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, mpi_intracomm) - test_model.init_lib() + test_model.init_lib(output=False) # First check that the API is advertised as initialized assert openmc.lib.is_initialized is True @@ -157,13 +257,13 @@ 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_lib() + model.init_lib(output=False) # Change fuel temperature and density and export properties cell = openmc.lib.cells[1] cell.set_temperature(600.0) cell.fill.set_density(5.0, 'g/cm3') - openmc.lib.export_properties() + openmc.lib.export_properties(output=False) # Import properties to existing model model.import_properties("properties.h5") @@ -203,17 +303,20 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): 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] + cli_flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] + cli_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] - test_model.init_lib() + test_model.init_lib(output=False) sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: - C_keff = sp.k_combined - C_flux = sp.get_tally(id=1).get_values()[0, 0, 0] + lib_keff = sp.k_combined + lib_flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] + lib_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] # and lets compare results - assert abs(C_keff - cli_keff) < 1e-13 - assert abs(C_flux - cli_flux) < 1e-13 + assert abs(lib_keff - cli_keff) < 1e-13 + assert abs(lib_flux - cli_flux) < 1e-13 + assert abs(lib_fiss - cli_fiss) < 1e-13 # Now we should make sure that the flags for items which should be handled # by init are properly set @@ -248,8 +351,8 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # 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_lib() - test_model.plot_geometry(output=True, convert=convert) + test_model.init_lib(output=False) + test_model.plot_geometry(output=False, convert=convert) # Now look for the files, expect to find test.ppm, plot_2.ppm, and if # convert is True, test.png, plot_2.png @@ -262,11 +365,11 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.finalize_lib() -def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): +def test_py_lib_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, mpi_intracomm) - test_model.init_lib() + test_model.init_lib(output=False) # Now we can call rotate_cells, translate_cells, update_densities, # update_cell_temperatures, and update_material_temperatures and make sure @@ -358,3 +461,106 @@ def test_py_C_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert abs(test_model.materials[0].volume - 2.) < 1e-13 test_model.finalize_lib() + + +# def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): +# mats, geom, settings, tals, plots, op_kwargs, chain_file_xml = \ +# pin_model_attributes +# with open('test_chain.xml', 'w') as f: +# f.write(chain_file_xml) +# test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + +# initial_mat = mats[0].clone() +# initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] + +# # Note that the chain file includes only U-235 fission to a stable Xe136 w/ +# # a yield of 100%. Thus all the U235 we lose becomes Xe136 + +# # In this test we first run without pre-initializing the shared library +# # data and then compare. Then we repeat with the C API already initialized +# # and make sure we get the same answer +# test_model.deplete([1e6], 'predictor', final_step=False, +# operator_kwargs=op_kwargs, +# power=1.) +# # Get the new Xe136 and U235 atom densities +# after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] +# after_u = mats[0].get_nuclide_atom_densities()['U235'][1] +# assert abs((after_xe + after_u) - initial_u) < 1e-15 +# assert test_model.is_initialized is False + +# # Reset the initial material densities +# mats[0].nuclides.clear() +# densities = initial_mat.get_nuclide_atom_densities() +# tot_density = 0. +# for nuc, density in densities.values(): +# mats[0].add_nuclide(nuc, density) +# tot_density += density +# mats[0].set_density('atom/b-cm', tot_density) + +# # Now we can re-run with the pre-initialized API +# test_model.init_lib(output=False) +# test_model.deplete([1e6], 'predictor', final_step=False, +# operator_kwargs=op_kwargs, +# power=1.) +# # Get the new Xe136 and U235 atom densities +# after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] +# after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] +# assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 +# assert test_model.is_initialized is True + +# # And end by comparing to the previous case +# assert abs(after_xe - after_lib_xe) < 1e-15 +# assert abs(after_u - after_lib_u) < 1e-15 + +# test_model.finalize_lib() + + +def test_calc_volumes(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, mpi_intracomm) + + # With no vol calcs, it should fail + with pytest.raises(ValueError): + test_model.calculate_volumes(output=False) + + # Add a cell and mat volume calc + material_vol_calc = openmc.VolumeCalculation( + [mats[2]], samples=1000, lower_left=(-.63, -.63, -100.), + upper_right=(.63, .63, 100.)) + cell_vol_calc = openmc.VolumeCalculation( + [geom.root_universe.cells[3]], samples=1000, + lower_left=(-.63, -.63, -100.), upper_right=(.63, .63, 100.)) + test_model.settings.volume_calculations = \ + [material_vol_calc, cell_vol_calc] + + # Now lets compute the volumes and check to see if it was applied + # First lets do without using the C-API + # Make sure the volumes are unassigned first + assert mats[2].volume is None + assert geom.root_universe.cells[3].volume is None + test_model.calculate_volumes(output=False, apply_volumes=True) + + # Now let's test that we have volumes assigned; we arent checking the + # value, just that the value was changed + assert mats[2].volume > 0. + assert geom.root_universe.cells[3].volume > 0. + + # Now reset the values + mats[2].volume = None + geom.root_universe.cells[3].volume = None + + # And do again with an initialized library + for file in ['volume_1.h5', 'volume_2.h5']: + file = Path(file) + file.unlink() + test_model.init_lib(output=False) + test_model.calculate_volumes(output=False, apply_volumes=True) + assert mats[2].volume > 0. + assert geom.root_universe.cells[3].volume > 0. + assert openmc.lib.materials[3].volume == mats[2].volume + + test_model.finalize_lib() + + + From 61d067f1f5a83b4167c9e9f6ea3efc4ec36aac17 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 11:19:11 -0500 Subject: [PATCH 12/19] oops; had test_model.py:test_deplete commented out. fixed --- tests/unit_tests/test_model.py | 86 +++++++++++++++++----------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4ab4c7eb7..07a356097 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -463,56 +463,56 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.finalize_lib() -# def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): -# mats, geom, settings, tals, plots, op_kwargs, chain_file_xml = \ -# pin_model_attributes -# with open('test_chain.xml', 'w') as f: -# f.write(chain_file_xml) -# test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) +def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): + mats, geom, settings, tals, plots, op_kwargs, chain_file_xml = \ + pin_model_attributes + with open('test_chain.xml', 'w') as f: + f.write(chain_file_xml) + test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) -# initial_mat = mats[0].clone() -# initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] + initial_mat = mats[0].clone() + initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] -# # Note that the chain file includes only U-235 fission to a stable Xe136 w/ -# # a yield of 100%. Thus all the U235 we lose becomes Xe136 + # Note that the chain file includes only U-235 fission to a stable Xe136 w/ + # a yield of 100%. Thus all the U235 we lose becomes Xe136 -# # In this test we first run without pre-initializing the shared library -# # data and then compare. Then we repeat with the C API already initialized -# # and make sure we get the same answer -# test_model.deplete([1e6], 'predictor', final_step=False, -# operator_kwargs=op_kwargs, -# power=1.) -# # Get the new Xe136 and U235 atom densities -# after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] -# after_u = mats[0].get_nuclide_atom_densities()['U235'][1] -# assert abs((after_xe + after_u) - initial_u) < 1e-15 -# assert test_model.is_initialized is False + # In this test we first run without pre-initializing the shared library + # data and then compare. Then we repeat with the C API already initialized + # and make sure we get the same answer + test_model.deplete([1e6], 'predictor', final_step=False, + operator_kwargs=op_kwargs, + power=1.) + # Get the new Xe136 and U235 atom densities + after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] + after_u = mats[0].get_nuclide_atom_densities()['U235'][1] + assert abs((after_xe + after_u) - initial_u) < 1e-15 + assert test_model.is_initialized is False -# # Reset the initial material densities -# mats[0].nuclides.clear() -# densities = initial_mat.get_nuclide_atom_densities() -# tot_density = 0. -# for nuc, density in densities.values(): -# mats[0].add_nuclide(nuc, density) -# tot_density += density -# mats[0].set_density('atom/b-cm', tot_density) + # Reset the initial material densities + mats[0].nuclides.clear() + densities = initial_mat.get_nuclide_atom_densities() + tot_density = 0. + for nuc, density in densities.values(): + mats[0].add_nuclide(nuc, density) + tot_density += density + mats[0].set_density('atom/b-cm', tot_density) -# # Now we can re-run with the pre-initialized API -# test_model.init_lib(output=False) -# test_model.deplete([1e6], 'predictor', final_step=False, -# operator_kwargs=op_kwargs, -# power=1.) -# # Get the new Xe136 and U235 atom densities -# after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] -# after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] -# assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 -# assert test_model.is_initialized is True + # Now we can re-run with the pre-initialized API + test_model.init_lib(output=False) + test_model.deplete([1e6], 'predictor', final_step=False, + operator_kwargs=op_kwargs, + power=1.) + # Get the new Xe136 and U235 atom densities + after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] + after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] + assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 + assert test_model.is_initialized is True -# # And end by comparing to the previous case -# assert abs(after_xe - after_lib_xe) < 1e-15 -# assert abs(after_u - after_lib_u) < 1e-15 + # And end by comparing to the previous case + assert abs(after_xe - after_lib_xe) < 1e-15 + assert abs(after_u - after_lib_u) < 1e-15 -# test_model.finalize_lib() + test_model.finalize_lib() def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): From b335f82792e60aaf7da9cd02fbe2921121405aeb Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 14:39:53 -0500 Subject: [PATCH 13/19] Made integrator quiet and fixed failing test caused by MPICommunicator types --- openmc/lib/core.py | 79 +++++++++++++++++----------------- openmc/model/model.py | 34 +++++++-------- tests/unit_tests/test_model.py | 4 +- 3 files changed, 58 insertions(+), 59 deletions(-) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 6f08f76b2..3b691721c 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -114,11 +114,8 @@ def global_bounding_box(): def calculate_volumes(output=True): """Run stochastic volume calculation""" - if output: + with quiet_dll(output): _dll.openmc_calculate_volumes() - else: - with quiet_dll(): - _dll.openmc_calculate_volumes() def current_batch(): @@ -152,11 +149,9 @@ def export_properties(filename=None, output=True): """ if filename is not None: filename = c_char_p(filename.encode()) - if output: + + with quiet_dll(output): _dll.openmc_properties_export(filename) - else: - with quiet_dll(): - _dll.openmc_properties_export(filename) def finalize(): @@ -272,11 +267,8 @@ def init(args=None, intracomm=None, output=True): address = MPI._addressof(intracomm) intracomm = c_void_p(address) - if output: + with quiet_dll(output): _dll.openmc_init(argc, argv, intracomm) - else: - with quiet_dll(): - _dll.openmc_init(argc, argv, intracomm) openmc.lib.is_initialized = True @@ -378,11 +370,8 @@ def plot_geometry(output=True): Whether or not to show output. Defaults to showing output """ - if output: + with quiet_dll(output): _dll.openmc_plot_geometry() - else: - with quiet_dll(): - _dll.openmc_plot_geometry() def reset(): @@ -406,11 +395,8 @@ def run(output=True): Whether or not to show output. Defaults to showing output """ - if output: + with quiet_dll(output): _dll.openmc_run() - else: - with quiet_dll(): - _dll.openmc_run() def simulation_init(): @@ -529,27 +515,40 @@ class _FortranObjectWithID(_FortranObject): @contextmanager -def quiet_dll(): - """This context manager allows us to suppress standard output from DLLs""" - sys.stdout.flush() - # Save the initial file descriptor states - initial_stdout = sys.stdout - initial_stdout_fno = os.dup(sys.stdout.fileno()) - # Get a garbage descriptor so we can throw away output - devnull = os.open(os.devnull, os.O_WRONLY) +def quiet_dll(output=True): + """This context manager allows us to suppress standard output from DLLs - # Get the current stdout stream and make a duplicate of it - new_stdout = os.dup(1) - # Copy the garbage output to the stdout stream - os.dup2(devnull, 1) - os.close(devnull) - # Now point stdout to the re-defined stdout - sys.stdout = os.fdopen(new_stdout, 'w') + Parameters + ---------- + output : bool + Denotes whether the output should be displayed (True) or not (False) - try: + .. versionadded:: 0.13.0 + + """ + + if output: yield - finally: - # Now we just clean up after ourselves and reset the streams - sys.stdout = initial_stdout + else: sys.stdout.flush() - os.dup2(initial_stdout_fno, 1) + # Save the initial file descriptor states + initial_stdout = sys.stdout + initial_stdout_fno = os.dup(sys.stdout.fileno()) + # Get a garbage descriptor so we can throw away output + devnull = os.open(os.devnull, os.O_WRONLY) + + # Get the current stdout stream and make a duplicate of it + new_stdout = os.dup(1) + # Copy the garbage output to the stdout stream + os.dup2(devnull, 1) + os.close(devnull) + # Now point stdout to the re-defined stdout + sys.stdout = os.fdopen(new_stdout, 'w') + + try: + yield + finally: + # Now we just clean up after ourselves and reset the streams + sys.stdout = initial_stdout + sys.stdout.flush() + os.dup2(initial_stdout_fno, 1) diff --git a/openmc/model/model.py b/openmc/model/model.py index c65470e3e..b35f50c87 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -52,8 +52,6 @@ class Model: Tallies information plots : openmc.Plots, optional Plot information - intracomm : mpi4py.MPI.Intracomm or None, optional - MPI intracommunicator Attributes ---------- @@ -67,8 +65,10 @@ class Model: Tallies information plots : openmc.Plots Plot information - intracomm : mpi4py.MPI.Intracomm or None - MPI intracommunicator + intracomm : mpi4py.MPI.Intracomm or openmc.DummyCommunicator + MPI intracommunicator; this defaults to the mpi4py world communicator + from if present, or a DummyCommunicator otherwise. If an alternative + communicator is desired, this parameter should be modified accordingly. """ @@ -91,7 +91,7 @@ class Model: if plots is not None: self.plots = plots - self.intracomm = intracomm + self.intracomm = openmc.comm # Store dictionaries to the materials and cells by ID and names if materials is None: @@ -191,12 +191,8 @@ class Model: @intracomm.setter def intracomm(self, intracomm): - if intracomm is None: - self._intracomm = openmc.comm - else: - check_type('intracomm', intracomm, - (openmc.MPI.Comm, openmc.DummyCommunicator)) - self._intracomm = intracomm + check_type('intracomm', intracomm, type(openmc.comm)) + self._intracomm = intracomm @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', @@ -272,7 +268,7 @@ class Model: if isinstance(self.intracomm, openmc.DummyCommunicator): # openmc.lib.init does not accept DummyCommunicator, and importing - # the DummyCommunicator class is overkill. Filter it here + # the DummyCommunicator class there is overkill. Filter it here intracomm = None else: intracomm = self.intracomm @@ -288,7 +284,8 @@ class Model: openmc.lib.finalize() def deplete(self, timesteps, method='cecm', final_step=True, - operator_kwargs=None, directory='.', **integrator_kwargs): + operator_kwargs=None, directory='.', output=True, + **integrator_kwargs): """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 @@ -310,6 +307,8 @@ class Model: 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 + output : bool + Capture OpenMC output from standard out integrator_kwargs : dict Remaining keyword arguments passed to the depletion Integrator initializer (e.g., :func:`openmc.deplete.integrator.cecm`). @@ -332,8 +331,9 @@ class Model: started_initialized = self.is_initialized with _change_directory(Path(directory)): - depletion_operator = \ - dep.Operator(self.geometry, self.settings, **op_kwargs) + with openmc.lib.quiet_dll(output): + depletion_operator = \ + dep.Operator(self.geometry, self.settings, **op_kwargs) # Tell depletion_operator.finalize NOT to clear C API memory when # it is done @@ -347,8 +347,8 @@ class Model: **integrator_kwargs) # Now perform the depletion - # TODO: add output parameter to integrate - integrator.integrate(final_step) + with openmc.lib.quiet_dll(output): + integrator.integrate(final_step) # Now make the python Materials match the C API material data for mat_id, mat in self._materials_by_id.items(): diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 07a356097..2760eadb4 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -481,7 +481,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # and make sure we get the same answer test_model.deplete([1e6], 'predictor', final_step=False, operator_kwargs=op_kwargs, - power=1.) + power=1., output=False) # Get the new Xe136 and U235 atom densities after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_u = mats[0].get_nuclide_atom_densities()['U235'][1] @@ -501,7 +501,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.init_lib(output=False) test_model.deplete([1e6], 'predictor', final_step=False, operator_kwargs=op_kwargs, - power=1.) + power=1., output=False) # Get the new Xe136 and U235 atom densities after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] From 20c8043c1c1046d4be2b45b17e7fbe2d1519f8c2 Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 15:15:58 -0500 Subject: [PATCH 14/19] (1) Made OpenMC load plots.xml upon initialization even if not in the plot runmode; (2) Made the finalize method clear plotting data; (3) allowed the openmc.lib and openmc.model package to call plot_geometry now that plots.xml would have been loaded --- include/openmc/plot.h | 7 ++++--- openmc/model/model.py | 18 ++++++------------ src/finalize.cpp | 3 +++ src/initialize.cpp | 4 +++- src/plot.cpp | 12 ++++++++++-- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/include/openmc/plot.h b/include/openmc/plot.h index aa15277aa..3d526cad6 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -30,9 +30,7 @@ namespace model { extern std::unordered_map plot_map; //!< map of plot ids to index extern vector plots; //!< Plot instance container -extern uint64_t - plotter_prn_seeds[N_STREAMS]; // Random number seeds used for plotter -extern int plotter_stream; // Stream index used by the plotter +extern uint64_t plotter_seed; // Stream index used by the plotter } // namespace model @@ -274,6 +272,9 @@ void voxel_finalize(hid_t dspace, hid_t dset, hid_t memspace); //! Read plot specifications from a plots.xml file void read_plots_xml(); +//! Clear memory +void free_memory_plot(); + //! Create a ppm image for a plot object //! \param[in] plot object void create_ppm(Plot const& pl); diff --git a/openmc/model/model.py b/openmc/model/model.py index b35f50c87..44479d7e7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -664,18 +664,12 @@ class Model: "before executing this method!") with _change_directory(Path(cwd)): - # TODO: openmc.is_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 - self.export_to_xml() - openmc.plot_geometry(output=output, openmc_exec=openmc_exec) - - # if self.is_initialized: - # # Compute the volumes - # openmc.lib.plot_geometry(output) - # else: - # self.export_to_xml() - # openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + if self.is_initialized: + # Compute the volumes + openmc.lib.plot_geometry(output) + else: + self.export_to_xml() + openmc.plot_geometry(output=output, openmc_exec=openmc_exec) if convert: for p in self.plots: diff --git a/src/finalize.cpp b/src/finalize.cpp index b1e09a37e..2f6c65aaf 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -15,6 +15,7 @@ #include "openmc/message_passing.h" #include "openmc/nuclide.h" #include "openmc/photon.h" +#include "openmc/plot.h" #include "openmc/random_lcg.h" #include "openmc/settings.h" #include "openmc/simulation.h" @@ -45,6 +46,7 @@ void free_memory() free_memory_mesh(); free_memory_tally(); free_memory_bank(); + free_memory_plot(); if (mpi::master) { free_memory_cmfd(); } @@ -128,6 +130,7 @@ int openmc_finalize() data::temperature_min = 0.0; data::temperature_max = INFTY; model::root_universe = -1; + model::plotter_seed = 1; openmc::openmc_set_seed(DEFAULT_SEED); // Deallocate arrays diff --git a/src/initialize.cpp b/src/initialize.cpp index e53767691..75ec20518 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -305,9 +305,11 @@ void read_input_xml() // Initialize distribcell_filters prepare_distribcell(); + // Read the plots.xml regardless of plot mode in case plots are requested + // via the API + read_plots_xml(); if (settings::run_mode == RunMode::PLOTTING) { // Read plots.xml if it exists - read_plots_xml(); if (mpi::master && settings::verbosity >= 5) print_plot(); diff --git a/src/plot.cpp b/src/plot.cpp index 789437691..217b09839 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -119,9 +119,11 @@ extern "C" int openmc_plot_geometry() void read_plots_xml() { - // Check if plots.xml exists + // Check if plots.xml exists; this is only necessary when the plot runmode is + // initiated. Otherwise, we want to read plots.xml because it may be called + // later via the API. In that case, its ok for a plots.xml to not exist std::string filename = settings::path_input + "plots.xml"; - if (!file_exists(filename)) { + if (!file_exists(filename) && settings::run_mode == RunMode::PLOTTING) { fatal_error(fmt::format("Plots XML file '{}' does not exist!", filename)); } @@ -138,6 +140,12 @@ void read_plots_xml() } } +void free_memory_plot() +{ + model::plots.clear(); + model::plot_map.clear(); +} + //============================================================================== // CREATE_PPM creates an image based on user input from a plots.xml // specification in the portable pixmap format (PPM) From e8081207d7df265f495779a5735cdfe5b34f704a Mon Sep 17 00:00:00 2001 From: agnelson Date: Thu, 30 Sep 2021 16:34:58 -0500 Subject: [PATCH 15/19] Modified the C API to support setting material temperatures and to support setting the event based flag. Fixed failing MPI test. --- include/openmc/capi.h | 1 + include/openmc/material.h | 8 ++--- include/openmc/settings.h | 2 +- openmc/executor.py | 17 ++++++---- openmc/lib/__init__.py | 2 ++ openmc/lib/material.py | 7 ++++ openmc/lib/settings.py | 1 + openmc/model/model.py | 52 ++++++++++++++---------------- src/material.cpp | 12 +++++++ tests/unit_tests/test_lib.py | 6 ++++ tests/unit_tests/test_model.py | 59 +++++++++++++++++----------------- 11 files changed, 100 insertions(+), 67 deletions(-) diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 0929a11f7..679919dfa 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -88,6 +88,7 @@ int openmc_material_set_densities( int openmc_material_set_id(int32_t index, int32_t id); int openmc_material_get_name(int32_t index, const char** name); int openmc_material_set_name(int32_t index, const char* name); +int openmc_material_set_temperature(int32_t index, double temperature); int openmc_material_set_volume(int32_t index, double volume); int openmc_material_filter_get_bins( int32_t index, const int32_t** bins, size_t* n); diff --git a/include/openmc/material.h b/include/openmc/material.h index 709d20573..b13cfe330 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -158,6 +158,10 @@ public: double density_; //!< Total atom density in [atom/b-cm] double density_gpcc_; //!< Total atom density in [g/cm^3] double volume_ {-1.0}; //!< Volume in [cm^3] + + double temperature_ {-1}; //!< Default temperature for material + //!< A negative indicates no default + //!< temperature was specified. bool fissionable_ { false}; //!< Does this material contain fissionable nuclides bool depletable_ {false}; //!< Is the material depletable? @@ -195,10 +199,6 @@ private: // Private data members gsl::index index_; - //! \brief Default temperature for cells containing this material. - //! - //! A negative value indicates no default temperature was specified. - double temperature_ {-1}; }; //============================================================================== diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 2d0261960..b96ab91fd 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -32,7 +32,7 @@ extern "C" bool cmfd_run; //!< is a CMFD run? extern bool delayed_photon_scaling; //!< Scale fission photon yield to include delayed extern "C" bool entropy_on; //!< calculate Shannon entropy? -extern bool event_based; //!< use event-based mode (instead of history-based) +extern "C" bool event_based; //!< use event-based mode (instead of history-based) extern bool legendre_to_tabular; //!< convert Legendre distributions to tabular? extern bool material_cell_offsets; //!< create material cells offsets? extern "C" bool output_summary; //!< write summary.h5? diff --git a/openmc/executor.py b/openmc/executor.py index fa55e1366..ef1bc25dc 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -7,8 +7,8 @@ import openmc 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): + tracks=False, event_based=None, openmc_exec='openmc', + mpi_args=None): """Run an OpenMC simulation. Parameters @@ -30,8 +30,9 @@ def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, :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 + event_based : None or bool, optional + Turns on event-based parallelism if True. If None, the value in + the Settings will be used. openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. mpi_args : list of str, optional @@ -61,8 +62,9 @@ def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, if geometry_debug: args.append('-g') - if event_based: - args.append('-e') + if event_based is not None: + if event_based: + args.append('-e') if isinstance(restart_file, str): args += ['-r', restart_file] @@ -70,6 +72,9 @@ def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, if tracks: args.append('-t') + if plot: + args.append('-p') + if mpi_args is not None: args = mpi_args + args diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index f1b9c291f..c14b0d9c2 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -61,4 +61,6 @@ from .math import * from .plot import * # Flag to denote whether or not openmc.lib.init has been called +# TODO: Establish and use a flag in the C++ code to represent the status of the +# openmc_init and openmc_finalize methods is_initialized = False diff --git a/openmc/lib/material.py b/openmc/lib/material.py index fde197d3d..c1800cb46 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -57,6 +57,9 @@ _dll.openmc_material_get_name.errcheck = _error_handler _dll.openmc_material_set_name.argtypes = [c_int32, c_char_p] _dll.openmc_material_set_name.restype = c_int _dll.openmc_material_set_name.errcheck = _error_handler +_dll.openmc_material_set_temperature.argtypes = [c_int32, c_double] +_dll.openmc_material_set_temperature.restype = c_int +_dll.openmc_material_set_temperature.errcheck = _error_handler _dll.openmc_material_set_volume.argtypes = [c_int32, c_double] _dll.openmc_material_set_volume.restype = c_int _dll.openmc_material_set_volume.errcheck = _error_handler @@ -165,6 +168,10 @@ class Material(_FortranObjectWithID): return None return volume.value + @temperature.setter + def temperature(self, temperature): + _dll.openmc_material_set_temperature(self._index, temperature) + @volume.setter def volume(self, volume): _dll.openmc_material_set_volume(self._index, volume) diff --git a/openmc/lib/settings.py b/openmc/lib/settings.py index 1eab4aa8a..2e9dd18df 100644 --- a/openmc/lib/settings.py +++ b/openmc/lib/settings.py @@ -34,6 +34,7 @@ class _Settings: restart_run = _DLLGlobal(c_bool, 'restart_run') run_CE = _DLLGlobal(c_bool, 'run_CE') verbosity = _DLLGlobal(c_int, 'verbosity') + event_based = _DLLGlobal(c_bool, 'event_based') @property def run_mode(self): diff --git a/openmc/model/model.py b/openmc/model/model.py index 44479d7e7..465ad3064 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -73,7 +73,7 @@ class Model: """ def __init__(self, geometry=None, materials=None, settings=None, - tallies=None, plots=None, intracomm=None): + tallies=None, plots=None): self.geometry = openmc.Geometry() self.materials = openmc.Materials() self.settings = openmc.Settings() @@ -145,8 +145,6 @@ class Model: @property 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 @@ -222,7 +220,7 @@ class Model: return cls(geometry, materials, settings) def init_lib(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=False): + tracks=False, output=True, event_based=None): """Initializes the model in memory via the C API .. versionadded:: 0.13.0 @@ -243,8 +241,9 @@ class Model: Write tracks for all particles. Defaults to False. output : bool Capture OpenMC output from standard out - event_based : bool, optional - Turns on event-based parallelism, instead of default history-based + event_based : None or bool, optional + Turns on event-based parallelism if True. If None, the value in + the Settings will be used. """ # TODO: right now the only way to set most of the above parameters via @@ -456,7 +455,7 @@ class Model: 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): + openmc_exec='openmc', mpi_args=None, event_based=None): """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 @@ -494,8 +493,9 @@ class Model: mpi_args : list of str, optional MPI execute command and any additional MPI arguments to pass, e.g. ['mpiexec', '-n', '8']. - event_based : bool, optional - Turns on event-based parallelism, instead of default history-based + event_based : None or bool, optional + Turns on event-based parallelism if True. If None, the value in + the Settings will be used. Returns ------- @@ -523,21 +523,21 @@ class Model: msg = f"{arg_name} must be set via Model.is_initialized(...)" raise ValueError(msg) + init_particles = openmc.lib.settings.particles if particles is not None: - init_particles = openmc.lib.settings.particles if isinstance(particles, Integral) and particles > 0: openmc.lib.settings.particles = particles - # 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 + init_event_based = openmc.lib.settings.event_based + if event_based is not None: + openmc.lib.settings.event_based = event_based # Then run using the C API openmc.lib.run(output) # Reset changes for the openmc.run kwargs handling - if particles is not None: - openmc.lib.settings.particles = init_particles + openmc.lib.settings.particles = init_particles + openmc.lib.settings.event_based = init_event_based else: # Then run via the command line @@ -697,10 +697,7 @@ class Model: 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 - if obj_type == 'material' and attrib_name == 'temperature': - raise NotImplementedError( - 'Setting a Material temperature is not yet supported!') + # And some items just dont make sense if obj_type == 'cell' and attrib_name == 'density': raise ValueError('Cannot set a Cell density!') @@ -751,10 +748,10 @@ class Model: # 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': + if attrib_name == 'density': lib_obj.set_density(value, density_units) + elif attrib_name == 'temperature' and obj_type == 'cell': + lib_obj.set_temperature(value) else: setattr(lib_obj, attrib_name, value) @@ -800,7 +797,8 @@ class Model: """ - self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'translation') + self._change_py_lib_attribs(names_or_ids, vector, 'cell', + 'translation') 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 @@ -822,8 +820,8 @@ class Model: """ - self._change_py_lib_attribs(names_or_ids, density, 'material', 'density', - density_units) + self._change_py_lib_attribs(names_or_ids, density, 'material', + 'density', density_units) def update_cell_temperatures(self, names_or_ids, temperature): """Update the temperature of a set of cells to the given value @@ -844,7 +842,7 @@ class Model: """ self._change_py_lib_attribs(names_or_ids, temperature, 'cell', - 'temperature') + 'temperature') def update_material_temperatures(self, names_or_ids, temperature): """Update the temperature of a set of materials to the given value @@ -865,7 +863,7 @@ class Model: """ self._change_py_lib_attribs(names_or_ids, temperature, 'material', - 'temperature') + 'temperature') def update_material_volumes(self, names_or_ids, volume): """Update the volume of a set of materials to the given value diff --git a/src/material.cpp b/src/material.cpp index ca29ce740..fb4b2675e 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1480,6 +1480,18 @@ extern "C" int openmc_material_set_name(int32_t index, const char* name) return 0; } +extern "C" int openmc_material_set_temperature(int32_t index, double temperature) +{ + if (index >= 0 && index < model::materials.size()) { + auto& m {model::materials[index]}; + m->temperature_ = temperature; + return 0; + } else { + set_errmsg("Index in materials array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } +} + extern "C" int openmc_material_set_volume(int32_t index, double volume) { if (index >= 0 && index < model::materials.size()) { diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 079b4f9eb..c21f85633 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -207,6 +207,11 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" + assert m.temperature == 293.6 + m.temperature = 400. + assert m.temperature == 400. + m.temperature == 293.6 + def test_properties_density(lib_init): m = openmc.lib.materials[1] @@ -263,6 +268,7 @@ def test_settings(lib_init): assert settings.generations_per_batch == 1 assert settings.particles == 100 assert settings.seed == 1 + assert settings.event_based is False settings.seed = 11 diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 2760eadb4..092ccfc3c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -5,7 +5,6 @@ from pathlib import Path from shutil import which import openmc import openmc.lib -from openmc.mpi import DummyCommunicator @pytest.fixture(scope='function') @@ -128,12 +127,14 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_model._cells_by_id == {} assert test_model._cells_by_name == {} assert test_model.is_initialized is False - assert isinstance(test_model.intracomm, DummyCommunicator) + assert hasattr(test_model.intracomm, 'rank') # 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, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm assert test_model.geometry is geom assert test_model.materials is mats assert test_model.settings is settings @@ -154,17 +155,10 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): '': [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 - if mpi_intracomm is not None: - def_params = [geom, mats, settings, tals, plots, mpi_intracomm] - else: - def_params = [geom, mats, settings, tals, plots] + 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 @@ -223,13 +217,14 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): 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_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, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm test_model.init_lib(output=False) # First check that the API is advertised as initialized @@ -257,6 +252,8 @@ 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() + if mpi_intracomm is not None: + model.intracomm = mpi_intracomm model.init_lib(output=False) # Change fuel temperature and density and export properties @@ -296,7 +293,9 @@ 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, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = 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. @@ -334,7 +333,9 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): 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, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = 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 @@ -367,7 +368,9 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_py_lib_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, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm test_model.init_lib(output=False) @@ -439,19 +442,13 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Now lets do the material temperature updates. # 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 - # 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 - # # Change the temperature - # test_model.update_material_temperatures(['UO2'], 600.) - # assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 - # assert abs(test_model.materials[0].temperature - 600.) < 1e-13 + assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 + # Change the temperature + test_model.update_material_temperatures(['UO2'], 600.) + assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 + assert abs(test_model.materials[0].temperature - 600.) < 1e-13 # And finally material volume - # import pdb; pdb.set_trace() assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 # The temperature on the material will be None because its just the default assert abs(test_model.materials[0].volume - 0.4831931368640985) < 1e-13 @@ -468,7 +465,9 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): pin_model_attributes with open('test_chain.xml', 'w') as f: f.write(chain_file_xml) - test_model = openmc.Model(geom, mats, settings, tals, plots, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm initial_mat = mats[0].clone() initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] @@ -518,7 +517,9 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_calc_volumes(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, mpi_intracomm) + test_model = openmc.Model(geom, mats, settings, tals, plots) + if mpi_intracomm is not None: + test_model.intracomm = mpi_intracomm # With no vol calcs, it should fail with pytest.raises(ValueError): From e1365bd0364862a018fe84397d2bcbe3982f3200 Mon Sep 17 00:00:00 2001 From: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Date: Fri, 1 Oct 2021 06:09:24 -0500 Subject: [PATCH 16/19] Applied suggested changes per @paulromano's code review Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 2 +- openmc/deplete/helpers.py | 2 +- openmc/deplete/operator.py | 2 +- openmc/deplete/results.py | 2 +- openmc/lib/core.py | 2 ++ openmc/model/model.py | 18 ++++++++---------- tests/unit_tests/test_deplete_chain.py | 2 +- tests/unit_tests/test_deplete_integrator.py | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index bf9f4da82..b9198696d 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -22,7 +22,7 @@ from uncertainties import ufloat from openmc.data import DataLibrary from openmc.lib import MaterialFilter, Tally from openmc.checkvalue import check_type, check_greater_than -from openmc import comm +from openmc.mpi import comm from .results import Results from .chain import Chain from .results_list import ResultsList diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index a8cf5212e..f22382923 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -10,7 +10,7 @@ import sys from numpy import dot, zeros, newaxis, asarray -from openmc import comm +from openmc.mpi import comm from openmc.checkvalue import check_type, check_greater_than from openmc.data import JOULE_PER_EV, REACTION_MT from openmc.lib import ( diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index e4db13f95..29370eb8c 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -20,7 +20,7 @@ from uncertainties import ufloat import openmc from openmc.checkvalue import check_value import openmc.lib -from openmc import comm +from openmc.mpi import comm from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 05d78f629..111c8e638 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -9,7 +9,7 @@ import copy import h5py import numpy as np -from openmc import comm, MPI +from openmc.mpi import comm, MPI from .reaction_rates import ReactionRates VERSION_RESULTS = (1, 1) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 3b691721c..92cad3330 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -363,6 +363,7 @@ def plot_geometry(output=True): """Plot geometry .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- @@ -388,6 +389,7 @@ def run(output=True): """Run simulation .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- diff --git a/openmc/model/model.py b/openmc/model/model.py index 465ad3064..ac54763c4 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -109,15 +109,13 @@ class Model: 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._cells_by_name[cell.name] = set() + self._cells_by_name[cell.name].add(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) + self._materials_by_name[mat.name] = set() + self._materials_by_name[mat.name].add(mat) @property def geometry(self): @@ -196,8 +194,7 @@ class Model: def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): """Create model from existing XML files - When initializing this way, the user must manually load plots, tallies, - the chain_file and fission_q attributes. + When initializing this way, the user must manually load plots and tallies. Parameters ---------- @@ -288,6 +285,8 @@ class Model: """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 + The *final_step*, *operator_kwargs*, *directory*, and *output* + arguments were added. Parameters ---------- @@ -319,8 +318,7 @@ class Model: elif isinstance(operator_kwargs, dict): op_kwargs = operator_kwargs else: - msg = "operator_kwargs must be a dict or None" - raise ValueError(msg) + raise ValueError("operator_kwargs must be a dict or None") # Import openmc.deplete here so the Model can be used even if the # shared library is unavailable. diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 0904ff39a..9769a49e3 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -7,7 +7,7 @@ import os from pathlib import Path import numpy as np -from openmc import comm +from openmc.mpi import comm from openmc.deplete import Chain, reaction_rates, nuclide, cram, pool import pytest diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index ec424b05b..a08d8738c 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -14,7 +14,7 @@ import numpy as np from uncertainties import ufloat import pytest -from openmc import comm +from openmc.mpi import comm from openmc.deplete import ( ReactionRates, Results, ResultsList, OperatorResult, PredictorIntegrator, CECMIntegrator, CF4Integrator, CELIIntegrator, EPCRK4Integrator, From e4476fc0b7b8e2f22ce7f6d7cf32c3c76276fb59 Mon Sep 17 00:00:00 2001 From: agnelson Date: Fri, 1 Oct 2021 09:21:41 -0500 Subject: [PATCH 17/19] Delayed mpi4py import until the user clearly indicated intent to use a communicator (or they already have created one and thus have already imported it); corrected versionchanged doc statements to include comments about what changed; added stackoverflow reference --- include/openmc/capi.h | 1 - include/openmc/material.h | 8 ++-- openmc/__init__.py | 1 - openmc/dummy_comm.py | 33 ++++++++++++++ openmc/executor.py | 17 +++---- openmc/lib/core.py | 9 +++- openmc/lib/material.py | 7 --- openmc/model/model.py | 82 ++++++++++++---------------------- openmc/mpi.py | 37 +-------------- src/material.cpp | 12 ----- tests/unit_tests/test_lib.py | 5 --- tests/unit_tests/test_model.py | 65 ++++++++------------------- 12 files changed, 102 insertions(+), 175 deletions(-) create mode 100644 openmc/dummy_comm.py diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 679919dfa..0929a11f7 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -88,7 +88,6 @@ int openmc_material_set_densities( int openmc_material_set_id(int32_t index, int32_t id); int openmc_material_get_name(int32_t index, const char** name); int openmc_material_set_name(int32_t index, const char* name); -int openmc_material_set_temperature(int32_t index, double temperature); int openmc_material_set_volume(int32_t index, double volume); int openmc_material_filter_get_bins( int32_t index, const int32_t** bins, size_t* n); diff --git a/include/openmc/material.h b/include/openmc/material.h index b13cfe330..709d20573 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -158,10 +158,6 @@ public: double density_; //!< Total atom density in [atom/b-cm] double density_gpcc_; //!< Total atom density in [g/cm^3] double volume_ {-1.0}; //!< Volume in [cm^3] - - double temperature_ {-1}; //!< Default temperature for material - //!< A negative indicates no default - //!< temperature was specified. bool fissionable_ { false}; //!< Does this material contain fissionable nuclides bool depletable_ {false}; //!< Is the material depletable? @@ -199,6 +195,10 @@ private: // Private data members gsl::index index_; + //! \brief Default temperature for cells containing this material. + //! + //! A negative value indicates no default temperature was specified. + double temperature_ {-1}; }; //============================================================================== diff --git a/openmc/__init__.py b/openmc/__init__.py index 7366e8383..d118e6a05 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -30,7 +30,6 @@ from openmc.plotter import * from openmc.search import * from openmc.polynomial import * from . import examples -from openmc.mpi import DummyCommunicator, MPI, comm # Import a few names from the model module from openmc.model import rectangular_prism, hexagonal_prism, Model diff --git a/openmc/dummy_comm.py b/openmc/dummy_comm.py new file mode 100644 index 000000000..7ac9be6c3 --- /dev/null +++ b/openmc/dummy_comm.py @@ -0,0 +1,33 @@ +import sys + + +class DummyCommunicator: + rank = 0 + size = 1 + + def allgather(self, sendobj): + return [sendobj] + + def allreduce(self, sendobj, op=None): + return sendobj + + def barrier(self): + pass + + def bcast(self, obj, root=0): + return obj + + def gather(self, sendobj, root=0): + return [sendobj] + + def py2f(self): + return 0 + + def reduce(self, sendobj, op=None, root=0): + return sendobj + + def scatter(self, sendobj, root=0): + return sendobj[0] + + def Abort(self, exit_code_or_msg): + sys.exit(exit_code_or_msg) diff --git a/openmc/executor.py b/openmc/executor.py index ef1bc25dc..c2974722c 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -5,11 +5,12 @@ import subprocess import openmc -def process_CLI_arguments(volume=False, geometry_debug=False, particles=None, - plot=False, restart_file=None, threads=None, - tracks=False, event_based=None, openmc_exec='openmc', - mpi_args=None): - """Run an OpenMC simulation. +def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, + plot=False, restart_file=None, threads=None, + tracks=False, event_based=None, + openmc_exec='openmc', mpi_args=None): + """Converts user-readable flags in to command-line arguments to be run with + the OpenMC executable via subprocess. Parameters ---------- @@ -227,8 +228,8 @@ def calculate_volumes(threads=None, output=True, cwd='.', """ - args = process_CLI_arguments(volume=True, threads=threads, - openmc_exec=openmc_exec, mpi_args=mpi_args) + args = _process_CLI_arguments(volume=True, threads=threads, + openmc_exec=openmc_exec, mpi_args=mpi_args) _run(args, output, cwd) @@ -275,7 +276,7 @@ def run(particles=None, threads=None, geometry_debug=False, """ - args = process_CLI_arguments( + 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) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 92cad3330..136e5a6d8 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -134,6 +134,7 @@ def export_properties(filename=None, output=True): """Export physical properties. .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- @@ -233,6 +234,7 @@ def init(args=None, intracomm=None, output=True): """Initialize OpenMC .. versionchanged:: 0.13.0 + The *output* argument was added. Parameters ---------- @@ -363,7 +365,7 @@ def plot_geometry(output=True): """Plot geometry .. versionchanged:: 0.13.0 - The *output* argument was added. + The *output* argument was added. Parameters ---------- @@ -389,7 +391,7 @@ def run(output=True): """Run simulation .. versionchanged:: 0.13.0 - The *output* argument was added. + The *output* argument was added. Parameters ---------- @@ -529,6 +531,9 @@ def quiet_dll(output=True): """ + # This contextmanager is modified from that provided here: + # https://stackoverflow.com/a/14797594 + if output: yield else: diff --git a/openmc/lib/material.py b/openmc/lib/material.py index c1800cb46..fde197d3d 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -57,9 +57,6 @@ _dll.openmc_material_get_name.errcheck = _error_handler _dll.openmc_material_set_name.argtypes = [c_int32, c_char_p] _dll.openmc_material_set_name.restype = c_int _dll.openmc_material_set_name.errcheck = _error_handler -_dll.openmc_material_set_temperature.argtypes = [c_int32, c_double] -_dll.openmc_material_set_temperature.restype = c_int -_dll.openmc_material_set_temperature.errcheck = _error_handler _dll.openmc_material_set_volume.argtypes = [c_int32, c_double] _dll.openmc_material_set_volume.restype = c_int _dll.openmc_material_set_volume.errcheck = _error_handler @@ -168,10 +165,6 @@ class Material(_FortranObjectWithID): return None return volume.value - @temperature.setter - def temperature(self, temperature): - _dll.openmc_material_set_temperature(self._index, temperature) - @volume.setter def volume(self, volume): _dll.openmc_material_set_volume(self._index, volume) diff --git a/openmc/model/model.py b/openmc/model/model.py index ac54763c4..904c60746 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -11,6 +11,8 @@ from contextlib import contextmanager import h5py import openmc +from openmc.dummy_comm import DummyCommunicator +from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value from openmc.exceptions import InvalidIDError @@ -39,6 +41,8 @@ class Model: materials appearing in the geometry. .. versionchanged:: 0.13.0 + The model information can now be loaded in to OpenMC directly via + openmc.lib Parameters ---------- @@ -65,10 +69,6 @@ class Model: Tallies information plots : openmc.Plots Plot information - intracomm : mpi4py.MPI.Intracomm or openmc.DummyCommunicator - MPI intracommunicator; this defaults to the mpi4py world communicator - from if present, or a DummyCommunicator otherwise. If an alternative - communicator is desired, this parameter should be modified accordingly. """ @@ -91,8 +91,6 @@ class Model: if plots is not None: self.plots = plots - self.intracomm = openmc.comm - # Store dictionaries to the materials and cells by ID and names if materials is None: mats = self.geometry.get_all_materials().values() @@ -137,10 +135,6 @@ class Model: def plots(self): return self._plots - @property - def intracomm(self): - return self._intracomm - @property def is_initialized(self): return openmc.lib.is_initialized @@ -185,16 +179,12 @@ class Model: for plot in plots: self._plots.append(plot) - @intracomm.setter - def intracomm(self, intracomm): - check_type('intracomm', intracomm, type(openmc.comm)) - self._intracomm = intracomm - @classmethod def from_xml(cls, geometry='geometry.xml', materials='materials.xml', settings='settings.xml'): """Create model from existing XML files - When initializing this way, the user must manually load plots and tallies. + When initializing this way, the user must manually load plots and + tallies. Parameters ---------- @@ -217,7 +207,7 @@ class Model: return cls(geometry, materials, settings) def init_lib(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=None): + tracks=False, output=True, event_based=None, intracomm=None): """Initializes the model in memory via the C API .. versionadded:: 0.13.0 @@ -241,6 +231,8 @@ class Model: event_based : None or bool, optional Turns on event-based parallelism if True. If None, the value in the Settings will be used. + intracomm : DummyCommnicator, mpi4py.MPI.Intracomm or None, optional + MPI intracommunicator """ # TODO: right now the only way to set most of the above parameters via @@ -249,7 +241,7 @@ class Model: # where it exists (here in init), but in the future the functionality # should be exposed so that it can be accessed via model.run(...) - args = openmc.process_CLI_arguments( + args = _process_CLI_arguments( volume=False, geometry_debug=geometry_debug, restart_file=restart_file, threads=threads, tracks=tracks, event_based=event_based) @@ -258,17 +250,18 @@ class Model: self.finalize_lib() - if self.intracomm.rank == 0: - self.export_to_xml() - self.intracomm.barrier() - - if isinstance(self.intracomm, openmc.DummyCommunicator): - # openmc.lib.init does not accept DummyCommunicator, and importing - # the DummyCommunicator class there is overkill. Filter it here - intracomm = None + # The Model object needs to be aware of the communicator so it can + # use it in certain cases, therefore lets store the communicator + if intracomm is not None: + self._intracomm = intracomm else: - intracomm = self.intracomm - openmc.lib.init(args=args, intracomm=intracomm, output=output) + self._intracomm = DummyCommunicator() + + if self._intracomm.rank == 0: + self.export_to_xml() + self._intracomm.barrier() + + openmc.lib.init(args=args, intracomm=self._intracomm, output=output) def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -285,8 +278,8 @@ class Model: """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 - The *final_step*, *operator_kwargs*, *directory*, and *output* - arguments were added. + The *final_step*, *operator_kwargs*, *directory*, and *output* + arguments were added. Parameters ---------- @@ -691,10 +684,14 @@ class Model: 'translation')) # 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 or material temperature + # so lets raise exceptions as needed if obj_type == 'cell' and attrib_name == 'volume': raise NotImplementedError( 'Setting a Cell volume is not supported!') + if obj_type == 'material' and attrib_name == 'temperature': + raise NotImplementedError( + 'Setting a material temperature is not supported!') # And some items just dont make sense if obj_type == 'cell' and attrib_name == 'density': @@ -748,7 +745,7 @@ class Model: lib_obj = obj_by_id[id_] if attrib_name == 'density': lib_obj.set_density(value, density_units) - elif attrib_name == 'temperature' and obj_type == 'cell': + elif attrib_name == 'temperature': lib_obj.set_temperature(value) else: setattr(lib_obj, attrib_name, value) @@ -842,27 +839,6 @@ class Model: self._change_py_lib_attribs(names_or_ids, temperature, 'cell', 'temperature') - 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 - The material names (if str) or id (if int) that are to be updated. - This parameter can include a mix of names and ids. - temperature : float - The temperature to apply in units of Kelvin - - """ - - self._change_py_lib_attribs(names_or_ids, temperature, 'material', - 'temperature') - def update_material_volumes(self, names_or_ids, volume): """Update the volume of a set of materials to the given value diff --git a/openmc/mpi.py b/openmc/mpi.py index b64a84bc6..cfc10c0d1 100644 --- a/openmc/mpi.py +++ b/openmc/mpi.py @@ -1,41 +1,8 @@ -import sys -from unittest.mock import Mock - - -class DummyCommunicator: - rank = 0 - size = 1 - - def allgather(self, sendobj): - return [sendobj] - - def allreduce(self, sendobj, op=None): - return sendobj - - def barrier(self): - pass - - def bcast(self, obj, root=0): - return obj - - def gather(self, sendobj, root=0): - return [sendobj] - - def py2f(self): - return 0 - - def reduce(self, sendobj, op=None, root=0): - return sendobj - - def scatter(self, sendobj, root=0): - return sendobj[0] - - def Abort(self, exit_code_or_msg): - sys.exit(exit_code_or_msg) - try: from mpi4py import MPI comm = MPI.COMM_WORLD except ImportError: + from unittest.mock import Mock MPI = Mock() + from openmc.dummy_comm import DummyCommunicator comm = DummyCommunicator() diff --git a/src/material.cpp b/src/material.cpp index fb4b2675e..ca29ce740 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1480,18 +1480,6 @@ extern "C" int openmc_material_set_name(int32_t index, const char* name) return 0; } -extern "C" int openmc_material_set_temperature(int32_t index, double temperature) -{ - if (index >= 0 && index < model::materials.size()) { - auto& m {model::materials[index]}; - m->temperature_ = temperature; - return 0; - } else { - set_errmsg("Index in materials array is out of bounds."); - return OPENMC_E_OUT_OF_BOUNDS; - } -} - extern "C" int openmc_material_set_volume(int32_t index, double volume) { if (index >= 0 && index < model::materials.size()) { diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index c21f85633..ccff33ac3 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -207,11 +207,6 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" - assert m.temperature == 293.6 - m.temperature = 400. - assert m.temperature == 400. - m.temperature == 293.6 - def test_properties_density(lib_init): m = openmc.lib.materials[1] diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 092ccfc3c..cffa5c249 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -127,14 +127,11 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_model._cells_by_id == {} assert test_model._cells_by_name == {} assert test_model.is_initialized is False - assert hasattr(test_model.intracomm, 'rank') # 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) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm assert test_model.geometry is geom assert test_model.materials is mats assert test_model.settings is settings @@ -142,7 +139,7 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert test_model.plots is plots 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]]} + '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], @@ -151,9 +148,9 @@ def test_init(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # 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[3], geom.root_universe.cells[4]], - 'inf fuel': [geom.root_universe.cells[2].fill.cells[1]]} + '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 # Finally test the parameter type checking by passing bad types and @@ -202,8 +199,8 @@ def test_from_xml(run_in_tmpdir, pin_model_attributes): {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], @@ -212,10 +209,10 @@ 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[3], - test_model.geometry.root_universe.cells[4]], - 'inf fuel': [test_model.geometry.root_universe.cells[2].fill.cells[1]]} + '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 @@ -223,9 +220,7 @@ 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) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) # First check that the API is advertised as initialized assert openmc.lib.is_initialized is True @@ -252,9 +247,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() - if mpi_intracomm is not None: - model.intracomm = mpi_intracomm - model.init_lib(output=False) + model.init_lib(output=False, intracomm=mpi_intracomm) # Change fuel temperature and density and export properties cell = openmc.lib.cells[1] @@ -294,8 +287,6 @@ 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) - if mpi_intracomm is not None: - test_model.intracomm = 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. @@ -305,7 +296,7 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): cli_flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] cli_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) sp_path = test_model.run(output=False) with openmc.StatePoint(sp_path) as sp: lib_keff = sp.k_combined @@ -334,8 +325,6 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): 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: - test_model.intracomm = 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 @@ -352,7 +341,7 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # 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_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) test_model.plot_geometry(output=False, convert=convert) # Now look for the files, expect to find test.ppm, plot_2.ppm, and if @@ -369,14 +358,11 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): def test_py_lib_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) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) # Now we can call rotate_cells, translate_cells, update_densities, - # update_cell_temperatures, and update_material_temperatures and make sure - # the changes have taken hold. + # and update_cell_temperatures and make sure the changes have taken hold. # For each we will first try bad inputs to make sure we get the right # errors and then we do a good one which calls the material by name and # then id to make sure it worked @@ -440,14 +426,6 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert abs(test_model.geometry.root_universe.cells[3].temperature - 600.) < 1e-13 - # Now lets do the material temperature updates. - # Check initial conditions - assert abs(openmc.lib.materials[1].temperature - 293.6) < 1e-13 - # Change the temperature - test_model.update_material_temperatures(['UO2'], 600.) - assert abs(openmc.lib.materials[1].temperature - 600.) < 1e-13 - assert abs(test_model.materials[0].temperature - 600.) < 1e-13 - # And finally material volume assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 # The temperature on the material will be None because its just the default @@ -466,8 +444,6 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): with open('test_chain.xml', 'w') as f: f.write(chain_file_xml) test_model = openmc.Model(geom, mats, settings, tals, plots) - if mpi_intracomm is not None: - test_model.intracomm = mpi_intracomm initial_mat = mats[0].clone() initial_u = initial_mat.get_nuclide_atom_densities()['U235'][1] @@ -497,7 +473,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): mats[0].set_density('atom/b-cm', tot_density) # Now we can re-run with the pre-initialized API - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) test_model.deplete([1e6], 'predictor', final_step=False, operator_kwargs=op_kwargs, power=1., output=False) @@ -518,8 +494,6 @@ def test_calc_volumes(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: - test_model.intracomm = mpi_intracomm # With no vol calcs, it should fail with pytest.raises(ValueError): @@ -555,13 +529,10 @@ def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): for file in ['volume_1.h5', 'volume_2.h5']: file = Path(file) file.unlink() - test_model.init_lib(output=False) + test_model.init_lib(output=False, intracomm=mpi_intracomm) test_model.calculate_volumes(output=False, apply_volumes=True) assert mats[2].volume > 0. assert geom.root_universe.cells[3].volume > 0. assert openmc.lib.materials[3].volume == mats[2].volume test_model.finalize_lib() - - - From 2cadd1d3ed2f64800ddb38eaf000d3d5f423f094 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 4 Oct 2021 15:39:00 -0500 Subject: [PATCH 18/19] Remove comma in UChicago Argonne, LLC Co-authored-by: Patrick Shriwise --- LICENSE | 2 +- docs/source/conf.py | 2 +- docs/source/license.rst | 2 +- man/man1/openmc.1 | 2 +- src/output.cpp | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/LICENSE b/LICENSE index 79f0c96f0..3523dd370 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +Copyright (c) 2011-2021 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/docs/source/conf.py b/docs/source/conf.py index 33fb515a5..a10494260 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,7 @@ master_doc = 'index' # General information about the project. project = 'OpenMC' -copyright = '2011-2021, Massachusetts Institute of Technology, UChicago Argonne, LLC, and OpenMC contributors' +copyright = '2011-2021, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/source/license.rst b/docs/source/license.rst index 5a3d40763..d8ea319aa 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2021 Massachusetts Institute of Technology, UChicago Argonne, +Copyright © 2011-2021 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index 989e846e0..f99389069 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -58,7 +58,7 @@ section libraries if the user has not specified the tag in .I materials.xml\fP. .SH LICENSE Copyright \(co 2011-2021 Massachusetts Institute of Technology, UChicago -Argonne, LLC, and OpenMC contributors. +Argonne LLC, and OpenMC contributors. .PP Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/output.cpp b/src/output.cpp index b78314823..7bc012296 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -73,7 +73,7 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2021 MIT, UChicago Argonne, LLC and contributors\n" + " Copyright | 2011-2021 MIT, UChicago Argonne LLC, and contributors\n" " License | https://docs.openmc.org/en/latest/license.html\n" " Version | {}.{}.{}{}\n", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : ""); @@ -335,7 +335,7 @@ void print_version() #ifdef GIT_SHA1 fmt::print("Git SHA1: {}\n", GIT_SHA1); #endif - fmt::print("Copyright (c) 2011-2021 MIT, UChicago Argonne, LLC, and " + fmt::print("Copyright (c) 2011-2021 MIT, UChicago Argonne LLC, and " "contributors\nMIT/X license at " "\n"); } From c9e91d42fbc0a8aca2e7cd70f464ee940c848a08 Mon Sep 17 00:00:00 2001 From: agnelson Date: Tue, 5 Oct 2021 08:39:24 -0500 Subject: [PATCH 19/19] Replacing manual absolute error tests with pytest.approx, improving documentation, and other small changes per the review by @paulroman. --- openmc/executor.py | 2 +- openmc/lib/core.py | 20 +++++++++---- openmc/model/model.py | 12 ++++---- tests/unit_tests/test_model.py | 53 +++++++++++++++++++--------------- 4 files changed, 53 insertions(+), 34 deletions(-) diff --git a/openmc/executor.py b/openmc/executor.py index c2974722c..2d4911ff2 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -52,7 +52,7 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, args = [openmc_exec] if volume: - args += ['--volume'] + args.append('--volume') if isinstance(particles, Integral) and particles > 0: args += ['-n', str(particles)] diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 136e5a6d8..e277d527a 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -112,7 +112,17 @@ def global_bounding_box(): def calculate_volumes(output=True): - """Run stochastic volume calculation""" + """Run stochastic volume calculation + + .. versionchanged:: 0.13.0 + The *output* argument was added. + + Parameters + ---------- + output : bool, optional + Whether or not to show output. Defaults to showing output + + """ with quiet_dll(output): _dll.openmc_calculate_volumes() @@ -140,7 +150,7 @@ def export_properties(filename=None, output=True): ---------- filename : str or None Filename to export properties to (defaults to "properties.h5") - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output See Also @@ -242,7 +252,7 @@ def init(args=None, intracomm=None, output=True): Command-line arguments intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output """ @@ -369,7 +379,7 @@ def plot_geometry(output=True): Parameters ---------- - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output """ @@ -395,7 +405,7 @@ def run(output=True): Parameters ---------- - output: bool, optional + output : bool, optional Whether or not to show output. Defaults to showing output """ diff --git a/openmc/model/model.py b/openmc/model/model.py index 904c60746..ad898fd0a 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -20,7 +20,7 @@ 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() + start_dir = Path.cwd() Path.mkdir(working_dir, exist_ok=True) os.chdir(working_dir) try: @@ -231,7 +231,7 @@ class Model: event_based : None or bool, optional Turns on event-based parallelism if True. If None, the value in the Settings will be used. - intracomm : DummyCommnicator, mpi4py.MPI.Intracomm or None, optional + intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator """ @@ -261,7 +261,10 @@ class Model: self.export_to_xml() self._intracomm.barrier() - openmc.lib.init(args=args, intracomm=self._intracomm, output=output) + # We cannot pass DummyCommunicator to openmc.lib.init so pass instead + # the user-provided intracomm which will either be None or an mpi4py + # communicator + openmc.lib.init(args=args, intracomm=intracomm, output=output) def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -610,8 +613,7 @@ class Model: if apply_volumes: # Load the results and add them to the model for i, vol_calc in enumerate(self.settings.volume_calculations): - f_name = f"volume_{i + 1}.h5" - vol_calc.load_results(f_name) + vol_calc.load_results(f"volume_{i + 1}.h5") # First add them to the Python side self.geometry.add_volume_information(vol_calc) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index cffa5c249..b1021a269 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,8 +1,10 @@ from math import pi -import numpy as np -import pytest from pathlib import Path from shutil import which + +import numpy as np +import pytest + import openmc import openmc.lib @@ -304,9 +306,9 @@ def test_run(run_in_tmpdir, pin_model_attributes, mpi_intracomm): lib_fiss = sp.get_tally(id=1).get_values(scores=['fission'])[0, 0, 0] # and lets compare results - assert abs(lib_keff - cli_keff) < 1e-13 - assert abs(lib_flux - cli_flux) < 1e-13 - assert abs(lib_fiss - cli_fiss) < 1e-13 + assert lib_keff.n == pytest.approx(cli_keff.n, abs=1e-13) + assert lib_flux == pytest.approx(cli_flux, abs=1e-13) + assert lib_fiss == pytest.approx(cli_fiss, abs=1e-13) # Now we should make sure that the flags for items which should be handled # by init are properly set @@ -348,7 +350,7 @@ def test_plots(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # convert is True, test.png, plot_2.png for fname in ['test.', 'plot_2.']: for ext in exts: - test_file = Path('./{}{}'.format(fname, ext)) + test_file = Path(f'./{fname}{ext}') assert test_file.exists() test_file.unlink() @@ -398,19 +400,20 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Now lets do the density updates. # Check initial conditions - assert abs(openmc.lib.materials[1].get_density( - 'atom/b-cm') - 0.06891296988603757) < 1e-13 + assert openmc.lib.materials[1].get_density('atom/b-cm') == \ + pytest.approx(0.06891296988603757, abs=1e-13) mat_a_dens = np.sum( [v[1] for v in test_model.materials[0]. get_nuclide_atom_densities().values()]) - assert abs(mat_a_dens - 0.06891296988603757) < 1e-8 + assert mat_a_dens == pytest.approx(0.06891296988603757, abs=1e-8) # Change the density test_model.update_densities(['UO2'], 2.) - assert abs(openmc.lib.materials[1].get_density('atom/b-cm') - 2.) < 1e-13 + assert openmc.lib.materials[1].get_density('atom/b-cm') == \ + pytest.approx(2., abs=1e-13) mat_a_dens = np.sum( [v[1] for v in test_model.materials[0]. get_nuclide_atom_densities().values()]) - assert abs(mat_a_dens - 2.) < 1e-8 + assert mat_a_dens == pytest.approx(2., abs=1e-8) # Now lets do the cell temperature updates. # Check initial conditions @@ -418,22 +421,26 @@ def test_py_lib_attributes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): {2: geom.root_universe.cells[2], 3: geom.root_universe.cells[3], 4: geom.root_universe.cells[4], 1: geom.root_universe.cells[2].fill.cells[1]} - assert abs(openmc.lib.cells[3].get_temperature() - 293.6) < 1e-13 + assert openmc.lib.cells[3].get_temperature() == \ + pytest.approx(293.6, abs=1e-13) assert test_model.geometry.root_universe.cells[3].temperature is None # Change the temperature test_model.update_cell_temperatures([3], 600.) - assert abs(openmc.lib.cells[3].get_temperature() - 600.) < 1e-13 - assert abs(test_model.geometry.root_universe.cells[3].temperature - - 600.) < 1e-13 + assert openmc.lib.cells[3].get_temperature() == \ + pytest.approx(600., abs=1e-13) + assert test_model.geometry.root_universe.cells[3].temperature == \ + pytest.approx(600., abs=1e-13) # And finally material volume - assert abs(openmc.lib.materials[1].volume - 0.4831931368640985) < 1e-13 + assert openmc.lib.materials[1].volume == \ + pytest.approx(0.4831931368640985, abs=1e-13) # The temperature on the material will be None because its just the default - assert abs(test_model.materials[0].volume - 0.4831931368640985) < 1e-13 + assert test_model.materials[0].volume == \ + pytest.approx(0.4831931368640985, abs=1e-13) # Change the temperature test_model.update_material_volumes(['UO2'], 2.) - assert abs(openmc.lib.materials[1].volume - 2.) < 1e-13 - assert abs(test_model.materials[0].volume - 2.) < 1e-13 + assert openmc.lib.materials[1].volume == pytest.approx(2., abs=1e-13) + assert test_model.materials[0].volume == pytest.approx(2., abs=1e-13) test_model.finalize_lib() @@ -460,7 +467,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Get the new Xe136 and U235 atom densities after_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_u = mats[0].get_nuclide_atom_densities()['U235'][1] - assert abs((after_xe + after_u) - initial_u) < 1e-15 + assert after_xe + after_u == pytest.approx(initial_u, abs=1e-15) assert test_model.is_initialized is False # Reset the initial material densities @@ -480,12 +487,12 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Get the new Xe136 and U235 atom densities after_lib_xe = mats[0].get_nuclide_atom_densities()['Xe136'][1] after_lib_u = mats[0].get_nuclide_atom_densities()['U235'][1] - assert abs((after_lib_xe + after_lib_u) - initial_u) < 1e-15 + assert after_lib_xe + after_lib_u == pytest.approx(initial_u, abs=1e-15) assert test_model.is_initialized is True # And end by comparing to the previous case - assert abs(after_xe - after_lib_xe) < 1e-15 - assert abs(after_u - after_lib_u) < 1e-15 + assert after_xe == pytest.approx(after_lib_xe, abs=1e-15) + assert after_u == pytest.approx(after_lib_u, abs=1e-15) test_model.finalize_lib()