From f8764416d2c727c5b1693c297c3f7994c8314d1b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 9 Feb 2018 13:29:04 -0600 Subject: [PATCH 01/68] Copy OpenDeplete files from commit 2d804c227a --- chains/chain_simple.xml | 49 + chains/chain_test.xml | 23 + docs/source/pythonapi/deplete/index.rst | 56 ++ .../pythonapi/deplete/integrator.CRAM16.rst | 6 + .../pythonapi/deplete/integrator.CRAM48.rst | 6 + .../pythonapi/deplete/integrator.cecm.rst | 6 + .../deplete/integrator.predictor.rst | 6 + .../deplete/integrator.save_results.rst | 6 + .../deplete/opendeplete.Concentrations.rst | 30 + .../deplete/opendeplete.ReactionRates.rst | 30 + .../pythonapi/deplete/opendeplete.Results.rst | 22 + openmc/deplete/__init__.py | 24 + openmc/deplete/atom_number.py | 236 +++++ openmc/deplete/depletion_chain.py | 472 ++++++++++ openmc/deplete/dummy_comm.py | 27 + openmc/deplete/function.py | 114 +++ openmc/deplete/integrator/__init__.py | 11 + openmc/deplete/integrator/cecm.py | 133 +++ openmc/deplete/integrator/cram.py | 185 ++++ openmc/deplete/integrator/predictor.py | 100 ++ openmc/deplete/integrator/save_results.py | 46 + openmc/deplete/nuclide.py | 178 ++++ openmc/deplete/openmc_wrapper.py | 853 ++++++++++++++++++ openmc/deplete/reaction_rates.py | 113 +++ openmc/deplete/results.py | 454 ++++++++++ openmc/deplete/utilities.py | 98 ++ scripts/example_geometry.py | 358 ++++++++ scripts/example_plot.py | 46 + scripts/example_run.py | 39 + scripts/make_chain.py | 60 ++ tests/deplete_tests/__init__.py | 0 tests/deplete_tests/dummy_geometry.py | 165 ++++ tests/deplete_tests/example_geometry.py | 1 + tests/deplete_tests/test_atom_number.py | 180 ++++ tests/deplete_tests/test_cecm_regression.py | 69 ++ tests/deplete_tests/test_cram.py | 48 + tests/deplete_tests/test_depletion_chain.py | 197 ++++ tests/deplete_tests/test_full.py | 119 +++ tests/deplete_tests/test_integrator.py | 116 +++ tests/deplete_tests/test_nuclide.py | 121 +++ .../test_predictor_regression.py | 68 ++ tests/deplete_tests/test_reaction_rates.py | 86 ++ tests/deplete_tests/test_reference.h5 | Bin 0 -> 165384 bytes tests/deplete_tests/test_utilities.py | 68 ++ 44 files changed, 5025 insertions(+) create mode 100644 chains/chain_simple.xml create mode 100644 chains/chain_test.xml create mode 100644 docs/source/pythonapi/deplete/index.rst create mode 100644 docs/source/pythonapi/deplete/integrator.CRAM16.rst create mode 100644 docs/source/pythonapi/deplete/integrator.CRAM48.rst create mode 100644 docs/source/pythonapi/deplete/integrator.cecm.rst create mode 100644 docs/source/pythonapi/deplete/integrator.predictor.rst create mode 100644 docs/source/pythonapi/deplete/integrator.save_results.rst create mode 100644 docs/source/pythonapi/deplete/opendeplete.Concentrations.rst create mode 100644 docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst create mode 100644 docs/source/pythonapi/deplete/opendeplete.Results.rst create mode 100644 openmc/deplete/__init__.py create mode 100644 openmc/deplete/atom_number.py create mode 100644 openmc/deplete/depletion_chain.py create mode 100644 openmc/deplete/dummy_comm.py create mode 100644 openmc/deplete/function.py create mode 100644 openmc/deplete/integrator/__init__.py create mode 100644 openmc/deplete/integrator/cecm.py create mode 100644 openmc/deplete/integrator/cram.py create mode 100644 openmc/deplete/integrator/predictor.py create mode 100644 openmc/deplete/integrator/save_results.py create mode 100644 openmc/deplete/nuclide.py create mode 100644 openmc/deplete/openmc_wrapper.py create mode 100644 openmc/deplete/reaction_rates.py create mode 100644 openmc/deplete/results.py create mode 100644 openmc/deplete/utilities.py create mode 100644 scripts/example_geometry.py create mode 100644 scripts/example_plot.py create mode 100644 scripts/example_run.py create mode 100644 scripts/make_chain.py create mode 100644 tests/deplete_tests/__init__.py create mode 100644 tests/deplete_tests/dummy_geometry.py create mode 120000 tests/deplete_tests/example_geometry.py create mode 100644 tests/deplete_tests/test_atom_number.py create mode 100644 tests/deplete_tests/test_cecm_regression.py create mode 100644 tests/deplete_tests/test_cram.py create mode 100644 tests/deplete_tests/test_depletion_chain.py create mode 100644 tests/deplete_tests/test_full.py create mode 100644 tests/deplete_tests/test_integrator.py create mode 100644 tests/deplete_tests/test_nuclide.py create mode 100644 tests/deplete_tests/test_predictor_regression.py create mode 100644 tests/deplete_tests/test_reaction_rates.py create mode 100644 tests/deplete_tests/test_reference.h5 create mode 100644 tests/deplete_tests/test_utilities.py diff --git a/chains/chain_simple.xml b/chains/chain_simple.xml new file mode 100644 index 0000000000..345da2237d --- /dev/null +++ b/chains/chain_simple.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + 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 + + + + diff --git a/chains/chain_test.xml b/chains/chain_test.xml new file mode 100644 index 0000000000..5985704063 --- /dev/null +++ b/chains/chain_test.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + 0.0253 + + A B + 0.0292737 0.002566345 + + + + diff --git a/docs/source/pythonapi/deplete/index.rst b/docs/source/pythonapi/deplete/index.rst new file mode 100644 index 0000000000..55380c7a1c --- /dev/null +++ b/docs/source/pythonapi/deplete/index.rst @@ -0,0 +1,56 @@ +.. _api: + +================= +API Documentation +================= + +Integrators +----------- + +.. toctree:: + :maxdepth: 2 + + integrator.predictor + integrator.cecm + +Integrator Helper Functions +--------------------------- +.. toctree:: + :maxdepth: 2 + + integrator.CRAM16 + integrator.CRAM48 + integrator.save_results + +Metaclasses +----------- + +.. autosummary:: + :toctree: generated + :nosignatures: + + opendeplete.Settings + opendeplete.Operator + +OpenMC Classes +-------------- + +.. autosummary:: + :toctree: generated + :nosignatures: + + opendeplete.OpenMCSettings + opendeplete.Materials + opendeplete.OpenMCOperator + +Data Classes +------------ +.. autosummary:: + :toctree: generated + :nosignatures: + + opendeplete.AtomNumber + opendeplete.DepletionChain + opendeplete.Nuclide + opendeplete.ReactionRates + opendeplete.Results diff --git a/docs/source/pythonapi/deplete/integrator.CRAM16.rst b/docs/source/pythonapi/deplete/integrator.CRAM16.rst new file mode 100644 index 0000000000..f9eba273ed --- /dev/null +++ b/docs/source/pythonapi/deplete/integrator.CRAM16.rst @@ -0,0 +1,6 @@ +integrator\.CRAM16 +================== + +.. currentmodule:: opendeplete.integrator + +.. autofunction:: CRAM16 diff --git a/docs/source/pythonapi/deplete/integrator.CRAM48.rst b/docs/source/pythonapi/deplete/integrator.CRAM48.rst new file mode 100644 index 0000000000..d7467a418a --- /dev/null +++ b/docs/source/pythonapi/deplete/integrator.CRAM48.rst @@ -0,0 +1,6 @@ +integrator\.CRAM48 +================== + +.. currentmodule:: opendeplete.integrator + +.. autofunction:: CRAM48 diff --git a/docs/source/pythonapi/deplete/integrator.cecm.rst b/docs/source/pythonapi/deplete/integrator.cecm.rst new file mode 100644 index 0000000000..507a638f69 --- /dev/null +++ b/docs/source/pythonapi/deplete/integrator.cecm.rst @@ -0,0 +1,6 @@ +integrator\.cecm +================= + +.. currentmodule:: opendeplete.integrator + +.. autofunction:: cecm diff --git a/docs/source/pythonapi/deplete/integrator.predictor.rst b/docs/source/pythonapi/deplete/integrator.predictor.rst new file mode 100644 index 0000000000..d6c0fd827c --- /dev/null +++ b/docs/source/pythonapi/deplete/integrator.predictor.rst @@ -0,0 +1,6 @@ +integrator\.predictor +===================== + +.. currentmodule:: opendeplete.integrator + +.. autofunction:: predictor diff --git a/docs/source/pythonapi/deplete/integrator.save_results.rst b/docs/source/pythonapi/deplete/integrator.save_results.rst new file mode 100644 index 0000000000..5c21dcb664 --- /dev/null +++ b/docs/source/pythonapi/deplete/integrator.save_results.rst @@ -0,0 +1,6 @@ +integrator\.save_results +======================== + +.. currentmodule:: opendeplete.integrator + +.. autofunction:: save_results diff --git a/docs/source/pythonapi/deplete/opendeplete.Concentrations.rst b/docs/source/pythonapi/deplete/opendeplete.Concentrations.rst new file mode 100644 index 0000000000..6fa07a970b --- /dev/null +++ b/docs/source/pythonapi/deplete/opendeplete.Concentrations.rst @@ -0,0 +1,30 @@ +opendeplete.Concentrations +========================== + +.. currentmodule:: opendeplete + +.. autoclass:: Concentrations + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~Concentrations.__init__ + ~Concentrations.convert_nested_dict + + + + + + .. rubric:: Attributes + + .. autosummary:: + + ~Concentrations.n_cell + ~Concentrations.n_nuc + + \ No newline at end of file diff --git a/docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst b/docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst new file mode 100644 index 0000000000..99e048b565 --- /dev/null +++ b/docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst @@ -0,0 +1,30 @@ +opendeplete.ReactionRates +========================= + +.. currentmodule:: opendeplete + +.. autoclass:: ReactionRates + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~ReactionRates.__init__ + + + + + + .. rubric:: Attributes + + .. autosummary:: + + ~ReactionRates.n_cell + ~ReactionRates.n_nuc + ~ReactionRates.n_react + + \ No newline at end of file diff --git a/docs/source/pythonapi/deplete/opendeplete.Results.rst b/docs/source/pythonapi/deplete/opendeplete.Results.rst new file mode 100644 index 0000000000..0ab8a1f711 --- /dev/null +++ b/docs/source/pythonapi/deplete/opendeplete.Results.rst @@ -0,0 +1,22 @@ +opendeplete.Results +=================== + +.. currentmodule:: opendeplete + +.. autoclass:: Results + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~Results.__init__ + + + + + + \ No newline at end of file diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py new file mode 100644 index 0000000000..994a51e12c --- /dev/null +++ b/openmc/deplete/__init__.py @@ -0,0 +1,24 @@ +""" +OpenDeplete +=========== + +A simple depletion front-end tool. +""" + +from .dummy_comm import DummyCommunicator +try: + from mpi4py import MPI + comm = MPI.COMM_WORLD + have_mpi = True +except ImportError: + comm = DummyCommunicator() + have_mpi = False + +from .nuclide import * +from .depletion_chain import * +from .openmc_wrapper import * +from .reaction_rates import * +from .function import * +from .results import * +from .integrator import * +from .utilities import * diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py new file mode 100644 index 0000000000..03bedbf531 --- /dev/null +++ b/openmc/deplete/atom_number.py @@ -0,0 +1,236 @@ +"""AtomNumber module. + +An ndarray to store atom densities with string, integer, or slice indexing. +""" + +import numpy as np + + +class AtomNumber(object): + """ AtomNumber module. + + An ndarray to store atom densities with string, integer, or slice indexing. + + Parameters + ---------- + mat_to_ind : OrderedDict of str to int + A dictionary mapping material ID as string to index. + nuc_to_ind : OrderedDict of str to int + A dictionary mapping nuclide name as string to index. + volume : OrderedDict of int to float + Volume of geometry. + n_mat_burn : int + Number of materials to be burned. + n_nuc_burn : int + Number of nuclides to be burned. + + Attributes + ---------- + mat_to_ind : OrderedDict of str to int + A dictionary mapping cell ID as string to index. + nuc_to_ind : OrderedDict of str to int + A dictionary mapping nuclide name as string to index. + volume : numpy.array + Volume of geometry indexed by mat_to_ind. If a volume is not found, + it defaults to 1 so that reading density still works correctly. + n_mat_burn : int + Number of materials to be burned. + n_nuc_burn : int + Number of nuclides to be burned. + n_mat : int + Number of materials. + n_nuc : int + Number of nucs. + number : numpy.array + Array storing total atoms indexed by the above dictionaries. + burn_nuc_list : list of str + A list of all nuclide material names. Used for sorting the simulation. + burn_mat_list : list of str + A list of all burning material names. Used for sorting the simulation. + """ + + def __init__(self, mat_to_ind, nuc_to_ind, volume, n_mat_burn, n_nuc_burn): + + self.mat_to_ind = mat_to_ind + self.nuc_to_ind = nuc_to_ind + + self.volume = np.ones(self.n_mat) + + for mat in volume: + if str(mat) in self.mat_to_ind: + ind = self.mat_to_ind[str(mat)] + self.volume[ind] = volume[mat] + + self.n_mat_burn = n_mat_burn + self.n_nuc_burn = n_nuc_burn + + self.number = np.zeros((self.n_mat, self.n_nuc)) + + # For performance, create storage for burn_nuc_list, burn_mat_list + self._burn_nuc_list = None + self._burn_mat_list = None + + def __getitem__(self, pos): + """ Retrieves total atom number from AtomNumber. + + Parameters + ---------- + pos : tuple + A two-length tuple containing a material index and a nuc index. + These indexes can be strings (which get converted to integers via + the dictionaries), integers used directly, or slices. + + Returns + ------- + numpy.array + The value indexed from self.number. + """ + + mat, nuc = pos + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + + return self.number[mat, nuc] + + def __setitem__(self, pos, val): + """ Sets total atom number into AtomNumber. + + Parameters + ---------- + pos : tuple + A two-length tuple containing a material index and a nuc index. + These indexes can be strings (which get converted to integers via + the dictionaries), integers used directly, or slices. + val : float + The value to set the array to. + """ + + mat, nuc = pos + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + + self.number[mat, nuc] = val + + def get_atom_density(self, mat, nuc): + """ Accesses atom density instead of total number. + + Parameters + ---------- + mat : str, int or slice + Material index. + nuc : str, int or slice + Nuclide index. + + Returns + ------- + numpy.array + The density indexed. + """ + + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + + return self[mat, nuc] / self.volume[mat] + + def set_atom_density(self, mat, nuc, val): + """ Sets atom density instead of total number. + + Parameters + ---------- + mat : str, int or slice + Material index. + nuc : str, int or slice + Nuclide index. + val : numpy.array + Array of values to set. + """ + + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + + self[mat, nuc] = val * self.volume[mat] + + def get_mat_slice(self, mat): + """ Gets atom quantity indexed by mats for all burned nuclides + + Parameters + ---------- + mat : str, int or slice + Material index. + + Returns + ------- + numpy.array + The slice requested. + """ + + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + + return self[mat, 0:self.n_nuc_burn] + + def set_mat_slice(self, mat, val): + """ Sets atom quantity indexed by mats for all burned nuclides + + Parameters + ---------- + mat : str, int or slice + Material index. + val : numpy.array + The slice to set. + """ + + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + + self[mat, 0:self.n_nuc_burn] = val + + @property + def n_mat(self): + """Number of materials.""" + return len(self.mat_to_ind) + + @property + def n_nuc(self): + """Number of nuclides.""" + return len(self.nuc_to_ind) + + @property + def burn_nuc_list(self): + """ burn_nuc_list : list of str + A list of all nuclide material names. Used for sorting the simulation. + """ + + if self._burn_nuc_list is None: + self._burn_nuc_list = [None] * self.n_nuc_burn + + for nuc in self.nuc_to_ind: + ind = self.nuc_to_ind[nuc] + if ind < self.n_nuc_burn: + self._burn_nuc_list[ind] = nuc + + return self._burn_nuc_list + + @property + def burn_mat_list(self): + """ burn_mat_list : list of str + A list of all burning material names. Used for sorting the simulation. + """ + + if self._burn_mat_list is None: + self._burn_mat_list = [None] * self.n_mat_burn + + for mat in self.mat_to_ind: + ind = self.mat_to_ind[mat] + if ind < self.n_mat_burn: + self._burn_mat_list[ind] = mat + + return self._burn_mat_list diff --git a/openmc/deplete/depletion_chain.py b/openmc/deplete/depletion_chain.py new file mode 100644 index 0000000000..05cc9db435 --- /dev/null +++ b/openmc/deplete/depletion_chain.py @@ -0,0 +1,472 @@ +"""depletion_chain module. + +This module contains information about a depletion chain. A depletion chain is +loaded from an .xml file and all the nuclides are linked together. +""" + +from collections import OrderedDict, defaultdict +from io import StringIO +from itertools import chain +import math +import re +import os + +from tqdm import tqdm +import scipy.sparse as sp +import openmc.data +# Try to use lxml if it is available. It preserves the order of attributes and +# provides a pretty-printer by default. If not available, use OpenMC function to +# pretty print. +try: + import lxml.etree as ET + _have_lxml = True +except ImportError: + import xml.etree.ElementTree as ET + from openmc.clean_xml import clean_xml_indentation + _have_lxml = False + +from .nuclide import Nuclide, DecayTuple, ReactionTuple + + +# tuple of (reaction name, possible MT values, (dA, dZ)) where dA is the change +# in the mass number and dZ is the change in the atomic number +_REACTIONS = [ + ('(n,2n)', set(chain([16], range(875, 892))), (-1, 0)), + ('(n,3n)', {17}, (-2, 0)), + ('(n,4n)', {37}, (-3, 0)), + ('(n,gamma)', {102}, (1, 0)), + ('(n,p)', set(chain([103], range(600, 650))), (0, -1)), + ('(n,a)', set(chain([107], range(800, 850))), (-3, -2)) +] + + +def _get_zai(s): + """Get ZAI value (10000*z + 10*A + metastable state) for sorting purposes""" + symbol, A, state = re.match(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)', s).groups() + Z = openmc.data.ATOMIC_NUMBER[symbol] + A = int(A) + state = int(state[2:]) if state else 0 + return 10000*Z + 10*A + state + + +def replace_missing(product, decay_data): + """Replace missing product with suitable decay daughter. + + Parameters + ---------- + product : str + Name of product in GND format, e.g. 'Y86_m1'. + decay_data : dict + Dictionary of decay data + + Returns + ------- + product : str + Replacement for missing product in GND format. + + """ + + symbol, A, state = re.match(r'([A-Zn][a-z]*)(\d+)((?:_m\d+)?)', + product).groups() + Z = openmc.data.ATOMIC_NUMBER[symbol] + A = int(A) + + # First check if ground state is available + if state: + metastable_state = int(state[2:]) + product = '{}{}'.format(symbol, A) + + # Find isotope with longest half-life + half_life = 0.0 + for nuclide, data in decay_data.items(): + m = re.match(r'{}(\d+)(?:_m\d+)?'.format(symbol), nuclide) + if m: + # If we find a stable nuclide, stop search + if data.nuclide['stable']: + mass_longest_lived = int(m.group(1)) + break + if data.half_life.nominal_value > half_life: + mass_longest_lived = int(m.group(1)) + half_life = data.half_life.nominal_value + + # If mass number of longest-lived isotope is less than that of missing + # product, assume it undergoes beta-. Otherwise assume beta+. + beta_minus = (mass_longest_lived < A) + + # Iterate until we find an existing nuclide + while product not in decay_data: + if Z > 98: + Z -= 2 + A -= 4 + else: + if beta_minus: + Z += 1 + else: + Z -= 1 + product = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) + + return product + + +class DepletionChain(object): + """ The DepletionChain class. + + This class contains a full representation of a depletion chain. + + Attributes + ---------- + n_nuclides : int + Number of nuclides in chain. + nuclides : list of Nuclide + List of nuclides in chain. + nuclide_dict : OrderedDict of str to int + Maps a nuclide name to an index in nuclides. + nuc_to_react_ind : OrderedDict of str to int + Dictionary mapping a nuclide name to an index in ReactionRates. + react_to_ind : OrderedDict of str to int + Dictionary mapping a reaction name to an index in ReactionRates. + + """ + + def __init__(self): + self.nuclides = [] + self.nuclide_dict = OrderedDict() + self.nuc_to_react_ind = OrderedDict() + self.react_to_ind = OrderedDict() + + @property + def n_nuclides(self): + """Number of nuclides in chain.""" + return len(self.nuclides) + + @classmethod + def from_endf(cls, decay_files, fpy_files, neutron_files): + """Create a depletion chain from ENDF files. + + Parameters + ---------- + decay_files : list of str + List of ENDF decay sub-library files + fpy_files : list of str + List of ENDF neutron-induced fission product yield sub-library files + neutron_files : list of str + List of ENDF neutron reaction sub-library files + + """ + depl_chain = cls() + + # Create dictionary mapping target to filename + reactions = {} + with tqdm(neutron_files) as pbar: + for f in pbar: + pbar.set_description('Processing {}'.format(os.path.basename(f))) + evaluation = openmc.data.endf.Evaluation(f) + name = evaluation.gnd_name + reactions[name] = {} + for mf, mt, nc, mod in evaluation.reaction_list: + if mf == 3: + file_obj = StringIO(evaluation.section[3, mt]) + openmc.data.endf.get_head_record(file_obj) + q_value = openmc.data.endf.get_cont_record(file_obj)[1] + reactions[name][mt] = q_value + + # Determine what decay and FPY nuclides are available + decay_data = {} + with tqdm(decay_files) as pbar: + for f in pbar: + pbar.set_description('Processing {}'.format(os.path.basename(f))) + data = openmc.data.Decay(f) + decay_data[data.nuclide['name']] = data + + fpy_data = {} + with tqdm(fpy_files) as pbar: + for f in pbar: + pbar.set_description('Processing {}'.format(os.path.basename(f))) + data = openmc.data.FissionProductYields(f) + fpy_data[data.nuclide['name']] = data + + print('Creating depletion_chain...') + missing_daughter = [] + missing_rx_product = [] + missing_fpy = [] + missing_fp = [] + + reaction_index = 0 + for idx, parent in enumerate(sorted(decay_data, key=_get_zai)): + data = decay_data[parent] + + nuclide = Nuclide() + nuclide.name = parent + + depl_chain.nuclides.append(nuclide) + depl_chain.nuclide_dict[parent] = idx + + if not data.nuclide['stable'] and data.half_life.nominal_value != 0.0: + nuclide.half_life = data.half_life.nominal_value + nuclide.decay_energy = sum(E.nominal_value for E in + data.average_energies.values()) + sum_br = 0.0 + for i, mode in enumerate(data.modes): + type_ = ','.join(mode.modes) + if mode.daughter in decay_data: + target = mode.daughter + else: + print('missing {} {} {}'.format(parent, ','.join(mode.modes), mode.daughter)) + target = replace_missing(mode.daughter, decay_data) + + # Write branching ratio, taking care to ensure sum is unity + br = mode.branching_ratio.nominal_value + sum_br += br + if i == len(data.modes) - 1 and sum_br != 1.0: + br = 1.0 - sum(m.branching_ratio.nominal_value + for m in data.modes[:-1]) + + # Append decay mode + nuclide.decay_modes.append(DecayTuple(type_, target, br)) + + if parent in reactions: + reactions_available = set(reactions[parent].keys()) + for name, mts, changes in _REACTIONS: + if mts & reactions_available: + delta_A, delta_Z = changes + A = data.nuclide['mass_number'] + delta_A + Z = data.nuclide['atomic_number'] + delta_Z + daughter = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) + + if name not in depl_chain.react_to_ind: + depl_chain.react_to_ind[name] = reaction_index + reaction_index += 1 + + if daughter not in decay_data: + missing_rx_product.append((parent, name, daughter)) + + # Store Q value + for mt in sorted(mts): + if mt in reactions[parent]: + q_value = reactions[parent][mt] + break + else: + q_value = 0.0 + + nuclide.reactions.append(ReactionTuple( + name, daughter, q_value, 1.0)) + + if any(mt in reactions_available for mt in [18, 19, 20, 21, 38]): + if parent in fpy_data: + q_value = reactions[parent][18] + nuclide.reactions.append( + ReactionTuple('fission', 0, q_value, 1.0)) + + if 'fission' not in depl_chain.react_to_ind: + depl_chain.react_to_ind['fission'] = reaction_index + reaction_index += 1 + else: + missing_fpy.append(parent) + + if parent in fpy_data: + fpy = fpy_data[parent] + + if fpy.energies is not None: + nuclide.yield_energies = fpy.energies + else: + nuclide.yield_energies = [0.0] + + for E, table in zip(nuclide.yield_energies, fpy.independent): + yield_replace = 0.0 + yields = defaultdict(float) + for product, y in table.items(): + # Handle fission products that have no decay data available + if product not in decay_data: + daughter = replace_missing(product, decay_data) + product = daughter + yield_replace += y.nominal_value + + yields[product] += y.nominal_value + + if yield_replace > 0.0: + missing_fp.append((parent, E, yield_replace)) + + nuclide.yield_data[E] = [] + for k in sorted(yields, key=_get_zai): + nuclide.yield_data[E].append((k, yields[k])) + + # Display warnings + if missing_daughter: + print('The following decay modes have daughters with no decay data:') + for mode in missing_daughter: + print(' {}'.format(mode)) + print('') + + if missing_rx_product: + print('The following reaction products have no decay data:') + for vals in missing_rx_product: + print('{} {} -> {}'.format(*vals)) + print('') + + if missing_fpy: + print('The following fissionable nuclides have no fission product yields:') + for parent in missing_fpy: + print(' ' + parent) + print('') + + if missing_fp: + print('The following nuclides have fission products with no decay data:') + for vals in missing_fp: + print(' {}, E={} eV (total yield={})'.format(*vals)) + + return depl_chain + + @classmethod + def xml_read(cls, filename): + """Reads a depletion chain XML file. + + Parameters + ---------- + filename : str + The path to the depletion chain XML file. + + Todo + ---- + Allow for branching on capture, etc. + """ + depl_chain = cls() + + # Load XML tree + try: + root = ET.parse(filename) + except: + if filename is None: + print("No chain specified, either manually or in environment variable OPENDEPLETE_CHAIN.") + else: + print('Decay chain "', filename, '" is invalid.') + raise + + reaction_index = 0 + for i, nuclide_elem in enumerate(root.findall('nuclide_table')): + nuc = Nuclide.xml_read(nuclide_elem) + depl_chain.nuclide_dict[nuc.name] = i + + # Check for reaction paths + for rx in nuc.reactions: + if rx.type not in depl_chain.react_to_ind: + depl_chain.react_to_ind[rx.type] = reaction_index + reaction_index += 1 + + depl_chain.nuclides.append(nuc) + + return depl_chain + + def xml_write(self, filename): + """Writes a depletion chain XML file. + + Parameters + ---------- + filename : str + The path to the depletion chain XML file. + + """ + + root_elem = ET.Element('depletion') + for nuclide in self.nuclides: + root_elem.append(nuclide.xml_write()) + + tree = ET.ElementTree(root_elem) + if _have_lxml: + tree.write(filename, encoding='utf-8', pretty_print=True) + else: + clean_xml_indentation(root_elem, spaces_per_level=2) + tree.write(filename, encoding='utf-8') + + def form_matrix(self, rates): + """ Forms depletion matrix. + + Parameters + ---------- + rates : numpy.ndarray + 2D array indexed by nuclide then by cell. + + Returns + ------- + scipy.sparse.csr_matrix + Sparse matrix representing depletion. + """ + + matrix = defaultdict(float) + reactions = set() + + for i, nuc in enumerate(self.nuclides): + + if nuc.n_decay_modes != 0: + # Decay paths + # Loss + decay_constant = math.log(2) / nuc.half_life + + if decay_constant != 0.0: + matrix[i, i] -= decay_constant + + # Gain + for _, target, branching_ratio in nuc.decay_modes: + # Allow for total annihilation for debug purposes + if target != 'Nothing': + branch_val = branching_ratio * decay_constant + + if branch_val != 0.0: + k = self.nuclide_dict[target] + matrix[k, i] += branch_val + + if nuc.name in self.nuc_to_react_ind: + # Extract all reactions for this nuclide in this cell + nuc_ind = self.nuc_to_react_ind[nuc.name] + nuc_rates = rates[nuc_ind, :] + + for r_type, target, _, br in nuc.reactions: + # Extract reaction index, and then final reaction rate + r_id = self.react_to_ind[r_type] + path_rate = nuc_rates[r_id] + + # Loss term -- make sure we only count loss once for + # reactions with branching ratios + if r_type not in reactions: + reactions.add(r_type) + if path_rate != 0.0: + matrix[i, i] -= path_rate + + # Gain term; allow for total annihilation for debug purposes + if target != 'Nothing': + if r_type != 'fission': + if path_rate != 0.0: + k = self.nuclide_dict[target] + matrix[k, i] += path_rate * br + else: + # Assume that we should always use thermal fission + # yields. At some point it would be nice to account + # for the energy-dependence.. + energy, data = sorted(nuc.yield_data.items())[0] + for product, y in data: + yield_val = y * path_rate + if yield_val != 0.0: + k = self.nuclide_dict[product] + matrix[k, i] += yield_val + + # Clear set of reactions + reactions.clear() + + # Use DOK matrix as intermediate representation, then convert to CSR and return + matrix_dok = sp.dok_matrix((self.n_nuclides, self.n_nuclides)) + dict.update(matrix_dok, matrix) + return matrix_dok.tocsr() + + def nuc_by_ind(self, ind): + """ Extracts nuclides from the list by dictionary key. + + Parameters + ---------- + ind : str + Name of nuclide. + + Returns + ------- + Nuclide + Nuclide object that corresponds to ind. + """ + return self.nuclides[self.nuclide_dict[ind]] diff --git a/openmc/deplete/dummy_comm.py b/openmc/deplete/dummy_comm.py new file mode 100644 index 0000000000..b3fa272648 --- /dev/null +++ b/openmc/deplete/dummy_comm.py @@ -0,0 +1,27 @@ +class DummyCommunicator(object): + 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] diff --git a/openmc/deplete/function.py b/openmc/deplete/function.py new file mode 100644 index 0000000000..74eb92422b --- /dev/null +++ b/openmc/deplete/function.py @@ -0,0 +1,114 @@ +"""function module. + +This module contains the Operator class, which is then passed to an integrator +to run a full depletion simulation. +""" + +from abc import ABCMeta, abstractmethod + +class Settings(object): + """ The Settings class. + + Contains all parameters necessary for the integrator. + + Attributes + ---------- + dt_vec : numpy.array + Array of time steps to take. + output_dir : str + Path to output directory to save results. + """ + + def __init__(self): + # Integrator specific + self.dt_vec = None + self.output_dir = None + +class Operator(metaclass=ABCMeta): + """ The Operator metaclass. + + This defines all functions that the integrator needs to operate. + + Attributes + ---------- + settings : Settings + Settings object. + """ + + def __init__(self, settings): + self.settings = settings + + @abstractmethod + def initial_condition(self): + """ Performs final setup and returns initial condition. + + Returns + ------- + list of numpy.array + Total density for initial conditions. + """ + + pass + + @abstractmethod + def eval(self, vec, print_out=True): + """ Runs a simulation. + + Parameters + ---------- + vec : list of numpy.array + Total atoms to be used in function. + print_out : bool, optional + Whether or not to print out time. + + Returns + ------- + k : float + Eigenvalue of the problem. + rates : ReactionRates + Reaction rates from this simulation. + seed : int + Seed for this simulation. + """ + + pass + + @abstractmethod + def get_results_info(self): + """ Returns volume list, cell lists, and nuc lists. + + Returns + ------- + volume : list of float + Volumes corresponding to materials in burn_list + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all cell IDs to be burned. Used for sorting the simulation. + full_burn_list : list of int + All burnable materials in the geometry. + """ + + pass + + @abstractmethod + def form_matrix(self, y, mat): + """ Forms the f(y) matrix in y' = f(y)y. + + Nominally a depletion matrix, this is abstracted on the off chance + that the function f has nothing to do with depletion at all. + + Parameters + ---------- + y : numpy.ndarray + An array representing y. + mat : int + Material id. + + Returns + ------- + scipy.sparse.csr_matrix + Sparse matrix representing f(y). + """ + + pass diff --git a/openmc/deplete/integrator/__init__.py b/openmc/deplete/integrator/__init__.py new file mode 100644 index 0000000000..607650dc69 --- /dev/null +++ b/openmc/deplete/integrator/__init__.py @@ -0,0 +1,11 @@ +""" +Integrator +=========== + +The integrator subcomponents. +""" + +from .cecm import * +from .cram import * +from .predictor import * +from .save_results import * diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py new file mode 100644 index 0000000000..4d9baebb2e --- /dev/null +++ b/openmc/deplete/integrator/cecm.py @@ -0,0 +1,133 @@ +""" The CE/CM integrator.""" + +import copy +from itertools import repeat +import os +from multiprocessing import Pool +import time + +from .. import comm +from .cram import CRAM48, cram_wrapper +from .save_results import save_results + + +def cecm(operator, print_out=True): + """The CE/CM integrator. + + Implements the second order CE/CM Predictor-Corrector algorithm [ref]_. + This algorithm is mathematically defined as: + + .. math:: + y' &= A(y, t) y(t) + + A_p &= A(y_n, t_n) + + y_m &= \\text{expm}(A_p h/2) y_n + + A_c &= A(y_m, t_n + h/2) + + y_{n+1} &= \\text{expm}(A_c h) y_n + + .. [ref] + Isotalo, Aarno. "Comparison of Neutronics-Depletion Coupling Schemes + for Burnup Calculations—Continued Study." Nuclear Science and + Engineering 180.3 (2015): 286-300. + + Parameters + ---------- + operator : Operator + The operator object to simulate on. + print_out : bool, optional + Whether or not to print out time. + """ + + # Save current directory + dir_home = os.getcwd() + + # Move to folder + os.makedirs(operator.settings.output_dir, exist_ok=True) + os.chdir(operator.settings.output_dir) + + # Generate initial conditions + vec = operator.initial_condition() + + n_mats = len(vec) + + t = 0.0 + + for i, dt in enumerate(operator.settings.dt_vec): + # Create vectors + x = [copy.deepcopy(vec)] + seeds = [] + eigvls = [] + rates_array = [] + + eigvl, rates, seed = operator.eval(x[0]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + t_start = time.time() + + chains = repeat(operator.chain, n_mats) + vecs = (x[0][i] for i in range(n_mats)) + rates = (rates_array[0][i, :, :] for i in range(n_mats)) + dts = repeat(dt/2, n_mats) + + with Pool() as pool: + iters = zip(chains, vecs, rates, dts) + x_result = list(pool.starmap(cram_wrapper, iters)) + + t_end = time.time() + if comm.rank == 0: + if print_out: + print("Time to matexp: ", t_end - t_start) + + x.append(x_result) + + eigvl, rates, seed = operator.eval(x[1]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + t_start = time.time() + + chains = repeat(operator.chain, n_mats) + vecs = (x[0][i] for i in range(n_mats)) + rates = (rates_array[1][i, :, :] for i in range(n_mats)) + dts = repeat(dt, n_mats) + + with Pool() as pool: + iters = zip(chains, vecs, rates, dts) + x_result = list(pool.starmap(cram_wrapper, iters)) + + t_end = time.time() + if comm.rank == 0: + if print_out: + print("Time to matexp: ", t_end - t_start) + + # Create results, write to disk + save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) + + t += dt + vec = copy.deepcopy(x_result) + + # Perform one last simulation + x = [copy.deepcopy(vec)] + seeds = [] + eigvls = [] + rates_array = [] + eigvl, rates, seed = operator.eval(x[0]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + # Create results, write to disk + save_results(operator, x, rates_array, eigvls, seeds, [t, t], + len(operator.settings.dt_vec)) + + # Return to origin + os.chdir(dir_home) diff --git a/openmc/deplete/integrator/cram.py b/openmc/deplete/integrator/cram.py new file mode 100644 index 0000000000..a18d8450c3 --- /dev/null +++ b/openmc/deplete/integrator/cram.py @@ -0,0 +1,185 @@ +""" Chebyshev Rational Approximation Method module + +Implements two different forms of CRAM for use in opendeplete. +""" + +import numpy as np +import scipy.sparse as sp +import scipy.sparse.linalg as sla + + +def cram_wrapper(chain, n0, rates, dt): + """Wraps depletion matrix creation / CRAM solve for multiprocess execution + + Parameters + ---------- + chain : DepletionChain + Depletion chain used to construct the burnup matrix + n0 : numpy.array + Vector to operate a matrix exponent on. + rates : numpy.ndarray + 2D array indexed by nuclide then by cell. + dt : float + Time to integrate to. + + Returns + ------- + numpy.array + Results of the matrix exponent. + """ + A = chain.form_matrix(rates) + return CRAM48(A, n0, dt) + + +def CRAM16(A, n0, dt): + """ Chebyshev Rational Approximation Method, order 16 + + Algorithm is the 16th order Chebyshev Rational Approximation Method, + implemented in the more stable incomplete partial fraction (IPF) form + [cram16]_. + + .. [cram16] + Pusa, Maria. "Higher-Order Chebyshev Rational Approximation Method and + Application to Burnup Equations." Nuclear Science and Engineering 182.3 + (2016). + + Parameters + ---------- + A : scipy.linalg.csr_matrix + Matrix to take exponent of. + n0 : numpy.array + Vector to operate a matrix exponent on. + dt : float + Time to integrate to. + + Returns + ------- + numpy.array + Results of the matrix exponent. + """ + + alpha = np.array([+2.124853710495224e-16, + +5.464930576870210e+3 - 3.797983575308356e+4j, + +9.045112476907548e+1 - 1.115537522430261e+3j, + +2.344818070467641e+2 - 4.228020157070496e+2j, + +9.453304067358312e+1 - 2.951294291446048e+2j, + +7.283792954673409e+2 - 1.205646080220011e+5j, + +3.648229059594851e+1 - 1.155509621409682e+2j, + +2.547321630156819e+1 - 2.639500283021502e+1j, + +2.394538338734709e+1 - 5.650522971778156e+0j], + dtype=np.complex128) + theta = np.array([+0.0, + +3.509103608414918 + 8.436198985884374j, + +5.948152268951177 + 3.587457362018322j, + -5.264971343442647 + 16.22022147316793j, + +1.419375897185666 + 10.92536348449672j, + +6.416177699099435 + 1.194122393370139j, + +4.993174737717997 + 5.996881713603942j, + -1.413928462488886 + 13.49772569889275j, + -10.84391707869699 + 19.27744616718165j], + dtype=np.complex128) + + n = A.shape[0] + + alpha0 = 2.124853710495224e-16 + + k = 8 + + y = np.array(n0, dtype=np.float64) + for l in range(1, k+1): + y = 2.0*np.real(alpha[l]*sla.spsolve(A*dt - theta[l]*sp.eye(n), y)) + y + + y *= alpha0 + return y + + +def CRAM48(A, n0, dt): + """ Chebyshev Rational Approximation Method, order 48 + + Algorithm is the 48th order Chebyshev Rational Approximation Method, + implemented in the more stable incomplete partial fraction (IPF) form + [cram48]_. + + .. [cram48] + Pusa, Maria. "Higher-Order Chebyshev Rational Approximation Method and + Application to Burnup Equations." Nuclear Science and Engineering 182.3 + (2016). + + Parameters + ---------- + A : scipy.linalg.csr_matrix + Matrix to take exponent of. + n0 : numpy.array + Vector to operate a matrix exponent on. + dt : float + Time to integrate to. + + Returns + ------- + numpy.array + Results of the matrix exponent. + """ + + theta_r = np.array([-4.465731934165702e+1, -5.284616241568964e+0, + -8.867715667624458e+0, +3.493013124279215e+0, + +1.564102508858634e+1, +1.742097597385893e+1, + -2.834466755180654e+1, +1.661569367939544e+1, + +8.011836167974721e+0, -2.056267541998229e+0, + +1.449208170441839e+1, +1.853807176907916e+1, + +9.932562704505182e+0, -2.244223871767187e+1, + +8.590014121680897e-1, -1.286192925744479e+1, + +1.164596909542055e+1, +1.806076684783089e+1, + +5.870672154659249e+0, -3.542938819659747e+1, + +1.901323489060250e+1, +1.885508331552577e+1, + -1.734689708174982e+1, +1.316284237125190e+1]) + theta_i = np.array([+6.233225190695437e+1, +4.057499381311059e+1, + +4.325515754166724e+1, +3.281615453173585e+1, + +1.558061616372237e+1, +1.076629305714420e+1, + +5.492841024648724e+1, +1.316994930024688e+1, + +2.780232111309410e+1, +3.794824788914354e+1, + +1.799988210051809e+1, +5.974332563100539e+0, + +2.532823409972962e+1, +5.179633600312162e+1, + +3.536456194294350e+1, +4.600304902833652e+1, + +2.287153304140217e+1, +8.368200580099821e+0, + +3.029700159040121e+1, +5.834381701800013e+1, + +1.194282058271408e+0, +3.583428564427879e+0, + +4.883941101108207e+1, +2.042951874827759e+1]) + theta = np.array(theta_r + theta_i * 1j, dtype=np.complex128) + + alpha_r = np.array([+6.387380733878774e+2, +1.909896179065730e+2, + +4.236195226571914e+2, +4.645770595258726e+2, + +7.765163276752433e+2, +1.907115136768522e+3, + +2.909892685603256e+3, +1.944772206620450e+2, + +1.382799786972332e+5, +5.628442079602433e+3, + +2.151681283794220e+2, +1.324720240514420e+3, + +1.617548476343347e+4, +1.112729040439685e+2, + +1.074624783191125e+2, +8.835727765158191e+1, + +9.354078136054179e+1, +9.418142823531573e+1, + +1.040012390717851e+2, +6.861882624343235e+1, + +8.766654491283722e+1, +1.056007619389650e+2, + +7.738987569039419e+1, +1.041366366475571e+2]) + alpha_i = np.array([-6.743912502859256e+2, -3.973203432721332e+2, + -2.041233768918671e+3, -1.652917287299683e+3, + -1.783617639907328e+4, -5.887068595142284e+4, + -9.953255345514560e+3, -1.427131226068449e+3, + -3.256885197214938e+6, -2.924284515884309e+4, + -1.121774011188224e+3, -6.370088443140973e+4, + -1.008798413156542e+6, -8.837109731680418e+1, + -1.457246116408180e+2, -6.388286188419360e+1, + -2.195424319460237e+2, -6.719055740098035e+2, + -1.693747595553868e+2, -1.177598523430493e+1, + -4.596464999363902e+3, -1.738294585524067e+3, + -4.311715386228984e+1, -2.777743732451969e+2]) + alpha = np.array(alpha_r + alpha_i * 1j, dtype=np.complex128) + n = A.shape[0] + + alpha0 = 2.258038182743983e-47 + + k = 24 + + y = np.array(n0, dtype=np.float64) + for l in range(k): + y = 2.0*np.real(alpha[l]*sla.spsolve(A*dt - theta[l]*sp.eye(n), y)) + y + + y *= alpha0 + return y diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py new file mode 100644 index 0000000000..6c9d538fd6 --- /dev/null +++ b/openmc/deplete/integrator/predictor.py @@ -0,0 +1,100 @@ +""" The Predictor algorithm.""" + +import copy +from itertools import repeat +import os +from multiprocessing import Pool +import time + +from .. import comm +from .cram import CRAM48, cram_wrapper +from .save_results import save_results + + +def predictor(operator, print_out=True): + """The basic predictor integrator. + + Implements the first order predictor algorithm. This algorithm is + mathematically defined as: + + .. math:: + y' &= A(y, t) y(t) + + A_p &= A(y_n, t_n) + + y_{n+1} &= \\text{expm}(A_p h) y_n + + Parameters + ---------- + operator : Operator + The operator object to simulate on. + print_out : bool, optional + Whether or not to print out time. + """ + + # Save current directory + dir_home = os.getcwd() + + # Move to folder + os.makedirs(operator.settings.output_dir, exist_ok=True) + os.chdir(operator.settings.output_dir) + + # Generate initial conditions + vec = operator.initial_condition() + + n_mats = len(vec) + + t = 0.0 + + for i, dt in enumerate(operator.settings.dt_vec): + # Create vectors + x = [copy.deepcopy(vec)] + seeds = [] + eigvls = [] + rates_array = [] + + eigvl, rates, seed = operator.eval(x[0]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + # Create results, write to disk + save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) + + t_start = time.time() + + chains = repeat(operator.chain, n_mats) + vecs = (x[0][i] for i in range(n_mats)) + rates = (rates_array[0][i, :, :] for i in range(n_mats)) + dts = repeat(dt, n_mats) + + with Pool() as pool: + iters = zip(chains, vecs, rates, dts) + x_result = list(pool.starmap(cram_wrapper, iters)) + + t_end = time.time() + if comm.rank == 0: + if print_out: + print("Time to matexp: ", t_end - t_start) + + t += dt + vec = copy.deepcopy(x_result) + + # Perform one last simulation + x = [copy.deepcopy(vec)] + seeds = [] + eigvls = [] + rates_array = [] + eigvl, rates, seed = operator.eval(x[0]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + # Create results, write to disk + save_results(operator, x, rates_array, eigvls, seeds, [t, t], + len(operator.settings.dt_vec)) + + # Return to origin + os.chdir(dir_home) diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py new file mode 100644 index 0000000000..35cbc7f3f1 --- /dev/null +++ b/openmc/deplete/integrator/save_results.py @@ -0,0 +1,46 @@ +""" Generic result saving code for integrators. + +""" +from opendeplete.results import Results, write_results + +def save_results(op, x, rates, eigvls, seeds, t, step_ind): + """ Creates and writes results to disk + + Parameters + ---------- + op : Function + The operator used to generate these results. + x : list of list of numpy.array + The prior x vectors. Indexed [i][cell] using the above equation. + rates : list of ReactionRates + The reaction rates for each substep. + eigvls : list of float + Eigenvalue for each substep + seeds : list of int + Seeds for each substep. + t : list of float + Time indices. + step_ind : int + Step index. + """ + + # Get indexing terms + vol_list, nuc_list, burn_list, full_burn_list = op.get_results_info() + + # Create results + stages = len(x) + results = Results() + results.allocate(vol_list, nuc_list, burn_list, full_burn_list, stages) + + n_mat = len(burn_list) + + for i in range(stages): + for mat_i in range(n_mat): + results[i, mat_i, :] = x[i][mat_i][:] + + results.k = eigvls + results.seeds = seeds + results.time = t + results.rates = rates + + write_results(results, "results.h5", step_ind) diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py new file mode 100644 index 0000000000..1208a9b3c3 --- /dev/null +++ b/openmc/deplete/nuclide.py @@ -0,0 +1,178 @@ +"""Nuclide module. + +Contains the per-nuclide components of a depletion chain. +""" + +from collections import namedtuple +try: + import lxml.etree as ET +except ImportError: + import xml.etree.ElementTree as ET + +DecayTuple = namedtuple('DecayTuple', 'type target branching_ratio') +ReactionTuple = namedtuple('ReactionTuple', 'type target Q branching_ratio') + + +class Nuclide(object): + """The Nuclide class. + + Contains everything in a depletion chain relating to a single nuclide. + + Attributes + ---------- + name : str + Name of nuclide. + half_life : float + Half life of nuclide in s^-1. + decay_energy : float + Energy deposited from decay in eV. + n_decay_modes : int + Number of decay pathways. + decay_modes : list of DecayTuple + Decay mode information. Each element of the list is a named tuple with + attributes 'type', 'target', and 'branching_ratio'. + n_reaction_paths : int + Number of possible reaction pathways. + reactions : list of ReactionTuple + Reaction information. Each element of the list is a named tuple with + attribute 'type', 'target', 'Q', and 'branching_ratio'. + yield_data : dict of float to list + Maps tabulated energy to list of (product, yield) for all + neutron-induced fission products. + yield_energies : list of float + Energies at which fission product yiels exist + + """ + + def __init__(self): + # Information about the nuclide + self.name = None + self.half_life = None + self.decay_energy = 0.0 + + # Decay paths + self.decay_modes = [] + + # Reaction paths + self.reactions = [] + + # Neutron fission yields, if present + self.yield_data = {} + self.yield_energies = [] + + @property + def n_decay_modes(self): + """Number of decay modes.""" + return len(self.decay_modes) + + @property + def n_reaction_paths(self): + """Number of possible reaction pathways.""" + return len(self.reactions) + + @classmethod + def xml_read(cls, element): + """Read nuclide from an XML element. + + Parameters + ---------- + element : xml.etree.ElementTree.Element + XML element to write nuclide data to + + Returns + ------- + nuc : Nuclide + Instance of a nuclide + + """ + nuc = cls() + nuc.name = element.get('name') + + # Check for half-life + if 'half_life' in element.attrib: + nuc.half_life = float(element.get('half_life')) + nuc.decay_energy = float(element.get('decay_energy', '0')) + + # Check for decay paths + for decay_elem in element.iter('decay_type'): + d_type = decay_elem.get('type') + target = decay_elem.get('target') + branching_ratio = float(decay_elem.get('branching_ratio')) + nuc.decay_modes.append(DecayTuple(d_type, target, branching_ratio)) + + # Check for reaction paths + for reaction_elem in element.iter('reaction_type'): + r_type = reaction_elem.get('type') + Q = float(reaction_elem.get('Q', '0')) + branching_ratio = float(reaction_elem.get('branching_ratio', '1')) + + # If the type is not fission, get target and Q value, otherwise + # just set null values + if r_type != 'fission': + target = reaction_elem.get('target') + else: + target = None + + # Append reaction + nuc.reactions.append(ReactionTuple( + r_type, target, Q, branching_ratio)) + + fpy_elem = element.find('neutron_fission_yields') + if fpy_elem is not None: + for yields_elem in fpy_elem.iter('fission_yields'): + E = float(yields_elem.get('energy')) + products = yields_elem.find('products').text.split() + yields = [float(y) for y in + yields_elem.find('data').text.split()] + nuc.yield_data[E] = list(zip(products, yields)) + nuc.yield_energies = list(sorted(nuc.yield_data.keys())) + + return nuc + + def xml_write(self): + """Write nuclide to XML element. + + Returns + ------- + elem : xml.etree.ElementTree.Element + XML element to write nuclide data to + + """ + elem = ET.Element('nuclide_table') + elem.set('name', self.name) + + if self.half_life is not None: + elem.set('half_life', str(self.half_life)) + elem.set('decay_modes', str(len(self.decay_modes))) + elem.set('decay_energy', str(self.decay_energy)) + for mode, daughter, br in self.decay_modes: + mode_elem = ET.SubElement(elem, 'decay_type') + mode_elem.set('type', mode) + mode_elem.set('target', daughter) + mode_elem.set('branching_ratio', str(br)) + + elem.set('reactions', str(len(self.reactions))) + for rx, daughter, Q, br in self.reactions: + rx_elem = ET.SubElement(elem, 'reaction_type') + rx_elem.set('type', rx) + rx_elem.set('Q', str(Q)) + if rx != 'fission': + rx_elem.set('target', daughter) + if br != 1.0: + rx_elem.set('branching_ratio', str(br)) + + if self.yield_data: + fpy_elem = ET.SubElement(elem, 'neutron_fission_yields') + energy_elem = ET.SubElement(fpy_elem, 'energies') + energy_elem.text = ' '.join(str(E) for E in self.yield_energies) + + for E in self.yield_energies: + yields_elem = ET.SubElement(fpy_elem, 'fission_yields') + yields_elem.set('energy', str(E)) + + products_elem = ET.SubElement(yields_elem, 'products') + products_elem.text = ' '.join(x[0] for x in self.yield_data[E]) + data_elem = ET.SubElement(yields_elem, 'data') + data_elem.text = ' '.join(str(x[1]) for x in self.yield_data[E]) + + return elem diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py new file mode 100644 index 0000000000..347dc71857 --- /dev/null +++ b/openmc/deplete/openmc_wrapper.py @@ -0,0 +1,853 @@ +""" The OpenMC wrapper module. + +This module implements the OpenDeplete -> OpenMC linkage. +""" + +import copy +from collections import OrderedDict +import os +import random +import sys +import time +try: + import lxml.etree as ET + _have_lxml = True +except ImportError: + import xml.etree.ElementTree as ET + from openmc.clean_xml import clean_xml_indentation + _have_lxml = False + +import h5py +import numpy as np +import openmc +import openmc.capi + +from . import comm +from .atom_number import AtomNumber +from .depletion_chain import DepletionChain +from .reaction_rates import ReactionRates +from .function import Settings, Operator + + +_JOULE_PER_EV = 1.6021766208e-19 + + +def chunks(items, n): + min_size, extra = divmod(len(items), n) + j = 0 + chunk_list = [] + for i in range(n): + chunk_size = min_size + int(i < extra) + chunk_list.append(items[j:j + chunk_size]) + j += chunk_size + return chunk_list + + +class OpenMCSettings(Settings): + """The OpenMCSettings class. + + Extends Settings to provide information OpenMC needs to run. + + Attributes + ---------- + dt_vec : numpy.array + Array of time steps to take. (From Settings) + tol : float + Tolerance for adaptive time stepping. (From Settings) + output_dir : str + Path to output directory to save results. (From Settings) + chain_file : str + Path to the depletion chain xml file. Defaults to the environment + variable "OPENDEPLETE_CHAIN" if it exists. + openmc_call : str + OpenMC executable path. Defaults to "openmc". + particles : int + Number of particles to simulate per batch. + batches : int + Number of batches. + inactive : int + Number of inactive batches. + lower_left : list of float + Coordinate of lower left of bounding box of geometry. + upper_right : list of float + Coordinate of upper right of bounding box of geometry. + entropy_dimension : list of int + Grid size of entropy. + dilute_initial : float, default 1.0e3 + Initial atom density to add for nuclides that are zero in initial + condition to ensure they exist in the decay chain. Only done for + nuclides with reaction rates. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. + constant_seed : int + If present, all runs will be performed with this seed. + power : float + Power of the reactor in W. For a 2D problem, the power can be given in + W/cm as long as the "volume" assigned to a depletion material is + actually an area in cm^2. + """ + + def __init__(self): + super().__init__() + # OpenMC specific + try: + self.chain_file = os.environ["OPENDEPLETE_CHAIN"] + except KeyError: + self.chain_file = None + self.openmc_call = "openmc" + self.particles = None + self.batches = None + self.inactive = None + self.lower_left = None + self.upper_right = None + self.entropy_dimension = None + self.dilute_initial = 1.0e3 + + # OpenMC testing specific + self.round_number = False + self.constant_seed = None + + # Depletion problem specific + self.power = None + + +class Materials(object): + """The Materials class. + + Contains information about cross sections for a cell. + + Attributes + ---------- + temperature : float + Temperature in Kelvin for each region. + sab : str or list of str + ENDF S(a,b) name for a region that needs S(a,b) data. Not set if no + S(a,b) needed for region. + """ + + def __init__(self): + self.temperature = None + self.sab = None + + +class OpenMCOperator(Operator): + """The OpenMC Operator class. + + Provides Operator functions for OpenMC. + + Parameters + ---------- + geometry : openmc.Geometry + The OpenMC geometry object. + settings : OpenMCSettings + Settings object. + + Attributes + ---------- + settings : OpenMCSettings + Settings object. (From Operator) + geometry : openmc.Geometry + The OpenMC geometry object. + materials : list of Materials + Materials to be used for this simulation. + seed : int + The RNG seed used in last OpenMC run. + number : AtomNumber + Total number of atoms in simulation. + participating_nuclides : set of str + A set listing all unique nuclides available from cross_sections.xml. + chain : DepletionChain + The depletion chain information necessary to form matrices and tallies. + reaction_rates : ReactionRates + Reaction rates from the last operator step. + power : OrderedDict of str to float + Material-by-Material power. Indexed by material ID. + mat_name : OrderedDict of str to int + The name of region each material is set to. Indexed by material ID. + burn_mat_to_id : OrderedDict of str to int + Dictionary mapping material ID (as a string) to an index in reaction_rates. + burn_nuc_to_id : OrderedDict of str to int + Dictionary mapping nuclide name (as a string) to an index in + reaction_rates. + n_nuc : int + Number of nuclides considered in the decay chain. + mat_tally_ind : OrderedDict of str to int + Dictionary mapping material ID to index in tally. + """ + + def __init__(self, geometry, settings): + super().__init__(settings) + + self.geometry = geometry + self.seed = 0 + self.number = None + self.participating_nuclides = None + self.reaction_rates = None + self.power = None + self.mat_name = OrderedDict() + self.burn_mat_to_ind = OrderedDict() + self.burn_nuc_to_ind = None + + # Read depletion chain + self.chain = DepletionChain.xml_read(settings.chain_file) + + # Clear out OpenMC, create task lists, distribute + if comm.rank == 0: + clean_up_openmc() + mat_burn_list, mat_not_burn_list, volume, self.mat_tally_ind, \ + nuc_dict = self.extract_mat_ids() + else: + # Dummy variables + mat_burn_list = None + mat_not_burn_list = None + volume = None + nuc_dict = None + self.mat_tally_ind = None + + mat_burn = comm.scatter(mat_burn_list) + mat_not_burn = comm.scatter(mat_not_burn_list) + nuc_dict = comm.bcast(nuc_dict) + volume = comm.bcast(volume) + self.mat_tally_ind = comm.bcast(self.mat_tally_ind) + + # Load participating nuclides + self.load_participating() + + # Extract number densities from the geometry + self.extract_number(mat_burn, mat_not_burn, volume, nuc_dict) + + # Create reaction rate tables + self.initialize_reaction_rates() + + def __del__(self): + openmc.capi.finalize() + + def extract_mat_ids(self): + """ Extracts materials and assigns them to processes. + + Returns + ------- + mat_burn_lists : list of list of int + List of burnable materials indexed by rank. + mat_not_burn_lists : list of list of int + List of non-burnable materials indexed by rank. + volume : OrderedDict of str to float + Volume of each cell + mat_tally_ind : OrderedDict of str to int + Dictionary mapping material ID to index in tally. + nuc_dict : OrderedDict of str to int + Nuclides in order of how they'll appear in the simulation. + """ + + mat_burn = set() + mat_not_burn = set() + nuc_set = set() + + volume = OrderedDict() + + # Iterate once through the geometry to get dictionaries + cells = self.geometry.get_all_material_cells() + for cell in cells.values(): + name = cell.name + + if isinstance(cell.fill, openmc.Material): + mat = cell.fill + for nuclide in mat.get_nuclide_densities(): + nuc_set.add(nuclide) + if mat.depletable: + mat_burn.add(str(mat.id)) + volume[str(mat.id)] = mat.volume + else: + mat_not_burn.add(str(mat.id)) + self.mat_name[mat.id] = name + else: + for mat in cell.fill: + for nuclide in mat.get_nuclide_densities(): + nuc_set.add(nuclide) + if mat.depletable: + mat_burn.add(str(mat.id)) + volume[str(mat.id)] = mat.volume + else: + mat_not_burn.add(str(mat.id)) + self.mat_name[mat.id] = name + + need_vol = [] + + for mat_id in volume: + if volume[mat_id] is None: + need_vol.append(mat_id) + + if need_vol: + exit("Need volumes for materials: " + str(need_vol)) + + # Sort the sets + mat_burn = sorted(mat_burn, key=int) + mat_not_burn = sorted(mat_not_burn, key=int) + nuc_set = sorted(nuc_set) + + # Construct a global nuclide dictionary, burned first + nuc_dict = copy.deepcopy(self.chain.nuclide_dict) + + i = len(nuc_dict) + + for nuc in nuc_set: + if nuc not in nuc_dict: + nuc_dict[nuc] = i + i += 1 + + # Decompose geometry + mat_burn_lists = chunks(mat_burn, comm.size) + mat_not_burn_lists = chunks(mat_not_burn, comm.size) + + mat_tally_ind = OrderedDict() + + for i, mat in enumerate(mat_burn): + mat_tally_ind[mat] = i + + return mat_burn_lists, mat_not_burn_lists, volume, mat_tally_ind, nuc_dict + + def extract_number(self, mat_burn, mat_not_burn, volume, nuc_dict): + """ Construct self.number read from geometry + + Parameters + ---------- + mat_burn : list of int + Materials to be burned managed by this thread. + mat_not_burn + Materials not to be burned managed by this thread. + volume : OrderedDict of str to float + Volumes for the above materials. + nuc_dict : OrderedDict of str to int + Nuclides to be used in the simulation. + """ + + # Same with materials + mat_dict = OrderedDict() + self.burn_mat_to_ind = OrderedDict() + i = 0 + for mat in mat_burn: + mat_dict[mat] = i + self.burn_mat_to_ind[mat] = i + i += 1 + + for mat in mat_not_burn: + mat_dict[mat] = i + i += 1 + + n_mat_burn = len(mat_burn) + n_nuc_burn = len(self.chain.nuclide_dict) + + self.number = AtomNumber(mat_dict, nuc_dict, volume, n_mat_burn, n_nuc_burn) + + if self.settings.dilute_initial != 0.0: + for nuc in self.burn_nuc_to_ind: + self.number.set_atom_density(np.s_[:], nuc, self.settings.dilute_initial) + + # Now extract the number densities and store + cells = self.geometry.get_all_material_cells() + for cell in cells.values(): + if isinstance(cell.fill, openmc.Material): + if str(cell.fill.id) in mat_dict: + self.set_number_from_mat(cell.fill) + else: + for mat in cell.fill: + if str(mat.id) in mat_dict: + self.set_number_from_mat(mat) + + def set_number_from_mat(self, mat): + """ Extracts material and number densities from openmc.Material + + Parameters + ---------- + mat : openmc.Materials + The material to read from + """ + + mat_id = str(mat.id) + mat_ind = self.number.mat_to_ind[mat_id] + + nuc_dens = mat.get_nuclide_atom_densities() + for nuclide in nuc_dens: + name = nuclide.name + number = nuc_dens[nuclide][1] * 1.0e24 + self.number.set_atom_density(mat_id, name, number) + + def initialize_reaction_rates(self): + """ Create reaction rates object. """ + self.reaction_rates = ReactionRates( + self.burn_mat_to_ind, + self.burn_nuc_to_ind, + self.chain.react_to_ind) + + self.chain.nuc_to_react_ind = self.burn_nuc_to_ind + + def eval(self, vec, print_out=True): + """ Runs a simulation. + + Parameters + ---------- + vec : list of numpy.array + Total atoms to be used in function. + print_out : bool, optional + Whether or not to print out time. + + Returns + ------- + mat : list of scipy.sparse.csr_matrix + Matrices for the next step. + k : float + Eigenvalue of the problem. + rates : ReactionRates + Reaction rates from this simulation. + seed : int + Seed for this simulation. + """ + + # Prevent OpenMC from complaining about re-creating tallies + clean_up_openmc() + + # Update status + self.set_density(vec) + + time_start = time.time() + + # Update material compositions and tally nuclides + self._update_materials() + openmc.capi.tallies[1].nuclides = self._get_tally_nuclides() + + # Run OpenMC + openmc.capi.reset() + openmc.capi.run() + + time_openmc = time.time() + + # Extract results + k = self.unpack_tallies_and_normalize() + + if comm.rank == 0: + time_unpack = time.time() + + if print_out: + print("Time to openmc: ", time_openmc - time_start) + print("Time to unpack: ", time_unpack - time_openmc) + + return k, copy.deepcopy(self.reaction_rates), self.seed + + def form_matrix(self, y, mat): + """ Forms the depletion matrix. + + Parameters + ---------- + y : numpy.ndarray + An array representing reaction rates for this cell. + mat : int + Material id. + + Returns + ------- + scipy.sparse.csr_matrix + Sparse matrix representing the depletion matrix. + """ + + return copy.deepcopy(self.chain.form_matrix(y[mat, :, :])) + + def initial_condition(self): + """ Performs final setup and returns initial condition. + + Returns + ------- + list of numpy.array + Total density for initial conditions. + """ + + # Create XML files + if comm.rank == 0: + self.geometry.export_to_xml() + self.generate_settings_xml() + self.generate_materials_xml() + + # Initialize OpenMC library + comm.barrier() + openmc.capi.init(comm) + + # Generate tallies in memory + self.generate_tallies() + + # Return number density vector + return self.total_density_list() + + def _update_materials(self): + """Updates material compositions in OpenMC on all processes.""" + + for rank in range(comm.size): + number_i = comm.bcast(self.number, root=rank) + + for mat in number_i.mat_to_ind: + nuclides = [] + densities = [] + for nuc in number_i.nuc_to_ind: + if nuc in self.participating_nuclides: + val = 1.0e-24 * number_i.get_atom_density(mat, nuc) + + # If nuclide is zero, do not add to the problem. + if val > 0.0: + if self.settings.round_number: + val_magnitude = np.floor(np.log10(val)) + val_scaled = val / 10**val_magnitude + val_round = round(val_scaled, 8) + + val = val_round * 10**val_magnitude + + nuclides.append(nuc) + densities.append(val) + else: + # Only output warnings if values are significantly + # negative. CRAM does not guarantee positive values. + if val < -1.0e-21: + print("WARNING: nuclide ", nuc, " in material ", mat, + " is negative (density = ", val, " at/barn-cm)") + number_i[mat, nuc] = 0.0 + + mat_internal = openmc.capi.materials[int(mat)] + mat_internal.set_densities(nuclides, densities) + + def generate_materials_xml(self): + """ Creates materials.xml from self.number. + + Due to uncertainty with how MPI interacts with OpenMC API, this + constructs the XML manually. The long term goal is to do this + through direct memory writing. + """ + + materials = openmc.Materials(self.geometry.get_all_materials() + .values()) + + # Sort nuclides according to order in AtomNumber object + nuclides = list(self.number.nuc_to_ind.keys()) + for mat in materials: + mat._nuclides.sort(key=lambda x: nuclides.index(x[0])) + + materials.export_to_xml() + + def generate_settings_xml(self): + """ Generates settings.xml. + + This function creates settings.xml using the value of the settings + variable. + + Todo + ---- + Rewrite to generalize source box. + """ + + batches = self.settings.batches + inactive = self.settings.inactive + particles = self.settings.particles + + # Just a generic settings file to get it running. + settings_file = openmc.Settings() + settings_file.batches = batches + settings_file.inactive = inactive + settings_file.particles = particles + settings_file.source = openmc.Source(space=openmc.stats.Box( + self.settings.lower_left, self.settings.upper_right)) + + if self.settings.entropy_dimension is not None: + entropy_mesh = openmc.Mesh() + entropy_mesh.lower_left = self.settings.lower_left + entropy_mesh.upper_right = self.settings.upper_right + entropy_mesh.dimension = self.settings.entropy_dimension + settings_file.entropy_mesh = entropy_mesh + + # Set seed + if self.settings.constant_seed is not None: + seed = self.settings.constant_seed + else: + seed = random.randint(1, sys.maxsize-1) + + settings_file.seed = self.seed = seed + + settings_file.export_to_xml() + + def _get_tally_nuclides(self): + nuc_set = set() + + # Create the set of all nuclides in the decay chain in cells marked for + # burning in which the number density is greater than zero. + for nuc in self.number.nuc_to_ind: + if nuc in self.participating_nuclides: + if np.sum(self.number[:, nuc]) > 0.0: + nuc_set.add(nuc) + + # Communicate which nuclides have nonzeros to rank 0 + if comm.rank == 0: + for i in range(1, comm.size): + nuc_newset = comm.recv(source=i, tag=i) + nuc_set |= nuc_newset + + else: + comm.send(nuc_set, dest=0, tag=comm.rank) + + if comm.rank == 0: + # Sort nuclides in the same order as self.number + nuc_list = [nuc for nuc in self.number.nuc_to_ind + if nuc in nuc_set] + else: + nuc_list = None + + # Store list of tally nuclides on each process + nuc_list = comm.bcast(nuc_list, root=0) + tally_nuclides = [nuc for nuc in nuc_list + if nuc in self.chain.nuclide_dict] + + return tally_nuclides + + def generate_tallies(self): + """Generates depletion tallies. + + Using information from self.depletion_chain as well as the nuclides + currently in the problem, this function automatically generates a + tally.xml for the simulation. + """ + + # Create tallies for depleting regions + materials = [openmc.capi.materials[int(i)] + for i in self.mat_tally_ind] + mat_filter = openmc.capi.MaterialFilter(materials, 1) + + # Set up a tally that has a material filter covering each depletable + # material and scores corresponding to all reactions that cause + # transmutation. The nuclides for the tally are set later when eval() is + # called. + tally_dep = openmc.capi.Tally(1) + tally_dep.scores = self.chain.react_to_ind.keys() + tally_dep.filters = [mat_filter] + + def total_density_list(self): + """ Returns a list of total density lists. + + This list is in the exact same order as depletion_matrix_list, so that + matrix exponentiation can be done easily. + + Returns + ------- + list of numpy.array + A list of np.arrays containing total atoms of each cell. + """ + + total_density = [self.number.get_mat_slice(i) for i in range(self.number.n_mat_burn)] + + return total_density + + def set_density(self, total_density): + """ Sets density. + + Sets the density in the exact same order as total_density_list outputs, + allowing for internal consistency + + Parameters + ---------- + total_density : list of numpy.array + Total atoms. + """ + + # Fill in values + for i in range(self.number.n_mat_burn): + self.number.set_mat_slice(i, total_density[i]) + + def unpack_tallies_and_normalize(self): + """ Unpack tallies from OpenMC + + This function reads the tallies generated by OpenMC (from the tally.xml + file generated in generate_tally_xml) normalizes them so that the total + power generated is new_power, and then stores them in the reaction rate + database. + + Returns + ------- + k : float + Eigenvalue of the last simulation. + + Todo + ---- + Provide units for power + """ + + rates = self.reaction_rates + rates[:, :, :] = 0.0 + + k_combined = openmc.capi.keff()[0] + + # Extract tally bins + materials = list(self.mat_tally_ind.keys()) + nuclides = openmc.capi.tallies[1].nuclides + reactions = list(self.chain.react_to_ind.keys()) + + # Form fast map + nuc_ind = [rates.nuc_to_ind[nuc] for nuc in nuclides] + react_ind = [rates.react_to_ind[react] for react in reactions] + + # Compute fission power + # TODO : improve this calculation + + # Keep track of energy produced from all reactions in eV per source + # particle + energy = 0.0 + + # Create arrays to store fission Q values, reaction rates, and nuclide + # numbers + fission_Q = np.zeros(rates.n_nuc) + rates_expanded = np.zeros((rates.n_nuc, rates.n_react)) + number = np.zeros(rates.n_nuc) + + fission_ind = rates.react_to_ind["fission"] + + for nuclide in self.chain.nuclides: + if nuclide.name in rates.nuc_to_ind: + for rx in nuclide.reactions: + if rx.type == 'fission': + ind = rates.nuc_to_ind[nuclide.name] + fission_Q[ind] = rx.Q + break + + # Extract results + for i, mat in enumerate(self.number.burn_mat_list): + # Get tally index + slab = materials.index(mat) + + # Get material results hyperslab + results = openmc.capi.tallies[1].results[slab, :, 1] + + # Zero out reaction rates and nuclide numbers + rates_expanded[:] = 0.0 + number[:] = 0.0 + + # Expand into our memory layout + j = 0 + for nuc, i_nuc_results in zip(nuclides, nuc_ind): + number[i_nuc_results] = self.number[mat, nuc] + for react in react_ind: + rates_expanded[i_nuc_results, react] = results[j] + j += 1 + + # Accumulate energy from fission + energy += np.dot(rates_expanded[:, fission_ind], fission_Q) + + # Divide by total number and store + for i_nuc_results in nuc_ind: + if number[i_nuc_results] != 0.0: + for react in react_ind: + rates_expanded[i_nuc_results, react] /= number[i_nuc_results] + + rates.rates[i, :, :] = rates_expanded + + # Reduce energy produced from all processes + energy = comm.allreduce(energy) + + # Determine power in eV/s + power = self.settings.power / _JOULE_PER_EV + + # Scale reaction rates to obtain units of reactions/sec + rates[:, :, :] *= power / energy + + return k_combined + + def load_participating(self): + """ Loads a cross_sections.xml file to find participating nuclides. + + This allows for nuclides that are important in the decay chain but not + important neutronically, or have no cross section data. + """ + + # Reads cross_sections.xml to create a dictionary containing + # participating (burning and not just decaying) nuclides. + + try: + filename = os.environ["OPENMC_CROSS_SECTIONS"] + except KeyError: + filename = None + + self.participating_nuclides = set() + + try: + tree = ET.parse(filename) + except: + if filename is None: + msg = "No cross_sections.xml specified in materials." + else: + msg = 'Cross section file "{}" is invalid.'.format(filename) + raise IOError(msg) + + root = tree.getroot() + self.burn_nuc_to_ind = OrderedDict() + nuc_ind = 0 + + for nuclide_node in root.findall('library'): + mats = nuclide_node.get('materials') + if not mats: + continue + for name in mats.split(): + # Make a burn list of the union of nuclides in cross_sections.xml + # and nuclides in depletion chain. + if name not in self.participating_nuclides: + self.participating_nuclides.add(name) + if name in self.chain.nuclide_dict: + self.burn_nuc_to_ind[name] = nuc_ind + nuc_ind += 1 + + @property + def n_nuc(self): + """Number of nuclides considered in the decay chain.""" + return len(self.chain.nuclides) + + def get_results_info(self): + """ Returns volume list, cell lists, and nuc lists. + + Returns + ------- + volume : dict of str float + Volumes corresponding to materials in full_burn_dict + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all cell IDs to be burned. Used for sorting the simulation. + full_burn_dict : OrderedDict of str to int + Maps cell name to index in global geometry. + """ + + nuc_list = self.number.burn_nuc_list + burn_list = self.number.burn_mat_list + + volume = {} + for i, mat in enumerate(burn_list): + volume[mat] = self.number.volume[i] + + # Combine volume dictionaries across processes + volume_list = comm.allgather(volume) + volume = {k: v for d in volume_list for k, v in d.items()} + + return volume, nuc_list, burn_list, self.mat_tally_ind + +def density_to_mat(dens_dict): + """ Generates an OpenMC material from a cell ID and self.number_density. + Parameters + ---------- + m_id : int + Cell ID. + Returns + ------- + openmc.Material + The OpenMC material filled with nuclides. + """ + + mat = openmc.Material() + for key in dens_dict: + mat.add_nuclide(key, 1.0e-24*dens_dict[key]) + mat.set_density('sum') + + return mat + +def clean_up_openmc(): + """ Resets all automatic indexing in OpenMC, as these get in the way. """ + openmc.reset_auto_ids() diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py new file mode 100644 index 0000000000..7b934027a5 --- /dev/null +++ b/openmc/deplete/reaction_rates.py @@ -0,0 +1,113 @@ +"""ReactionRates module. + +An ndarray to store reaction rates with string, integer, or slice indexing. +""" + +import numpy as np + + +class ReactionRates(object): + """ ReactionRates class. + + An ndarray to store reaction rates with string, integer, or slice indexing. + + Parameters + ---------- + mat_to_ind : OrderedDict of str to int + A dictionary mapping material ID as string to index. + nuc_to_ind : OrderedDict of str to int + A dictionary mapping nuclide name as string to index. + react_to_ind : OrderedDict of str to int + A dictionary mapping reaction name as string to index. + + Attributes + ---------- + mat_to_ind : OrderedDict of str to int + A dictionary mapping cell ID as string to index. + nuc_to_ind : OrderedDict of str to int + A dictionary mapping nuclide name as string to index. + react_to_ind : OrderedDict of str to int + A dictionary mapping reaction name as string to index. + n_mat : int + Number of materials. + n_nuc : int + Number of nucs. + n_react : int + Number of reactions. + rates : numpy.array + Array storing rates indexed by the above dictionaries. + """ + + def __init__(self, mat_to_ind, nuc_to_ind, react_to_ind): + + self.mat_to_ind = mat_to_ind + self.nuc_to_ind = nuc_to_ind + self.react_to_ind = react_to_ind + + self.rates = np.zeros((self.n_mat, self.n_nuc, self.n_react)) + + def __getitem__(self, pos): + """ Retrieves an item from reaction_rates. + + Parameters + ---------- + pos : tuple + A three-length tuple containing a material index, a nuc index, and a + reaction index. These indexes can be strings (which get converted + to integers via the dictionaries), integers used directly, or + slices. + + Returns + ------- + numpy.array + The value indexed from self.rates. + """ + + mat, nuc, react = pos + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + if isinstance(react, str): + react = self.react_to_ind[react] + + return self.rates[mat, nuc, react] + + def __setitem__(self, pos, val): + """ Sets an item from reaction_rates. + + Parameters + ---------- + pos : tuple + A three-length tuple containing a material index, a nuc index, and a + reaction index. These indexes can be strings (which get converted + to integers via the dictionaries), integers used directly, or + slices. + val : float + The value to set the array to. + """ + + mat, nuc, react = pos + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + if isinstance(react, str): + react = self.react_to_ind[react] + + self.rates[mat, nuc, react] = val + + @property + def n_mat(self): + """Number of cells.""" + return len(self.mat_to_ind) + + @property + def n_nuc(self): + """Number of nucs.""" + return len(self.nuc_to_ind) + + @property + def n_react(self): + """Number of reactions.""" + return len(self.react_to_ind) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py new file mode 100644 index 0000000000..c0ec1627ea --- /dev/null +++ b/openmc/deplete/results.py @@ -0,0 +1,454 @@ +""" The results module. + +Contains results generation and saving capabilities. +""" + +from collections import OrderedDict +import copy + +import numpy as np +import h5py + +from . import comm, have_mpi +from .reaction_rates import ReactionRates + +RESULTS_VERSION = 2 + +class Results(object): + """ Contains output of opendeplete. + + Attributes + ---------- + k : list of float + Eigenvalue for each substep. + seeds : list of int + Seeds for each substep. + time : list of float + Time at beginning, end of step, in seconds. + n_mat : int + Number of mats. + n_nuc : int + Number of nuclides. + rates : list of ReactionRates + The reaction rates for each substep. + volume : OrderedDict of int to float + Dictionary mapping mat id to volume. + mat_to_ind : OrderedDict of str to int + A dictionary mapping mat ID as string to index. + nuc_to_ind : OrderedDict of str to int + A dictionary mapping nuclide name as string to index. + mat_to_hdf5_ind : OrderedDict of str to int + A dictionary mapping mat ID as string to global index. + n_hdf5_mats : int + Number of materials in entire geometry. + n_stages : int + Number of stages in simulation. + data : numpy.array + Atom quantity, stored by stage, mat, then by nuclide. + """ + + def __init__(self): + self.k = None + self.seeds = None + self.time = None + self.p_terms = None + self.rates = None + self.volume = None + + self.mat_to_ind = None + self.nuc_to_ind = None + self.mat_to_hdf5_ind = None + + self.data = None + + def allocate(self, volume, nuc_list, burn_list, full_burn_dict, stages): + """ Allocates memory of Results. + + Parameters + ---------- + volume : dict of str float + Volumes corresponding to materials in full_burn_dict + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all mat IDs to be burned. Used for sorting the simulation. + full_burn_dict : dict of str to int + Map of material name to id in global geometry. + stages : int + Number of stages in simulation. + """ + + self.volume = copy.deepcopy(volume) + self.nuc_to_ind = OrderedDict() + self.mat_to_ind = OrderedDict() + self.mat_to_hdf5_ind = copy.deepcopy(full_burn_dict) + + for i, mat in enumerate(burn_list): + self.mat_to_ind[mat] = i + + for i, nuc in enumerate(nuc_list): + self.nuc_to_ind[nuc] = i + + # Create storage array + self.data = np.zeros((stages, self.n_mat, self.n_nuc)) + + @property + def n_mat(self): + """Number of mats.""" + return len(self.mat_to_ind) + + @property + def n_nuc(self): + """Number of nuclides.""" + return len(self.nuc_to_ind) + + @property + def n_hdf5_mats(self): + """Number of materials in entire geometry.""" + return len(self.mat_to_hdf5_ind) + + @property + def n_stages(self): + """Number of stages in simulation.""" + return self.data.shape[0] + + def __getitem__(self, pos): + """ Retrieves an item from results. + + Parameters + ---------- + pos : tuple + A three-length tuple containing a stage index, mat index and a nuc + index. All can be integers or slices. The second two can be + strings corresponding to their respective dictionary. + + Returns + ------- + float + The atoms for stage, mat, nuc + """ + + stage, mat, nuc = pos + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + + return self.data[stage, mat, nuc] + + def __setitem__(self, pos, val): + """ Sets an item from results. + + Parameters + ---------- + pos : tuple + A three-length tuple containing a stage index, mat index and a nuc + index. All can be integers or slices. The second two can be + strings corresponding to their respective dictionary. + + val : float + The value to set data to. + """ + + stage, mat, nuc = pos + if isinstance(mat, str): + mat = self.mat_to_ind[mat] + if isinstance(nuc, str): + nuc = self.nuc_to_ind[nuc] + + self.data[stage, mat, nuc] = val + + def create_hdf5(self, handle): + """ Creates file structure for a blank HDF5 file. + + Parameters + ---------- + handle : h5py.File or h5py.Group + An hdf5 file or group type to store this in. + """ + + # Create and save the 5 dictionaries: + # quantities + # self.mat_to_ind -> self.volume (TODO: support for changing volumes) + # self.nuc_to_ind + # reactions + # self.rates[0].nuc_to_ind (can be different from above, above is superset) + # self.rates[0].react_to_ind + # these are shared by every step of the simulation, and should be deduplicated. + + # Store concentration mat and nuclide dictionaries (along with volumes) + + handle.create_dataset("version", data=RESULTS_VERSION) + + mat_int = sorted([int(mat) for mat in self.mat_to_hdf5_ind]) + mat_list = [str(mat) for mat in mat_int] + nuc_list = sorted(self.nuc_to_ind.keys()) + rxn_list = sorted(self.rates[0].react_to_ind.keys()) + + n_mats = self.n_hdf5_mats + n_nuc_number = len(nuc_list) + n_nuc_rxn = len(self.rates[0].nuc_to_ind) + n_rxn = len(rxn_list) + n_stages = self.n_stages + + mat_group = handle.create_group("cells") + + for mat in mat_list: + mat_single_group = mat_group.create_group(mat) + mat_single_group.attrs["index"] = self.mat_to_hdf5_ind[mat] + mat_single_group.attrs["volume"] = self.volume[mat] + + nuc_group = handle.create_group("nuclides") + + for nuc in nuc_list: + nuc_single_group = nuc_group.create_group(nuc) + nuc_single_group.attrs["atom number index"] = self.nuc_to_ind[nuc] + if nuc in self.rates[0].nuc_to_ind: + nuc_single_group.attrs["reaction rate index"] = self.rates[0].nuc_to_ind[nuc] + + rxn_group = handle.create_group("reactions") + + for rxn in rxn_list: + rxn_single_group = rxn_group.create_group(rxn) + rxn_single_group.attrs["index"] = self.rates[0].react_to_ind[rxn] + + # Construct array storage + + handle.create_dataset("number", (1, n_stages, n_mats, n_nuc_number), + maxshape=(None, n_stages, n_mats, n_nuc_number), + chunks=(1, 1, n_mats, n_nuc_number), + dtype='float64') + + handle.create_dataset("reaction rates", (1, n_stages, n_mats, n_nuc_rxn, n_rxn), + maxshape=(None, n_stages, n_mats, n_nuc_rxn, n_rxn), + chunks=(1, 1, n_mats, n_nuc_rxn, n_rxn), + dtype='float64') + + handle.create_dataset("eigenvalues", (1, n_stages), + maxshape=(None, n_stages), dtype='float64') + + handle.create_dataset("seeds", (1, n_stages), maxshape=(None, n_stages), dtype='int64') + + handle.create_dataset("time", (1, 2), maxshape=(None, 2), dtype='float64') + + def to_hdf5(self, handle, index): + """ Converts results object into an hdf5 object. + + Parameters + ---------- + handle : h5py.File or h5py.Group + An hdf5 file or group type to store this in. + index : int + What step is this? + """ + + if "/number" not in handle: + comm.barrier() + self.create_hdf5(handle) + + comm.barrier() + + # Grab handles + number_dset = handle["/number"] + rxn_dset = handle["/reaction rates"] + eigenvalues_dset = handle["/eigenvalues"] + seeds_dset = handle["/seeds"] + time_dset = handle["/time"] + + # Get number of results stored + number_shape = list(number_dset.shape) + number_results = number_shape[0] + + new_shape = index + 1 + + if number_results < new_shape: + # Extend first dimension by 1 + number_shape[0] = new_shape + number_dset.resize(number_shape) + + rxn_shape = list(rxn_dset.shape) + rxn_shape[0] = new_shape + rxn_dset.resize(rxn_shape) + + eigenvalues_shape = list(eigenvalues_dset.shape) + eigenvalues_shape[0] = new_shape + eigenvalues_dset.resize(eigenvalues_shape) + + seeds_shape = list(seeds_dset.shape) + seeds_shape[0] = new_shape + seeds_dset.resize(seeds_shape) + + time_shape = list(time_dset.shape) + time_shape[0] = new_shape + time_dset.resize(time_shape) + + # If nothing to write, just return + if len(self.mat_to_ind) == 0: + return + + # Add data + # Note, for the last step, self.n_stages = 1, even if n_stages != 1. + n_stages = self.n_stages + inds = [self.mat_to_hdf5_ind[mat] for mat in self.mat_to_ind] + low = min(inds) + high = max(inds) + for i in range(n_stages): + number_dset[index, i, low:high+1, :] = self.data[i, :, :] + rxn_dset[index, i, low:high+1, :, :] = self.rates[i][:, :, :] + if comm.rank == 0: + eigenvalues_dset[index, i] = self.k[i] + seeds_dset[index, i] = self.seeds[i] + if comm.rank == 0: + time_dset[index, :] = self.time + + def from_hdf5(self, handle, index): + """ Loads results object from HDF5. + + Parameters + ---------- + handle : h5py.File or h5py.Group + An hdf5 file or group type to load from. + index : int + What step is this? + """ + + # Grab handles + number_dset = handle["/number"] + eigenvalues_dset = handle["/eigenvalues"] + seeds_dset = handle["/seeds"] + time_dset = handle["/time"] + + self.data = number_dset[index, :, :, :] + self.k = eigenvalues_dset[index, :] + self.seeds = seeds_dset[index, :] + self.time = time_dset[index, :] + + # Reconstruct dictionaries + self.volume = OrderedDict() + self.mat_to_ind = OrderedDict() + self.nuc_to_ind = OrderedDict() + rxn_nuc_to_ind = OrderedDict() + rxn_to_ind = OrderedDict() + + for mat in handle["/cells"]: + mat_handle = handle["/cells/" + mat] + vol = mat_handle.attrs["volume"] + ind = mat_handle.attrs["index"] + + self.volume[mat] = vol + self.mat_to_ind[mat] = ind + + for nuc in handle["/nuclides"]: + nuc_handle = handle["/nuclides/" + nuc] + ind_atom = nuc_handle.attrs["atom number index"] + self.nuc_to_ind[nuc] = ind_atom + + if "reaction rate index" in nuc_handle.attrs: + rxn_nuc_to_ind[nuc] = nuc_handle.attrs["reaction rate index"] + + for rxn in handle["/reactions"]: + rxn_handle = handle["/reactions/" + rxn] + rxn_to_ind[rxn] = rxn_handle.attrs["index"] + + self.rates = [] + # Reconstruct reactions + for i in range(self.n_stages): + rate = ReactionRates(self.mat_to_ind, rxn_nuc_to_ind, rxn_to_ind) + + rate.rates = handle["/reaction rates"][index, i, :, :, :] + self.rates.append(rate) + + +def get_dict(number): + """ Given an operator nested dictionary, output indexing dictionaries. + + These indexing dictionaries map mat IDs and nuclide names to indices + inside of Results.data. + + Parameters + ---------- + number : AtomNumber + The object to extract dictionaries from + + Returns + ------- + mat_to_ind : OrderedDict of str to int + Maps mat strings to index in array. + nuc_to_ind : OrderedDict of str to int + Maps nuclide strings to index in array. + """ + mat_to_ind = OrderedDict() + nuc_to_ind = OrderedDict() + + for nuc in number.nuc_to_ind: + nuc_ind = number.nuc_to_ind[nuc] + if nuc_ind < number.n_nuc_burn: + nuc_to_ind[nuc] = nuc_ind + + for mat in number.mat_to_ind: + mat_ind = number.mat_to_ind[mat] + if mat_ind < number.n_mat_burn: + mat_to_ind[mat] = mat_ind + + return mat_to_ind, nuc_to_ind + + +def write_results(result, filename, index): + """ Outputs result to an .hdf5 file. + + Parameters + ---------- + result : Results + Object to be stored in a file. + filename : String + Target filename. + index : int + What step is this? + """ + + if have_mpi and h5py.get_config().mpi: + kwargs = {'driver': 'mpio', 'comm': comm} + else: + kwargs = {} + + kwargs['mode'] = "w" if index == 0 else "a" + + with h5py.File(filename, **kwargs) as handle: + result.to_hdf5(handle, index) + + +def read_results(filename): + """ Reads out a list of results objects from an hdf5 file. + + Parameters + ---------- + filename : str + The filename to read from. + + Returns + ------- + results : list of Results + The result objects. + """ + + file = h5py.File(filename, "r") + + assert file["/version"].value == RESULTS_VERSION + + # Grab handles + number_dset = file["/number"] + + # Get number of results stored + number_shape = list(number_dset.shape) + number_results = number_shape[0] + + results = [] + + for i in range(number_results): + result = Results() + result.from_hdf5(file, i) + results.append(result) + + file.close() + + return results diff --git a/openmc/deplete/utilities.py b/openmc/deplete/utilities.py new file mode 100644 index 0000000000..54632ed9c2 --- /dev/null +++ b/openmc/deplete/utilities.py @@ -0,0 +1,98 @@ +""" The utilities module. + +Contains functions that can be used to post-process objects that come out of +the results module. +""" + +import numpy as np + +def evaluate_single_nuclide(results, cell, nuc): + """ Evaluates a single nuclide in a single cell from a results list. + + Parameters + ---------- + results : list of results + The results to extract data from. Must be sorted and continuous. + cell : str + Cell name to evaluate + nuc : str + Nuclide name to evaluate + + Returns + ------- + time : numpy.array + Time vector. + concentration : numpy.array + Total number of atoms in the cell. + """ + + n_points = len(results) + time = np.zeros(n_points) + concentration = np.zeros(n_points) + + # Evaluate value in each region + for i, result in enumerate(results): + time[i] = result.time[0] + concentration[i] = result[0, cell, nuc] + + return time, concentration + +def evaluate_reaction_rate(results, cell, nuc, rxn): + """ Evaluates a single nuclide reaction rate in a single cell from a results list. + + Parameters + ---------- + results : list of Results + The results to extract data from. Must be sorted and continuous. + cell : str + Cell name to evaluate + nuc : str + Nuclide name to evaluate + rxn : str + Reaction rate to evaluate + + Returns + ------- + time : numpy.array + Time vector. + rate : numpy.array + Reaction rate. + """ + + n_points = len(results) + time = np.zeros(n_points) + rate = np.zeros(n_points) + # Evaluate value in each region + for i, result in enumerate(results): + time[i] = result.time[0] + rate[i] = result.rates[0][cell, nuc, rxn] * result[0, cell, nuc] + + return time, rate + +def evaluate_eigenvalue(results): + """ Evaluates the eigenvalue from a results list. + + Parameters + ---------- + results : list of Results + The results to extract data from. Must be sorted and continuous. + + Returns + ------- + time : numpy.array + Time vector. + eigenvalue : numpy.array + Eigenvalue. + """ + + n_points = len(results) + time = np.zeros(n_points) + eigenvalue = np.zeros(n_points) + + # Evaluate value in each region + for i, result in enumerate(results): + + time[i] = result.time[0] + eigenvalue[i] = result.k[0] + + return time, eigenvalue diff --git a/scripts/example_geometry.py b/scripts/example_geometry.py new file mode 100644 index 0000000000..9afcc0d464 --- /dev/null +++ b/scripts/example_geometry.py @@ -0,0 +1,358 @@ +"""An example file showing how to make a geometry. + +This particular example creates a 3x3 geometry, with 8 regular pins and one +Gd-157 2 wt-percent enriched. All pins are segmented. +""" + +from collections import OrderedDict +import math + +import numpy as np +import openmc + +from opendeplete import density_to_mat + + +def generate_initial_number_density(): + """ Generates initial number density. + + These results were from a CASMO5 run in which the gadolinium pin was + loaded with 2 wt percent of Gd-157. + """ + + # Concentration to be used for all fuel pins + fuel_dict = OrderedDict() + fuel_dict['U235'] = 1.05692e21 + fuel_dict['U234'] = 1.00506e19 + fuel_dict['U238'] = 2.21371e22 + fuel_dict['O16'] = 4.62954e22 + fuel_dict['O17'] = 1.127684e20 + fuel_dict['I135'] = 1.0e10 + fuel_dict['Xe135'] = 1.0e10 + fuel_dict['Xe136'] = 1.0e10 + fuel_dict['Cs135'] = 1.0e10 + fuel_dict['Gd156'] = 1.0e10 + fuel_dict['Gd157'] = 1.0e10 + # fuel_dict['O18'] = 9.51352e19 # Does not exist in ENDF71, merged into 17 + + # Concentration to be used for the gadolinium fuel pin + fuel_gd_dict = OrderedDict() + fuel_gd_dict['U235'] = 1.03579e21 + fuel_gd_dict['U238'] = 2.16943e22 + fuel_gd_dict['Gd156'] = 3.95517E+10 + fuel_gd_dict['Gd157'] = 1.08156e20 + fuel_gd_dict['O16'] = 4.64035e22 + fuel_dict['I135'] = 1.0e10 + fuel_dict['Xe136'] = 1.0e10 + fuel_dict['Xe135'] = 1.0e10 + fuel_dict['Cs135'] = 1.0e10 + # There are a whole bunch of 1e-10 stuff here. + + # Concentration to be used for cladding + clad_dict = OrderedDict() + clad_dict['O16'] = 3.07427e20 + clad_dict['O17'] = 7.48868e17 + clad_dict['Cr50'] = 3.29620e18 + clad_dict['Cr52'] = 6.35639e19 + clad_dict['Cr53'] = 7.20763e18 + clad_dict['Cr54'] = 1.79413e18 + clad_dict['Fe54'] = 5.57350e18 + clad_dict['Fe56'] = 8.74921e19 + clad_dict['Fe57'] = 2.02057e18 + clad_dict['Fe58'] = 2.68901e17 + clad_dict['Cr50'] = 3.29620e18 + clad_dict['Cr52'] = 6.35639e19 + clad_dict['Cr53'] = 7.20763e18 + clad_dict['Cr54'] = 1.79413e18 + clad_dict['Ni58'] = 2.51631e19 + clad_dict['Ni60'] = 9.69278e18 + clad_dict['Ni61'] = 4.21338e17 + clad_dict['Ni62'] = 1.34341e18 + clad_dict['Ni64'] = 3.43127e17 + clad_dict['Zr90'] = 2.18320e22 + clad_dict['Zr91'] = 4.76104e21 + clad_dict['Zr92'] = 7.27734e21 + clad_dict['Zr94'] = 7.37494e21 + clad_dict['Zr96'] = 1.18814e21 + clad_dict['Sn112'] = 4.67352e18 + clad_dict['Sn114'] = 3.17992e18 + clad_dict['Sn115'] = 1.63814e18 + clad_dict['Sn116'] = 7.00546e19 + clad_dict['Sn117'] = 3.70027e19 + clad_dict['Sn118'] = 1.16694e20 + clad_dict['Sn119'] = 4.13872e19 + clad_dict['Sn120'] = 1.56973e20 + clad_dict['Sn122'] = 2.23076e19 + clad_dict['Sn124'] = 2.78966e19 + + # Gap concentration + # Funny enough, the example problem uses air. + gap_dict = OrderedDict() + gap_dict['O16'] = 7.86548e18 + gap_dict['O17'] = 2.99548e15 + gap_dict['N14'] = 3.38646e19 + gap_dict['N15'] = 1.23717e17 + + # Concentration to be used for coolant + # No boron + cool_dict = OrderedDict() + cool_dict['H1'] = 4.68063e22 + cool_dict['O16'] = 2.33427e22 + cool_dict['O17'] = 8.89086e18 + + # Store these dictionaries in the initial conditions dictionary + initial_density = OrderedDict() + initial_density['fuel_gd'] = fuel_gd_dict + initial_density['fuel'] = fuel_dict + initial_density['gap'] = gap_dict + initial_density['clad'] = clad_dict + initial_density['cool'] = cool_dict + + # Set up libraries to use + temperature = OrderedDict() + sab = OrderedDict() + + # Toggle betweeen MCNP and NNDC data + MCNP = False + + if MCNP: + temperature['fuel_gd'] = 900.0 + temperature['fuel'] = 900.0 + # We approximate temperature of everything as 600K, even though it was + # actually 580K. + temperature['gap'] = 600.0 + temperature['clad'] = 600.0 + temperature['cool'] = 600.0 + else: + temperature['fuel_gd'] = 293.6 + temperature['fuel'] = 293.6 + temperature['gap'] = 293.6 + temperature['clad'] = 293.6 + temperature['cool'] = 293.6 + + sab['cool'] = 'c_H_in_H2O' + + # Set up burnable materials + burn = OrderedDict() + burn['fuel_gd'] = True + burn['fuel'] = True + burn['gap'] = False + burn['clad'] = False + burn['cool'] = False + + return temperature, sab, initial_density, burn + +def segment_pin(n_rings, n_wedges, r_fuel, r_gap, r_clad): + """ Calculates a segmented pin. + + Separates a pin with n_rings and n_wedges. All cells have equal volume. + Pin is centered at origin. + """ + + # Calculate all the volumes of interest + v_fuel = math.pi * r_fuel**2 + v_gap = math.pi * r_gap**2 - v_fuel + v_clad = math.pi * r_clad**2 - v_fuel - v_gap + v_ring = v_fuel / n_rings + v_segment = v_ring / n_wedges + + # Compute ring radiuses + r_rings = np.zeros(n_rings) + + for i in range(n_rings): + r_rings[i] = math.sqrt(1.0/(math.pi) * v_ring * (i+1)) + + # Compute thetas + theta = np.linspace(0, 2*math.pi, n_wedges + 1) + + # Compute surfaces + fuel_rings = [openmc.ZCylinder(x0=0, y0=0, R=r_rings[i]) + for i in range(n_rings)] + + fuel_wedges = [openmc.Plane(A=math.cos(theta[i]), B=math.sin(theta[i])) + for i in range(n_wedges)] + + gap_ring = openmc.ZCylinder(x0=0, y0=0, R=r_gap) + clad_ring = openmc.ZCylinder(x0=0, y0=0, R=r_clad) + + # Create cells + fuel_cells = [] + if n_wedges == 1: + for i in range(n_rings): + cell = openmc.Cell(name='fuel') + if i == 0: + cell.region = -fuel_rings[0] + else: + cell.region = +fuel_rings[i-1] & -fuel_rings[i] + fuel_cells.append(cell) + else: + for i in range(n_rings): + for j in range(n_wedges): + cell = openmc.Cell(name='fuel') + if i == 0: + if j != n_wedges-1: + cell.region = (-fuel_rings[0] + & +fuel_wedges[j] + & -fuel_wedges[j+1]) + else: + cell.region = (-fuel_rings[0] + & +fuel_wedges[j] + & -fuel_wedges[0]) + else: + if j != n_wedges-1: + cell.region = (+fuel_rings[i-1] + & -fuel_rings[i] + & +fuel_wedges[j] + & -fuel_wedges[j+1]) + else: + cell.region = (+fuel_rings[i-1] + & -fuel_rings[i] + & +fuel_wedges[j] + & -fuel_wedges[0]) + fuel_cells.append(cell) + + # Gap ring + gap_cell = openmc.Cell(name='gap') + gap_cell.region = +fuel_rings[-1] & -gap_ring + fuel_cells.append(gap_cell) + + # Clad ring + clad_cell = openmc.Cell(name='clad') + clad_cell.region = +gap_ring & -clad_ring + fuel_cells.append(clad_cell) + + # Moderator + mod_cell = openmc.Cell(name='cool') + mod_cell.region = +clad_ring + fuel_cells.append(mod_cell) + + # Form universe + fuel_u = openmc.Universe() + fuel_u.add_cells(fuel_cells) + + return fuel_u, v_segment, v_gap, v_clad + +def generate_geometry(n_rings, n_wedges): + """ Generates example geometry. + + This function creates the initial geometry, a 9 pin reflective problem. + One pin, containing gadolinium, is discretized into sectors. + + In addition to what one would do with the general OpenMC geometry code, it + is necessary to create a dictionary, volume, that maps a cell ID to a + volume. Further, by naming cells the same as the above materials, the code + can automatically handle the mapping. + + Parameters + ---------- + n_rings : int + Number of rings to generate for the geometry + n_wedges : int + Number of wedges to generate for the geometry + """ + + pitch = 1.26197 + r_fuel = 0.412275 + r_gap = 0.418987 + r_clad = 0.476121 + + n_pin = 3 + + # This table describes the 'fuel' to actual type mapping + # It's not necessary to do it this way. Just adjust the initial conditions + # below. + mapping = ['fuel', 'fuel', 'fuel', + 'fuel', 'fuel_gd', 'fuel', + 'fuel', 'fuel', 'fuel'] + + # Form pin cell + fuel_u, v_segment, v_gap, v_clad = segment_pin(n_rings, n_wedges, r_fuel, r_gap, r_clad) + + # Form lattice + all_water_c = openmc.Cell(name='cool') + all_water_u = openmc.Universe(cells=(all_water_c, )) + + lattice = openmc.RectLattice() + lattice.pitch = [pitch]*2 + lattice.lower_left = [-pitch*n_pin/2, -pitch*n_pin/2] + lattice_array = [[fuel_u for i in range(n_pin)] for j in range(n_pin)] + lattice.universes = lattice_array + lattice.outer = all_water_u + + # Bound universe + x_low = openmc.XPlane(x0=-pitch*n_pin/2, boundary_type='reflective') + x_high = openmc.XPlane(x0=pitch*n_pin/2, boundary_type='reflective') + y_low = openmc.YPlane(y0=-pitch*n_pin/2, boundary_type='reflective') + y_high = openmc.YPlane(y0=pitch*n_pin/2, boundary_type='reflective') + z_low = openmc.ZPlane(z0=-10, boundary_type='reflective') + z_high = openmc.ZPlane(z0=10, boundary_type='reflective') + + # Compute bounding box + lower_left = [-pitch*n_pin/2, -pitch*n_pin/2, -10] + upper_right = [pitch*n_pin/2, pitch*n_pin/2, 10] + + root_c = openmc.Cell(fill=lattice) + root_c.region = (+x_low & -x_high + & +y_low & -y_high + & +z_low & -z_high) + root_u = openmc.Universe(universe_id=0, cells=(root_c, )) + geometry = openmc.Geometry(root_u) + + v_cool = pitch**2 - (v_gap + v_clad + n_rings * n_wedges * v_segment) + + # Store volumes for later usage + volume = {'fuel': v_segment, 'gap':v_gap, 'clad':v_clad, 'cool':v_cool} + + return geometry, volume, mapping, lower_left, upper_right + +def generate_problem(n_rings=5, n_wedges=8): + """ Merges geometry and materials. + + This function initializes the materials for each cell using the dictionaries + provided by generate_initial_number_density. It is assumed a cell named + 'fuel' will have further region differentiation (see mapping). + + Parameters + ---------- + n_rings : int, optional + Number of rings to generate for the geometry + n_wedges : int, optional + Number of wedges to generate for the geometry + """ + + # Get materials dictionary, geometry, and volumes + temperature, sab, initial_density, burn = generate_initial_number_density() + geometry, volume, mapping, lower_left, upper_right = generate_geometry(n_rings, n_wedges) + + # Apply distribmats, fill geometry + cells = geometry.root_universe.get_all_cells() + for cell_id in cells: + cell = cells[cell_id] + if cell.name == 'fuel': + + omc_mats = [] + + for cell_type in mapping: + omc_mat = density_to_mat(initial_density[cell_type]) + + if cell_type in sab: + omc_mat.add_s_alpha_beta(sab[cell_type]) + omc_mat.temperature = temperature[cell_type] + omc_mat.depletable = burn[cell_type] + omc_mat.volume = volume['fuel'] + + omc_mats.append(omc_mat) + + cell.fill = omc_mats + elif cell.name != '': + omc_mat = density_to_mat(initial_density[cell.name]) + + if cell.name in sab: + omc_mat.add_s_alpha_beta(sab[cell.name]) + omc_mat.temperature = temperature[cell.name] + omc_mat.depletable = burn[cell.name] + omc_mat.volume = volume[cell.name] + + cell.fill = omc_mat + + return geometry, lower_left, upper_right diff --git a/scripts/example_plot.py b/scripts/example_plot.py new file mode 100644 index 0000000000..d2c6ee9d6a --- /dev/null +++ b/scripts/example_plot.py @@ -0,0 +1,46 @@ +"""An example file showing how to plot data from a simulation.""" + +import matplotlib.pyplot as plt + +from opendeplete import read_results, \ + evaluate_single_nuclide, \ + evaluate_reaction_rate, \ + evaluate_eigenvalue + +# Set variables for where the data is, and what we want to read out. +result_folder = "test" + +# Load data +results = read_results(result_folder + "/results.h5") + +cell = "5" +nuc = "Gd157" +rxn = "(n,gamma)" + +# Total number of nuclides +plt.figure() +# Pointwise data +x, y = evaluate_single_nuclide(results, cell, nuc) +plt.semilogy(x, y) + +plt.xlabel("Time, s") +plt.ylabel("Total Number") +plt.savefig("number.pdf") + +# Reaction rate +plt.figure() +x, y = evaluate_reaction_rate(results, cell, nuc, rxn) +plt.plot(x, y) +plt.xlabel("Time, s") +plt.ylabel("Reaction Rate, 1/s") + +plt.savefig("rate.pdf") + +# Eigenvalue +plt.figure() +x, y = evaluate_eigenvalue(results) +plt.plot(x, y) +plt.xlabel("Time, s") +plt.ylabel("Eigenvalue") + +plt.savefig("eigvl.pdf") diff --git a/scripts/example_run.py b/scripts/example_run.py new file mode 100644 index 0000000000..bb80f65820 --- /dev/null +++ b/scripts/example_run.py @@ -0,0 +1,39 @@ +"""An example file showing how to run a simulation.""" + +import numpy as np +import opendeplete + +import example_geometry + +# Load geometry from example +geometry, lower_left, upper_right = example_geometry.generate_problem() + +# Create dt vector for 5.5 months with 15 day timesteps +dt1 = 15*24*60*60 # 15 days +dt2 = 5.5*30*24*60*60 # 5.5 months +N = np.floor(dt2/dt1) + +dt = np.repeat([dt1], N) + +# Create settings variable +settings = opendeplete.OpenMCSettings() + +settings.openmc_call = "openmc" +# An example for mpiexec: +# settings.openmc_call = ["mpiexec", "openmc"] +settings.particles = 1000 +settings.batches = 100 +settings.inactive = 40 +settings.lower_left = lower_left +settings.upper_right = upper_right +settings.entropy_dimension = [10, 10, 1] + +joule_per_mev = 1.6021766208e-13 +settings.power = 2.337e15*4*joule_per_mev # MeV/second cm from CASMO +settings.dt_vec = dt +settings.output_dir = 'test' + +op = opendeplete.OpenMCOperator(geometry, settings) + +# Perform simulation using the MCNPX/MCNP6 algorithm +opendeplete.integrator.cecm(op) diff --git a/scripts/make_chain.py b/scripts/make_chain.py new file mode 100644 index 0000000000..2e0d9d3bc4 --- /dev/null +++ b/scripts/make_chain.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +import glob +import os +from zipfile import ZipFile + +import requests +from tqdm import tqdm +import opendeplete + + +urls = [ + 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-neutrons.zip', + 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-decay.zip', + 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-nfy.zip' +] + + +def download_file(url): + response = requests.get(url, stream=True) + filesize = int(response.headers.get('content-length')) + + # Check if file already downloaded + basename = url.split('/')[-1] + if os.path.exists(basename): + if os.path.getsize(basename) == filesize: + return basename + else: + overwrite = input('Overwrite {}? ([y]/n) '.format(basename)) + if overwrite.lower().startswith('n'): + return basename + + with open(basename, 'wb') as f: + with tqdm(desc='Downloading {}'.format(basename), + total=filesize, unit='B', unit_scale=True) as pbar: + for i, chunk in enumerate(response.iter_content(chunk_size=4096)): + pbar.update(4096) + if chunk: + f.write(chunk) + + return basename + + +def main(): + for url in urls: + basename = download_file(url) + with ZipFile(basename, 'r') as zf: + print('Extracting {}...'.format(basename)) + zf.extractall() + + decay_files = glob.glob(os.path.join('decay', '*.endf')) + nfy_files = glob.glob(os.path.join('nfy', '*.endf')) + neutron_files = glob.glob(os.path.join('neutrons', '*.endf')) + + chain = opendeplete.DepletionChain.from_endf(decay_files, nfy_files, neutron_files) + chain.xml_write('chain_endfb71.xml') + + +if __name__ == '__main__': + main() diff --git a/tests/deplete_tests/__init__.py b/tests/deplete_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/deplete_tests/dummy_geometry.py b/tests/deplete_tests/dummy_geometry.py new file mode 100644 index 0000000000..6101519411 --- /dev/null +++ b/tests/deplete_tests/dummy_geometry.py @@ -0,0 +1,165 @@ +""" The OpenMC wrapper module. + +This module implements the OpenDeplete -> OpenMC linkage. +""" + +import numpy as np +import scipy.sparse as sp + +from opendeplete.reaction_rates import ReactionRates +from opendeplete.function import Operator + +class DummyGeometry(Operator): + """ This is a dummy geometry class with no statistical uncertainty. + + y_1' = sin(y_2) y_1 + cos(y_1) y_2 + y_2' = -cos(y_2) y_1 + sin(y_1) y_2 + + y_1(0) = 1 + y_2(0) = 1 + + y_1(1.5) ~ 2.3197067076743316 + y_2(1.5) ~ 3.1726475740397628 + + """ + + def __init__(self, settings): + Operator.__init__(self, settings) + + @property + def chain(self): + return self + + def eval(self, vec, print_out=False): + """ Evaluates F(y) + + Parameters + ---------- + vec : list of numpy.array + Total atoms to be used in function. + print_out : bool, optional, ignored + Whether or not to print out time. + + Returns + ------- + k : float + Zero. + rates : ReactionRates + Reaction rates from this simulation. + seed : int + Zero. + """ + + cell_to_ind = {"1" : 0} + nuc_to_ind = {"1" : 0, "2" : 1} + react_to_ind = {"1" : 0} + + reaction_rates = ReactionRates(cell_to_ind, nuc_to_ind, react_to_ind) + + reaction_rates[0, 0, 0] = vec[0][0] + reaction_rates[0, 1, 0] = vec[0][1] + + # Create a fake rates object + + return 0.0, reaction_rates, 0 + + def form_matrix(self, rates): + """ Forms the f(y) matrix in y' = f(y)y. + + Nominally a depletion matrix, this is abstracted on the off chance + that the function f has nothing to do with depletion at all. + + Parameters + ---------- + rates : numpy.ndarray + Slice of reaction rates for a single material + + Returns + ------- + scipy.sparse.csr_matrix + Sparse matrix representing f(y). + """ + + y_1 = rates[0, 0] + y_2 = rates[1, 0] + + mat = np.zeros((2, 2)) + a11 = np.sin(y_2) + a12 = np.cos(y_1) + a21 = -np.cos(y_2) + a22 = np.sin(y_1) + + return sp.csr_matrix(np.array([[a11, a12], [a21, a22]])) + + @property + def volume(self): + """ + volume : dict of str float + Volumes of material + """ + + return {"1": 0.0} + + @property + def nuc_list(self): + """ + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + """ + + return ["1", "2"] + + @property + def burn_list(self): + """ + burn_list : list of str + A list of all cell IDs to be burned. Used for sorting the simulation. + """ + + return ["1"] + + @property + def mat_tally_ind(self): + """Maps cell name to index in global geometry.""" + return {"1": 0} + + + @property + def reaction_rates(self): + """ + reaction_rates : ReactionRates + Reaction rates from the last operator step. + """ + cell_to_ind = {"1" : 0} + nuc_to_ind = {"1" : 0, "2" : 1} + react_to_ind = {"1" : 0} + + return ReactionRates(cell_to_ind, nuc_to_ind, react_to_ind) + + def initial_condition(self): + """ Returns initial vector. + + Returns + ------- + list of numpy.array + Total density for initial conditions. + """ + + return [np.array((1.0, 1.0))] + + def get_results_info(self): + """ Returns volume list, cell lists, and nuc lists. + + Returns + ------- + volume : dict of str float + Volumes corresponding to materials in full_burn_dict + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all cell IDs to be burned. Used for sorting the simulation. + full_burn_dict : OrderedDict of str to int + Maps cell name to index in global geometry. + """ + + return self.volume, self.nuc_list, self.burn_list, self.mat_tally_ind diff --git a/tests/deplete_tests/example_geometry.py b/tests/deplete_tests/example_geometry.py new file mode 120000 index 0000000000..1071aabc05 --- /dev/null +++ b/tests/deplete_tests/example_geometry.py @@ -0,0 +1 @@ +../../scripts/example_geometry.py \ No newline at end of file diff --git a/tests/deplete_tests/test_atom_number.py b/tests/deplete_tests/test_atom_number.py new file mode 100644 index 0000000000..9a17230f86 --- /dev/null +++ b/tests/deplete_tests/test_atom_number.py @@ -0,0 +1,180 @@ +""" Tests for atom_number.py. """ + +import unittest + +import numpy as np + +from opendeplete import atom_number + +class TestAtomNumber(unittest.TestCase): + """ Tests for the AtomNumber class. """ + + def test_indexing(self): + """Tests the __getitem__ and __setitem__ routines simultaneously.""" + + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + number["10000", "U238"] = 1.0 + number["10001", "U238"] = 2.0 + number["10000", "U235"] = 3.0 + number["10001", "U235"] = 4.0 + + # String indexing + self.assertEqual(number["10000", "U238"], 1.0) + self.assertEqual(number["10001", "U238"], 2.0) + self.assertEqual(number["10000", "U235"], 3.0) + self.assertEqual(number["10001", "U235"], 4.0) + + # Int indexing + self.assertEqual(number[0, 0], 1.0) + self.assertEqual(number[1, 0], 2.0) + self.assertEqual(number[0, 1], 3.0) + self.assertEqual(number[1, 1], 4.0) + + number[0, 0] = 5.0 + + self.assertEqual(number[0, 0], 5.0) + self.assertEqual(number["10000", "U238"], 5.0) + + def test_n_mat(self): + """ Test number of materials property. """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + self.assertEqual(number.n_mat, 2) + + def test_n_nuc(self): + """ Test number of nuclides property. """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + self.assertEqual(number.n_nuc, 3) + + def test_burn_nuc_list(self): + """ Test the list of burned nuclides property """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + self.assertEqual(number.burn_nuc_list, ["U238", "U235"]) + + def test_burn_mat_list(self): + """ Test the list of burned nuclides property """ + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + self.assertEqual(number.burn_mat_list, ["10000", "10001"]) + + def test_density_indexing(self): + """Tests the get and set_atom_density routines simultaneously.""" + + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + number.set_atom_density("10000", "U238", 1.0) + number.set_atom_density("10001", "U238", 2.0) + number.set_atom_density("10002", "U238", 3.0) + number.set_atom_density("10000", "U235", 4.0) + number.set_atom_density("10001", "U235", 5.0) + number.set_atom_density("10002", "U235", 6.0) + number.set_atom_density("10000", "U234", 7.0) + number.set_atom_density("10001", "U234", 8.0) + number.set_atom_density("10002", "U234", 9.0) + + # String indexing + self.assertEqual(number.get_atom_density("10000", "U238"), 1.0) + self.assertEqual(number.get_atom_density("10001", "U238"), 2.0) + self.assertEqual(number.get_atom_density("10002", "U238"), 3.0) + self.assertEqual(number.get_atom_density("10000", "U235"), 4.0) + self.assertEqual(number.get_atom_density("10001", "U235"), 5.0) + self.assertEqual(number.get_atom_density("10002", "U235"), 6.0) + self.assertEqual(number.get_atom_density("10000", "U234"), 7.0) + self.assertEqual(number.get_atom_density("10001", "U234"), 8.0) + self.assertEqual(number.get_atom_density("10002", "U234"), 9.0) + + # Int indexing + self.assertEqual(number.get_atom_density(0, 0), 1.0) + self.assertEqual(number.get_atom_density(1, 0), 2.0) + self.assertEqual(number.get_atom_density(2, 0), 3.0) + self.assertEqual(number.get_atom_density(0, 1), 4.0) + self.assertEqual(number.get_atom_density(1, 1), 5.0) + self.assertEqual(number.get_atom_density(2, 1), 6.0) + self.assertEqual(number.get_atom_density(0, 2), 7.0) + self.assertEqual(number.get_atom_density(1, 2), 8.0) + self.assertEqual(number.get_atom_density(2, 2), 9.0) + + + number.set_atom_density(0, 0, 5.0) + + self.assertEqual(number.get_atom_density(0, 0), 5.0) + + # Verify volume is used correctly + self.assertEqual(number[0, 0], 5.0 * 0.38) + self.assertEqual(number[1, 0], 2.0 * 0.21) + self.assertEqual(number[2, 0], 3.0 * 1.0) + self.assertEqual(number[0, 1], 4.0 * 0.38) + self.assertEqual(number[1, 1], 5.0 * 0.21) + self.assertEqual(number[2, 1], 6.0 * 1.0) + self.assertEqual(number[0, 2], 7.0 * 0.38) + self.assertEqual(number[1, 2], 8.0 * 0.21) + self.assertEqual(number[2, 2], 9.0 * 1.0) + + def test_get_mat_slice(self): + """Tests getting slices.""" + + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + number.number = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) + + sl = number.get_mat_slice(0) + + np.testing.assert_array_equal(sl, np.array([1.0, 2.0])) + + sl = number.get_mat_slice("10000") + + np.testing.assert_array_equal(sl, np.array([1.0, 2.0])) + + def test_set_mat_slice(self): + """Tests getting slices.""" + + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + number.set_mat_slice(0, [1.0, 2.0]) + + self.assertEqual(number[0, 0], 1.0) + self.assertEqual(number[0, 1], 2.0) + + number.set_mat_slice("10000", [3.0, 4.0]) + + self.assertEqual(number[0, 0], 3.0) + self.assertEqual(number[0, 1], 4.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_cecm_regression.py b/tests/deplete_tests/test_cecm_regression.py new file mode 100644 index 0000000000..23a6342000 --- /dev/null +++ b/tests/deplete_tests/test_cecm_regression.py @@ -0,0 +1,69 @@ +""" Regression tests for cecm.py""" + +import os +import unittest + +import numpy as np + +import opendeplete +from opendeplete import results +from opendeplete import utilities +import test.dummy_geometry as dummy_geometry + + +class TestCECMRegression(unittest.TestCase): + """ Regression tests for opendeplete.integrator.cecm algorithm. + + These tests integrate a simple test problem described in dummy_geometry.py. + """ + + @classmethod + def setUpClass(cls): + """ Save current directory in case integrator crashes.""" + cls.cwd = os.getcwd() + cls.results = "test_integrator_regression" + + def test_cecm(self): + """ Integral regression test of integrator algorithm using CE/CM. """ + + settings = opendeplete.Settings() + settings.dt_vec = [0.75, 0.75] + settings.output_dir = self.results + + op = dummy_geometry.DummyGeometry(settings) + + # Perform simulation using the MCNPX/MCNP6 algorithm + opendeplete.cecm(op, print_out=False) + + # Load the files + res = results.read_results(settings.output_dir + "/results.h5") + + _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") + _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") + + # Mathematica solution + s1 = [1.86872629872102, 1.395525772416039] + s2 = [2.18097439443550, 2.69429754646747] + + tol = 1.0e-13 + + self.assertLess(np.absolute(y1[1] - s1[0]), tol) + self.assertLess(np.absolute(y2[1] - s1[1]), tol) + + self.assertLess(np.absolute(y1[2] - s2[0]), tol) + self.assertLess(np.absolute(y2[2] - s2[1]), tol) + + @classmethod + def tearDownClass(cls): + """ Clean up files""" + + os.chdir(cls.cwd) + + opendeplete.comm.barrier() + if opendeplete.comm.rank == 0: + os.remove(os.path.join(cls.results, "results.h5")) + os.rmdir(cls.results) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_cram.py b/tests/deplete_tests/test_cram.py new file mode 100644 index 0000000000..2744adbf48 --- /dev/null +++ b/tests/deplete_tests/test_cram.py @@ -0,0 +1,48 @@ +""" Tests for cram.py """ + +import unittest + +import numpy as np +import scipy.sparse as sp + +from opendeplete.integrator import CRAM16, CRAM48 + +class TestCram(unittest.TestCase): + """ Tests for cram.py + + Compares a few Mathematica matrix exponentials to CRAM16/CRAM48. + """ + + def test_CRAM16(self): + """ Test 16-term CRAM. """ + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 0.1 + + z = CRAM16(mat, x, dt) + + # Solution from mathematica + z0 = np.array((0.904837418035960, 0.576799023327476)) + + tol = 1.0e-15 + + self.assertLess(np.linalg.norm(z - z0), tol) + + def test_CRAM48(self): + """ Test 48-term CRAM. """ + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 0.1 + + z = CRAM48(mat, x, dt) + + # Solution from mathematica + z0 = np.array((0.904837418035960, 0.576799023327476)) + + tol = 1.0e-15 + + self.assertLess(np.linalg.norm(z - z0), tol) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_depletion_chain.py b/tests/deplete_tests/test_depletion_chain.py new file mode 100644 index 0000000000..216d1e68f7 --- /dev/null +++ b/tests/deplete_tests/test_depletion_chain.py @@ -0,0 +1,197 @@ +""" Tests for depletion_chain.py""" + +from collections import OrderedDict +import os +import unittest + +import numpy as np + +from opendeplete import comm, depletion_chain, reaction_rates, nuclide + + +class TestDepletionChain(unittest.TestCase): + """ Tests for DepletionChain class.""" + + def test__init__(self): + """ Test depletion chain initialization.""" + dep = depletion_chain.DepletionChain() + + self.assertIsInstance(dep.nuclides, list) + self.assertIsInstance(dep.nuclide_dict, OrderedDict) + self.assertIsInstance(dep.react_to_ind, OrderedDict) + + def test_n_nuclides(self): + """ Test depletion chain n_nuclides parameter. """ + dep = depletion_chain.DepletionChain() + + dep.nuclides = ["NucA", "NucB", "NucC"] + + self.assertEqual(dep.n_nuclides, 3) + + def test_from_endf(self): + """Test depletion chain building from ENDF. Empty at the moment until we figure + out a good way to unit-test this.""" + pass + + def test_xml_read(self): + """ Read chain_test.xml and ensure all values are correct. """ + # Unfortunately, this routine touches a lot of the code, but most of + # the components external to depletion_chain.py are simple storage + # types. + + dep = depletion_chain.DepletionChain.xml_read("chains/chain_test.xml") + + # Basic checks + self.assertEqual(dep.n_nuclides, 3) + + # A tests + nuc = dep.nuclides[dep.nuclide_dict["A"]] + + self.assertEqual(nuc.name, "A") + self.assertEqual(nuc.half_life, 2.36520E+04) + self.assertEqual(nuc.n_decay_modes, 2) + modes = nuc.decay_modes + self.assertEqual([m.target for m in modes], ["B", "C"]) + self.assertEqual([m.type for m in modes], ["beta1", "beta2"]) + self.assertEqual([m.branching_ratio for m in modes], [0.6, 0.4]) + self.assertEqual(nuc.n_reaction_paths, 1) + self.assertEqual([r.target for r in nuc.reactions], ["C"]) + self.assertEqual([r.type for r in nuc.reactions], ["(n,gamma)"]) + self.assertEqual([r.branching_ratio for r in nuc.reactions], [1.0]) + + # B tests + nuc = dep.nuclides[dep.nuclide_dict["B"]] + + self.assertEqual(nuc.name, "B") + self.assertEqual(nuc.half_life, 3.29040E+04) + self.assertEqual(nuc.n_decay_modes, 1) + modes = nuc.decay_modes + self.assertEqual([m.target for m in modes], ["A"]) + self.assertEqual([m.type for m in modes], ["beta"]) + self.assertEqual([m.branching_ratio for m in modes], [1.0]) + self.assertEqual(nuc.n_reaction_paths, 1) + self.assertEqual([r.target for r in nuc.reactions], ["C"]) + self.assertEqual([r.type for r in nuc.reactions], ["(n,gamma)"]) + self.assertEqual([r.branching_ratio for r in nuc.reactions], [1.0]) + + # C tests + nuc = dep.nuclides[dep.nuclide_dict["C"]] + + self.assertEqual(nuc.name, "C") + self.assertEqual(nuc.n_decay_modes, 0) + self.assertEqual(nuc.n_reaction_paths, 3) + self.assertEqual([r.target for r in nuc.reactions], [None, "A", "B"]) + self.assertEqual([r.type for r in nuc.reactions], ["fission", "(n,gamma)", "(n,gamma)"]) + self.assertEqual([r.branching_ratio for r in nuc.reactions], [1.0, 0.7, 0.3]) + + # Yield tests + self.assertEqual(nuc.yield_energies, [0.0253]) + self.assertEqual(list(nuc.yield_data.keys()), [0.0253]) + self.assertEqual(nuc.yield_data[0.0253], + [("A", 0.0292737), ("B", 0.002566345)]) + + def test_xml_write(self): + """Test writing a depletion chain to XML.""" + + # Prevent different MPI ranks from conflicting + filename = 'test%u.xml' % comm.rank + + A = nuclide.Nuclide() + A.name = "A" + A.half_life = 2.36520e4 + A.decay_modes = [ + nuclide.DecayTuple("beta1", "B", 0.6), + nuclide.DecayTuple("beta2", "C", 0.4) + ] + A.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)] + + B = nuclide.Nuclide() + B.name = "B" + B.half_life = 3.29040e4 + B.decay_modes = [nuclide.DecayTuple("beta", "A", 1.0)] + B.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)] + + C = nuclide.Nuclide() + C.name = "C" + C.reactions = [ + nuclide.ReactionTuple("fission", None, 2.0e8, 1.0), + nuclide.ReactionTuple("(n,gamma)", "A", 0.0, 0.7), + nuclide.ReactionTuple("(n,gamma)", "B", 0.0, 0.3) + ] + C.yield_energies = [0.0253] + C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} + + chain = depletion_chain.DepletionChain() + chain.nuclides = [A, B, C] + chain.xml_write(filename) + + original = open('chains/chain_test.xml', 'r').read() + chain_xml = open(filename, 'r').read() + self.assertEqual(original, chain_xml) + + os.remove(filename) + + def test_form_matrix(self): + """ Using chain_test, and a dummy reaction rate, compute the matrix. """ + # Relies on test_xml_read passing. + + dep = depletion_chain.DepletionChain.xml_read("chains/chain_test.xml") + + cell_ind = {"10000": 0, "10001": 1} + nuc_ind = {"A": 0, "B": 1, "C": 2} + react_ind = dep.react_to_ind + + react = reaction_rates.ReactionRates(cell_ind, nuc_ind, react_ind) + + dep.nuc_to_react_ind = nuc_ind + + react["10000", "C", "fission"] = 1.0 + react["10000", "A", "(n,gamma)"] = 2.0 + react["10000", "B", "(n,gamma)"] = 3.0 + react["10000", "C", "(n,gamma)"] = 4.0 + + mat = dep.form_matrix(react[0, :, :]) + # Loss A, decay, (n, gamma) + mat00 = -np.log(2) / 2.36520E+04 - 2 + # A -> B, decay, 0.6 branching ratio + mat10 = np.log(2) / 2.36520E+04 * 0.6 + # A -> C, decay, 0.4 branching ratio + (n,gamma) + mat20 = np.log(2) / 2.36520E+04 * 0.4 + 2 + + # B -> A, decay, 1.0 branching ratio + mat01 = np.log(2)/3.29040E+04 + # Loss B, decay, (n, gamma) + mat11 = -np.log(2)/3.29040E+04 - 3 + # B -> C, (n, gamma) + mat21 = 3 + + # C -> A fission, (n, gamma) + mat02 = 0.0292737 * 1.0 + 4.0 * 0.7 + # C -> B fission, (n, gamma) + mat12 = 0.002566345 * 1.0 + 4.0 * 0.3 + # Loss C, fission, (n, gamma) + mat22 = -1.0 - 4.0 + + self.assertEqual(mat[0, 0], mat00) + self.assertEqual(mat[1, 0], mat10) + self.assertEqual(mat[2, 0], mat20) + self.assertEqual(mat[0, 1], mat01) + self.assertEqual(mat[1, 1], mat11) + self.assertEqual(mat[2, 1], mat21) + self.assertEqual(mat[0, 2], mat02) + self.assertEqual(mat[1, 2], mat12) + self.assertEqual(mat[2, 2], mat22) + + def test_nuc_by_ind(self): + """ Test nuc_by_ind converter function. """ + dep = depletion_chain.DepletionChain() + + dep.nuclides = ["NucA", "NucB", "NucC"] + dep.nuclide_dict = {"NucA" : 0, "NucB" : 1, "NucC" : 2} + + self.assertEqual("NucA", dep.nuc_by_ind("NucA")) + self.assertEqual("NucB", dep.nuc_by_ind("NucB")) + self.assertEqual("NucC", dep.nuc_by_ind("NucC")) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_full.py b/tests/deplete_tests/test_full.py new file mode 100644 index 0000000000..f9a6c7493d --- /dev/null +++ b/tests/deplete_tests/test_full.py @@ -0,0 +1,119 @@ +""" Full system test suite. """ + +import shutil +import unittest + +import numpy as np + +import opendeplete +from opendeplete import results +from opendeplete import utilities +import test.example_geometry as example_geometry + + +class TestFull(unittest.TestCase): + """ Full system test suite. + + Runs an entire OpenMC simulation with depletion coupling and verifies + that the outputs match a reference file. Sensitive to changes in + OpenMC. + """ + + def test_full(self): + """ + This test runs a complete OpenMC simulation and tests the outputs. + It will take a while. + """ + + n_rings = 2 + n_wedges = 4 + + # Load geometry from example + geometry, lower_left, upper_right = \ + example_geometry.generate_problem(n_rings=n_rings, n_wedges=n_wedges) + + # Create dt vector for 3 steps with 15 day timesteps + dt1 = 15*24*60*60 # 15 days + dt2 = 1.5*30*24*60*60 # 1.5 months + N = np.floor(dt2/dt1) + + dt = np.repeat([dt1], N) + + # Create settings variable + settings = opendeplete.OpenMCSettings() + + settings.chain_file = "chains/chain_simple.xml" + settings.openmc_call = "openmc" + settings.openmc_npernode = 2 + settings.particles = 100 + settings.batches = 100 + settings.inactive = 40 + settings.lower_left = lower_left + settings.upper_right = upper_right + settings.entropy_dimension = [10, 10, 1] + + settings.round_number = True + settings.constant_seed = 1 + + joule_per_mev = 1.6021766208e-13 + settings.power = 2.337e15*4*joule_per_mev # MeV/second cm from CASMO + settings.dt_vec = dt + settings.output_dir = "test_full" + + op = opendeplete.OpenMCOperator(geometry, settings) + + # Perform simulation using the predictor algorithm + opendeplete.integrator.predictor(op) + + # Load the files + res_test = results.read_results(settings.output_dir + "/results.h5") + + # Load the reference + res_old = results.read_results("test/test_reference.h5") + + # Assert same mats + for mat in res_old[0].mat_to_ind: + self.assertIn(mat, res_test[0].mat_to_ind, + msg="Cell " + mat + " not in new results.") + for nuc in res_old[0].nuc_to_ind: + self.assertIn(nuc, res_test[0].nuc_to_ind, + msg="Nuclide " + nuc + " not in new results.") + + for mat in res_test[0].mat_to_ind: + self.assertIn(mat, res_old[0].mat_to_ind, + msg="Cell " + mat + " not in old results.") + for nuc in res_test[0].nuc_to_ind: + self.assertIn(nuc, res_old[0].nuc_to_ind, + msg="Nuclide " + nuc + " not in old results.") + + for mat in res_test[0].mat_to_ind: + for nuc in res_test[0].nuc_to_ind: + _, y_test = utilities.evaluate_single_nuclide(res_test, mat, nuc) + _, y_old = utilities.evaluate_single_nuclide(res_old, mat, nuc) + + # Test each point + + tol = 1.0e-6 + + correct = True + for i, ref in enumerate(y_old): + if ref != y_test[i]: + if ref != 0.0: + if np.abs(y_test[i] - ref) / ref > tol: + correct = False + else: + correct = False + + self.assertTrue(correct, + msg="Discrepancy in mat " + mat + " and nuc " + nuc + + "\n" + str(y_old) + "\n" + str(y_test)) + + def tearDown(self): + """ Clean up files""" + opendeplete.comm.barrier() + if opendeplete.comm.rank == 0: + shutil.rmtree("test_full", ignore_errors=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_integrator.py b/tests/deplete_tests/test_integrator.py new file mode 100644 index 0000000000..7e121ce167 --- /dev/null +++ b/tests/deplete_tests/test_integrator.py @@ -0,0 +1,116 @@ +""" Tests for integrator.py """ + +import copy +import os +import unittest +from unittest.mock import MagicMock + +import numpy as np + +from opendeplete import integrator, ReactionRates, results, comm + + +class TestIntegrator(unittest.TestCase): + """ Tests for integrator.py + + It is worth noting that opendeplete.integrate is extremely complex, to + the point I am unsure if it can be reasonably unit-tested. For the time + being, it will be left unimplemented and testing will be done via + regression (in test_integrator_regression.py) + """ + + def test_save_results(self): + """ Test data save module """ + + stages = 3 + + np.random.seed(comm.rank) + + # Mock geometry + op = MagicMock() + + vol_dict = {} + full_burn_dict = {} + + j = 0 + for i in range(comm.size): + vol_dict[str(2*i)] = 1.2 + vol_dict[str(2*i + 1)] = 1.2 + full_burn_dict[str(2*i)] = j + full_burn_dict[str(2*i + 1)] = j + 1 + j += 2 + + burn_list = [str(i) for i in range(2*comm.rank, 2*comm.rank + 2)] + nuc_list = ["na", "nb"] + + op.get_results_info.return_value = vol_dict, nuc_list, burn_list, full_burn_dict + + # Construct x + x1 = [] + x2 = [] + + for i in range(stages): + x1.append([np.random.rand(2), np.random.rand(2)]) + x2.append([np.random.rand(2), np.random.rand(2)]) + + # Construct r + cell_dict = {s:i for i, s in enumerate(burn_list)} + r1 = ReactionRates(cell_dict, {"na":0, "nb":1}, {"ra":0, "rb":1}) + r1.rates = np.random.rand(2, 2, 2) + + rate1 = [] + rate2 = [] + + for i in range(stages): + rate1.append(copy.deepcopy(r1)) + r1.rates = np.random.rand(2, 2, 2) + rate2.append(copy.deepcopy(r1)) + r1.rates = np.random.rand(2, 2, 2) + + # Create global terms + eigvl1 = np.random.rand(stages) + eigvl2 = np.random.rand(stages) + seed1 = [np.random.randint(100) for i in range(stages)] + seed2 = [np.random.randint(100) for i in range(stages)] + + eigvl1 = comm.bcast(eigvl1, root=0) + eigvl2 = comm.bcast(eigvl2, root=0) + seed1 = comm.bcast(seed1, root=0) + seed2 = comm.bcast(seed2, root=0) + + t1 = [0.0, 1.0] + t2 = [1.0, 2.0] + + integrator.save_results(op, x1, rate1, eigvl1, seed1, t1, 0) + integrator.save_results(op, x2, rate2, eigvl2, seed2, t2, 1) + + # Load the files + res = results.read_results("results.h5") + + for i in range(stages): + for mat_i, mat in enumerate(burn_list): + + for nuc_i, nuc in enumerate(nuc_list): + self.assertEqual(res[0][i, mat, nuc], x1[i][mat_i][nuc_i]) + self.assertEqual(res[1][i, mat, nuc], x2[i][mat_i][nuc_i]) + np.testing.assert_array_equal(res[0].rates[i][mat, nuc, :], + rate1[i][mat, nuc, :]) + np.testing.assert_array_equal(res[1].rates[i][mat, nuc, :], + rate2[i][mat, nuc, :]) + + np.testing.assert_array_equal(res[0].k, eigvl1) + np.testing.assert_array_equal(res[0].seeds, seed1) + np.testing.assert_array_equal(res[0].time, t1) + + np.testing.assert_array_equal(res[1].k, eigvl2) + np.testing.assert_array_equal(res[1].seeds, seed2) + np.testing.assert_array_equal(res[1].time, t2) + + # Delete files + comm.barrier() + if comm.rank == 0: + os.remove("results.h5") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_nuclide.py b/tests/deplete_tests/test_nuclide.py new file mode 100644 index 0000000000..c5439b2aa3 --- /dev/null +++ b/tests/deplete_tests/test_nuclide.py @@ -0,0 +1,121 @@ +""" Tests for nuclide.py. """ + +import unittest +import xml.etree.ElementTree as ET + +from opendeplete import nuclide + + +class TestNuclide(unittest.TestCase): + """ Tests for the nuclide class. """ + + def test_n_decay_modes(self): + """ Test the decay mode count parameter. """ + + nuc = nuclide.Nuclide() + + nuc.decay_modes = [ + nuclide.DecayTuple("beta1", "a", 0.5), + nuclide.DecayTuple("beta2", "b", 0.3), + nuclide.DecayTuple("beta3", "c", 0.2) + ] + + self.assertEqual(nuc.n_decay_modes, 3) + + def test_n_reaction_paths(self): + """ Test the reaction path count parameter. """ + + nuc = nuclide.Nuclide() + + nuc.reactions = [ + nuclide.ReactionTuple("(n,2n)", "a", 0.0, 1.0), + nuclide.ReactionTuple("(n,3n)", "b", 0.0, 1.0), + nuclide.ReactionTuple("(n,4n)", "c", 0.0, 1.0) + ] + + self.assertEqual(nuc.n_reaction_paths, 3) + + def test_xml_read(self): + """Test reading nuclide data from an XML element.""" + + data = """ + + + + + + + + + + 0.0253 + + Te134 Zr100 Xe138 + 0.062155 0.0497641 0.0481413 + + + + """ + + element = ET.fromstring(data) + u235 = nuclide.Nuclide.xml_read(element) + + self.assertEqual(u235.decay_modes, [ + nuclide.DecayTuple('sf', 'U235', 7.2e-11), + nuclide.DecayTuple('alpha', 'Th231', 1 - 7.2e-11) + ]) + self.assertEqual(u235.reactions, [ + nuclide.ReactionTuple('(n,2n)', 'U234', -5297781.0, 1.0), + nuclide.ReactionTuple('(n,3n)', 'U233', -12142300.0, 1.0), + nuclide.ReactionTuple('(n,4n)', 'U232', -17885600.0, 1.0), + nuclide.ReactionTuple('(n,gamma)', 'U236', 6545200.0, 1.0), + nuclide.ReactionTuple('fission', None, 193405400.0, 1.0), + ]) + self.assertEqual(u235.yield_energies, [0.0253]) + self.assertEqual(u235.yield_data, { + 0.0253: [('Te134', 0.062155), ('Zr100', 0.0497641), + ('Xe138', 0.0481413)] + }) + + def test_xml_write(self): + """Test writing nuclide data to an XML element.""" + + C = nuclide.Nuclide() + C.name = "C" + C.half_life = 0.123 + C.decay_modes = [ + nuclide.DecayTuple('beta-', 'B', 0.99), + nuclide.DecayTuple('alpha', 'D', 0.01) + ] + C.reactions = [ + nuclide.ReactionTuple('fission', None, 2.0e8, 1.0), + nuclide.ReactionTuple('(n,gamma)', 'A', 0.0, 1.0) + ] + C.yield_energies = [0.0253] + C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} + element = C.xml_write() + + self.assertEqual(element.get("half_life"), "0.123") + + decay_elems = element.findall("decay_type") + self.assertEqual(len(decay_elems), 2) + self.assertEqual(decay_elems[0].get("type"), "beta-") + self.assertEqual(decay_elems[0].get("target"), "B") + self.assertEqual(decay_elems[0].get("branching_ratio"), "0.99") + self.assertEqual(decay_elems[1].get("type"), "alpha") + self.assertEqual(decay_elems[1].get("target"), "D") + self.assertEqual(decay_elems[1].get("branching_ratio"), "0.01") + + rx_elems = element.findall("reaction_type") + self.assertEqual(len(rx_elems), 2) + self.assertEqual(rx_elems[0].get("type"), "fission") + self.assertEqual(float(rx_elems[0].get("Q")), 2.0e8) + self.assertEqual(rx_elems[1].get("type"), "(n,gamma)") + self.assertEqual(rx_elems[1].get("target"), "A") + self.assertEqual(float(rx_elems[1].get("Q")), 0.0) + + self.assertIsNotNone(element.find('neutron_fission_yields')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_predictor_regression.py b/tests/deplete_tests/test_predictor_regression.py new file mode 100644 index 0000000000..c72ae8a475 --- /dev/null +++ b/tests/deplete_tests/test_predictor_regression.py @@ -0,0 +1,68 @@ +""" Regression tests for predictor.py""" + +import os +import unittest + +import numpy as np + +import opendeplete +from opendeplete import results +from opendeplete import utilities +import test.dummy_geometry as dummy_geometry + +class TestPredictorRegression(unittest.TestCase): + """ Regression tests for opendeplete.integrator.predictor algorithm. + + These tests integrate a simple test problem described in dummy_geometry.py. + """ + + @classmethod + def setUpClass(cls): + """ Save current directory in case integrator crashes.""" + cls.cwd = os.getcwd() + cls.results = "test_integrator_regression" + + def test_predictor(self): + """ Integral regression test of integrator algorithm using CE/CM. """ + + settings = opendeplete.Settings() + settings.dt_vec = [0.75, 0.75] + settings.output_dir = self.results + + op = dummy_geometry.DummyGeometry(settings) + + # Perform simulation using the predictor algorithm + opendeplete.predictor(op, print_out=False) + + # Load the files + res = results.read_results(settings.output_dir + "/results.h5") + + _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") + _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") + + # Mathematica solution + s1 = [2.46847546272295, 0.986431226850467] + s2 = [4.11525874568034, -0.0581692232513460] + + tol = 1.0e-13 + + self.assertLess(np.absolute(y1[1] - s1[0]), tol) + self.assertLess(np.absolute(y2[1] - s1[1]), tol) + + self.assertLess(np.absolute(y1[2] - s2[0]), tol) + self.assertLess(np.absolute(y2[2] - s2[1]), tol) + + @classmethod + def tearDownClass(cls): + """ Clean up files""" + + os.chdir(cls.cwd) + + opendeplete.comm.barrier() + if opendeplete.comm.rank == 0: + os.remove(os.path.join(cls.results, "results.h5")) + os.rmdir(cls.results) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_reaction_rates.py b/tests/deplete_tests/test_reaction_rates.py new file mode 100644 index 0000000000..4821ec18cd --- /dev/null +++ b/tests/deplete_tests/test_reaction_rates.py @@ -0,0 +1,86 @@ +""" Tests for reaction_rates.py. """ + +import unittest + +from opendeplete import reaction_rates + + +class TestReactionRates(unittest.TestCase): + """ Tests for the ReactionRates class. """ + + def test_indexing(self): + """Tests the __getitem__ and __setitem__ routines simultaneously.""" + + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1} + + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + + rates["10000", "U238", "fission"] = 1.0 + rates["10001", "U238", "fission"] = 2.0 + rates["10000", "U235", "fission"] = 3.0 + rates["10001", "U235", "fission"] = 4.0 + rates["10000", "U238", "(n,gamma)"] = 5.0 + rates["10001", "U238", "(n,gamma)"] = 6.0 + rates["10000", "U235", "(n,gamma)"] = 7.0 + rates["10001", "U235", "(n,gamma)"] = 8.0 + + # String indexing + self.assertEqual(rates["10000", "U238", "fission"], 1.0) + self.assertEqual(rates["10001", "U238", "fission"], 2.0) + self.assertEqual(rates["10000", "U235", "fission"], 3.0) + self.assertEqual(rates["10001", "U235", "fission"], 4.0) + self.assertEqual(rates["10000", "U238", "(n,gamma)"], 5.0) + self.assertEqual(rates["10001", "U238", "(n,gamma)"], 6.0) + self.assertEqual(rates["10000", "U235", "(n,gamma)"], 7.0) + self.assertEqual(rates["10001", "U235", "(n,gamma)"], 8.0) + + # Int indexing + self.assertEqual(rates[0, 0, 0], 1.0) + self.assertEqual(rates[1, 0, 0], 2.0) + self.assertEqual(rates[0, 1, 0], 3.0) + self.assertEqual(rates[1, 1, 0], 4.0) + self.assertEqual(rates[0, 0, 1], 5.0) + self.assertEqual(rates[1, 0, 1], 6.0) + self.assertEqual(rates[0, 1, 1], 7.0) + self.assertEqual(rates[1, 1, 1], 8.0) + + rates[0, 0, 0] = 5.0 + + self.assertEqual(rates[0, 0, 0], 5.0) + self.assertEqual(rates["10000", "U238", "fission"], 5.0) + + def test_n_mat(self): + """ Test number of materials property. """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} + + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + + self.assertEqual(rates.n_mat, 2) + + def test_n_nuc(self): + """ Test number of nuclides property. """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} + + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + + self.assertEqual(rates.n_nuc, 3) + + def test_n_react(self): + """ Test number of reactions property. """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} + + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + + self.assertEqual(rates.n_react, 4) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/deplete_tests/test_reference.h5 b/tests/deplete_tests/test_reference.h5 new file mode 100644 index 0000000000000000000000000000000000000000..ef3ae0090943bc7ecdc01a1afaa70c0216e029f0 GIT binary patch literal 165384 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

+|}q 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 literal 0 HcmV?d00001 diff --git a/tests/deplete_tests/test_utilities.py b/tests/deplete_tests/test_utilities.py new file mode 100644 index 0000000000..faa1a500b4 --- /dev/null +++ b/tests/deplete_tests/test_utilities.py @@ -0,0 +1,68 @@ +""" Full system test suite. """ + +import unittest + +import numpy as np + +from opendeplete import results +from opendeplete import utilities + + +class TestUtilities(unittest.TestCase): + """ Tests the utilities classes. + + This also tests the results read/write code. + """ + + def test_evaluate_single_nuclide(self): + """ Tests evaluating single nuclide utility code. + """ + + # Load the reference + res = results.read_results("test/test_reference.h5") + + x, y = utilities.evaluate_single_nuclide(res, "1", "Xe135") + + x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + y_ref = [6.6747328233649218e+08, 3.5519299354458244e+14, + 3.4599104054580338e+14, 3.3821165110278112e+14] + + np.testing.assert_array_equal(x, x_ref) + np.testing.assert_array_equal(y, y_ref) + + def test_evaluate_reaction_rate(self): + """ Tests evaluating reaction rate utility code. + """ + + # Load the reference + res = results.read_results("test/test_reference.h5") + + x, y = utilities.evaluate_reaction_rate(res, "1", "Xe135", "(n,gamma)") + + x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + xe_ref = np.array([6.6747328233649218e+08, 3.5519299354458244e+14, + 3.4599104054580338e+14, 3.3821165110278112e+14]) + r_ref = np.array([4.0643598574337784e-05, 4.1457730544386974e-05, + 3.4121248544056681e-05, 3.9204686657643301e-05]) + + np.testing.assert_array_equal(x, x_ref) + np.testing.assert_array_equal(y, xe_ref * r_ref) + + def test_evaluate_eigenvalue(self): + """ Tests evaluating eigenvalue + """ + + # Load the reference + res = results.read_results("test/test_reference.h5") + + x, y = utilities.evaluate_eigenvalue(res) + + x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + y_ref = [1.1921986054449838, 1.1712785643938586, 1.1927099024502694, 1.2269183590698847] + + np.testing.assert_array_equal(x, x_ref) + np.testing.assert_array_equal(y, y_ref) + + +if __name__ == '__main__': + unittest.main() From 37f552a5dcdcb2d16d9db21e95f099c214c3bda7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 9 Feb 2018 14:01:59 -0600 Subject: [PATCH 02/68] Fix deplete imports --- openmc/deplete/__init__.py | 6 +-- openmc/deplete/atom_number.py | 18 +++---- openmc/deplete/depletion_chain.py | 18 +++---- openmc/deplete/function.py | 14 ++--- openmc/deplete/integrator/save_results.py | 3 +- openmc/deplete/openmc_wrapper.py | 52 +++++++++---------- openmc/deplete/reaction_rates.py | 6 +-- openmc/deplete/results.py | 23 ++++---- openmc/deplete/utilities.py | 10 ++-- scripts/example_geometry.py | 3 +- scripts/example_plot.py | 7 +-- scripts/example_run.py | 8 +-- scripts/make_chain.py | 4 +- tests/deplete_tests/dummy_geometry.py | 22 +++----- tests/deplete_tests/test_atom_number.py | 12 ++--- tests/deplete_tests/test_cecm_regression.py | 16 +++--- tests/deplete_tests/test_cram.py | 2 +- tests/deplete_tests/test_depletion_chain.py | 3 +- tests/deplete_tests/test_full.py | 22 ++++---- tests/deplete_tests/test_integrator.py | 3 +- tests/deplete_tests/test_nuclide.py | 2 +- .../test_predictor_regression.py | 16 +++--- tests/deplete_tests/test_reaction_rates.py | 2 +- tests/deplete_tests/test_utilities.py | 15 +++--- 24 files changed, 141 insertions(+), 146 deletions(-) diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 994a51e12c..4bdde3935e 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -1,8 +1,8 @@ """ -OpenDeplete -=========== +openmc.deplete +============== -A simple depletion front-end tool. +A depletion front-end tool. """ from .dummy_comm import DummyCommunicator diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 03bedbf531..63c9af8364 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -7,7 +7,7 @@ import numpy as np class AtomNumber(object): - """ AtomNumber module. + """AtomNumber module. An ndarray to store atom densities with string, integer, or slice indexing. @@ -71,7 +71,7 @@ class AtomNumber(object): self._burn_mat_list = None def __getitem__(self, pos): - """ Retrieves total atom number from AtomNumber. + """Retrieves total atom number from AtomNumber. Parameters ---------- @@ -95,7 +95,7 @@ class AtomNumber(object): return self.number[mat, nuc] def __setitem__(self, pos, val): - """ Sets total atom number into AtomNumber. + """Sets total atom number into AtomNumber. Parameters ---------- @@ -116,7 +116,7 @@ class AtomNumber(object): self.number[mat, nuc] = val def get_atom_density(self, mat, nuc): - """ Accesses atom density instead of total number. + """Accesses atom density instead of total number. Parameters ---------- @@ -139,7 +139,7 @@ class AtomNumber(object): return self[mat, nuc] / self.volume[mat] def set_atom_density(self, mat, nuc, val): - """ Sets atom density instead of total number. + """Sets atom density instead of total number. Parameters ---------- @@ -159,7 +159,7 @@ class AtomNumber(object): self[mat, nuc] = val * self.volume[mat] def get_mat_slice(self, mat): - """ Gets atom quantity indexed by mats for all burned nuclides + """Gets atom quantity indexed by mats for all burned nuclides Parameters ---------- @@ -178,7 +178,7 @@ class AtomNumber(object): return self[mat, 0:self.n_nuc_burn] def set_mat_slice(self, mat, val): - """ Sets atom quantity indexed by mats for all burned nuclides + """Sets atom quantity indexed by mats for all burned nuclides Parameters ---------- @@ -205,7 +205,7 @@ class AtomNumber(object): @property def burn_nuc_list(self): - """ burn_nuc_list : list of str + """burn_nuc_list : list of str A list of all nuclide material names. Used for sorting the simulation. """ @@ -221,7 +221,7 @@ class AtomNumber(object): @property def burn_mat_list(self): - """ burn_mat_list : list of str + """burn_mat_list : list of str A list of all burning material names. Used for sorting the simulation. """ diff --git a/openmc/deplete/depletion_chain.py b/openmc/deplete/depletion_chain.py index 05cc9db435..af126035f6 100644 --- a/openmc/deplete/depletion_chain.py +++ b/openmc/deplete/depletion_chain.py @@ -11,9 +11,6 @@ import math import re import os -from tqdm import tqdm -import scipy.sparse as sp -import openmc.data # Try to use lxml if it is available. It preserves the order of attributes and # provides a pretty-printer by default. If not available, use OpenMC function to # pretty print. @@ -22,9 +19,12 @@ try: _have_lxml = True except ImportError: import xml.etree.ElementTree as ET - from openmc.clean_xml import clean_xml_indentation _have_lxml = False +from tqdm import tqdm +import scipy.sparse as sp +import openmc.data +from openmc.clean_xml import clean_xml_indentation from .nuclide import Nuclide, DecayTuple, ReactionTuple @@ -109,7 +109,7 @@ def replace_missing(product, decay_data): class DepletionChain(object): - """ The DepletionChain class. + """The DepletionChain class. This class contains a full representation of a depletion chain. @@ -334,7 +334,7 @@ class DepletionChain(object): # Load XML tree try: root = ET.parse(filename) - except: + except Exception: if filename is None: print("No chain specified, either manually or in environment variable OPENDEPLETE_CHAIN.") else: @@ -374,11 +374,11 @@ class DepletionChain(object): if _have_lxml: tree.write(filename, encoding='utf-8', pretty_print=True) else: - clean_xml_indentation(root_elem, spaces_per_level=2) + clean_xml_indentation(root_elem) tree.write(filename, encoding='utf-8') def form_matrix(self, rates): - """ Forms depletion matrix. + """Forms depletion matrix. Parameters ---------- @@ -457,7 +457,7 @@ class DepletionChain(object): return matrix_dok.tocsr() def nuc_by_ind(self, ind): - """ Extracts nuclides from the list by dictionary key. + """Extracts nuclides from the list by dictionary key. Parameters ---------- diff --git a/openmc/deplete/function.py b/openmc/deplete/function.py index 74eb92422b..bcc055e67b 100644 --- a/openmc/deplete/function.py +++ b/openmc/deplete/function.py @@ -6,8 +6,9 @@ to run a full depletion simulation. from abc import ABCMeta, abstractmethod + class Settings(object): - """ The Settings class. + """The Settings class. Contains all parameters necessary for the integrator. @@ -24,8 +25,9 @@ class Settings(object): self.dt_vec = None self.output_dir = None + class Operator(metaclass=ABCMeta): - """ The Operator metaclass. + """The Operator metaclass. This defines all functions that the integrator needs to operate. @@ -40,7 +42,7 @@ class Operator(metaclass=ABCMeta): @abstractmethod def initial_condition(self): - """ Performs final setup and returns initial condition. + """Performs final setup and returns initial condition. Returns ------- @@ -52,7 +54,7 @@ class Operator(metaclass=ABCMeta): @abstractmethod def eval(self, vec, print_out=True): - """ Runs a simulation. + """Runs a simulation. Parameters ---------- @@ -75,7 +77,7 @@ class Operator(metaclass=ABCMeta): @abstractmethod def get_results_info(self): - """ Returns volume list, cell lists, and nuc lists. + """Returns volume list, cell lists, and nuc lists. Returns ------- @@ -93,7 +95,7 @@ class Operator(metaclass=ABCMeta): @abstractmethod def form_matrix(self, y, mat): - """ Forms the f(y) matrix in y' = f(y)y. + """Forms the f(y) matrix in y' = f(y)y. Nominally a depletion matrix, this is abstracted on the off chance that the function f has nothing to do with depletion at all. diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py index 35cbc7f3f1..4f20b52fde 100644 --- a/openmc/deplete/integrator/save_results.py +++ b/openmc/deplete/integrator/save_results.py @@ -1,7 +1,8 @@ """ Generic result saving code for integrators. """ -from opendeplete.results import Results, write_results +from ..results import Results, write_results + def save_results(op, x, rates, eigvls, seeds, t, step_ind): """ Creates and writes results to disk diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 347dc71857..05b5057d20 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -1,6 +1,6 @@ -""" The OpenMC wrapper module. +"""The OpenMC wrapper module. -This module implements the OpenDeplete -> OpenMC linkage. +This module implements the depletion -> OpenMC linkage. """ import copy @@ -14,14 +14,13 @@ try: _have_lxml = True except ImportError: import xml.etree.ElementTree as ET - from openmc.clean_xml import clean_xml_indentation _have_lxml = False import h5py import numpy as np + import openmc import openmc.capi - from . import comm from .atom_number import AtomNumber from .depletion_chain import DepletionChain @@ -194,7 +193,7 @@ class OpenMCOperator(Operator): # Clear out OpenMC, create task lists, distribute if comm.rank == 0: - clean_up_openmc() + openmc.reset_auto_ids() mat_burn_list, mat_not_burn_list, volume, self.mat_tally_ind, \ nuc_dict = self.extract_mat_ids() else: @@ -224,7 +223,7 @@ class OpenMCOperator(Operator): openmc.capi.finalize() def extract_mat_ids(self): - """ Extracts materials and assigns them to processes. + """Extracts materials and assigns them to processes. Returns ------- @@ -308,7 +307,7 @@ class OpenMCOperator(Operator): return mat_burn_lists, mat_not_burn_lists, volume, mat_tally_ind, nuc_dict def extract_number(self, mat_burn, mat_not_burn, volume, nuc_dict): - """ Construct self.number read from geometry + """Construct self.number read from geometry Parameters ---------- @@ -356,7 +355,7 @@ class OpenMCOperator(Operator): self.set_number_from_mat(mat) def set_number_from_mat(self, mat): - """ Extracts material and number densities from openmc.Material + """Extracts material and number densities from openmc.Material Parameters ---------- @@ -369,12 +368,11 @@ class OpenMCOperator(Operator): nuc_dens = mat.get_nuclide_atom_densities() for nuclide in nuc_dens: - name = nuclide.name number = nuc_dens[nuclide][1] * 1.0e24 - self.number.set_atom_density(mat_id, name, number) + self.number.set_atom_density(mat_id, nuclide, number) def initialize_reaction_rates(self): - """ Create reaction rates object. """ + """Create reaction rates object. """ self.reaction_rates = ReactionRates( self.burn_mat_to_ind, self.burn_nuc_to_ind, @@ -383,7 +381,7 @@ class OpenMCOperator(Operator): self.chain.nuc_to_react_ind = self.burn_nuc_to_ind def eval(self, vec, print_out=True): - """ Runs a simulation. + """Runs a simulation. Parameters ---------- @@ -405,7 +403,7 @@ class OpenMCOperator(Operator): """ # Prevent OpenMC from complaining about re-creating tallies - clean_up_openmc() + openmc.reset_auto_ids() # Update status self.set_density(vec) @@ -435,7 +433,7 @@ class OpenMCOperator(Operator): return k, copy.deepcopy(self.reaction_rates), self.seed def form_matrix(self, y, mat): - """ Forms the depletion matrix. + """Forms the depletion matrix. Parameters ---------- @@ -453,7 +451,7 @@ class OpenMCOperator(Operator): return copy.deepcopy(self.chain.form_matrix(y[mat, :, :])) def initial_condition(self): - """ Performs final setup and returns initial condition. + """Performs final setup and returns initial condition. Returns ------- @@ -513,7 +511,7 @@ class OpenMCOperator(Operator): mat_internal.set_densities(nuclides, densities) def generate_materials_xml(self): - """ Creates materials.xml from self.number. + """Creates materials.xml from self.number. Due to uncertainty with how MPI interacts with OpenMC API, this constructs the XML manually. The long term goal is to do this @@ -531,7 +529,7 @@ class OpenMCOperator(Operator): materials.export_to_xml() def generate_settings_xml(self): - """ Generates settings.xml. + """Generates settings.xml. This function creates settings.xml using the value of the settings variable. @@ -625,7 +623,7 @@ class OpenMCOperator(Operator): tally_dep.filters = [mat_filter] def total_density_list(self): - """ Returns a list of total density lists. + """Returns a list of total density lists. This list is in the exact same order as depletion_matrix_list, so that matrix exponentiation can be done easily. @@ -641,7 +639,7 @@ class OpenMCOperator(Operator): return total_density def set_density(self, total_density): - """ Sets density. + """Sets density. Sets the density in the exact same order as total_density_list outputs, allowing for internal consistency @@ -657,7 +655,7 @@ class OpenMCOperator(Operator): self.number.set_mat_slice(i, total_density[i]) def unpack_tallies_and_normalize(self): - """ Unpack tallies from OpenMC + """Unpack tallies from OpenMC This function reads the tallies generated by OpenMC (from the tally.xml file generated in generate_tally_xml) normalizes them so that the total @@ -754,7 +752,7 @@ class OpenMCOperator(Operator): return k_combined def load_participating(self): - """ Loads a cross_sections.xml file to find participating nuclides. + """Loads a cross_sections.xml file to find participating nuclides. This allows for nuclides that are important in the decay chain but not important neutronically, or have no cross section data. @@ -772,7 +770,7 @@ class OpenMCOperator(Operator): try: tree = ET.parse(filename) - except: + except Exception: if filename is None: msg = "No cross_sections.xml specified in materials." else: @@ -802,7 +800,7 @@ class OpenMCOperator(Operator): return len(self.chain.nuclides) def get_results_info(self): - """ Returns volume list, cell lists, and nuc lists. + """Returns volume list, cell lists, and nuc lists. Returns ------- @@ -829,8 +827,10 @@ class OpenMCOperator(Operator): return volume, nuc_list, burn_list, self.mat_tally_ind + def density_to_mat(dens_dict): - """ Generates an OpenMC material from a cell ID and self.number_density. + """Generates an OpenMC material from a cell ID and self.number_density. + Parameters ---------- m_id : int @@ -847,7 +847,3 @@ def density_to_mat(dens_dict): mat.set_density('sum') return mat - -def clean_up_openmc(): - """ Resets all automatic indexing in OpenMC, as these get in the way. """ - openmc.reset_auto_ids() diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index 7b934027a5..de3a6a7280 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -7,7 +7,7 @@ import numpy as np class ReactionRates(object): - """ ReactionRates class. + """ReactionRates class. An ndarray to store reaction rates with string, integer, or slice indexing. @@ -47,7 +47,7 @@ class ReactionRates(object): self.rates = np.zeros((self.n_mat, self.n_nuc, self.n_react)) def __getitem__(self, pos): - """ Retrieves an item from reaction_rates. + """Retrieves an item from reaction_rates. Parameters ---------- @@ -74,7 +74,7 @@ class ReactionRates(object): return self.rates[mat, nuc, react] def __setitem__(self, pos, val): - """ Sets an item from reaction_rates. + """Sets an item from reaction_rates. Parameters ---------- diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index c0ec1627ea..ae096b8ce5 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -1,4 +1,4 @@ -""" The results module. +"""The results module. Contains results generation and saving capabilities. """ @@ -14,8 +14,9 @@ from .reaction_rates import ReactionRates RESULTS_VERSION = 2 + class Results(object): - """ Contains output of opendeplete. + """Contains output of opendeplete. Attributes ---------- @@ -62,7 +63,7 @@ class Results(object): self.data = None def allocate(self, volume, nuc_list, burn_list, full_burn_dict, stages): - """ Allocates memory of Results. + """Allocates memory of Results. Parameters ---------- @@ -113,7 +114,7 @@ class Results(object): return self.data.shape[0] def __getitem__(self, pos): - """ Retrieves an item from results. + """Retrieves an item from results. Parameters ---------- @@ -137,7 +138,7 @@ class Results(object): return self.data[stage, mat, nuc] def __setitem__(self, pos, val): - """ Sets an item from results. + """Sets an item from results. Parameters ---------- @@ -159,7 +160,7 @@ class Results(object): self.data[stage, mat, nuc] = val def create_hdf5(self, handle): - """ Creates file structure for a blank HDF5 file. + """Creates file structure for a blank HDF5 file. Parameters ---------- @@ -232,7 +233,7 @@ class Results(object): handle.create_dataset("time", (1, 2), maxshape=(None, 2), dtype='float64') def to_hdf5(self, handle, index): - """ Converts results object into an hdf5 object. + """Converts results object into an hdf5 object. Parameters ---------- @@ -302,7 +303,7 @@ class Results(object): time_dset[index, :] = self.time def from_hdf5(self, handle, index): - """ Loads results object from HDF5. + """Loads results object from HDF5. Parameters ---------- @@ -360,7 +361,7 @@ class Results(object): def get_dict(number): - """ Given an operator nested dictionary, output indexing dictionaries. + """Given an operator nested dictionary, output indexing dictionaries. These indexing dictionaries map mat IDs and nuclide names to indices inside of Results.data. @@ -394,7 +395,7 @@ def get_dict(number): def write_results(result, filename, index): - """ Outputs result to an .hdf5 file. + """Outputs result to an .hdf5 file. Parameters ---------- @@ -418,7 +419,7 @@ def write_results(result, filename, index): def read_results(filename): - """ Reads out a list of results objects from an hdf5 file. + """Reads out a list of results objects from an hdf5 file. Parameters ---------- diff --git a/openmc/deplete/utilities.py b/openmc/deplete/utilities.py index 54632ed9c2..5433edce44 100644 --- a/openmc/deplete/utilities.py +++ b/openmc/deplete/utilities.py @@ -1,4 +1,4 @@ -""" The utilities module. +"""The utilities module. Contains functions that can be used to post-process objects that come out of the results module. @@ -6,8 +6,9 @@ the results module. import numpy as np + def evaluate_single_nuclide(results, cell, nuc): - """ Evaluates a single nuclide in a single cell from a results list. + """Evaluates a single nuclide in a single cell from a results list. Parameters ---------- @@ -38,7 +39,7 @@ def evaluate_single_nuclide(results, cell, nuc): return time, concentration def evaluate_reaction_rate(results, cell, nuc, rxn): - """ Evaluates a single nuclide reaction rate in a single cell from a results list. + """Evaluates a single nuclide reaction rate in a single cell from a results list. Parameters ---------- @@ -69,8 +70,9 @@ def evaluate_reaction_rate(results, cell, nuc, rxn): return time, rate + def evaluate_eigenvalue(results): - """ Evaluates the eigenvalue from a results list. + """Evaluates the eigenvalue from a results list. Parameters ---------- diff --git a/scripts/example_geometry.py b/scripts/example_geometry.py index 9afcc0d464..09ce0576f1 100644 --- a/scripts/example_geometry.py +++ b/scripts/example_geometry.py @@ -9,8 +9,7 @@ import math import numpy as np import openmc - -from opendeplete import density_to_mat +from openmc.deplete import density_to_mat def generate_initial_number_density(): diff --git a/scripts/example_plot.py b/scripts/example_plot.py index d2c6ee9d6a..c92fef6bf2 100644 --- a/scripts/example_plot.py +++ b/scripts/example_plot.py @@ -1,11 +1,8 @@ """An example file showing how to plot data from a simulation.""" import matplotlib.pyplot as plt - -from opendeplete import read_results, \ - evaluate_single_nuclide, \ - evaluate_reaction_rate, \ - evaluate_eigenvalue +from openmc.deplete import (read_results, evaluate_single_nuclide, + evaluate_reaction_rate, evaluate_eigenvalue) # Set variables for where the data is, and what we want to read out. result_folder = "test" diff --git a/scripts/example_run.py b/scripts/example_run.py index bb80f65820..82d0883c3a 100644 --- a/scripts/example_run.py +++ b/scripts/example_run.py @@ -1,7 +1,7 @@ """An example file showing how to run a simulation.""" import numpy as np -import opendeplete +import openmc.deplete import example_geometry @@ -16,7 +16,7 @@ N = np.floor(dt2/dt1) dt = np.repeat([dt1], N) # Create settings variable -settings = opendeplete.OpenMCSettings() +settings = openmc.deplete.OpenMCSettings() settings.openmc_call = "openmc" # An example for mpiexec: @@ -33,7 +33,7 @@ settings.power = 2.337e15*4*joule_per_mev # MeV/second cm from CASMO settings.dt_vec = dt settings.output_dir = 'test' -op = opendeplete.OpenMCOperator(geometry, settings) +op = openmc.deplete.OpenMCOperator(geometry, settings) # Perform simulation using the MCNPX/MCNP6 algorithm -opendeplete.integrator.cecm(op) +openmc.deplete.integrator.cecm(op) diff --git a/scripts/make_chain.py b/scripts/make_chain.py index 2e0d9d3bc4..ccf4ef9b7b 100644 --- a/scripts/make_chain.py +++ b/scripts/make_chain.py @@ -6,7 +6,7 @@ from zipfile import ZipFile import requests from tqdm import tqdm -import opendeplete +import openmc.deplete urls = [ @@ -52,7 +52,7 @@ def main(): nfy_files = glob.glob(os.path.join('nfy', '*.endf')) neutron_files = glob.glob(os.path.join('neutrons', '*.endf')) - chain = opendeplete.DepletionChain.from_endf(decay_files, nfy_files, neutron_files) + chain = openmc.deplete.DepletionChain.from_endf(decay_files, nfy_files, neutron_files) chain.xml_write('chain_endfb71.xml') diff --git a/tests/deplete_tests/dummy_geometry.py b/tests/deplete_tests/dummy_geometry.py index 6101519411..614cc726e3 100644 --- a/tests/deplete_tests/dummy_geometry.py +++ b/tests/deplete_tests/dummy_geometry.py @@ -1,16 +1,11 @@ -""" The OpenMC wrapper module. - -This module implements the OpenDeplete -> OpenMC linkage. -""" - import numpy as np import scipy.sparse as sp +from openmc.deplete.reaction_rates import ReactionRates +from openmc.deplete.function import Operator -from opendeplete.reaction_rates import ReactionRates -from opendeplete.function import Operator class DummyGeometry(Operator): - """ This is a dummy geometry class with no statistical uncertainty. + """This is a dummy geometry class with no statistical uncertainty. y_1' = sin(y_2) y_1 + cos(y_1) y_2 y_2' = -cos(y_2) y_1 + sin(y_1) y_2 @@ -24,14 +19,14 @@ class DummyGeometry(Operator): """ def __init__(self, settings): - Operator.__init__(self, settings) + super().__init__(settings) @property def chain(self): return self def eval(self, vec, print_out=False): - """ Evaluates F(y) + """Evaluates F(y) Parameters ---------- @@ -60,11 +55,10 @@ class DummyGeometry(Operator): reaction_rates[0, 1, 0] = vec[0][1] # Create a fake rates object - return 0.0, reaction_rates, 0 def form_matrix(self, rates): - """ Forms the f(y) matrix in y' = f(y)y. + """Forms the f(y) matrix in y' = f(y)y. Nominally a depletion matrix, this is abstracted on the off chance that the function f has nothing to do with depletion at all. @@ -137,7 +131,7 @@ class DummyGeometry(Operator): return ReactionRates(cell_to_ind, nuc_to_ind, react_to_ind) def initial_condition(self): - """ Returns initial vector. + """Returns initial vector. Returns ------- @@ -148,7 +142,7 @@ class DummyGeometry(Operator): return [np.array((1.0, 1.0))] def get_results_info(self): - """ Returns volume list, cell lists, and nuc lists. + """Returns volume list, cell lists, and nuc lists. Returns ------- diff --git a/tests/deplete_tests/test_atom_number.py b/tests/deplete_tests/test_atom_number.py index 9a17230f86..d36b96d38d 100644 --- a/tests/deplete_tests/test_atom_number.py +++ b/tests/deplete_tests/test_atom_number.py @@ -3,11 +3,11 @@ import unittest import numpy as np +from openmc.deplete import atom_number -from opendeplete import atom_number class TestAtomNumber(unittest.TestCase): - """ Tests for the AtomNumber class. """ + """Tests for the AtomNumber class.""" def test_indexing(self): """Tests the __getitem__ and __setitem__ routines simultaneously.""" @@ -41,7 +41,7 @@ class TestAtomNumber(unittest.TestCase): self.assertEqual(number["10000", "U238"], 5.0) def test_n_mat(self): - """ Test number of materials property. """ + """Test number of materials property. """ mat_to_ind = {"10000" : 0, "10001" : 1} nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} volume = {"10000" : 0.38, "10001" : 0.21} @@ -51,7 +51,7 @@ class TestAtomNumber(unittest.TestCase): self.assertEqual(number.n_mat, 2) def test_n_nuc(self): - """ Test number of nuclides property. """ + """Test number of nuclides property.""" mat_to_ind = {"10000" : 0, "10001" : 1} nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} volume = {"10000" : 0.38, "10001" : 0.21} @@ -61,7 +61,7 @@ class TestAtomNumber(unittest.TestCase): self.assertEqual(number.n_nuc, 3) def test_burn_nuc_list(self): - """ Test the list of burned nuclides property """ + """Test the list of burned nuclides property""" mat_to_ind = {"10000" : 0, "10001" : 1} nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} volume = {"10000" : 0.38, "10001" : 0.21} @@ -71,7 +71,7 @@ class TestAtomNumber(unittest.TestCase): self.assertEqual(number.burn_nuc_list, ["U238", "U235"]) def test_burn_mat_list(self): - """ Test the list of burned nuclides property """ + """Test the list of burned nuclides property""" mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} volume = {"10000" : 0.38, "10001" : 0.21} diff --git a/tests/deplete_tests/test_cecm_regression.py b/tests/deplete_tests/test_cecm_regression.py index 23a6342000..0d6966c82c 100644 --- a/tests/deplete_tests/test_cecm_regression.py +++ b/tests/deplete_tests/test_cecm_regression.py @@ -4,11 +4,11 @@ import os import unittest import numpy as np +import openmc.deplete +from openmc.deplete import results +from openmc.deplete import utilities -import opendeplete -from opendeplete import results -from opendeplete import utilities -import test.dummy_geometry as dummy_geometry +from . import dummy_geometry class TestCECMRegression(unittest.TestCase): @@ -26,14 +26,14 @@ class TestCECMRegression(unittest.TestCase): def test_cecm(self): """ Integral regression test of integrator algorithm using CE/CM. """ - settings = opendeplete.Settings() + settings = openmc.deplete.Settings() settings.dt_vec = [0.75, 0.75] settings.output_dir = self.results op = dummy_geometry.DummyGeometry(settings) # Perform simulation using the MCNPX/MCNP6 algorithm - opendeplete.cecm(op, print_out=False) + openmc.deplete.cecm(op, print_out=False) # Load the files res = results.read_results(settings.output_dir + "/results.h5") @@ -59,8 +59,8 @@ class TestCECMRegression(unittest.TestCase): os.chdir(cls.cwd) - opendeplete.comm.barrier() - if opendeplete.comm.rank == 0: + openmc.deplete.comm.barrier() + if openmc.deplete.comm.rank == 0: os.remove(os.path.join(cls.results, "results.h5")) os.rmdir(cls.results) diff --git a/tests/deplete_tests/test_cram.py b/tests/deplete_tests/test_cram.py index 2744adbf48..10f41fe2b6 100644 --- a/tests/deplete_tests/test_cram.py +++ b/tests/deplete_tests/test_cram.py @@ -4,8 +4,8 @@ import unittest import numpy as np import scipy.sparse as sp +from openmc.deplete.integrator import CRAM16, CRAM48 -from opendeplete.integrator import CRAM16, CRAM48 class TestCram(unittest.TestCase): """ Tests for cram.py diff --git a/tests/deplete_tests/test_depletion_chain.py b/tests/deplete_tests/test_depletion_chain.py index 216d1e68f7..06abbba0f6 100644 --- a/tests/deplete_tests/test_depletion_chain.py +++ b/tests/deplete_tests/test_depletion_chain.py @@ -5,8 +5,7 @@ import os import unittest import numpy as np - -from opendeplete import comm, depletion_chain, reaction_rates, nuclide +from openmc.deplete import comm, depletion_chain, reaction_rates, nuclide class TestDepletionChain(unittest.TestCase): diff --git a/tests/deplete_tests/test_full.py b/tests/deplete_tests/test_full.py index f9a6c7493d..88c409554c 100644 --- a/tests/deplete_tests/test_full.py +++ b/tests/deplete_tests/test_full.py @@ -2,13 +2,14 @@ import shutil import unittest +from os.path import join, dirname import numpy as np +import openmc.deplete +from openmc.deplete import results +from openmc.deplete import utilities -import opendeplete -from opendeplete import results -from opendeplete import utilities -import test.example_geometry as example_geometry +from . import example_geometry class TestFull(unittest.TestCase): @@ -40,7 +41,7 @@ class TestFull(unittest.TestCase): dt = np.repeat([dt1], N) # Create settings variable - settings = opendeplete.OpenMCSettings() + settings = openmc.deplete.OpenMCSettings() settings.chain_file = "chains/chain_simple.xml" settings.openmc_call = "openmc" @@ -60,16 +61,17 @@ class TestFull(unittest.TestCase): settings.dt_vec = dt settings.output_dir = "test_full" - op = opendeplete.OpenMCOperator(geometry, settings) + op = openmc.deplete.OpenMCOperator(geometry, settings) # Perform simulation using the predictor algorithm - opendeplete.integrator.predictor(op) + openmc.deplete.integrator.predictor(op) # Load the files res_test = results.read_results(settings.output_dir + "/results.h5") # Load the reference - res_old = results.read_results("test/test_reference.h5") + filename = join(dirname(__file__), 'test_reference.h5') + res_old = results.read_results(filename) # Assert same mats for mat in res_old[0].mat_to_ind: @@ -110,8 +112,8 @@ class TestFull(unittest.TestCase): def tearDown(self): """ Clean up files""" - opendeplete.comm.barrier() - if opendeplete.comm.rank == 0: + openmc.deplete.comm.barrier() + if openmc.deplete.comm.rank == 0: shutil.rmtree("test_full", ignore_errors=True) diff --git a/tests/deplete_tests/test_integrator.py b/tests/deplete_tests/test_integrator.py index 7e121ce167..9b4cbe7802 100644 --- a/tests/deplete_tests/test_integrator.py +++ b/tests/deplete_tests/test_integrator.py @@ -6,8 +6,7 @@ import unittest from unittest.mock import MagicMock import numpy as np - -from opendeplete import integrator, ReactionRates, results, comm +from openmc.deplete import integrator, ReactionRates, results, comm class TestIntegrator(unittest.TestCase): diff --git a/tests/deplete_tests/test_nuclide.py b/tests/deplete_tests/test_nuclide.py index c5439b2aa3..2d379030d8 100644 --- a/tests/deplete_tests/test_nuclide.py +++ b/tests/deplete_tests/test_nuclide.py @@ -3,7 +3,7 @@ import unittest import xml.etree.ElementTree as ET -from opendeplete import nuclide +from openmc.deplete import nuclide class TestNuclide(unittest.TestCase): diff --git a/tests/deplete_tests/test_predictor_regression.py b/tests/deplete_tests/test_predictor_regression.py index c72ae8a475..a41ca9ce79 100644 --- a/tests/deplete_tests/test_predictor_regression.py +++ b/tests/deplete_tests/test_predictor_regression.py @@ -4,11 +4,11 @@ import os import unittest import numpy as np +import openmc.deplete +from openmc.deplete import results +from openmc.deplete import utilities -import opendeplete -from opendeplete import results -from opendeplete import utilities -import test.dummy_geometry as dummy_geometry +from . import dummy_geometry class TestPredictorRegression(unittest.TestCase): """ Regression tests for opendeplete.integrator.predictor algorithm. @@ -25,14 +25,14 @@ class TestPredictorRegression(unittest.TestCase): def test_predictor(self): """ Integral regression test of integrator algorithm using CE/CM. """ - settings = opendeplete.Settings() + settings = openmc.deplete.Settings() settings.dt_vec = [0.75, 0.75] settings.output_dir = self.results op = dummy_geometry.DummyGeometry(settings) # Perform simulation using the predictor algorithm - opendeplete.predictor(op, print_out=False) + openmc.deplete.predictor(op, print_out=False) # Load the files res = results.read_results(settings.output_dir + "/results.h5") @@ -58,8 +58,8 @@ class TestPredictorRegression(unittest.TestCase): os.chdir(cls.cwd) - opendeplete.comm.barrier() - if opendeplete.comm.rank == 0: + openmc.deplete.comm.barrier() + if openmc.deplete.comm.rank == 0: os.remove(os.path.join(cls.results, "results.h5")) os.rmdir(cls.results) diff --git a/tests/deplete_tests/test_reaction_rates.py b/tests/deplete_tests/test_reaction_rates.py index 4821ec18cd..2139be16c2 100644 --- a/tests/deplete_tests/test_reaction_rates.py +++ b/tests/deplete_tests/test_reaction_rates.py @@ -2,7 +2,7 @@ import unittest -from opendeplete import reaction_rates +from openmc.deplete import reaction_rates class TestReactionRates(unittest.TestCase): diff --git a/tests/deplete_tests/test_utilities.py b/tests/deplete_tests/test_utilities.py index faa1a500b4..fb60df5b7d 100644 --- a/tests/deplete_tests/test_utilities.py +++ b/tests/deplete_tests/test_utilities.py @@ -1,11 +1,11 @@ """ Full system test suite. """ import unittest +from os.path import join, dirname import numpy as np - -from opendeplete import results -from opendeplete import utilities +from openmc.deplete import results +from openmc.deplete import utilities class TestUtilities(unittest.TestCase): @@ -19,7 +19,8 @@ class TestUtilities(unittest.TestCase): """ # Load the reference - res = results.read_results("test/test_reference.h5") + filename = join(dirname(__file__), 'test_reference.h5') + res = results.read_results(filename) x, y = utilities.evaluate_single_nuclide(res, "1", "Xe135") @@ -35,7 +36,8 @@ class TestUtilities(unittest.TestCase): """ # Load the reference - res = results.read_results("test/test_reference.h5") + filename = join(dirname(__file__), 'test_reference.h5') + res = results.read_results(filename) x, y = utilities.evaluate_reaction_rate(res, "1", "Xe135", "(n,gamma)") @@ -53,7 +55,8 @@ class TestUtilities(unittest.TestCase): """ # Load the reference - res = results.read_results("test/test_reference.h5") + filename = join(dirname(__file__), 'test_reference.h5') + res = results.read_results(filename) x, y = utilities.evaluate_eigenvalue(res) From 0a772252cc4c64295b25832555f397bc3e431007 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 9 Feb 2018 14:51:36 -0600 Subject: [PATCH 03/68] Fix bug in openmc_tally_set_scores (for depletion reactions) --- src/tallies/tally_header.F90 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/tallies/tally_header.F90 b/src/tallies/tally_header.F90 index 474e77d1ce..9604d2382a 100644 --- a/src/tallies/tally_header.F90 +++ b/src/tallies/tally_header.F90 @@ -732,8 +732,10 @@ contains integer :: MT character(C_CHAR), pointer :: string(:) character(len=:, kind=C_CHAR), allocatable :: score_ + logical :: depletion_rx err = E_UNASSIGNED + depletion_rx = .false. if (index >= 1 .and. index <= size(tallies)) then associate (t => tallies(index) % obj) if (allocated(t % score_bins)) deallocate(t % score_bins) @@ -757,10 +759,13 @@ contains t % score_bins(i) = SCORE_NU_SCATTER case ('(n,2n)') t % score_bins(i) = N_2N + depletion_rx = .true. case ('(n,3n)') t % score_bins(i) = N_3N + depletion_rx = .true. case ('(n,4n)') t % score_bins(i) = N_4N + depletion_rx = .true. case ('absorption') t % score_bins(i) = SCORE_ABSORPTION case ('fission', '18') @@ -829,8 +834,10 @@ contains t % score_bins(i) = N_NC case ('(n,gamma)') t % score_bins(i) = N_GAMMA + depletion_rx = .true. case ('(n,p)') t % score_bins(i) = N_P + depletion_rx = .true. case ('(n,d)') t % score_bins(i) = N_D case ('(n,t)') @@ -839,6 +846,7 @@ contains t % score_bins(i) = N_3HE case ('(n,a)') t % score_bins(i) = N_A + depletion_rx = .true. case ('(n,2a)') t % score_bins(i) = N_2A case ('(n,3a)') @@ -879,6 +887,7 @@ contains end do err = 0 + t % depletion_rx = depletion_rx end associate else err = E_OUT_OF_BOUNDS From 8549f22e1408dd6e87184746b833b82f50e07d66 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 9 Feb 2018 15:42:00 -0600 Subject: [PATCH 04/68] Move depletion tests into normal regression/unit test directories --- tests/deplete_tests/__init__.py | 0 tests/{deplete_tests => }/dummy_geometry.py | 0 .../example_geometry.py | 0 .../test_deplete_full.py} | 0 .../test_deplete_utilities.py} | 0 .../test_reference.h5 | Bin .../test_deplete_atom_number.py} | 0 .../test_deplete_cecm.py} | 2 +- .../test_deplete_cram.py} | 0 .../test_deplete_integrator.py} | 0 .../test_deplete_nuclide.py} | 0 .../test_deplete_predictor.py} | 2 +- .../test_deplete_reaction.py} | 0 .../test_depletion_chain.py | 0 14 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 tests/deplete_tests/__init__.py rename tests/{deplete_tests => }/dummy_geometry.py (100%) rename tests/{deplete_tests => regression_tests}/example_geometry.py (100%) rename tests/{deplete_tests/test_full.py => regression_tests/test_deplete_full.py} (100%) rename tests/{deplete_tests/test_utilities.py => regression_tests/test_deplete_utilities.py} (100%) rename tests/{deplete_tests => regression_tests}/test_reference.h5 (100%) rename tests/{deplete_tests/test_atom_number.py => unit_tests/test_deplete_atom_number.py} (100%) rename tests/{deplete_tests/test_cecm_regression.py => unit_tests/test_deplete_cecm.py} (98%) rename tests/{deplete_tests/test_cram.py => unit_tests/test_deplete_cram.py} (100%) rename tests/{deplete_tests/test_integrator.py => unit_tests/test_deplete_integrator.py} (100%) rename tests/{deplete_tests/test_nuclide.py => unit_tests/test_deplete_nuclide.py} (100%) rename tests/{deplete_tests/test_predictor_regression.py => unit_tests/test_deplete_predictor.py} (98%) rename tests/{deplete_tests/test_reaction_rates.py => unit_tests/test_deplete_reaction.py} (100%) rename tests/{deplete_tests => unit_tests}/test_depletion_chain.py (100%) diff --git a/tests/deplete_tests/__init__.py b/tests/deplete_tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/deplete_tests/dummy_geometry.py b/tests/dummy_geometry.py similarity index 100% rename from tests/deplete_tests/dummy_geometry.py rename to tests/dummy_geometry.py diff --git a/tests/deplete_tests/example_geometry.py b/tests/regression_tests/example_geometry.py similarity index 100% rename from tests/deplete_tests/example_geometry.py rename to tests/regression_tests/example_geometry.py diff --git a/tests/deplete_tests/test_full.py b/tests/regression_tests/test_deplete_full.py similarity index 100% rename from tests/deplete_tests/test_full.py rename to tests/regression_tests/test_deplete_full.py diff --git a/tests/deplete_tests/test_utilities.py b/tests/regression_tests/test_deplete_utilities.py similarity index 100% rename from tests/deplete_tests/test_utilities.py rename to tests/regression_tests/test_deplete_utilities.py diff --git a/tests/deplete_tests/test_reference.h5 b/tests/regression_tests/test_reference.h5 similarity index 100% rename from tests/deplete_tests/test_reference.h5 rename to tests/regression_tests/test_reference.h5 diff --git a/tests/deplete_tests/test_atom_number.py b/tests/unit_tests/test_deplete_atom_number.py similarity index 100% rename from tests/deplete_tests/test_atom_number.py rename to tests/unit_tests/test_deplete_atom_number.py diff --git a/tests/deplete_tests/test_cecm_regression.py b/tests/unit_tests/test_deplete_cecm.py similarity index 98% rename from tests/deplete_tests/test_cecm_regression.py rename to tests/unit_tests/test_deplete_cecm.py index 0d6966c82c..34c3435a76 100644 --- a/tests/deplete_tests/test_cecm_regression.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -8,7 +8,7 @@ import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities -from . import dummy_geometry +from tests import dummy_geometry class TestCECMRegression(unittest.TestCase): diff --git a/tests/deplete_tests/test_cram.py b/tests/unit_tests/test_deplete_cram.py similarity index 100% rename from tests/deplete_tests/test_cram.py rename to tests/unit_tests/test_deplete_cram.py diff --git a/tests/deplete_tests/test_integrator.py b/tests/unit_tests/test_deplete_integrator.py similarity index 100% rename from tests/deplete_tests/test_integrator.py rename to tests/unit_tests/test_deplete_integrator.py diff --git a/tests/deplete_tests/test_nuclide.py b/tests/unit_tests/test_deplete_nuclide.py similarity index 100% rename from tests/deplete_tests/test_nuclide.py rename to tests/unit_tests/test_deplete_nuclide.py diff --git a/tests/deplete_tests/test_predictor_regression.py b/tests/unit_tests/test_deplete_predictor.py similarity index 98% rename from tests/deplete_tests/test_predictor_regression.py rename to tests/unit_tests/test_deplete_predictor.py index a41ca9ce79..6ad2007d9c 100644 --- a/tests/deplete_tests/test_predictor_regression.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -8,7 +8,7 @@ import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities -from . import dummy_geometry +from tests import dummy_geometry class TestPredictorRegression(unittest.TestCase): """ Regression tests for opendeplete.integrator.predictor algorithm. diff --git a/tests/deplete_tests/test_reaction_rates.py b/tests/unit_tests/test_deplete_reaction.py similarity index 100% rename from tests/deplete_tests/test_reaction_rates.py rename to tests/unit_tests/test_deplete_reaction.py diff --git a/tests/deplete_tests/test_depletion_chain.py b/tests/unit_tests/test_depletion_chain.py similarity index 100% rename from tests/deplete_tests/test_depletion_chain.py rename to tests/unit_tests/test_depletion_chain.py From 592fae536f31521d09bd68a80d465550e6b34978 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 9 Feb 2018 16:17:12 -0600 Subject: [PATCH 05/68] Fix file locations for depletion tests. Convert regression ones to pytest --- tests/conftest.py | 11 ++ tests/regression_tests/test_deplete_full.py | 156 ++++++++---------- .../test_deplete_utilities.py | 107 +++++------- tests/unit_tests/conftest.py | 9 - tests/unit_tests/test_depletion_chain.py | 10 +- 5 files changed, 133 insertions(+), 160 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 422c036fb3..dc55e8e1e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +import pytest + from tests.regression_tests import config as regression_config @@ -15,3 +17,12 @@ def pytest_configure(config): for opt in opts: if config.getoption(opt) is not None: regression_config[opt] = config.getoption(opt) + + +@pytest.fixture +def run_in_tmpdir(tmpdir): + orig = tmpdir.chdir() + try: + yield + finally: + orig.chdir() diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index 88c409554c..78039abc7e 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -1,121 +1,105 @@ """ Full system test suite. """ +from math import floor import shutil import unittest -from os.path import join, dirname +from pathlib import Path import numpy as np import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities -from . import example_geometry +from .example_geometry import generate_problem -class TestFull(unittest.TestCase): - """ Full system test suite. +def test_full(run_in_tmpdir): + """Full system test suite. Runs an entire OpenMC simulation with depletion coupling and verifies that the outputs match a reference file. Sensitive to changes in OpenMC. + + This test runs a complete OpenMC simulation and tests the outputs. + It will take a while. """ - def test_full(self): - """ - This test runs a complete OpenMC simulation and tests the outputs. - It will take a while. - """ + n_rings = 2 + n_wedges = 4 - n_rings = 2 - n_wedges = 4 + # Load geometry from example + geometry, lower_left, upper_right = generate_problem(n_rings, n_wedges) - # Load geometry from example - geometry, lower_left, upper_right = \ - example_geometry.generate_problem(n_rings=n_rings, n_wedges=n_wedges) + # Create dt vector for 3 steps with 15 day timesteps + dt1 = 15.*24*60*60 # 15 days + dt2 = 1.5*30*24*60*60 # 1.5 months + N = floor(dt2/dt1) + dt = np.full(N, dt1) - # Create dt vector for 3 steps with 15 day timesteps - dt1 = 15*24*60*60 # 15 days - dt2 = 1.5*30*24*60*60 # 1.5 months - N = np.floor(dt2/dt1) + # Create settings variable + settings = openmc.deplete.OpenMCSettings() - dt = np.repeat([dt1], N) - # Create settings variable - settings = openmc.deplete.OpenMCSettings() + chain_file = str(Path(__file__).parents[2] / 'chains' / 'chain_simple.xml') + settings.chain_file = chain_file + settings.openmc_call = "openmc" + settings.openmc_npernode = 2 + settings.particles = 100 + settings.batches = 100 + settings.inactive = 40 + settings.lower_left = lower_left + settings.upper_right = upper_right + settings.entropy_dimension = [10, 10, 1] - settings.chain_file = "chains/chain_simple.xml" - settings.openmc_call = "openmc" - settings.openmc_npernode = 2 - settings.particles = 100 - settings.batches = 100 - settings.inactive = 40 - settings.lower_left = lower_left - settings.upper_right = upper_right - settings.entropy_dimension = [10, 10, 1] + settings.round_number = True + settings.constant_seed = 1 - settings.round_number = True - settings.constant_seed = 1 + joule_per_mev = 1.6021766208e-13 + settings.power = 2.337e15*4*joule_per_mev # MeV/second cm from CASMO + settings.dt_vec = dt + settings.output_dir = "test_full" - joule_per_mev = 1.6021766208e-13 - settings.power = 2.337e15*4*joule_per_mev # MeV/second cm from CASMO - settings.dt_vec = dt - settings.output_dir = "test_full" + op = openmc.deplete.OpenMCOperator(geometry, settings) - op = openmc.deplete.OpenMCOperator(geometry, settings) + # Perform simulation using the predictor algorithm + openmc.deplete.integrator.predictor(op) - # Perform simulation using the predictor algorithm - openmc.deplete.integrator.predictor(op) + # Load the files + res_test = results.read_results(settings.output_dir + "/results.h5") - # Load the files - res_test = results.read_results(settings.output_dir + "/results.h5") + # Load the reference + filename = str(Path(__file__).with_name('test_reference.h5')) + res_old = results.read_results(filename) - # Load the reference - filename = join(dirname(__file__), 'test_reference.h5') - res_old = results.read_results(filename) + # Assert same mats + for mat in res_old[0].mat_to_ind: + assert mat in res_test[0].mat_to_ind, \ + "Material {} not in new results.".format(mat) + for nuc in res_old[0].nuc_to_ind: + assert nuc in res_test[0].nuc_to_ind, \ + "Nuclide {} not in new results.".format(nuc) - # Assert same mats - for mat in res_old[0].mat_to_ind: - self.assertIn(mat, res_test[0].mat_to_ind, - msg="Cell " + mat + " not in new results.") - for nuc in res_old[0].nuc_to_ind: - self.assertIn(nuc, res_test[0].nuc_to_ind, - msg="Nuclide " + nuc + " not in new results.") + for mat in res_test[0].mat_to_ind: + assert mat in res_old[0].mat_to_ind, \ + "Material {} not in old results.".format(mat) + for nuc in res_test[0].nuc_to_ind: + assert nuc in res_old[0].nuc_to_ind, \ + "Nuclide {} not in old results.".format(nuc) - for mat in res_test[0].mat_to_ind: - self.assertIn(mat, res_old[0].mat_to_ind, - msg="Cell " + mat + " not in old results.") + tol = 1.0e-6 + for mat in res_test[0].mat_to_ind: for nuc in res_test[0].nuc_to_ind: - self.assertIn(nuc, res_old[0].nuc_to_ind, - msg="Nuclide " + nuc + " not in old results.") + _, y_test = utilities.evaluate_single_nuclide(res_test, mat, nuc) + _, y_old = utilities.evaluate_single_nuclide(res_old, mat, nuc) - for mat in res_test[0].mat_to_ind: - for nuc in res_test[0].nuc_to_ind: - _, y_test = utilities.evaluate_single_nuclide(res_test, mat, nuc) - _, y_old = utilities.evaluate_single_nuclide(res_old, mat, nuc) + # Test each point + correct = True + for i, ref in enumerate(y_old): + if ref != y_test[i]: + if ref != 0.0: + correct = np.abs(y_test[i] - ref) / ref <= tol + else: + correct = False - # Test each point - - tol = 1.0e-6 - - correct = True - for i, ref in enumerate(y_old): - if ref != y_test[i]: - if ref != 0.0: - if np.abs(y_test[i] - ref) / ref > tol: - correct = False - else: - correct = False - - self.assertTrue(correct, - msg="Discrepancy in mat " + mat + " and nuc " + nuc - + "\n" + str(y_old) + "\n" + str(y_test)) - - def tearDown(self): - """ Clean up files""" - openmc.deplete.comm.barrier() - if openmc.deplete.comm.rank == 0: - shutil.rmtree("test_full", ignore_errors=True) - - -if __name__ == '__main__': - unittest.main() + assert correct, "Discrepancy in mat {} and nuc {}\n{}\n{}".format( + mat, nuc, y_old, y_test) diff --git a/tests/regression_tests/test_deplete_utilities.py b/tests/regression_tests/test_deplete_utilities.py index fb60df5b7d..f9d34aa75a 100644 --- a/tests/regression_tests/test_deplete_utilities.py +++ b/tests/regression_tests/test_deplete_utilities.py @@ -1,71 +1,54 @@ -""" Full system test suite. """ +""" Tests the utilities classes. -import unittest -from os.path import join, dirname +This also tests the results read/write code. +""" + +from pathlib import Path import numpy as np +import pytest from openmc.deplete import results from openmc.deplete import utilities -class TestUtilities(unittest.TestCase): - """ Tests the utilities classes. - - This also tests the results read/write code. - """ - - def test_evaluate_single_nuclide(self): - """ Tests evaluating single nuclide utility code. - """ - - # Load the reference - filename = join(dirname(__file__), 'test_reference.h5') - res = results.read_results(filename) - - x, y = utilities.evaluate_single_nuclide(res, "1", "Xe135") - - x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - y_ref = [6.6747328233649218e+08, 3.5519299354458244e+14, - 3.4599104054580338e+14, 3.3821165110278112e+14] - - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, y_ref) - - def test_evaluate_reaction_rate(self): - """ Tests evaluating reaction rate utility code. - """ - - # Load the reference - filename = join(dirname(__file__), 'test_reference.h5') - res = results.read_results(filename) - - x, y = utilities.evaluate_reaction_rate(res, "1", "Xe135", "(n,gamma)") - - x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - xe_ref = np.array([6.6747328233649218e+08, 3.5519299354458244e+14, - 3.4599104054580338e+14, 3.3821165110278112e+14]) - r_ref = np.array([4.0643598574337784e-05, 4.1457730544386974e-05, - 3.4121248544056681e-05, 3.9204686657643301e-05]) - - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, xe_ref * r_ref) - - def test_evaluate_eigenvalue(self): - """ Tests evaluating eigenvalue - """ - - # Load the reference - filename = join(dirname(__file__), 'test_reference.h5') - res = results.read_results(filename) - - x, y = utilities.evaluate_eigenvalue(res) - - x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - y_ref = [1.1921986054449838, 1.1712785643938586, 1.1927099024502694, 1.2269183590698847] - - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, y_ref) +@pytest.fixture +def res(): + """Load the reference results""" + filename = str(Path(__file__).with_name('test_reference.h5')) + return results.read_results(filename) -if __name__ == '__main__': - unittest.main() +def test_evaluate_single_nuclide(res): + """Tests evaluating single nuclide utility code.""" + x, y = utilities.evaluate_single_nuclide(res, "1", "Xe135") + + x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + y_ref = [6.6747328233649218e+08, 3.5519299354458244e+14, + 3.4599104054580338e+14, 3.3821165110278112e+14] + + np.testing.assert_array_equal(x, x_ref) + np.testing.assert_array_equal(y, y_ref) + +def test_evaluate_reaction_rate(res): + """Tests evaluating reaction rate utility code.""" + x, y = utilities.evaluate_reaction_rate(res, "1", "Xe135", "(n,gamma)") + + x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + xe_ref = np.array([6.6747328233649218e+08, 3.5519299354458244e+14, + 3.4599104054580338e+14, 3.3821165110278112e+14]) + r_ref = np.array([4.0643598574337784e-05, 4.1457730544386974e-05, + 3.4121248544056681e-05, 3.9204686657643301e-05]) + + np.testing.assert_array_equal(x, x_ref) + np.testing.assert_array_equal(y, xe_ref * r_ref) + + +def test_evaluate_eigenvalue(res): + """Tests evaluating eigenvalue.""" + x, y = utilities.evaluate_eigenvalue(res) + + x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + y_ref = [1.1921986054449838, 1.1712785643938586, 1.1927099024502694, 1.2269183590698847] + + np.testing.assert_array_equal(x, x_ref) + np.testing.assert_array_equal(y, y_ref) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index d618b85def..1434eaf3b4 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -2,15 +2,6 @@ import openmc import pytest -@pytest.fixture -def run_in_tmpdir(tmpdir): - orig = tmpdir.chdir() - try: - yield - finally: - orig.chdir() - - @pytest.fixture(scope='module') def uo2(): m = openmc.Material(material_id=100, name='UO2') diff --git a/tests/unit_tests/test_depletion_chain.py b/tests/unit_tests/test_depletion_chain.py index 06abbba0f6..de7180e881 100644 --- a/tests/unit_tests/test_depletion_chain.py +++ b/tests/unit_tests/test_depletion_chain.py @@ -3,11 +3,15 @@ from collections import OrderedDict import os import unittest +from pathlib import Path import numpy as np from openmc.deplete import comm, depletion_chain, reaction_rates, nuclide +_test_filename = str(Path(__file__).parents[2] / 'chains' / 'chain_test.xml') + + class TestDepletionChain(unittest.TestCase): """ Tests for DepletionChain class.""" @@ -38,7 +42,7 @@ class TestDepletionChain(unittest.TestCase): # the components external to depletion_chain.py are simple storage # types. - dep = depletion_chain.DepletionChain.xml_read("chains/chain_test.xml") + dep = depletion_chain.DepletionChain.xml_read(_test_filename) # Basic checks self.assertEqual(dep.n_nuclides, 3) @@ -124,7 +128,7 @@ class TestDepletionChain(unittest.TestCase): chain.nuclides = [A, B, C] chain.xml_write(filename) - original = open('chains/chain_test.xml', 'r').read() + original = open(_test_filename, 'r').read() chain_xml = open(filename, 'r').read() self.assertEqual(original, chain_xml) @@ -134,7 +138,7 @@ class TestDepletionChain(unittest.TestCase): """ Using chain_test, and a dummy reaction rate, compute the matrix. """ # Relies on test_xml_read passing. - dep = depletion_chain.DepletionChain.xml_read("chains/chain_test.xml") + dep = depletion_chain.DepletionChain.xml_read(_test_filename) cell_ind = {"10000": 0, "10001": 1} nuc_ind = {"A": 0, "B": 1, "C": 2} From 2e358a2ca474371298dbbbe954527a3bcd764f78 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 9 Feb 2018 16:48:32 -0600 Subject: [PATCH 06/68] Release resources properly from Operator class --- openmc/deplete/integrator/cecm.py | 3 +++ openmc/deplete/integrator/predictor.py | 3 +++ openmc/deplete/openmc_wrapper.py | 7 ++++--- tests/dummy_geometry.py | 3 +++ tests/unit_tests/test_capi.py | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 4d9baebb2e..699ccc2033 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -131,3 +131,6 @@ def cecm(operator, print_out=True): # Return to origin os.chdir(dir_home) + + # Release resources + operator.finalize() diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 6c9d538fd6..7b41e66493 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -98,3 +98,6 @@ def predictor(operator, print_out=True): # Return to origin os.chdir(dir_home) + + # Release resources + operator.finalize() diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 05b5057d20..5955a5defd 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -219,9 +219,6 @@ class OpenMCOperator(Operator): # Create reaction rate tables self.initialize_reaction_rates() - def __del__(self): - openmc.capi.finalize() - def extract_mat_ids(self): """Extracts materials and assigns them to processes. @@ -475,6 +472,10 @@ class OpenMCOperator(Operator): # Return number density vector return self.total_density_list() + def finalize(self): + """Finalize a depletion simulation and release resources.""" + openmc.capi.finalize() + def _update_materials(self): """Updates material compositions in OpenMC on all processes.""" diff --git a/tests/dummy_geometry.py b/tests/dummy_geometry.py index 614cc726e3..ecdce567d6 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_geometry.py @@ -21,6 +21,9 @@ class DummyGeometry(Operator): def __init__(self, settings): super().__init__(settings) + def finalize(self): + pass + @property def chain(self): return self diff --git a/tests/unit_tests/test_capi.py b/tests/unit_tests/test_capi.py index 618f54e4f9..8bc6c3d15c 100644 --- a/tests/unit_tests/test_capi.py +++ b/tests/unit_tests/test_capi.py @@ -1,4 +1,4 @@ -from collections import Mapping +from collections.abc import Mapping import os import numpy as np From 3b31892816831c72354ca8de457c4bbb1afa39e0 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 12 Feb 2018 13:34:35 -0600 Subject: [PATCH 07/68] Make sure entropy/UFS mesh index get cleared during finalize --- src/api.F90 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api.F90 b/src/api.F90 index f07e5e38a5..d79a32d945 100644 --- a/src/api.F90 +++ b/src/api.F90 @@ -121,6 +121,8 @@ contains energy_min_neutron = ZERO entropy_on = .false. gen_per_batch = 1 + index_entropy_mesh = -1 + index_ufs_mesh = -1 keff = ONE legendre_to_tabular = .true. legendre_to_tabular_points = 33 From ef99788ff47cd7ff75c06e61f514fc6fe516f913 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 06:37:17 -0600 Subject: [PATCH 08/68] Get rid of OpenMCSettings.openmc_call, which doesn't make sense anymore --- openmc/deplete/openmc_wrapper.py | 3 --- scripts/example_run.py | 3 --- tests/regression_tests/test_deplete_full.py | 2 -- 3 files changed, 8 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 5955a5defd..66de945cda 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -58,8 +58,6 @@ class OpenMCSettings(Settings): chain_file : str Path to the depletion chain xml file. Defaults to the environment variable "OPENDEPLETE_CHAIN" if it exists. - openmc_call : str - OpenMC executable path. Defaults to "openmc". particles : int Number of particles to simulate per batch. batches : int @@ -94,7 +92,6 @@ class OpenMCSettings(Settings): self.chain_file = os.environ["OPENDEPLETE_CHAIN"] except KeyError: self.chain_file = None - self.openmc_call = "openmc" self.particles = None self.batches = None self.inactive = None diff --git a/scripts/example_run.py b/scripts/example_run.py index 82d0883c3a..78d7dceddc 100644 --- a/scripts/example_run.py +++ b/scripts/example_run.py @@ -18,9 +18,6 @@ dt = np.repeat([dt1], N) # Create settings variable settings = openmc.deplete.OpenMCSettings() -settings.openmc_call = "openmc" -# An example for mpiexec: -# settings.openmc_call = ["mpiexec", "openmc"] settings.particles = 1000 settings.batches = 100 settings.inactive = 40 diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index 78039abc7e..dda1501fbc 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -42,8 +42,6 @@ def test_full(run_in_tmpdir): chain_file = str(Path(__file__).parents[2] / 'chains' / 'chain_simple.xml') settings.chain_file = chain_file - settings.openmc_call = "openmc" - settings.openmc_npernode = 2 settings.particles = 100 settings.batches = 100 settings.inactive = 40 From 26852f79f478a67a45ff790a8af2502f01116233 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 07:22:16 -0600 Subject: [PATCH 09/68] Add tqdm to dependencies. Install mpi4py on Travis --- setup.py | 2 +- tools/ci/travis-install.sh | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2a42dd65dd..ee11f414b6 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ kwargs = { # Required dependencies 'install_requires': [ 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', - 'pandas', 'lxml', 'uncertainties' + 'pandas', 'lxml', 'uncertainties', 'tqdm' ], # Optional dependencies diff --git a/tools/ci/travis-install.sh b/tools/ci/travis-install.sh index 4921534db9..2342cdadbc 100755 --- a/tools/ci/travis-install.sh +++ b/tools/ci/travis-install.sh @@ -14,10 +14,15 @@ pip install cython pip install --upgrade pytest # Pandas stopped supporting Python 3.4 with version 0.21 -if [[ "$TRAVIS_PYTHON_VERSION" == "3.4" ]]; then +if [[ $TRAVIS_PYTHON_VERSION == "3.4" ]]; then pip install pandas==0.20.3 fi +# Install mpi4py for MPI configurations +if [[ $MPI == 'y' ]]; then + pip install --no-binary=mpi4py mpi4py +fi + # Build and install OpenMC executable python tools/ci/travis-install.py From 43147b70eb62ce983443ee3f17ac7bee49b6dd60 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 10:17:30 -0600 Subject: [PATCH 10/68] Change xml_write -> export_to_xml, xml_read -> from_xml for consistency --- openmc/deplete/depletion_chain.py | 8 ++++---- openmc/deplete/nuclide.py | 4 ++-- openmc/deplete/openmc_wrapper.py | 2 +- scripts/make_chain.py | 2 +- tests/unit_tests/test_deplete_nuclide.py | 8 ++++---- tests/unit_tests/test_depletion_chain.py | 15 ++++++++------- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/openmc/deplete/depletion_chain.py b/openmc/deplete/depletion_chain.py index af126035f6..9f6b7cfeda 100644 --- a/openmc/deplete/depletion_chain.py +++ b/openmc/deplete/depletion_chain.py @@ -317,7 +317,7 @@ class DepletionChain(object): return depl_chain @classmethod - def xml_read(cls, filename): + def from_xml(cls, filename): """Reads a depletion chain XML file. Parameters @@ -343,7 +343,7 @@ class DepletionChain(object): reaction_index = 0 for i, nuclide_elem in enumerate(root.findall('nuclide_table')): - nuc = Nuclide.xml_read(nuclide_elem) + nuc = Nuclide.from_xml(nuclide_elem) depl_chain.nuclide_dict[nuc.name] = i # Check for reaction paths @@ -356,7 +356,7 @@ class DepletionChain(object): return depl_chain - def xml_write(self, filename): + def export_to_xml(self, filename): """Writes a depletion chain XML file. Parameters @@ -368,7 +368,7 @@ class DepletionChain(object): root_elem = ET.Element('depletion') for nuclide in self.nuclides: - root_elem.append(nuclide.xml_write()) + root_elem.append(nuclide.to_xml_element()) tree = ET.ElementTree(root_elem) if _have_lxml: diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index 1208a9b3c3..17cf4d9b8f 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -71,7 +71,7 @@ class Nuclide(object): return len(self.reactions) @classmethod - def xml_read(cls, element): + def from_xml(cls, element): """Read nuclide from an XML element. Parameters @@ -129,7 +129,7 @@ class Nuclide(object): return nuc - def xml_write(self): + def to_xml_element(self): """Write nuclide to XML element. Returns diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 66de945cda..88b4971222 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -186,7 +186,7 @@ class OpenMCOperator(Operator): self.burn_nuc_to_ind = None # Read depletion chain - self.chain = DepletionChain.xml_read(settings.chain_file) + self.chain = DepletionChain.from_xml(settings.chain_file) # Clear out OpenMC, create task lists, distribute if comm.rank == 0: diff --git a/scripts/make_chain.py b/scripts/make_chain.py index ccf4ef9b7b..e2b0a23405 100644 --- a/scripts/make_chain.py +++ b/scripts/make_chain.py @@ -53,7 +53,7 @@ def main(): neutron_files = glob.glob(os.path.join('neutrons', '*.endf')) chain = openmc.deplete.DepletionChain.from_endf(decay_files, nfy_files, neutron_files) - chain.xml_write('chain_endfb71.xml') + chain.export_to_xml('chain_endfb71.xml') if __name__ == '__main__': diff --git a/tests/unit_tests/test_deplete_nuclide.py b/tests/unit_tests/test_deplete_nuclide.py index 2d379030d8..ebcabc5ca9 100644 --- a/tests/unit_tests/test_deplete_nuclide.py +++ b/tests/unit_tests/test_deplete_nuclide.py @@ -35,7 +35,7 @@ class TestNuclide(unittest.TestCase): self.assertEqual(nuc.n_reaction_paths, 3) - def test_xml_read(self): + def test_from_xml(self): """Test reading nuclide data from an XML element.""" data = """ @@ -58,7 +58,7 @@ class TestNuclide(unittest.TestCase): """ element = ET.fromstring(data) - u235 = nuclide.Nuclide.xml_read(element) + u235 = nuclide.Nuclide.from_xml(element) self.assertEqual(u235.decay_modes, [ nuclide.DecayTuple('sf', 'U235', 7.2e-11), @@ -77,7 +77,7 @@ class TestNuclide(unittest.TestCase): ('Xe138', 0.0481413)] }) - def test_xml_write(self): + def test_to_xml_element(self): """Test writing nuclide data to an XML element.""" C = nuclide.Nuclide() @@ -93,7 +93,7 @@ class TestNuclide(unittest.TestCase): ] C.yield_energies = [0.0253] C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} - element = C.xml_write() + element = C.to_xml_element() self.assertEqual(element.get("half_life"), "0.123") diff --git a/tests/unit_tests/test_depletion_chain.py b/tests/unit_tests/test_depletion_chain.py index de7180e881..ba9e32db47 100644 --- a/tests/unit_tests/test_depletion_chain.py +++ b/tests/unit_tests/test_depletion_chain.py @@ -36,13 +36,13 @@ class TestDepletionChain(unittest.TestCase): out a good way to unit-test this.""" pass - def test_xml_read(self): + def test_from_xml(self): """ Read chain_test.xml and ensure all values are correct. """ # Unfortunately, this routine touches a lot of the code, but most of # the components external to depletion_chain.py are simple storage # types. - dep = depletion_chain.DepletionChain.xml_read(_test_filename) + dep = depletion_chain.DepletionChain.from_xml(_test_filename) # Basic checks self.assertEqual(dep.n_nuclides, 3) @@ -93,11 +93,11 @@ class TestDepletionChain(unittest.TestCase): self.assertEqual(nuc.yield_data[0.0253], [("A", 0.0292737), ("B", 0.002566345)]) - def test_xml_write(self): + def test_export_to_xml(self): """Test writing a depletion chain to XML.""" # Prevent different MPI ranks from conflicting - filename = 'test%u.xml' % comm.rank + filename = 'test{}.xml'.format(comm.rank) A = nuclide.Nuclide() A.name = "A" @@ -126,7 +126,7 @@ class TestDepletionChain(unittest.TestCase): chain = depletion_chain.DepletionChain() chain.nuclides = [A, B, C] - chain.xml_write(filename) + chain.export_to_xml(filename) original = open(_test_filename, 'r').read() chain_xml = open(filename, 'r').read() @@ -136,9 +136,9 @@ class TestDepletionChain(unittest.TestCase): def test_form_matrix(self): """ Using chain_test, and a dummy reaction rate, compute the matrix. """ - # Relies on test_xml_read passing. + # Relies on test_from_xml passing. - dep = depletion_chain.DepletionChain.xml_read(_test_filename) + dep = depletion_chain.DepletionChain.from_xml(_test_filename) cell_ind = {"10000": 0, "10001": 1} nuc_ind = {"A": 0, "B": 1, "C": 2} @@ -196,5 +196,6 @@ class TestDepletionChain(unittest.TestCase): self.assertEqual("NucB", dep.nuc_by_ind("NucB")) self.assertEqual("NucC", dep.nuc_by_ind("NucC")) + if __name__ == '__main__': unittest.main() From 1ee27edc8c935f48894dbefb37bfe48f2db06583 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 10:45:39 -0600 Subject: [PATCH 11/68] Rename DepletionChain -> Chain --- docs/source/pythonapi/deplete/index.rst | 24 +++++++-------- .../pythonapi/deplete/integrator.CRAM16.rst | 2 +- .../pythonapi/deplete/integrator.CRAM48.rst | 2 +- .../pythonapi/deplete/integrator.cecm.rst | 2 +- .../deplete/integrator.predictor.rst | 2 +- .../deplete/integrator.save_results.rst | 2 +- .../deplete/opendeplete.Concentrations.rst | 30 ------------------- .../deplete/opendeplete.ReactionRates.rst | 30 ------------------- .../pythonapi/deplete/opendeplete.Results.rst | 22 -------------- openmc/data/data.py | 3 +- openmc/deplete/__init__.py | 2 +- .../deplete/{depletion_chain.py => chain.py} | 8 ++--- openmc/deplete/openmc_wrapper.py | 20 ++++++------- scripts/make_chain.py | 2 +- ...pletion_chain.py => test_deplete_chain.py} | 20 ++++++------- 15 files changed, 44 insertions(+), 127 deletions(-) delete mode 100644 docs/source/pythonapi/deplete/opendeplete.Concentrations.rst delete mode 100644 docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst delete mode 100644 docs/source/pythonapi/deplete/opendeplete.Results.rst rename openmc/deplete/{depletion_chain.py => chain.py} (99%) rename tests/unit_tests/{test_depletion_chain.py => test_deplete_chain.py} (92%) diff --git a/docs/source/pythonapi/deplete/index.rst b/docs/source/pythonapi/deplete/index.rst index 55380c7a1c..30d2d42611 100644 --- a/docs/source/pythonapi/deplete/index.rst +++ b/docs/source/pythonapi/deplete/index.rst @@ -17,7 +17,7 @@ Integrator Helper Functions --------------------------- .. toctree:: :maxdepth: 2 - + integrator.CRAM16 integrator.CRAM48 integrator.save_results @@ -29,8 +29,8 @@ Metaclasses :toctree: generated :nosignatures: - opendeplete.Settings - opendeplete.Operator + openmc.deplete.Settings + openmc.deplete.Operator OpenMC Classes -------------- @@ -39,18 +39,18 @@ OpenMC Classes :toctree: generated :nosignatures: - opendeplete.OpenMCSettings - opendeplete.Materials - opendeplete.OpenMCOperator + openmc.deplete.OpenMCSettings + openmc.deplete.Materials + openmc.deplete.OpenMCOperator Data Classes ------------ .. autosummary:: :toctree: generated :nosignatures: - - opendeplete.AtomNumber - opendeplete.DepletionChain - opendeplete.Nuclide - opendeplete.ReactionRates - opendeplete.Results + + openmc.deplete.AtomNumber + openmc.deplete.Chain + openmc.deplete.Nuclide + openmc.deplete.ReactionRates + openmc.deplete.Results diff --git a/docs/source/pythonapi/deplete/integrator.CRAM16.rst b/docs/source/pythonapi/deplete/integrator.CRAM16.rst index f9eba273ed..a0dc648056 100644 --- a/docs/source/pythonapi/deplete/integrator.CRAM16.rst +++ b/docs/source/pythonapi/deplete/integrator.CRAM16.rst @@ -1,6 +1,6 @@ integrator\.CRAM16 ================== -.. currentmodule:: opendeplete.integrator +.. currentmodule:: openmc.deplete.integrator .. autofunction:: CRAM16 diff --git a/docs/source/pythonapi/deplete/integrator.CRAM48.rst b/docs/source/pythonapi/deplete/integrator.CRAM48.rst index d7467a418a..f9720f7ad9 100644 --- a/docs/source/pythonapi/deplete/integrator.CRAM48.rst +++ b/docs/source/pythonapi/deplete/integrator.CRAM48.rst @@ -1,6 +1,6 @@ integrator\.CRAM48 ================== -.. currentmodule:: opendeplete.integrator +.. currentmodule:: openmc.deplete.integrator .. autofunction:: CRAM48 diff --git a/docs/source/pythonapi/deplete/integrator.cecm.rst b/docs/source/pythonapi/deplete/integrator.cecm.rst index 507a638f69..4851b20b34 100644 --- a/docs/source/pythonapi/deplete/integrator.cecm.rst +++ b/docs/source/pythonapi/deplete/integrator.cecm.rst @@ -1,6 +1,6 @@ integrator\.cecm ================= -.. currentmodule:: opendeplete.integrator +.. currentmodule:: openmc.deplete.integrator .. autofunction:: cecm diff --git a/docs/source/pythonapi/deplete/integrator.predictor.rst b/docs/source/pythonapi/deplete/integrator.predictor.rst index d6c0fd827c..2243e77f71 100644 --- a/docs/source/pythonapi/deplete/integrator.predictor.rst +++ b/docs/source/pythonapi/deplete/integrator.predictor.rst @@ -1,6 +1,6 @@ integrator\.predictor ===================== -.. currentmodule:: opendeplete.integrator +.. currentmodule:: openmc.deplete.integrator .. autofunction:: predictor diff --git a/docs/source/pythonapi/deplete/integrator.save_results.rst b/docs/source/pythonapi/deplete/integrator.save_results.rst index 5c21dcb664..f9c830cd5b 100644 --- a/docs/source/pythonapi/deplete/integrator.save_results.rst +++ b/docs/source/pythonapi/deplete/integrator.save_results.rst @@ -1,6 +1,6 @@ integrator\.save_results ======================== -.. currentmodule:: opendeplete.integrator +.. currentmodule:: openmc.deplete.integrator .. autofunction:: save_results diff --git a/docs/source/pythonapi/deplete/opendeplete.Concentrations.rst b/docs/source/pythonapi/deplete/opendeplete.Concentrations.rst deleted file mode 100644 index 6fa07a970b..0000000000 --- a/docs/source/pythonapi/deplete/opendeplete.Concentrations.rst +++ /dev/null @@ -1,30 +0,0 @@ -opendeplete.Concentrations -========================== - -.. currentmodule:: opendeplete - -.. autoclass:: Concentrations - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~Concentrations.__init__ - ~Concentrations.convert_nested_dict - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~Concentrations.n_cell - ~Concentrations.n_nuc - - \ No newline at end of file diff --git a/docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst b/docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst deleted file mode 100644 index 99e048b565..0000000000 --- a/docs/source/pythonapi/deplete/opendeplete.ReactionRates.rst +++ /dev/null @@ -1,30 +0,0 @@ -opendeplete.ReactionRates -========================= - -.. currentmodule:: opendeplete - -.. autoclass:: ReactionRates - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~ReactionRates.__init__ - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~ReactionRates.n_cell - ~ReactionRates.n_nuc - ~ReactionRates.n_react - - \ No newline at end of file diff --git a/docs/source/pythonapi/deplete/opendeplete.Results.rst b/docs/source/pythonapi/deplete/opendeplete.Results.rst deleted file mode 100644 index 0ab8a1f711..0000000000 --- a/docs/source/pythonapi/deplete/opendeplete.Results.rst +++ /dev/null @@ -1,22 +0,0 @@ -opendeplete.Results -=================== - -.. currentmodule:: opendeplete - -.. autoclass:: Results - - - .. automethod:: __init__ - - - .. rubric:: Methods - - .. autosummary:: - - ~Results.__init__ - - - - - - \ No newline at end of file diff --git a/openmc/data/data.py b/openmc/data/data.py index a7c0e536f6..523ac9769d 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -319,8 +319,9 @@ def water_density(temperature, pressure=0.1013): # The value of the Boltzman constant in units of eV / K K_BOLTZMANN = 8.6173303e-5 -# Used for converting units in ACE data +# Unit conversions EV_PER_MEV = 1.0e6 +JOULE_PER_EV = 1.6021766208e-19 # Avogadro's constant AVOGADRO = 6.022140857e23 diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 4bdde3935e..19d1d13201 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -15,7 +15,7 @@ except ImportError: have_mpi = False from .nuclide import * -from .depletion_chain import * +from .chain import * from .openmc_wrapper import * from .reaction_rates import * from .function import * diff --git a/openmc/deplete/depletion_chain.py b/openmc/deplete/chain.py similarity index 99% rename from openmc/deplete/depletion_chain.py rename to openmc/deplete/chain.py index 9f6b7cfeda..03decb72ab 100644 --- a/openmc/deplete/depletion_chain.py +++ b/openmc/deplete/chain.py @@ -1,4 +1,4 @@ -"""depletion_chain module. +"""chain module. This module contains information about a depletion chain. A depletion chain is loaded from an .xml file and all the nuclides are linked together. @@ -108,10 +108,8 @@ def replace_missing(product, decay_data): return product -class DepletionChain(object): - """The DepletionChain class. - - This class contains a full representation of a depletion chain. +class Chain(object): + """Full representation of a depletion chain. Attributes ---------- diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 88b4971222..5c7a1aebaa 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -21,14 +21,14 @@ import numpy as np import openmc import openmc.capi +from openmc.data import JOULE_PER_EV from . import comm from .atom_number import AtomNumber -from .depletion_chain import DepletionChain +from .chain import Chain from .reaction_rates import ReactionRates from .function import Settings, Operator -_JOULE_PER_EV = 1.6021766208e-19 def chunks(items, n): @@ -149,13 +149,13 @@ class OpenMCOperator(Operator): Materials to be used for this simulation. seed : int The RNG seed used in last OpenMC run. - number : AtomNumber + number : openmc.deplete.AtomNumber Total number of atoms in simulation. participating_nuclides : set of str A set listing all unique nuclides available from cross_sections.xml. - chain : DepletionChain + chain : openmc.deplete.Chain The depletion chain information necessary to form matrices and tallies. - reaction_rates : ReactionRates + reaction_rates : openmc.deplete.ReactionRates Reaction rates from the last operator step. power : OrderedDict of str to float Material-by-Material power. Indexed by material ID. @@ -186,7 +186,7 @@ class OpenMCOperator(Operator): self.burn_nuc_to_ind = None # Read depletion chain - self.chain = DepletionChain.from_xml(settings.chain_file) + self.chain = Chain.from_xml(settings.chain_file) # Clear out OpenMC, create task lists, distribute if comm.rank == 0: @@ -390,7 +390,7 @@ class OpenMCOperator(Operator): Matrices for the next step. k : float Eigenvalue of the problem. - rates : ReactionRates + rates : openmc.deplete.ReactionRates Reaction rates from this simulation. seed : int Seed for this simulation. @@ -602,11 +602,11 @@ class OpenMCOperator(Operator): def generate_tallies(self): """Generates depletion tallies. - Using information from self.depletion_chain as well as the nuclides + Using information from the depletion chain as well as the nuclides currently in the problem, this function automatically generates a tally.xml for the simulation. - """ + """ # Create tallies for depleting regions materials = [openmc.capi.materials[int(i)] for i in self.mat_tally_ind] @@ -742,7 +742,7 @@ class OpenMCOperator(Operator): energy = comm.allreduce(energy) # Determine power in eV/s - power = self.settings.power / _JOULE_PER_EV + power = self.settings.power / JOULE_PER_EV # Scale reaction rates to obtain units of reactions/sec rates[:, :, :] *= power / energy diff --git a/scripts/make_chain.py b/scripts/make_chain.py index e2b0a23405..6d64f34b3c 100644 --- a/scripts/make_chain.py +++ b/scripts/make_chain.py @@ -52,7 +52,7 @@ def main(): nfy_files = glob.glob(os.path.join('nfy', '*.endf')) neutron_files = glob.glob(os.path.join('neutrons', '*.endf')) - chain = openmc.deplete.DepletionChain.from_endf(decay_files, nfy_files, neutron_files) + chain = openmc.deplete.Chain.from_endf(decay_files, nfy_files, neutron_files) chain.export_to_xml('chain_endfb71.xml') diff --git a/tests/unit_tests/test_depletion_chain.py b/tests/unit_tests/test_deplete_chain.py similarity index 92% rename from tests/unit_tests/test_depletion_chain.py rename to tests/unit_tests/test_deplete_chain.py index ba9e32db47..6166f15612 100644 --- a/tests/unit_tests/test_depletion_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -1,4 +1,4 @@ -""" Tests for depletion_chain.py""" +"""Tests for depletion chains""" from collections import OrderedDict import os @@ -6,18 +6,18 @@ import unittest from pathlib import Path import numpy as np -from openmc.deplete import comm, depletion_chain, reaction_rates, nuclide +from openmc.deplete import comm, Chain, reaction_rates, nuclide _test_filename = str(Path(__file__).parents[2] / 'chains' / 'chain_test.xml') -class TestDepletionChain(unittest.TestCase): - """ Tests for DepletionChain class.""" +class TestChain(unittest.TestCase): + """ Tests for Chain class.""" def test__init__(self): """ Test depletion chain initialization.""" - dep = depletion_chain.DepletionChain() + dep = Chain() self.assertIsInstance(dep.nuclides, list) self.assertIsInstance(dep.nuclide_dict, OrderedDict) @@ -25,7 +25,7 @@ class TestDepletionChain(unittest.TestCase): def test_n_nuclides(self): """ Test depletion chain n_nuclides parameter. """ - dep = depletion_chain.DepletionChain() + dep = Chain() dep.nuclides = ["NucA", "NucB", "NucC"] @@ -42,7 +42,7 @@ class TestDepletionChain(unittest.TestCase): # the components external to depletion_chain.py are simple storage # types. - dep = depletion_chain.DepletionChain.from_xml(_test_filename) + dep = Chain.from_xml(_test_filename) # Basic checks self.assertEqual(dep.n_nuclides, 3) @@ -124,7 +124,7 @@ class TestDepletionChain(unittest.TestCase): C.yield_energies = [0.0253] C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} - chain = depletion_chain.DepletionChain() + chain = Chain() chain.nuclides = [A, B, C] chain.export_to_xml(filename) @@ -138,7 +138,7 @@ class TestDepletionChain(unittest.TestCase): """ Using chain_test, and a dummy reaction rate, compute the matrix. """ # Relies on test_from_xml passing. - dep = depletion_chain.DepletionChain.from_xml(_test_filename) + dep = Chain.from_xml(_test_filename) cell_ind = {"10000": 0, "10001": 1} nuc_ind = {"A": 0, "B": 1, "C": 2} @@ -187,7 +187,7 @@ class TestDepletionChain(unittest.TestCase): def test_nuc_by_ind(self): """ Test nuc_by_ind converter function. """ - dep = depletion_chain.DepletionChain() + dep = Chain() dep.nuclides = ["NucA", "NucB", "NucC"] dep.nuclide_dict = {"NucA" : 0, "NucB" : 1, "NucC" : 2} From dba919c6109009670cc41decae356588c47c305f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 11:24:24 -0600 Subject: [PATCH 12/68] Convert deplete unit tests to use pytest --- openmc/deplete/integrator/cram.py | 4 +- openmc/deplete/results.py | 2 +- tests/regression_tests/test_deplete_full.py | 1 - tests/unit_tests/test_deplete_atom_number.py | 321 ++++++++-------- tests/unit_tests/test_deplete_cecm.py | 75 ++-- tests/unit_tests/test_deplete_chain.py | 367 +++++++++---------- tests/unit_tests/test_deplete_cram.py | 61 ++- tests/unit_tests/test_deplete_integrator.py | 156 ++++---- tests/unit_tests/test_deplete_nuclide.py | 165 ++++----- tests/unit_tests/test_deplete_predictor.py | 74 ++-- tests/unit_tests/test_deplete_reaction.py | 140 ++++--- 11 files changed, 631 insertions(+), 735 deletions(-) diff --git a/openmc/deplete/integrator/cram.py b/openmc/deplete/integrator/cram.py index a18d8450c3..56476384c6 100644 --- a/openmc/deplete/integrator/cram.py +++ b/openmc/deplete/integrator/cram.py @@ -1,6 +1,6 @@ -""" Chebyshev Rational Approximation Method module +"""Chebyshev Rational Approximation Method module -Implements two different forms of CRAM for use in opendeplete. +Implements two different forms of CRAM for use in openmc.deplete. """ import numpy as np diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index ae096b8ce5..c08b3ef40d 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -16,7 +16,7 @@ RESULTS_VERSION = 2 class Results(object): - """Contains output of opendeplete. + """Contains output of a depletion run. Attributes ---------- diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index dda1501fbc..5f4af7a737 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -2,7 +2,6 @@ from math import floor import shutil -import unittest from pathlib import Path import numpy as np diff --git a/tests/unit_tests/test_deplete_atom_number.py b/tests/unit_tests/test_deplete_atom_number.py index d36b96d38d..e3eb22aa55 100644 --- a/tests/unit_tests/test_deplete_atom_number.py +++ b/tests/unit_tests/test_deplete_atom_number.py @@ -1,180 +1,177 @@ -""" Tests for atom_number.py. """ - -import unittest +""" Tests for the AtomNumber class """ import numpy as np from openmc.deplete import atom_number -class TestAtomNumber(unittest.TestCase): - """Tests for the AtomNumber class.""" +def test_indexing(): + """Tests the __getitem__ and __setitem__ routines simultaneously.""" - def test_indexing(self): - """Tests the __getitem__ and __setitem__ routines simultaneously.""" + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number["10000", "U238"] = 1.0 + number["10001", "U238"] = 2.0 + number["10000", "U235"] = 3.0 + number["10001", "U235"] = 4.0 - number["10000", "U238"] = 1.0 - number["10001", "U238"] = 2.0 - number["10000", "U235"] = 3.0 - number["10001", "U235"] = 4.0 + # String indexing + assert number["10000", "U238"] == 1.0 + assert number["10001", "U238"] == 2.0 + assert number["10000", "U235"] == 3.0 + assert number["10001", "U235"] == 4.0 - # String indexing - self.assertEqual(number["10000", "U238"], 1.0) - self.assertEqual(number["10001", "U238"], 2.0) - self.assertEqual(number["10000", "U235"], 3.0) - self.assertEqual(number["10001", "U235"], 4.0) + # Int indexing + assert number[0, 0] == 1.0 + assert number[1, 0] == 2.0 + assert number[0, 1] == 3.0 + assert number[1, 1] == 4.0 - # Int indexing - self.assertEqual(number[0, 0], 1.0) - self.assertEqual(number[1, 0], 2.0) - self.assertEqual(number[0, 1], 3.0) - self.assertEqual(number[1, 1], 4.0) + number[0, 0] = 5.0 - number[0, 0] = 5.0 - - self.assertEqual(number[0, 0], 5.0) - self.assertEqual(number["10000", "U238"], 5.0) - - def test_n_mat(self): - """Test number of materials property. """ - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - self.assertEqual(number.n_mat, 2) - - def test_n_nuc(self): - """Test number of nuclides property.""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - self.assertEqual(number.n_nuc, 3) - - def test_burn_nuc_list(self): - """Test the list of burned nuclides property""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - self.assertEqual(number.burn_nuc_list, ["U238", "U235"]) - - def test_burn_mat_list(self): - """Test the list of burned nuclides property""" - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - self.assertEqual(number.burn_mat_list, ["10000", "10001"]) - - def test_density_indexing(self): - """Tests the get and set_atom_density routines simultaneously.""" - - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - number.set_atom_density("10000", "U238", 1.0) - number.set_atom_density("10001", "U238", 2.0) - number.set_atom_density("10002", "U238", 3.0) - number.set_atom_density("10000", "U235", 4.0) - number.set_atom_density("10001", "U235", 5.0) - number.set_atom_density("10002", "U235", 6.0) - number.set_atom_density("10000", "U234", 7.0) - number.set_atom_density("10001", "U234", 8.0) - number.set_atom_density("10002", "U234", 9.0) - - # String indexing - self.assertEqual(number.get_atom_density("10000", "U238"), 1.0) - self.assertEqual(number.get_atom_density("10001", "U238"), 2.0) - self.assertEqual(number.get_atom_density("10002", "U238"), 3.0) - self.assertEqual(number.get_atom_density("10000", "U235"), 4.0) - self.assertEqual(number.get_atom_density("10001", "U235"), 5.0) - self.assertEqual(number.get_atom_density("10002", "U235"), 6.0) - self.assertEqual(number.get_atom_density("10000", "U234"), 7.0) - self.assertEqual(number.get_atom_density("10001", "U234"), 8.0) - self.assertEqual(number.get_atom_density("10002", "U234"), 9.0) - - # Int indexing - self.assertEqual(number.get_atom_density(0, 0), 1.0) - self.assertEqual(number.get_atom_density(1, 0), 2.0) - self.assertEqual(number.get_atom_density(2, 0), 3.0) - self.assertEqual(number.get_atom_density(0, 1), 4.0) - self.assertEqual(number.get_atom_density(1, 1), 5.0) - self.assertEqual(number.get_atom_density(2, 1), 6.0) - self.assertEqual(number.get_atom_density(0, 2), 7.0) - self.assertEqual(number.get_atom_density(1, 2), 8.0) - self.assertEqual(number.get_atom_density(2, 2), 9.0) + assert number[0, 0] == 5.0 + assert number["10000", "U238"] == 5.0 - number.set_atom_density(0, 0, 5.0) +def test_n_mat(): + """Test number of materials property. """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} - self.assertEqual(number.get_atom_density(0, 0), 5.0) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - # Verify volume is used correctly - self.assertEqual(number[0, 0], 5.0 * 0.38) - self.assertEqual(number[1, 0], 2.0 * 0.21) - self.assertEqual(number[2, 0], 3.0 * 1.0) - self.assertEqual(number[0, 1], 4.0 * 0.38) - self.assertEqual(number[1, 1], 5.0 * 0.21) - self.assertEqual(number[2, 1], 6.0 * 1.0) - self.assertEqual(number[0, 2], 7.0 * 0.38) - self.assertEqual(number[1, 2], 8.0 * 0.21) - self.assertEqual(number[2, 2], 9.0 * 1.0) - - def test_get_mat_slice(self): - """Tests getting slices.""" - - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - number.number = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) - - sl = number.get_mat_slice(0) - - np.testing.assert_array_equal(sl, np.array([1.0, 2.0])) - - sl = number.get_mat_slice("10000") - - np.testing.assert_array_equal(sl, np.array([1.0, 2.0])) - - def test_set_mat_slice(self): - """Tests getting slices.""" - - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - number.set_mat_slice(0, [1.0, 2.0]) - - self.assertEqual(number[0, 0], 1.0) - self.assertEqual(number[0, 1], 2.0) - - number.set_mat_slice("10000", [3.0, 4.0]) - - self.assertEqual(number[0, 0], 3.0) - self.assertEqual(number[0, 1], 4.0) + assert number.n_mat == 2 -if __name__ == '__main__': - unittest.main() +def test_n_nuc(): + """Test number of nuclides property.""" + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + assert number.n_nuc == 3 + + +def test_burn_nuc_list(): + """Test the list of burned nuclides property""" + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + assert number.burn_nuc_list == ["U238", "U235"] + + +def test_burn_mat_list(): + """Test the list of burned nuclides property""" + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + assert number.burn_mat_list == ["10000", "10001"] + + +def test_density_indexing(): + """Tests the get and set_atom_density routines simultaneously.""" + + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + number.set_atom_density("10000", "U238", 1.0) + number.set_atom_density("10001", "U238", 2.0) + number.set_atom_density("10002", "U238", 3.0) + number.set_atom_density("10000", "U235", 4.0) + number.set_atom_density("10001", "U235", 5.0) + number.set_atom_density("10002", "U235", 6.0) + number.set_atom_density("10000", "U234", 7.0) + number.set_atom_density("10001", "U234", 8.0) + number.set_atom_density("10002", "U234", 9.0) + + # String indexing + assert number.get_atom_density("10000", "U238") == 1.0 + assert number.get_atom_density("10001", "U238") == 2.0 + assert number.get_atom_density("10002", "U238") == 3.0 + assert number.get_atom_density("10000", "U235") == 4.0 + assert number.get_atom_density("10001", "U235") == 5.0 + assert number.get_atom_density("10002", "U235") == 6.0 + assert number.get_atom_density("10000", "U234") == 7.0 + assert number.get_atom_density("10001", "U234") == 8.0 + assert number.get_atom_density("10002", "U234") == 9.0 + + # Int indexing + assert number.get_atom_density(0, 0) == 1.0 + assert number.get_atom_density(1, 0) == 2.0 + assert number.get_atom_density(2, 0) == 3.0 + assert number.get_atom_density(0, 1) == 4.0 + assert number.get_atom_density(1, 1) == 5.0 + assert number.get_atom_density(2, 1) == 6.0 + assert number.get_atom_density(0, 2) == 7.0 + assert number.get_atom_density(1, 2) == 8.0 + assert number.get_atom_density(2, 2) == 9.0 + + + number.set_atom_density(0, 0, 5.0) + assert number.get_atom_density(0, 0) == 5.0 + + # Verify volume is used correctly + assert number[0, 0] == 5.0 * 0.38 + assert number[1, 0] == 2.0 * 0.21 + assert number[2, 0] == 3.0 * 1.0 + assert number[0, 1] == 4.0 * 0.38 + assert number[1, 1] == 5.0 * 0.21 + assert number[2, 1] == 6.0 * 1.0 + assert number[0, 2] == 7.0 * 0.38 + assert number[1, 2] == 8.0 * 0.21 + assert number[2, 2] == 9.0 * 1.0 + + +def test_get_mat_slice(): + """Tests getting slices.""" + + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + number.number = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) + + sl = number.get_mat_slice(0) + + np.testing.assert_array_equal(sl, np.array([1.0, 2.0])) + + sl = number.get_mat_slice("10000") + + np.testing.assert_array_equal(sl, np.array([1.0, 2.0])) + + +def test_set_mat_slice(): + """Tests getting slices.""" + + mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + volume = {"10000" : 0.38, "10001" : 0.21} + + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + + number.set_mat_slice(0, [1.0, 2.0]) + + assert number[0, 0] == 1.0 + assert number[0, 1] == 2.0 + + number.set_mat_slice("10000", [3.0, 4.0]) + + assert number[0, 0] == 3.0 + assert number[0, 1] == 4.0 diff --git a/tests/unit_tests/test_deplete_cecm.py b/tests/unit_tests/test_deplete_cecm.py index 34c3435a76..264ad21671 100644 --- a/tests/unit_tests/test_deplete_cecm.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -1,9 +1,9 @@ -""" Regression tests for cecm.py""" +"""Regression tests for openmc.deplete.integrator.cecm algorithm. -import os -import unittest +These tests integrate a simple test problem described in dummy_geometry.py. +""" -import numpy as np +from pytest import approx import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities @@ -11,59 +11,30 @@ from openmc.deplete import utilities from tests import dummy_geometry -class TestCECMRegression(unittest.TestCase): - """ Regression tests for opendeplete.integrator.cecm algorithm. +def test_cecm(run_in_tmpdir): + """Integral regression test of integrator algorithm using CE/CM.""" - These tests integrate a simple test problem described in dummy_geometry.py. - """ + settings = openmc.deplete.Settings() + settings.dt_vec = [0.75, 0.75] + settings.output_dir = "test_integrator_regression" - @classmethod - def setUpClass(cls): - """ Save current directory in case integrator crashes.""" - cls.cwd = os.getcwd() - cls.results = "test_integrator_regression" + op = dummy_geometry.DummyGeometry(settings) - def test_cecm(self): - """ Integral regression test of integrator algorithm using CE/CM. """ + # Perform simulation using the MCNPX/MCNP6 algorithm + openmc.deplete.cecm(op, print_out=False) - settings = openmc.deplete.Settings() - settings.dt_vec = [0.75, 0.75] - settings.output_dir = self.results + # Load the files + res = results.read_results(settings.output_dir + "/results.h5") - op = dummy_geometry.DummyGeometry(settings) + _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") + _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") - # Perform simulation using the MCNPX/MCNP6 algorithm - openmc.deplete.cecm(op, print_out=False) + # Mathematica solution + s1 = [1.86872629872102, 1.395525772416039] + s2 = [2.18097439443550, 2.69429754646747] - # Load the files - res = results.read_results(settings.output_dir + "/results.h5") + assert y1[1] == approx(s1[0]) + assert y2[1] == approx(s1[1]) - _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") - _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") - - # Mathematica solution - s1 = [1.86872629872102, 1.395525772416039] - s2 = [2.18097439443550, 2.69429754646747] - - tol = 1.0e-13 - - self.assertLess(np.absolute(y1[1] - s1[0]), tol) - self.assertLess(np.absolute(y2[1] - s1[1]), tol) - - self.assertLess(np.absolute(y1[2] - s2[0]), tol) - self.assertLess(np.absolute(y2[2] - s2[1]), tol) - - @classmethod - def tearDownClass(cls): - """ Clean up files""" - - os.chdir(cls.cwd) - - openmc.deplete.comm.barrier() - if openmc.deplete.comm.rank == 0: - os.remove(os.path.join(cls.results, "results.h5")) - os.rmdir(cls.results) - - -if __name__ == '__main__': - unittest.main() + assert y1[2] == approx(s2[0]) + assert y2[2] == approx(s2[1]) diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 6166f15612..064b878afe 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -1,8 +1,7 @@ -"""Tests for depletion chains""" +"""Tests for openmc.deplete.Chain class.""" -from collections import OrderedDict +from collections.abc import Mapping import os -import unittest from pathlib import Path import numpy as np @@ -12,190 +11,184 @@ from openmc.deplete import comm, Chain, reaction_rates, nuclide _test_filename = str(Path(__file__).parents[2] / 'chains' / 'chain_test.xml') -class TestChain(unittest.TestCase): - """ Tests for Chain class.""" +def test_init(): + """Test depletion chain initialization.""" + dep = Chain() - def test__init__(self): - """ Test depletion chain initialization.""" - dep = Chain() - - self.assertIsInstance(dep.nuclides, list) - self.assertIsInstance(dep.nuclide_dict, OrderedDict) - self.assertIsInstance(dep.react_to_ind, OrderedDict) - - def test_n_nuclides(self): - """ Test depletion chain n_nuclides parameter. """ - dep = Chain() - - dep.nuclides = ["NucA", "NucB", "NucC"] - - self.assertEqual(dep.n_nuclides, 3) - - def test_from_endf(self): - """Test depletion chain building from ENDF. Empty at the moment until we figure - out a good way to unit-test this.""" - pass - - def test_from_xml(self): - """ Read chain_test.xml and ensure all values are correct. """ - # Unfortunately, this routine touches a lot of the code, but most of - # the components external to depletion_chain.py are simple storage - # types. - - dep = Chain.from_xml(_test_filename) - - # Basic checks - self.assertEqual(dep.n_nuclides, 3) - - # A tests - nuc = dep.nuclides[dep.nuclide_dict["A"]] - - self.assertEqual(nuc.name, "A") - self.assertEqual(nuc.half_life, 2.36520E+04) - self.assertEqual(nuc.n_decay_modes, 2) - modes = nuc.decay_modes - self.assertEqual([m.target for m in modes], ["B", "C"]) - self.assertEqual([m.type for m in modes], ["beta1", "beta2"]) - self.assertEqual([m.branching_ratio for m in modes], [0.6, 0.4]) - self.assertEqual(nuc.n_reaction_paths, 1) - self.assertEqual([r.target for r in nuc.reactions], ["C"]) - self.assertEqual([r.type for r in nuc.reactions], ["(n,gamma)"]) - self.assertEqual([r.branching_ratio for r in nuc.reactions], [1.0]) - - # B tests - nuc = dep.nuclides[dep.nuclide_dict["B"]] - - self.assertEqual(nuc.name, "B") - self.assertEqual(nuc.half_life, 3.29040E+04) - self.assertEqual(nuc.n_decay_modes, 1) - modes = nuc.decay_modes - self.assertEqual([m.target for m in modes], ["A"]) - self.assertEqual([m.type for m in modes], ["beta"]) - self.assertEqual([m.branching_ratio for m in modes], [1.0]) - self.assertEqual(nuc.n_reaction_paths, 1) - self.assertEqual([r.target for r in nuc.reactions], ["C"]) - self.assertEqual([r.type for r in nuc.reactions], ["(n,gamma)"]) - self.assertEqual([r.branching_ratio for r in nuc.reactions], [1.0]) - - # C tests - nuc = dep.nuclides[dep.nuclide_dict["C"]] - - self.assertEqual(nuc.name, "C") - self.assertEqual(nuc.n_decay_modes, 0) - self.assertEqual(nuc.n_reaction_paths, 3) - self.assertEqual([r.target for r in nuc.reactions], [None, "A", "B"]) - self.assertEqual([r.type for r in nuc.reactions], ["fission", "(n,gamma)", "(n,gamma)"]) - self.assertEqual([r.branching_ratio for r in nuc.reactions], [1.0, 0.7, 0.3]) - - # Yield tests - self.assertEqual(nuc.yield_energies, [0.0253]) - self.assertEqual(list(nuc.yield_data.keys()), [0.0253]) - self.assertEqual(nuc.yield_data[0.0253], - [("A", 0.0292737), ("B", 0.002566345)]) - - def test_export_to_xml(self): - """Test writing a depletion chain to XML.""" - - # Prevent different MPI ranks from conflicting - filename = 'test{}.xml'.format(comm.rank) - - A = nuclide.Nuclide() - A.name = "A" - A.half_life = 2.36520e4 - A.decay_modes = [ - nuclide.DecayTuple("beta1", "B", 0.6), - nuclide.DecayTuple("beta2", "C", 0.4) - ] - A.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)] - - B = nuclide.Nuclide() - B.name = "B" - B.half_life = 3.29040e4 - B.decay_modes = [nuclide.DecayTuple("beta", "A", 1.0)] - B.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)] - - C = nuclide.Nuclide() - C.name = "C" - C.reactions = [ - nuclide.ReactionTuple("fission", None, 2.0e8, 1.0), - nuclide.ReactionTuple("(n,gamma)", "A", 0.0, 0.7), - nuclide.ReactionTuple("(n,gamma)", "B", 0.0, 0.3) - ] - C.yield_energies = [0.0253] - C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} - - chain = Chain() - chain.nuclides = [A, B, C] - chain.export_to_xml(filename) - - original = open(_test_filename, 'r').read() - chain_xml = open(filename, 'r').read() - self.assertEqual(original, chain_xml) - - os.remove(filename) - - def test_form_matrix(self): - """ Using chain_test, and a dummy reaction rate, compute the matrix. """ - # Relies on test_from_xml passing. - - dep = Chain.from_xml(_test_filename) - - cell_ind = {"10000": 0, "10001": 1} - nuc_ind = {"A": 0, "B": 1, "C": 2} - react_ind = dep.react_to_ind - - react = reaction_rates.ReactionRates(cell_ind, nuc_ind, react_ind) - - dep.nuc_to_react_ind = nuc_ind - - react["10000", "C", "fission"] = 1.0 - react["10000", "A", "(n,gamma)"] = 2.0 - react["10000", "B", "(n,gamma)"] = 3.0 - react["10000", "C", "(n,gamma)"] = 4.0 - - mat = dep.form_matrix(react[0, :, :]) - # Loss A, decay, (n, gamma) - mat00 = -np.log(2) / 2.36520E+04 - 2 - # A -> B, decay, 0.6 branching ratio - mat10 = np.log(2) / 2.36520E+04 * 0.6 - # A -> C, decay, 0.4 branching ratio + (n,gamma) - mat20 = np.log(2) / 2.36520E+04 * 0.4 + 2 - - # B -> A, decay, 1.0 branching ratio - mat01 = np.log(2)/3.29040E+04 - # Loss B, decay, (n, gamma) - mat11 = -np.log(2)/3.29040E+04 - 3 - # B -> C, (n, gamma) - mat21 = 3 - - # C -> A fission, (n, gamma) - mat02 = 0.0292737 * 1.0 + 4.0 * 0.7 - # C -> B fission, (n, gamma) - mat12 = 0.002566345 * 1.0 + 4.0 * 0.3 - # Loss C, fission, (n, gamma) - mat22 = -1.0 - 4.0 - - self.assertEqual(mat[0, 0], mat00) - self.assertEqual(mat[1, 0], mat10) - self.assertEqual(mat[2, 0], mat20) - self.assertEqual(mat[0, 1], mat01) - self.assertEqual(mat[1, 1], mat11) - self.assertEqual(mat[2, 1], mat21) - self.assertEqual(mat[0, 2], mat02) - self.assertEqual(mat[1, 2], mat12) - self.assertEqual(mat[2, 2], mat22) - - def test_nuc_by_ind(self): - """ Test nuc_by_ind converter function. """ - dep = Chain() - - dep.nuclides = ["NucA", "NucB", "NucC"] - dep.nuclide_dict = {"NucA" : 0, "NucB" : 1, "NucC" : 2} - - self.assertEqual("NucA", dep.nuc_by_ind("NucA")) - self.assertEqual("NucB", dep.nuc_by_ind("NucB")) - self.assertEqual("NucC", dep.nuc_by_ind("NucC")) + assert isinstance(dep.nuclides, list) + assert isinstance(dep.nuclide_dict, Mapping) + assert isinstance(dep.react_to_ind, Mapping) -if __name__ == '__main__': - unittest.main() +def test_n_nuclides(): + """Test depletion chain n_nuclides parameter.""" + dep = Chain() + dep.nuclides = ["NucA", "NucB", "NucC"] + + assert dep.n_nuclides == 3 + + +def test_from_endf(): + """Test depletion chain building from ENDF. Empty at the moment until we figure + out a good way to unit-test this.""" + pass + + +def test_from_xml(): + """Read chain_test.xml and ensure all values are correct.""" + # Unfortunately, this routine touches a lot of the code, but most of + # the components external to depletion_chain.py are simple storage + # types. + + dep = Chain.from_xml(_test_filename) + + # Basic checks + assert dep.n_nuclides == 3 + + # A tests + nuc = dep.nuclides[dep.nuclide_dict["A"]] + + assert nuc.name == "A" + assert nuc.half_life == 2.36520E+04 + assert nuc.n_decay_modes == 2 + modes = nuc.decay_modes + assert [m.target for m in modes] == ["B", "C"] + assert [m.type for m in modes] == ["beta1", "beta2"] + assert [m.branching_ratio for m in modes] == [0.6, 0.4] + assert nuc.n_reaction_paths == 1 + assert [r.target for r in nuc.reactions] == ["C"] + assert [r.type for r in nuc.reactions] == ["(n,gamma)"] + assert [r.branching_ratio for r in nuc.reactions] == [1.0] + + # B tests + nuc = dep.nuclides[dep.nuclide_dict["B"]] + + assert nuc.name == "B" + assert nuc.half_life == 3.29040E+04 + assert nuc.n_decay_modes == 1 + modes = nuc.decay_modes + assert [m.target for m in modes] == ["A"] + assert [m.type for m in modes] == ["beta"] + assert [m.branching_ratio for m in modes] == [1.0] + assert nuc.n_reaction_paths == 1 + assert [r.target for r in nuc.reactions] == ["C"] + assert [r.type for r in nuc.reactions] == ["(n,gamma)"] + assert [r.branching_ratio for r in nuc.reactions] == [1.0] + + # C tests + nuc = dep.nuclides[dep.nuclide_dict["C"]] + + assert nuc.name == "C" + assert nuc.n_decay_modes == 0 + assert nuc.n_reaction_paths == 3 + assert [r.target for r in nuc.reactions] == [None, "A", "B"] + assert [r.type for r in nuc.reactions] == ["fission", "(n,gamma)", "(n,gamma)"] + assert [r.branching_ratio for r in nuc.reactions] == [1.0, 0.7, 0.3] + + # Yield tests + assert nuc.yield_energies == [0.0253] + assert list(nuc.yield_data) == [0.0253] + assert nuc.yield_data[0.0253] == [("A", 0.0292737), ("B", 0.002566345)] + + +def test_export_to_xml(run_in_tmpdir): + """Test writing a depletion chain to XML.""" + + # Prevent different MPI ranks from conflicting + filename = 'test{}.xml'.format(comm.rank) + + A = nuclide.Nuclide() + A.name = "A" + A.half_life = 2.36520e4 + A.decay_modes = [ + nuclide.DecayTuple("beta1", "B", 0.6), + nuclide.DecayTuple("beta2", "C", 0.4) + ] + A.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)] + + B = nuclide.Nuclide() + B.name = "B" + B.half_life = 3.29040e4 + B.decay_modes = [nuclide.DecayTuple("beta", "A", 1.0)] + B.reactions = [nuclide.ReactionTuple("(n,gamma)", "C", 0.0, 1.0)] + + C = nuclide.Nuclide() + C.name = "C" + C.reactions = [ + nuclide.ReactionTuple("fission", None, 2.0e8, 1.0), + nuclide.ReactionTuple("(n,gamma)", "A", 0.0, 0.7), + nuclide.ReactionTuple("(n,gamma)", "B", 0.0, 0.3) + ] + C.yield_energies = [0.0253] + C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} + + chain = Chain() + chain.nuclides = [A, B, C] + chain.export_to_xml(filename) + + original = open(_test_filename, 'r').read() + chain_xml = open(filename, 'r').read() + assert original == chain_xml + + +def test_form_matrix(): + """ Using chain_test, and a dummy reaction rate, compute the matrix. """ + # Relies on test_from_xml passing. + + dep = Chain.from_xml(_test_filename) + + cell_ind = {"10000": 0, "10001": 1} + nuc_ind = {"A": 0, "B": 1, "C": 2} + react_ind = dep.react_to_ind + + react = reaction_rates.ReactionRates(cell_ind, nuc_ind, react_ind) + + dep.nuc_to_react_ind = nuc_ind + + react["10000", "C", "fission"] = 1.0 + react["10000", "A", "(n,gamma)"] = 2.0 + react["10000", "B", "(n,gamma)"] = 3.0 + react["10000", "C", "(n,gamma)"] = 4.0 + + mat = dep.form_matrix(react[0, :, :]) + # Loss A, decay, (n, gamma) + mat00 = -np.log(2) / 2.36520E+04 - 2 + # A -> B, decay, 0.6 branching ratio + mat10 = np.log(2) / 2.36520E+04 * 0.6 + # A -> C, decay, 0.4 branching ratio + (n,gamma) + mat20 = np.log(2) / 2.36520E+04 * 0.4 + 2 + + # B -> A, decay, 1.0 branching ratio + mat01 = np.log(2)/3.29040E+04 + # Loss B, decay, (n, gamma) + mat11 = -np.log(2)/3.29040E+04 - 3 + # B -> C, (n, gamma) + mat21 = 3 + + # C -> A fission, (n, gamma) + mat02 = 0.0292737 * 1.0 + 4.0 * 0.7 + # C -> B fission, (n, gamma) + mat12 = 0.002566345 * 1.0 + 4.0 * 0.3 + # Loss C, fission, (n, gamma) + mat22 = -1.0 - 4.0 + + assert mat[0, 0] == mat00 + assert mat[1, 0] == mat10 + assert mat[2, 0] == mat20 + assert mat[0, 1] == mat01 + assert mat[1, 1] == mat11 + assert mat[2, 1] == mat21 + assert mat[0, 2] == mat02 + assert mat[1, 2] == mat12 + assert mat[2, 2] == mat22 + + +def test_nuc_by_ind(): + """ Test nuc_by_ind converter function. """ + dep = Chain() + dep.nuclides = ["NucA", "NucB", "NucC"] + dep.nuclide_dict = {"NucA" : 0, "NucB" : 1, "NucC" : 2} + + assert "NucA" == dep.nuc_by_ind("NucA") + assert "NucB" == dep.nuc_by_ind("NucB") + assert "NucC" == dep.nuc_by_ind("NucC") diff --git a/tests/unit_tests/test_deplete_cram.py b/tests/unit_tests/test_deplete_cram.py index 10f41fe2b6..21b93d17e3 100644 --- a/tests/unit_tests/test_deplete_cram.py +++ b/tests/unit_tests/test_deplete_cram.py @@ -1,48 +1,37 @@ -""" Tests for cram.py """ +""" Tests for cram.py -import unittest +Compares a few Mathematica matrix exponentials to CRAM16/CRAM48. +""" +from pytest import approx import numpy as np import scipy.sparse as sp from openmc.deplete.integrator import CRAM16, CRAM48 -class TestCram(unittest.TestCase): - """ Tests for cram.py +def test_CRAM16(): + """Test 16-term CRAM.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 0.1 - Compares a few Mathematica matrix exponentials to CRAM16/CRAM48. - """ + z = CRAM16(mat, x, dt) - def test_CRAM16(self): - """ Test 16-term CRAM. """ - x = np.array([1.0, 1.0]) - mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) - dt = 0.1 + # Solution from mathematica + z0 = np.array((0.904837418035960, 0.576799023327476)) - z = CRAM16(mat, x, dt) - - # Solution from mathematica - z0 = np.array((0.904837418035960, 0.576799023327476)) - - tol = 1.0e-15 - - self.assertLess(np.linalg.norm(z - z0), tol) - - def test_CRAM48(self): - """ Test 48-term CRAM. """ - x = np.array([1.0, 1.0]) - mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) - dt = 0.1 - - z = CRAM48(mat, x, dt) - - # Solution from mathematica - z0 = np.array((0.904837418035960, 0.576799023327476)) - - tol = 1.0e-15 - - self.assertLess(np.linalg.norm(z - z0), tol) + assert z == approx(z0) -if __name__ == '__main__': - unittest.main() +def test_CRAM48(): + """Test 48-term CRAM.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 0.1 + + z = CRAM48(mat, x, dt) + + # Solution from mathematica + z0 = np.array((0.904837418035960, 0.576799023327476)) + + assert z == approx(z0) diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 9b4cbe7802..3b6eed42dc 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -1,115 +1,101 @@ -""" Tests for integrator.py """ +"""Tests for integrator.py + +It is worth noting that openmc.deplete.integrate is extremely complex, to the +point I am unsure if it can be reasonably unit-tested. For the time being, it +will be left unimplemented and testing will be done via regression. + +""" import copy import os -import unittest from unittest.mock import MagicMock import numpy as np from openmc.deplete import integrator, ReactionRates, results, comm -class TestIntegrator(unittest.TestCase): - """ Tests for integrator.py +def test_save_results(run_in_tmpdir): + """Test data save module""" - It is worth noting that opendeplete.integrate is extremely complex, to - the point I am unsure if it can be reasonably unit-tested. For the time - being, it will be left unimplemented and testing will be done via - regression (in test_integrator_regression.py) - """ + stages = 3 - def test_save_results(self): - """ Test data save module """ + np.random.seed(comm.rank) - stages = 3 + # Mock geometry + op = MagicMock() - np.random.seed(comm.rank) + vol_dict = {} + full_burn_dict = {} - # Mock geometry - op = MagicMock() + j = 0 + for i in range(comm.size): + vol_dict[str(2*i)] = 1.2 + vol_dict[str(2*i + 1)] = 1.2 + full_burn_dict[str(2*i)] = j + full_burn_dict[str(2*i + 1)] = j + 1 + j += 2 - vol_dict = {} - full_burn_dict = {} + burn_list = [str(i) for i in range(2*comm.rank, 2*comm.rank + 2)] + nuc_list = ["na", "nb"] - j = 0 - for i in range(comm.size): - vol_dict[str(2*i)] = 1.2 - vol_dict[str(2*i + 1)] = 1.2 - full_burn_dict[str(2*i)] = j - full_burn_dict[str(2*i + 1)] = j + 1 - j += 2 + op.get_results_info.return_value = vol_dict, nuc_list, burn_list, full_burn_dict - burn_list = [str(i) for i in range(2*comm.rank, 2*comm.rank + 2)] - nuc_list = ["na", "nb"] + # Construct x + x1 = [] + x2 = [] - op.get_results_info.return_value = vol_dict, nuc_list, burn_list, full_burn_dict + for i in range(stages): + x1.append([np.random.rand(2), np.random.rand(2)]) + x2.append([np.random.rand(2), np.random.rand(2)]) - # Construct x - x1 = [] - x2 = [] + # Construct r + cell_dict = {s:i for i, s in enumerate(burn_list)} + r1 = ReactionRates(cell_dict, {"na":0, "nb":1}, {"ra":0, "rb":1}) + r1.rates = np.random.rand(2, 2, 2) - for i in range(stages): - x1.append([np.random.rand(2), np.random.rand(2)]) - x2.append([np.random.rand(2), np.random.rand(2)]) + rate1 = [] + rate2 = [] - # Construct r - cell_dict = {s:i for i, s in enumerate(burn_list)} - r1 = ReactionRates(cell_dict, {"na":0, "nb":1}, {"ra":0, "rb":1}) + for i in range(stages): + rate1.append(copy.deepcopy(r1)) + r1.rates = np.random.rand(2, 2, 2) + rate2.append(copy.deepcopy(r1)) r1.rates = np.random.rand(2, 2, 2) - rate1 = [] - rate2 = [] + # Create global terms + eigvl1 = np.random.rand(stages) + eigvl2 = np.random.rand(stages) + seed1 = [np.random.randint(100) for i in range(stages)] + seed2 = [np.random.randint(100) for i in range(stages)] - for i in range(stages): - rate1.append(copy.deepcopy(r1)) - r1.rates = np.random.rand(2, 2, 2) - rate2.append(copy.deepcopy(r1)) - r1.rates = np.random.rand(2, 2, 2) + eigvl1 = comm.bcast(eigvl1, root=0) + eigvl2 = comm.bcast(eigvl2, root=0) + seed1 = comm.bcast(seed1, root=0) + seed2 = comm.bcast(seed2, root=0) - # Create global terms - eigvl1 = np.random.rand(stages) - eigvl2 = np.random.rand(stages) - seed1 = [np.random.randint(100) for i in range(stages)] - seed2 = [np.random.randint(100) for i in range(stages)] + t1 = [0.0, 1.0] + t2 = [1.0, 2.0] - eigvl1 = comm.bcast(eigvl1, root=0) - eigvl2 = comm.bcast(eigvl2, root=0) - seed1 = comm.bcast(seed1, root=0) - seed2 = comm.bcast(seed2, root=0) + integrator.save_results(op, x1, rate1, eigvl1, seed1, t1, 0) + integrator.save_results(op, x2, rate2, eigvl2, seed2, t2, 1) - t1 = [0.0, 1.0] - t2 = [1.0, 2.0] + # Load the files + res = results.read_results("results.h5") - integrator.save_results(op, x1, rate1, eigvl1, seed1, t1, 0) - integrator.save_results(op, x2, rate2, eigvl2, seed2, t2, 1) + for i in range(stages): + for mat_i, mat in enumerate(burn_list): + for nuc_i, nuc in enumerate(nuc_list): + assert res[0][i, mat, nuc] == x1[i][mat_i][nuc_i] + assert res[1][i, mat, nuc] == x2[i][mat_i][nuc_i] + np.testing.assert_array_equal(res[0].rates[i][mat, nuc, :], + rate1[i][mat, nuc, :]) + np.testing.assert_array_equal(res[1].rates[i][mat, nuc, :], + rate2[i][mat, nuc, :]) - # Load the files - res = results.read_results("results.h5") + np.testing.assert_array_equal(res[0].k, eigvl1) + np.testing.assert_array_equal(res[0].seeds, seed1) + np.testing.assert_array_equal(res[0].time, t1) - for i in range(stages): - for mat_i, mat in enumerate(burn_list): - - for nuc_i, nuc in enumerate(nuc_list): - self.assertEqual(res[0][i, mat, nuc], x1[i][mat_i][nuc_i]) - self.assertEqual(res[1][i, mat, nuc], x2[i][mat_i][nuc_i]) - np.testing.assert_array_equal(res[0].rates[i][mat, nuc, :], - rate1[i][mat, nuc, :]) - np.testing.assert_array_equal(res[1].rates[i][mat, nuc, :], - rate2[i][mat, nuc, :]) - - np.testing.assert_array_equal(res[0].k, eigvl1) - np.testing.assert_array_equal(res[0].seeds, seed1) - np.testing.assert_array_equal(res[0].time, t1) - - np.testing.assert_array_equal(res[1].k, eigvl2) - np.testing.assert_array_equal(res[1].seeds, seed2) - np.testing.assert_array_equal(res[1].time, t2) - - # Delete files - comm.barrier() - if comm.rank == 0: - os.remove("results.h5") - - -if __name__ == '__main__': - unittest.main() + np.testing.assert_array_equal(res[1].k, eigvl2) + np.testing.assert_array_equal(res[1].seeds, seed2) + np.testing.assert_array_equal(res[1].time, t2) diff --git a/tests/unit_tests/test_deplete_nuclide.py b/tests/unit_tests/test_deplete_nuclide.py index ebcabc5ca9..2add13f866 100644 --- a/tests/unit_tests/test_deplete_nuclide.py +++ b/tests/unit_tests/test_deplete_nuclide.py @@ -1,44 +1,42 @@ -""" Tests for nuclide.py. """ +"""Tests for the openmc.deplete.Nuclide class.""" -import unittest import xml.etree.ElementTree as ET from openmc.deplete import nuclide -class TestNuclide(unittest.TestCase): - """ Tests for the nuclide class. """ +def test_n_decay_modes(): + """ Test the decay mode count parameter. """ - def test_n_decay_modes(self): - """ Test the decay mode count parameter. """ + nuc = nuclide.Nuclide() - nuc = nuclide.Nuclide() + nuc.decay_modes = [ + nuclide.DecayTuple("beta1", "a", 0.5), + nuclide.DecayTuple("beta2", "b", 0.3), + nuclide.DecayTuple("beta3", "c", 0.2) + ] - nuc.decay_modes = [ - nuclide.DecayTuple("beta1", "a", 0.5), - nuclide.DecayTuple("beta2", "b", 0.3), - nuclide.DecayTuple("beta3", "c", 0.2) - ] + assert nuc.n_decay_modes == 3 - self.assertEqual(nuc.n_decay_modes, 3) - def test_n_reaction_paths(self): - """ Test the reaction path count parameter. """ +def test_n_reaction_paths(): + """ Test the reaction path count parameter. """ - nuc = nuclide.Nuclide() + nuc = nuclide.Nuclide() - nuc.reactions = [ - nuclide.ReactionTuple("(n,2n)", "a", 0.0, 1.0), - nuclide.ReactionTuple("(n,3n)", "b", 0.0, 1.0), - nuclide.ReactionTuple("(n,4n)", "c", 0.0, 1.0) - ] + nuc.reactions = [ + nuclide.ReactionTuple("(n,2n)", "a", 0.0, 1.0), + nuclide.ReactionTuple("(n,3n)", "b", 0.0, 1.0), + nuclide.ReactionTuple("(n,4n)", "c", 0.0, 1.0) + ] - self.assertEqual(nuc.n_reaction_paths, 3) + assert nuc.n_reaction_paths == 3 - def test_from_xml(self): - """Test reading nuclide data from an XML element.""" - data = """ +def test_from_xml(): + """Test reading nuclide data from an XML element.""" + + data = """ @@ -55,67 +53,64 @@ class TestNuclide(unittest.TestCase): - """ + """ - element = ET.fromstring(data) - u235 = nuclide.Nuclide.from_xml(element) + element = ET.fromstring(data) + u235 = nuclide.Nuclide.from_xml(element) - self.assertEqual(u235.decay_modes, [ - nuclide.DecayTuple('sf', 'U235', 7.2e-11), - nuclide.DecayTuple('alpha', 'Th231', 1 - 7.2e-11) - ]) - self.assertEqual(u235.reactions, [ - nuclide.ReactionTuple('(n,2n)', 'U234', -5297781.0, 1.0), - nuclide.ReactionTuple('(n,3n)', 'U233', -12142300.0, 1.0), - nuclide.ReactionTuple('(n,4n)', 'U232', -17885600.0, 1.0), - nuclide.ReactionTuple('(n,gamma)', 'U236', 6545200.0, 1.0), - nuclide.ReactionTuple('fission', None, 193405400.0, 1.0), - ]) - self.assertEqual(u235.yield_energies, [0.0253]) - self.assertEqual(u235.yield_data, { - 0.0253: [('Te134', 0.062155), ('Zr100', 0.0497641), - ('Xe138', 0.0481413)] - }) - - def test_to_xml_element(self): - """Test writing nuclide data to an XML element.""" - - C = nuclide.Nuclide() - C.name = "C" - C.half_life = 0.123 - C.decay_modes = [ - nuclide.DecayTuple('beta-', 'B', 0.99), - nuclide.DecayTuple('alpha', 'D', 0.01) - ] - C.reactions = [ - nuclide.ReactionTuple('fission', None, 2.0e8, 1.0), - nuclide.ReactionTuple('(n,gamma)', 'A', 0.0, 1.0) - ] - C.yield_energies = [0.0253] - C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} - element = C.to_xml_element() - - self.assertEqual(element.get("half_life"), "0.123") - - decay_elems = element.findall("decay_type") - self.assertEqual(len(decay_elems), 2) - self.assertEqual(decay_elems[0].get("type"), "beta-") - self.assertEqual(decay_elems[0].get("target"), "B") - self.assertEqual(decay_elems[0].get("branching_ratio"), "0.99") - self.assertEqual(decay_elems[1].get("type"), "alpha") - self.assertEqual(decay_elems[1].get("target"), "D") - self.assertEqual(decay_elems[1].get("branching_ratio"), "0.01") - - rx_elems = element.findall("reaction_type") - self.assertEqual(len(rx_elems), 2) - self.assertEqual(rx_elems[0].get("type"), "fission") - self.assertEqual(float(rx_elems[0].get("Q")), 2.0e8) - self.assertEqual(rx_elems[1].get("type"), "(n,gamma)") - self.assertEqual(rx_elems[1].get("target"), "A") - self.assertEqual(float(rx_elems[1].get("Q")), 0.0) - - self.assertIsNotNone(element.find('neutron_fission_yields')) + assert u235.decay_modes == [ + nuclide.DecayTuple('sf', 'U235', 7.2e-11), + nuclide.DecayTuple('alpha', 'Th231', 1 - 7.2e-11) + ] + assert u235.reactions == [ + nuclide.ReactionTuple('(n,2n)', 'U234', -5297781.0, 1.0), + nuclide.ReactionTuple('(n,3n)', 'U233', -12142300.0, 1.0), + nuclide.ReactionTuple('(n,4n)', 'U232', -17885600.0, 1.0), + nuclide.ReactionTuple('(n,gamma)', 'U236', 6545200.0, 1.0), + nuclide.ReactionTuple('fission', None, 193405400.0, 1.0), + ] + assert u235.yield_energies == [0.0253] + assert u235.yield_data == { + 0.0253: [('Te134', 0.062155), ('Zr100', 0.0497641), + ('Xe138', 0.0481413)] + } -if __name__ == '__main__': - unittest.main() +def test_to_xml_element(): + """Test writing nuclide data to an XML element.""" + + C = nuclide.Nuclide() + C.name = "C" + C.half_life = 0.123 + C.decay_modes = [ + nuclide.DecayTuple('beta-', 'B', 0.99), + nuclide.DecayTuple('alpha', 'D', 0.01) + ] + C.reactions = [ + nuclide.ReactionTuple('fission', None, 2.0e8, 1.0), + nuclide.ReactionTuple('(n,gamma)', 'A', 0.0, 1.0) + ] + C.yield_energies = [0.0253] + C.yield_data = {0.0253: [("A", 0.0292737), ("B", 0.002566345)]} + element = C.to_xml_element() + + assert element.get("half_life") == "0.123" + + decay_elems = element.findall("decay_type") + assert len(decay_elems) == 2 + assert decay_elems[0].get("type") == "beta-" + assert decay_elems[0].get("target") == "B" + assert decay_elems[0].get("branching_ratio") == "0.99" + assert decay_elems[1].get("type") == "alpha" + assert decay_elems[1].get("target") == "D" + assert decay_elems[1].get("branching_ratio") == "0.01" + + rx_elems = element.findall("reaction_type") + assert len(rx_elems) == 2 + assert rx_elems[0].get("type") == "fission" + assert float(rx_elems[0].get("Q")) == 2.0e8 + assert rx_elems[1].get("type") == "(n,gamma)" + assert rx_elems[1].get("target") == "A" + assert float(rx_elems[1].get("Q")) == 0.0 + + assert element.find('neutron_fission_yields') is not None diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index 6ad2007d9c..d4b2efd330 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -1,68 +1,40 @@ -""" Regression tests for predictor.py""" +"""Regression tests for openmc.deplete.integrator.predictor algorithm. -import os -import unittest +These tests integrate a simple test problem described in dummy_geometry.py. +""" -import numpy as np +from pytest import approx import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities from tests import dummy_geometry -class TestPredictorRegression(unittest.TestCase): - """ Regression tests for opendeplete.integrator.predictor algorithm. - These tests integrate a simple test problem described in dummy_geometry.py. - """ +def test_predictor(): + """Integral regression test of integrator algorithm using predictor/corrector""" - @classmethod - def setUpClass(cls): - """ Save current directory in case integrator crashes.""" - cls.cwd = os.getcwd() - cls.results = "test_integrator_regression" + settings = openmc.deplete.Settings() + settings.dt_vec = [0.75, 0.75] + settings.output_dir = "test_integrator_regression" - def test_predictor(self): - """ Integral regression test of integrator algorithm using CE/CM. """ + op = dummy_geometry.DummyGeometry(settings) - settings = openmc.deplete.Settings() - settings.dt_vec = [0.75, 0.75] - settings.output_dir = self.results + # Perform simulation using the predictor algorithm + openmc.deplete.predictor(op, print_out=False) - op = dummy_geometry.DummyGeometry(settings) + # Load the files + res = results.read_results(settings.output_dir + "/results.h5") - # Perform simulation using the predictor algorithm - openmc.deplete.predictor(op, print_out=False) + _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") + _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") - # Load the files - res = results.read_results(settings.output_dir + "/results.h5") + # Mathematica solution + s1 = [2.46847546272295, 0.986431226850467] + s2 = [4.11525874568034, -0.0581692232513460] - _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") - _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") + assert y1[1] == approx(s1[0]) + assert y2[1] == approx(s1[1]) - # Mathematica solution - s1 = [2.46847546272295, 0.986431226850467] - s2 = [4.11525874568034, -0.0581692232513460] - - tol = 1.0e-13 - - self.assertLess(np.absolute(y1[1] - s1[0]), tol) - self.assertLess(np.absolute(y2[1] - s1[1]), tol) - - self.assertLess(np.absolute(y1[2] - s2[0]), tol) - self.assertLess(np.absolute(y2[2] - s2[1]), tol) - - @classmethod - def tearDownClass(cls): - """ Clean up files""" - - os.chdir(cls.cwd) - - openmc.deplete.comm.barrier() - if openmc.deplete.comm.rank == 0: - os.remove(os.path.join(cls.results, "results.h5")) - os.rmdir(cls.results) - - -if __name__ == '__main__': - unittest.main() + assert y1[2] == approx(s2[0]) + assert y2[2] == approx(s2[1]) diff --git a/tests/unit_tests/test_deplete_reaction.py b/tests/unit_tests/test_deplete_reaction.py index 2139be16c2..a98535e1d8 100644 --- a/tests/unit_tests/test_deplete_reaction.py +++ b/tests/unit_tests/test_deplete_reaction.py @@ -1,86 +1,80 @@ -""" Tests for reaction_rates.py. """ - -import unittest +"""Tests for the openmc.deplete.ReactionRates class.""" from openmc.deplete import reaction_rates -class TestReactionRates(unittest.TestCase): - """ Tests for the ReactionRates class. """ +def test_indexing(): + """Tests the __getitem__ and __setitem__ routines simultaneously.""" - def test_indexing(self): - """Tests the __getitem__ and __setitem__ routines simultaneously.""" + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1} - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1} + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + rates["10000", "U238", "fission"] = 1.0 + rates["10001", "U238", "fission"] = 2.0 + rates["10000", "U235", "fission"] = 3.0 + rates["10001", "U235", "fission"] = 4.0 + rates["10000", "U238", "(n,gamma)"] = 5.0 + rates["10001", "U238", "(n,gamma)"] = 6.0 + rates["10000", "U235", "(n,gamma)"] = 7.0 + rates["10001", "U235", "(n,gamma)"] = 8.0 - rates["10000", "U238", "fission"] = 1.0 - rates["10001", "U238", "fission"] = 2.0 - rates["10000", "U235", "fission"] = 3.0 - rates["10001", "U235", "fission"] = 4.0 - rates["10000", "U238", "(n,gamma)"] = 5.0 - rates["10001", "U238", "(n,gamma)"] = 6.0 - rates["10000", "U235", "(n,gamma)"] = 7.0 - rates["10001", "U235", "(n,gamma)"] = 8.0 + # String indexing + assert rates["10000", "U238", "fission"] == 1.0 + assert rates["10001", "U238", "fission"] == 2.0 + assert rates["10000", "U235", "fission"] == 3.0 + assert rates["10001", "U235", "fission"] == 4.0 + assert rates["10000", "U238", "(n,gamma)"] == 5.0 + assert rates["10001", "U238", "(n,gamma)"] == 6.0 + assert rates["10000", "U235", "(n,gamma)"] == 7.0 + assert rates["10001", "U235", "(n,gamma)"] == 8.0 - # String indexing - self.assertEqual(rates["10000", "U238", "fission"], 1.0) - self.assertEqual(rates["10001", "U238", "fission"], 2.0) - self.assertEqual(rates["10000", "U235", "fission"], 3.0) - self.assertEqual(rates["10001", "U235", "fission"], 4.0) - self.assertEqual(rates["10000", "U238", "(n,gamma)"], 5.0) - self.assertEqual(rates["10001", "U238", "(n,gamma)"], 6.0) - self.assertEqual(rates["10000", "U235", "(n,gamma)"], 7.0) - self.assertEqual(rates["10001", "U235", "(n,gamma)"], 8.0) + # Int indexing + assert rates[0, 0, 0] == 1.0 + assert rates[1, 0, 0] == 2.0 + assert rates[0, 1, 0] == 3.0 + assert rates[1, 1, 0] == 4.0 + assert rates[0, 0, 1] == 5.0 + assert rates[1, 0, 1] == 6.0 + assert rates[0, 1, 1] == 7.0 + assert rates[1, 1, 1] == 8.0 - # Int indexing - self.assertEqual(rates[0, 0, 0], 1.0) - self.assertEqual(rates[1, 0, 0], 2.0) - self.assertEqual(rates[0, 1, 0], 3.0) - self.assertEqual(rates[1, 1, 0], 4.0) - self.assertEqual(rates[0, 0, 1], 5.0) - self.assertEqual(rates[1, 0, 1], 6.0) - self.assertEqual(rates[0, 1, 1], 7.0) - self.assertEqual(rates[1, 1, 1], 8.0) + rates[0, 0, 0] = 5.0 - rates[0, 0, 0] = 5.0 - - self.assertEqual(rates[0, 0, 0], 5.0) - self.assertEqual(rates["10000", "U238", "fission"], 5.0) - - def test_n_mat(self): - """ Test number of materials property. """ - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) - - self.assertEqual(rates.n_mat, 2) - - def test_n_nuc(self): - """ Test number of nuclides property. """ - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) - - self.assertEqual(rates.n_nuc, 3) - - def test_n_react(self): - """ Test number of reactions property. """ - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) - - self.assertEqual(rates.n_react, 4) + assert rates[0, 0, 0] == 5.0 + assert rates["10000", "U238", "fission"] == 5.0 -if __name__ == '__main__': - unittest.main() +def test_n_mat(): + """Test number of materials property.""" + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} + + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + + assert rates.n_mat == 2 + + +def test_n_nuc(): + """Test number of nuclides property.""" + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} + + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + + assert rates.n_nuc == 3 + + +def test_n_react(): + """ Test number of reactions property. """ + mat_to_ind = {"10000" : 0, "10001" : 1} + nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} + react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} + + rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + + assert rates.n_react == 4 From 939d47cffa59fd8570a60fbb255cb4a4a8164dff Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 12:46:23 -0600 Subject: [PATCH 13/68] Simplify OpenMCSettings class (can properly delegate to openmc.Settings) --- openmc/deplete/openmc_wrapper.py | 116 ++++++-------------- scripts/example_run.py | 18 +-- tests/regression_tests/test_deplete_full.py | 29 +++-- 3 files changed, 56 insertions(+), 107 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 5c7a1aebaa..bf0a124710 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -31,7 +31,7 @@ from .function import Settings, Operator -def chunks(items, n): +def _chunks(items, n): min_size, extra = divmod(len(items), n) j = 0 chunk_list = [] @@ -43,70 +43,61 @@ def chunks(items, n): class OpenMCSettings(Settings): - """The OpenMCSettings class. - - Extends Settings to provide information OpenMC needs to run. + """Extends Settings to provide information OpenMC needs to run. Attributes ---------- dt_vec : numpy.array - Array of time steps to take. (From Settings) - tol : float - Tolerance for adaptive time stepping. (From Settings) + Array of time steps to in units of [s] output_dir : str - Path to output directory to save results. (From Settings) + Path to output directory to save results. chain_file : str - Path to the depletion chain xml file. Defaults to the environment - variable "OPENDEPLETE_CHAIN" if it exists. - particles : int - Number of particles to simulate per batch. - batches : int - Number of batches. - inactive : int - Number of inactive batches. - lower_left : list of float - Coordinate of lower left of bounding box of geometry. - upper_right : list of float - Coordinate of upper right of bounding box of geometry. - entropy_dimension : list of int - Grid size of entropy. - dilute_initial : float, default 1.0e3 + Path to the depletion chain xml file. Defaults to the + :envvar:`OPENDEPLETE_CHAIN` environment variable if it exists. + dilute_initial : float Initial atom density to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for - nuclides with reaction rates. + nuclides with reaction rates. Defaults to 1.0e3. round_number : bool Whether or not to round output to OpenMC to 8 digits. Useful in testing, as OpenMC is incredibly sensitive to exact values. - constant_seed : int - If present, all runs will be performed with this seed. power : float - Power of the reactor in W. For a 2D problem, the power can be given in + Power of the reactor in [W]. For a 2D problem, the power can be given in W/cm as long as the "volume" assigned to a depletion material is actually an area in cm^2. + settings : openmc.Settings + Settings for OpenMC simulations + """ + _depletion_attrs = {'dt_vec', 'output_dir', 'chain_file', 'dilute_initial', + 'round_number', 'power'} + def __init__(self): super().__init__() - # OpenMC specific try: self.chain_file = os.environ["OPENDEPLETE_CHAIN"] except KeyError: self.chain_file = None - self.particles = None - self.batches = None - self.inactive = None - self.lower_left = None - self.upper_right = None - self.entropy_dimension = None self.dilute_initial = 1.0e3 - - # OpenMC testing specific self.round_number = False - self.constant_seed = None - - # Depletion problem specific self.power = None + # Avoid setattr to create OpenMC settings + self.__dict__['settings'] = openmc.Settings() + + def __setattr__(self, name, value): + if name in self._depletion_attrs: + self.__dict__[name] = value + else: + setattr(self.__dict__['settings'], name, value) + + def __getattr__(self, name): + if name in self._depletion_attrs: + return self.__dict__[name] + else: + return getattr(self.__dict__['settings'], name) + class Materials(object): """The Materials class. @@ -290,8 +281,8 @@ class OpenMCOperator(Operator): i += 1 # Decompose geometry - mat_burn_lists = chunks(mat_burn, comm.size) - mat_not_burn_lists = chunks(mat_not_burn, comm.size) + mat_burn_lists = _chunks(mat_burn, comm.size) + mat_not_burn_lists = _chunks(mat_not_burn, comm.size) mat_tally_ind = OrderedDict() @@ -456,7 +447,7 @@ class OpenMCOperator(Operator): # Create XML files if comm.rank == 0: self.geometry.export_to_xml() - self.generate_settings_xml() + self.settings.settings.export_to_xml() self.generate_materials_xml() # Initialize OpenMC library @@ -526,46 +517,6 @@ class OpenMCOperator(Operator): materials.export_to_xml() - def generate_settings_xml(self): - """Generates settings.xml. - - This function creates settings.xml using the value of the settings - variable. - - Todo - ---- - Rewrite to generalize source box. - """ - - batches = self.settings.batches - inactive = self.settings.inactive - particles = self.settings.particles - - # Just a generic settings file to get it running. - settings_file = openmc.Settings() - settings_file.batches = batches - settings_file.inactive = inactive - settings_file.particles = particles - settings_file.source = openmc.Source(space=openmc.stats.Box( - self.settings.lower_left, self.settings.upper_right)) - - if self.settings.entropy_dimension is not None: - entropy_mesh = openmc.Mesh() - entropy_mesh.lower_left = self.settings.lower_left - entropy_mesh.upper_right = self.settings.upper_right - entropy_mesh.dimension = self.settings.entropy_dimension - settings_file.entropy_mesh = entropy_mesh - - # Set seed - if self.settings.constant_seed is not None: - seed = self.settings.constant_seed - else: - seed = random.randint(1, sys.maxsize-1) - - settings_file.seed = self.seed = seed - - settings_file.export_to_xml() - def _get_tally_nuclides(self): nuc_set = set() @@ -581,7 +532,6 @@ class OpenMCOperator(Operator): for i in range(1, comm.size): nuc_newset = comm.recv(source=i, tag=i) nuc_set |= nuc_newset - else: comm.send(nuc_set, dest=0, tag=comm.rank) diff --git a/scripts/example_run.py b/scripts/example_run.py index 78d7dceddc..56d42b21a7 100644 --- a/scripts/example_run.py +++ b/scripts/example_run.py @@ -1,6 +1,8 @@ """An example file showing how to run a simulation.""" import numpy as np +import openmc +from openmc.data import JOULE_PER_EV import openmc.deplete import example_geometry @@ -15,20 +17,18 @@ N = np.floor(dt2/dt1) dt = np.repeat([dt1], N) -# Create settings variable +# Depletion settings settings = openmc.deplete.OpenMCSettings() +settings.power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO +settings.dt_vec = dt +settings.output_dir = 'test' +# OpenMC-delegated settings settings.particles = 1000 settings.batches = 100 settings.inactive = 40 -settings.lower_left = lower_left -settings.upper_right = upper_right -settings.entropy_dimension = [10, 10, 1] - -joule_per_mev = 1.6021766208e-13 -settings.power = 2.337e15*4*joule_per_mev # MeV/second cm from CASMO -settings.dt_vec = dt -settings.output_dir = 'test' +settings.source = openmc.Source(space=openmc.stats.Box(lower_left, upper_right)) +settings.verbosity = 3 op = openmc.deplete.OpenMCOperator(geometry, settings) diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index 5f4af7a737..fcaa60eee5 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -5,6 +5,8 @@ import shutil from pathlib import Path import numpy as np +import openmc +from openmc.data import JOULE_PER_EV import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities @@ -35,26 +37,23 @@ def test_full(run_in_tmpdir): N = floor(dt2/dt1) dt = np.full(N, dt1) - # Create settings variable + # Depletion settings settings = openmc.deplete.OpenMCSettings() + settings.chain_file = str(Path(__file__).parents[2] / 'chains' / + 'chain_simple.xml') + settings.power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO + settings.dt_vec = dt + settings.output_dir = "test_full" + settings.round_number = True - - chain_file = str(Path(__file__).parents[2] / 'chains' / 'chain_simple.xml') - settings.chain_file = chain_file + # Add OpenMC-specific settings settings.particles = 100 settings.batches = 100 settings.inactive = 40 - settings.lower_left = lower_left - settings.upper_right = upper_right - settings.entropy_dimension = [10, 10, 1] - - settings.round_number = True - settings.constant_seed = 1 - - joule_per_mev = 1.6021766208e-13 - settings.power = 2.337e15*4*joule_per_mev # MeV/second cm from CASMO - settings.dt_vec = dt - settings.output_dir = "test_full" + space = openmc.stats.Box(lower_left, upper_right) + settings.source = openmc.Source(space=space) + settings.seed = 1 + settings.verbosity = 3 op = openmc.deplete.OpenMCOperator(geometry, settings) From f494fecf21b7e8de0066c27acdd1258e06641694 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 12:57:56 -0600 Subject: [PATCH 14/68] Change Operator.eval() -> Operator.__call__() --- openmc/deplete/function.py | 35 +++---- openmc/deplete/integrator/cecm.py | 6 +- openmc/deplete/integrator/predictor.py | 4 +- openmc/deplete/openmc_wrapper.py | 104 ++++++++++----------- tests/dummy_geometry.py | 16 ++-- tests/unit_tests/test_deplete_predictor.py | 2 +- 6 files changed, 84 insertions(+), 83 deletions(-) diff --git a/openmc/deplete/function.py b/openmc/deplete/function.py index bcc055e67b..b9694d88b1 100644 --- a/openmc/deplete/function.py +++ b/openmc/deplete/function.py @@ -27,33 +27,19 @@ class Settings(object): class Operator(metaclass=ABCMeta): - """The Operator metaclass. - - This defines all functions that the integrator needs to operate. + """Abstract class defining all methods needed for the integrator. Attributes ---------- settings : Settings Settings object. - """ + """ def __init__(self, settings): self.settings = settings @abstractmethod - def initial_condition(self): - """Performs final setup and returns initial condition. - - Returns - ------- - list of numpy.array - Total density for initial conditions. - """ - - pass - - @abstractmethod - def eval(self, vec, print_out=True): + def __call__(self, vec, print_out=True): """Runs a simulation. Parameters @@ -72,6 +58,17 @@ class Operator(metaclass=ABCMeta): seed : int Seed for this simulation. """ + pass + + @abstractmethod + def initial_condition(self): + """Performs final setup and returns initial condition. + + Returns + ------- + list of numpy.array + Total density for initial conditions. + """ pass @@ -114,3 +111,7 @@ class Operator(metaclass=ABCMeta): """ pass + + @abstractmethod + def finalize(self): + pass diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 699ccc2033..57a87c703e 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -62,7 +62,7 @@ def cecm(operator, print_out=True): eigvls = [] rates_array = [] - eigvl, rates, seed = operator.eval(x[0]) + eigvl, rates, seed = operator(x[0]) eigvls.append(eigvl) seeds.append(seed) @@ -86,7 +86,7 @@ def cecm(operator, print_out=True): x.append(x_result) - eigvl, rates, seed = operator.eval(x[1]) + eigvl, rates, seed = operator(x[1]) eigvls.append(eigvl) seeds.append(seed) @@ -119,7 +119,7 @@ def cecm(operator, print_out=True): seeds = [] eigvls = [] rates_array = [] - eigvl, rates, seed = operator.eval(x[0]) + eigvl, rates, seed = operator(x[0]) eigvls.append(eigvl) seeds.append(seed) diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 7b41e66493..1b8d00600c 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -53,7 +53,7 @@ def predictor(operator, print_out=True): eigvls = [] rates_array = [] - eigvl, rates, seed = operator.eval(x[0]) + eigvl, rates, seed = operator(x[0]) eigvls.append(eigvl) seeds.append(seed) @@ -86,7 +86,7 @@ def predictor(operator, print_out=True): seeds = [] eigvls = [] rates_array = [] - eigvl, rates, seed = operator.eval(x[0]) + eigvl, rates, seed = operator(x[0]) eigvls.append(eigvl) seeds.append(seed) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index bf0a124710..3dce9db7b9 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -207,6 +207,58 @@ class OpenMCOperator(Operator): # Create reaction rate tables self.initialize_reaction_rates() + def __call__(self, vec, print_out=True): + """Runs a simulation. + + Parameters + ---------- + vec : list of numpy.array + Total atoms to be used in function. + print_out : bool, optional + Whether or not to print out time. + + Returns + ------- + mat : list of scipy.sparse.csr_matrix + Matrices for the next step. + k : float + Eigenvalue of the problem. + rates : openmc.deplete.ReactionRates + Reaction rates from this simulation. + seed : int + Seed for this simulation. + """ + + # Prevent OpenMC from complaining about re-creating tallies + openmc.reset_auto_ids() + + # Update status + self.set_density(vec) + + time_start = time.time() + + # Update material compositions and tally nuclides + self._update_materials() + openmc.capi.tallies[1].nuclides = self._get_tally_nuclides() + + # Run OpenMC + openmc.capi.reset() + openmc.capi.run() + + time_openmc = time.time() + + # Extract results + k = self.unpack_tallies_and_normalize() + + if comm.rank == 0: + time_unpack = time.time() + + if print_out: + print("Time to openmc: ", time_openmc - time_start) + print("Time to unpack: ", time_unpack - time_openmc) + + return k, copy.deepcopy(self.reaction_rates), self.seed + def extract_mat_ids(self): """Extracts materials and assigns them to processes. @@ -365,58 +417,6 @@ class OpenMCOperator(Operator): self.chain.nuc_to_react_ind = self.burn_nuc_to_ind - def eval(self, vec, print_out=True): - """Runs a simulation. - - Parameters - ---------- - vec : list of numpy.array - Total atoms to be used in function. - print_out : bool, optional - Whether or not to print out time. - - Returns - ------- - mat : list of scipy.sparse.csr_matrix - Matrices for the next step. - k : float - Eigenvalue of the problem. - rates : openmc.deplete.ReactionRates - Reaction rates from this simulation. - seed : int - Seed for this simulation. - """ - - # Prevent OpenMC from complaining about re-creating tallies - openmc.reset_auto_ids() - - # Update status - self.set_density(vec) - - time_start = time.time() - - # Update material compositions and tally nuclides - self._update_materials() - openmc.capi.tallies[1].nuclides = self._get_tally_nuclides() - - # Run OpenMC - openmc.capi.reset() - openmc.capi.run() - - time_openmc = time.time() - - # Extract results - k = self.unpack_tallies_and_normalize() - - if comm.rank == 0: - time_unpack = time.time() - - if print_out: - print("Time to openmc: ", time_openmc - time_start) - print("Time to unpack: ", time_unpack - time_openmc) - - return k, copy.deepcopy(self.reaction_rates), self.seed - def form_matrix(self, y, mat): """Forms the depletion matrix. diff --git a/tests/dummy_geometry.py b/tests/dummy_geometry.py index ecdce567d6..585c1b11cd 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_geometry.py @@ -21,14 +21,7 @@ class DummyGeometry(Operator): def __init__(self, settings): super().__init__(settings) - def finalize(self): - pass - - @property - def chain(self): - return self - - def eval(self, vec, print_out=False): + def __call__(self, vec, print_out=False): """Evaluates F(y) Parameters @@ -60,6 +53,13 @@ class DummyGeometry(Operator): # Create a fake rates object return 0.0, reaction_rates, 0 + def finalize(self): + pass + + @property + def chain(self): + return self + def form_matrix(self, rates): """Forms the f(y) matrix in y' = f(y)y. diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index d4b2efd330..d808c46b8e 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -11,7 +11,7 @@ from openmc.deplete import utilities from tests import dummy_geometry -def test_predictor(): +def test_predictor(run_in_tmpdir): """Integral regression test of integrator algorithm using predictor/corrector""" settings = openmc.deplete.Settings() From fc6b3bd9d9a36b23efced294eb32b0e9a30b9a76 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 13:39:40 -0600 Subject: [PATCH 15/68] Make Results.from_hdf5 a classmethod --- openmc/deplete/integrator/cecm.py | 2 +- openmc/deplete/results.py | 62 +++++++++++++------------------ 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 57a87c703e..5432f172d0 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -30,7 +30,7 @@ def cecm(operator, print_out=True): .. [ref] Isotalo, Aarno. "Comparison of Neutronics-Depletion Coupling Schemes - for Burnup Calculations—Continued Study." Nuclear Science and + for Burnup Calculations-Continued Study." Nuclear Science and Engineering 180.3 (2015): 286-300. Parameters diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index c08b3ef40d..1a2ee4e437 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -302,7 +302,8 @@ class Results(object): if comm.rank == 0: time_dset[index, :] = self.time - def from_hdf5(self, handle, index): + @classmethod + def from_hdf5(cls, handle, index): """Loads results object from HDF5. Parameters @@ -312,6 +313,7 @@ class Results(object): index : int What step is this? """ + results = cls() # Grab handles number_dset = handle["/number"] @@ -319,15 +321,15 @@ class Results(object): seeds_dset = handle["/seeds"] time_dset = handle["/time"] - self.data = number_dset[index, :, :, :] - self.k = eigenvalues_dset[index, :] - self.seeds = seeds_dset[index, :] - self.time = time_dset[index, :] + results.data = number_dset[index, :, :, :] + results.k = eigenvalues_dset[index, :] + results.seeds = seeds_dset[index, :] + results.time = time_dset[index, :] # Reconstruct dictionaries - self.volume = OrderedDict() - self.mat_to_ind = OrderedDict() - self.nuc_to_ind = OrderedDict() + results.volume = OrderedDict() + results.mat_to_ind = OrderedDict() + results.nuc_to_ind = OrderedDict() rxn_nuc_to_ind = OrderedDict() rxn_to_ind = OrderedDict() @@ -336,13 +338,13 @@ class Results(object): vol = mat_handle.attrs["volume"] ind = mat_handle.attrs["index"] - self.volume[mat] = vol - self.mat_to_ind[mat] = ind + results.volume[mat] = vol + results.mat_to_ind[mat] = ind for nuc in handle["/nuclides"]: nuc_handle = handle["/nuclides/" + nuc] ind_atom = nuc_handle.attrs["atom number index"] - self.nuc_to_ind[nuc] = ind_atom + results.nuc_to_ind[nuc] = ind_atom if "reaction rate index" in nuc_handle.attrs: rxn_nuc_to_ind[nuc] = nuc_handle.attrs["reaction rate index"] @@ -351,13 +353,15 @@ class Results(object): rxn_handle = handle["/reactions/" + rxn] rxn_to_ind[rxn] = rxn_handle.attrs["index"] - self.rates = [] + results.rates = [] # Reconstruct reactions - for i in range(self.n_stages): - rate = ReactionRates(self.mat_to_ind, rxn_nuc_to_ind, rxn_to_ind) + for i in range(results.n_stages): + rate = ReactionRates(results.mat_to_ind, rxn_nuc_to_ind, rxn_to_ind) rate.rates = handle["/reaction rates"][index, i, :, :, :] - self.rates.append(rate) + results.rates.append(rate) + + return results def get_dict(number): @@ -419,7 +423,7 @@ def write_results(result, filename, index): def read_results(filename): - """Reads out a list of results objects from an hdf5 file. + """Return a list of Results objects from an HDF5 file. Parameters ---------- @@ -430,26 +434,12 @@ def read_results(filename): ------- results : list of Results The result objects. + """ + with h5py.File(filename, "r") as fh: + assert fh["version"].value == RESULTS_VERSION - file = h5py.File(filename, "r") + # Get number of results stored + n = fh["number"].value.shape[0] - assert file["/version"].value == RESULTS_VERSION - - # Grab handles - number_dset = file["/number"] - - # Get number of results stored - number_shape = list(number_dset.shape) - number_results = number_shape[0] - - results = [] - - for i in range(number_results): - result = Results() - result.from_hdf5(file, i) - results.append(result) - - file.close() - - return results + return [Results.from_hdf5(fh, i) for i in range(n)] From 730623246f53e25fd6875b04953ae3633aab4e75 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Feb 2018 15:36:00 -0600 Subject: [PATCH 16/68] Rename results.h5 -> depletion_results.h5. Use /materials in HDF5 file --- openmc/deplete/integrator/save_results.py | 4 ++-- openmc/deplete/results.py | 12 ++++-------- scripts/example_plot.py | 2 +- scripts/example_run.py | 1 - tests/regression_tests/test_deplete_full.py | 2 +- tests/regression_tests/test_reference.h5 | Bin 165384 -> 231608 bytes tests/unit_tests/test_deplete_cecm.py | 2 +- tests/unit_tests/test_deplete_integrator.py | 2 +- tests/unit_tests/test_deplete_predictor.py | 2 +- 9 files changed, 11 insertions(+), 16 deletions(-) diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py index 4f20b52fde..f580af838c 100644 --- a/openmc/deplete/integrator/save_results.py +++ b/openmc/deplete/integrator/save_results.py @@ -9,7 +9,7 @@ def save_results(op, x, rates, eigvls, seeds, t, step_ind): Parameters ---------- - op : Function + op : openmc.deplete.Operator The operator used to generate these results. x : list of list of numpy.array The prior x vectors. Indexed [i][cell] using the above equation. @@ -44,4 +44,4 @@ def save_results(op, x, rates, eigvls, seeds, t, step_ind): results.time = t results.rates = rates - write_results(results, "results.h5", step_ind) + write_results(results, "depletion_results.h5", step_ind) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 1a2ee4e437..fd48bb4df1 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -52,7 +52,6 @@ class Results(object): self.k = None self.seeds = None self.time = None - self.p_terms = None self.rates = None self.volume = None @@ -192,7 +191,7 @@ class Results(object): n_rxn = len(rxn_list) n_stages = self.n_stages - mat_group = handle.create_group("cells") + mat_group = handle.create_group("materials") for mat in mat_list: mat_single_group = mat_group.create_group(mat) @@ -333,24 +332,21 @@ class Results(object): rxn_nuc_to_ind = OrderedDict() rxn_to_ind = OrderedDict() - for mat in handle["/cells"]: - mat_handle = handle["/cells/" + mat] + for mat, mat_handle in handle["/materials"].items(): vol = mat_handle.attrs["volume"] ind = mat_handle.attrs["index"] results.volume[mat] = vol results.mat_to_ind[mat] = ind - for nuc in handle["/nuclides"]: - nuc_handle = handle["/nuclides/" + nuc] + for nuc, nuc_handle in handle["/nuclides"].items(): ind_atom = nuc_handle.attrs["atom number index"] results.nuc_to_ind[nuc] = ind_atom if "reaction rate index" in nuc_handle.attrs: rxn_nuc_to_ind[nuc] = nuc_handle.attrs["reaction rate index"] - for rxn in handle["/reactions"]: - rxn_handle = handle["/reactions/" + rxn] + for rxn, rxn_handle in handle["/reactions"].items(): rxn_to_ind[rxn] = rxn_handle.attrs["index"] results.rates = [] diff --git a/scripts/example_plot.py b/scripts/example_plot.py index c92fef6bf2..ab5ac204d8 100644 --- a/scripts/example_plot.py +++ b/scripts/example_plot.py @@ -8,7 +8,7 @@ from openmc.deplete import (read_results, evaluate_single_nuclide, result_folder = "test" # Load data -results = read_results(result_folder + "/results.h5") +results = read_results(result_folder + "/deplete_results.h5") cell = "5" nuc = "Gd157" diff --git a/scripts/example_run.py b/scripts/example_run.py index 56d42b21a7..30b6bdc2ee 100644 --- a/scripts/example_run.py +++ b/scripts/example_run.py @@ -28,7 +28,6 @@ settings.particles = 1000 settings.batches = 100 settings.inactive = 40 settings.source = openmc.Source(space=openmc.stats.Box(lower_left, upper_right)) -settings.verbosity = 3 op = openmc.deplete.OpenMCOperator(geometry, settings) diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index fcaa60eee5..e775e7af86 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -61,7 +61,7 @@ def test_full(run_in_tmpdir): openmc.deplete.integrator.predictor(op) # Load the files - res_test = results.read_results(settings.output_dir + "/results.h5") + res_test = results.read_results(settings.output_dir + "/depletion_results.h5") # Load the reference filename = str(Path(__file__).with_name('test_reference.h5')) diff --git a/tests/regression_tests/test_reference.h5 b/tests/regression_tests/test_reference.h5 index ef3ae0090943bc7ecdc01a1afaa70c0216e029f0..f832e3e2635c8d25a09609c38830227a3a551958 100644 GIT binary patch delta 27268 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 Q!*hq`vcUB6KWAq7$Bg66+$yY`3sm<}8}c9W2{B cSQs0YbHWrkOgv~leZqD|r|n&980&5V0K4QAp8x;= diff --git a/tests/unit_tests/test_deplete_cecm.py b/tests/unit_tests/test_deplete_cecm.py index 264ad21671..7fe8c6a043 100644 --- a/tests/unit_tests/test_deplete_cecm.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -24,7 +24,7 @@ def test_cecm(run_in_tmpdir): openmc.deplete.cecm(op, print_out=False) # Load the files - res = results.read_results(settings.output_dir + "/results.h5") + res = results.read_results(settings.output_dir + "/depletion_results.h5") _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 3b6eed42dc..964f99eee3 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -80,7 +80,7 @@ def test_save_results(run_in_tmpdir): integrator.save_results(op, x2, rate2, eigvl2, seed2, t2, 1) # Load the files - res = results.read_results("results.h5") + res = results.read_results("depletion_results.h5") for i in range(stages): for mat_i, mat in enumerate(burn_list): diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index d808c46b8e..8ec8964bbd 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -24,7 +24,7 @@ def test_predictor(run_in_tmpdir): openmc.deplete.predictor(op, print_out=False) # Load the files - res = results.read_results(settings.output_dir + "/results.h5") + res = results.read_results(settings.output_dir + "/depletion_results.h5") _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") From b3106a65044a4978ad36fe75e9ca8ab0aae0092b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 06:37:46 -0600 Subject: [PATCH 17/68] Make Operator a context manager. Smart handling of output_dir --- openmc/deplete/__init__.py | 2 +- openmc/deplete/{function.py => abc.py} | 32 ++++- openmc/deplete/integrator/cecm.py | 142 +++++++++----------- openmc/deplete/integrator/predictor.py | 94 ++++++------- openmc/deplete/openmc_wrapper.py | 13 +- tests/dummy_geometry.py | 5 +- tests/regression_tests/test_deplete_full.py | 3 +- tests/unit_tests/test_deplete_cecm.py | 2 +- tests/unit_tests/test_deplete_predictor.py | 2 +- 9 files changed, 144 insertions(+), 151 deletions(-) rename openmc/deplete/{function.py => abc.py} (78%) diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 19d1d13201..2467b973a9 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -18,7 +18,7 @@ from .nuclide import * from .chain import * from .openmc_wrapper import * from .reaction_rates import * -from .function import * +from .abc import * from .results import * from .integrator import * from .utilities import * diff --git a/openmc/deplete/function.py b/openmc/deplete/abc.py similarity index 78% rename from openmc/deplete/function.py rename to openmc/deplete/abc.py index b9694d88b1..63a80b45c7 100644 --- a/openmc/deplete/function.py +++ b/openmc/deplete/abc.py @@ -4,6 +4,9 @@ This module contains the Operator class, which is then passed to an integrator to run a full depletion simulation. """ +import os +from pathlib import Path + from abc import ABCMeta, abstractmethod @@ -16,14 +19,22 @@ class Settings(object): ---------- dt_vec : numpy.array Array of time steps to take. - output_dir : str + output_dir : pathlib.Path Path to output directory to save results. - """ + """ def __init__(self): # Integrator specific self.dt_vec = None - self.output_dir = None + self.output_dir = Path('.') + + @property + def output_dir(self): + return self._output_dir + + @output_dir.setter + def output_dir(self, output_dir): + self._output_dir = Path(output_dir) class Operator(metaclass=ABCMeta): @@ -60,6 +71,20 @@ class Operator(metaclass=ABCMeta): """ pass + def __enter__(self): + # Save current directory and move to specific output directory + self._orig_dir = os.getcwd() + self.settings.output_dir.mkdir(exist_ok=True) + + # In Python 3.6+, chdir accepts a Path directly + os.chdir(str(self.settings.output_dir)) + + return self.initial_condition() + + def __exit__(self, exc_type, exc_value, traceback): + self.finalize() + os.chdir(self._orig_dir) + @abstractmethod def initial_condition(self): """Performs final setup and returns initial condition. @@ -112,6 +137,5 @@ class Operator(metaclass=ABCMeta): pass - @abstractmethod def finalize(self): pass diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 5432f172d0..148b02b4ad 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -41,96 +41,82 @@ def cecm(operator, print_out=True): Whether or not to print out time. """ - # Save current directory - dir_home = os.getcwd() - - # Move to folder - os.makedirs(operator.settings.output_dir, exist_ok=True) - os.chdir(operator.settings.output_dir) - # Generate initial conditions - vec = operator.initial_condition() + with operator as vec: + n_mats = len(vec) - n_mats = len(vec) + t = 0.0 - t = 0.0 + for i, dt in enumerate(operator.settings.dt_vec): + # Create vectors + x = [copy.deepcopy(vec)] + seeds = [] + eigvls = [] + rates_array = [] - for i, dt in enumerate(operator.settings.dt_vec): - # Create vectors + eigvl, rates, seed = operator(x[0]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + t_start = time.time() + + chains = repeat(operator.chain, n_mats) + vecs = (x[0][i] for i in range(n_mats)) + rates = (rates_array[0][i, :, :] for i in range(n_mats)) + dts = repeat(dt/2, n_mats) + + with Pool() as pool: + iters = zip(chains, vecs, rates, dts) + x_result = list(pool.starmap(cram_wrapper, iters)) + + t_end = time.time() + if comm.rank == 0: + if print_out: + print("Time to matexp: ", t_end - t_start) + + x.append(x_result) + + eigvl, rates, seed = operator(x[1]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + t_start = time.time() + + chains = repeat(operator.chain, n_mats) + vecs = (x[0][i] for i in range(n_mats)) + rates = (rates_array[1][i, :, :] for i in range(n_mats)) + dts = repeat(dt, n_mats) + + with Pool() as pool: + iters = zip(chains, vecs, rates, dts) + x_result = list(pool.starmap(cram_wrapper, iters)) + + t_end = time.time() + if comm.rank == 0: + if print_out: + print("Time to matexp: ", t_end - t_start) + + # Create results, write to disk + save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) + + t += dt + vec = copy.deepcopy(x_result) + + # Perform one last simulation x = [copy.deepcopy(vec)] seeds = [] eigvls = [] rates_array = [] - eigvl, rates, seed = operator(x[0]) eigvls.append(eigvl) seeds.append(seed) rates_array.append(rates) - t_start = time.time() - - chains = repeat(operator.chain, n_mats) - vecs = (x[0][i] for i in range(n_mats)) - rates = (rates_array[0][i, :, :] for i in range(n_mats)) - dts = repeat(dt/2, n_mats) - - with Pool() as pool: - iters = zip(chains, vecs, rates, dts) - x_result = list(pool.starmap(cram_wrapper, iters)) - - t_end = time.time() - if comm.rank == 0: - if print_out: - print("Time to matexp: ", t_end - t_start) - - x.append(x_result) - - eigvl, rates, seed = operator(x[1]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) - - t_start = time.time() - - chains = repeat(operator.chain, n_mats) - vecs = (x[0][i] for i in range(n_mats)) - rates = (rates_array[1][i, :, :] for i in range(n_mats)) - dts = repeat(dt, n_mats) - - with Pool() as pool: - iters = zip(chains, vecs, rates, dts) - x_result = list(pool.starmap(cram_wrapper, iters)) - - t_end = time.time() - if comm.rank == 0: - if print_out: - print("Time to matexp: ", t_end - t_start) - # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) - - t += dt - vec = copy.deepcopy(x_result) - - # Perform one last simulation - x = [copy.deepcopy(vec)] - seeds = [] - eigvls = [] - rates_array = [] - eigvl, rates, seed = operator(x[0]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) - - # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t], - len(operator.settings.dt_vec)) - - # Return to origin - os.chdir(dir_home) - - # Release resources - operator.finalize() + save_results(operator, x, rates_array, eigvls, seeds, [t, t], + len(operator.settings.dt_vec)) diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 1b8d00600c..8d499d1ff4 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -32,27 +32,52 @@ def predictor(operator, print_out=True): Whether or not to print out time. """ - # Save current directory - dir_home = os.getcwd() - - # Move to folder - os.makedirs(operator.settings.output_dir, exist_ok=True) - os.chdir(operator.settings.output_dir) - # Generate initial conditions - vec = operator.initial_condition() + with operator as vec: + n_mats = len(vec) - n_mats = len(vec) + t = 0.0 - t = 0.0 + for i, dt in enumerate(operator.settings.dt_vec): + # Create vectors + x = [copy.deepcopy(vec)] + seeds = [] + eigvls = [] + rates_array = [] - for i, dt in enumerate(operator.settings.dt_vec): - # Create vectors + eigvl, rates, seed = operator(x[0]) + + eigvls.append(eigvl) + seeds.append(seed) + rates_array.append(rates) + + # Create results, write to disk + save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) + + t_start = time.time() + + chains = repeat(operator.chain, n_mats) + vecs = (x[0][i] for i in range(n_mats)) + rates = (rates_array[0][i, :, :] for i in range(n_mats)) + dts = repeat(dt, n_mats) + + with Pool() as pool: + iters = zip(chains, vecs, rates, dts) + x_result = list(pool.starmap(cram_wrapper, iters)) + + t_end = time.time() + if comm.rank == 0: + if print_out: + print("Time to matexp: ", t_end - t_start) + + t += dt + vec = copy.deepcopy(x_result) + + # Perform one last simulation x = [copy.deepcopy(vec)] seeds = [] eigvls = [] rates_array = [] - eigvl, rates, seed = operator(x[0]) eigvls.append(eigvl) @@ -60,44 +85,5 @@ def predictor(operator, print_out=True): rates_array.append(rates) # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) - - t_start = time.time() - - chains = repeat(operator.chain, n_mats) - vecs = (x[0][i] for i in range(n_mats)) - rates = (rates_array[0][i, :, :] for i in range(n_mats)) - dts = repeat(dt, n_mats) - - with Pool() as pool: - iters = zip(chains, vecs, rates, dts) - x_result = list(pool.starmap(cram_wrapper, iters)) - - t_end = time.time() - if comm.rank == 0: - if print_out: - print("Time to matexp: ", t_end - t_start) - - t += dt - vec = copy.deepcopy(x_result) - - # Perform one last simulation - x = [copy.deepcopy(vec)] - seeds = [] - eigvls = [] - rates_array = [] - eigvl, rates, seed = operator(x[0]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) - - # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t], - len(operator.settings.dt_vec)) - - # Return to origin - os.chdir(dir_home) - - # Release resources - operator.finalize() + save_results(operator, x, rates_array, eigvls, seeds, [t, t], + len(operator.settings.dt_vec)) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 3dce9db7b9..701a35b7d3 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -23,12 +23,10 @@ import openmc import openmc.capi from openmc.data import JOULE_PER_EV from . import comm +from .abc import Settings, Operator from .atom_number import AtomNumber from .chain import Chain from .reaction_rates import ReactionRates -from .function import Settings, Operator - - def _chunks(items, n): @@ -49,7 +47,7 @@ class OpenMCSettings(Settings): ---------- dt_vec : numpy.array Array of time steps to in units of [s] - output_dir : str + output_dir : pathlib.Path Path to output directory to save results. chain_file : str Path to the depletion chain xml file. Defaults to the @@ -70,7 +68,7 @@ class OpenMCSettings(Settings): """ - _depletion_attrs = {'dt_vec', 'output_dir', 'chain_file', 'dilute_initial', + _depletion_attrs = {'dt_vec', '_output_dir', 'chain_file', 'dilute_initial', 'round_number', 'power'} def __init__(self): @@ -87,7 +85,10 @@ class OpenMCSettings(Settings): self.__dict__['settings'] = openmc.Settings() def __setattr__(self, name, value): - if name in self._depletion_attrs: + if hasattr(self.__class__, name): + prop = getattr(self.__class__, name) + prop.fset(self, value) + elif name in self._depletion_attrs: self.__dict__[name] = value else: setattr(self.__dict__['settings'], name, value) diff --git a/tests/dummy_geometry.py b/tests/dummy_geometry.py index 585c1b11cd..aab396b85a 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_geometry.py @@ -1,7 +1,7 @@ import numpy as np import scipy.sparse as sp from openmc.deplete.reaction_rates import ReactionRates -from openmc.deplete.function import Operator +from openmc.deplete.abc import Operator class DummyGeometry(Operator): @@ -53,9 +53,6 @@ class DummyGeometry(Operator): # Create a fake rates object return 0.0, reaction_rates, 0 - def finalize(self): - pass - @property def chain(self): return self diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index e775e7af86..c809ba0536 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -43,7 +43,6 @@ def test_full(run_in_tmpdir): 'chain_simple.xml') settings.power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO settings.dt_vec = dt - settings.output_dir = "test_full" settings.round_number = True # Add OpenMC-specific settings @@ -61,7 +60,7 @@ def test_full(run_in_tmpdir): openmc.deplete.integrator.predictor(op) # Load the files - res_test = results.read_results(settings.output_dir + "/depletion_results.h5") + res_test = results.read_results(settings.output_dir / "depletion_results.h5") # Load the reference filename = str(Path(__file__).with_name('test_reference.h5')) diff --git a/tests/unit_tests/test_deplete_cecm.py b/tests/unit_tests/test_deplete_cecm.py index 7fe8c6a043..66c3ee156c 100644 --- a/tests/unit_tests/test_deplete_cecm.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -24,7 +24,7 @@ def test_cecm(run_in_tmpdir): openmc.deplete.cecm(op, print_out=False) # Load the files - res = results.read_results(settings.output_dir + "/depletion_results.h5") + res = results.read_results(settings.output_dir / "depletion_results.h5") _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index 8ec8964bbd..f1133f87d2 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -24,7 +24,7 @@ def test_predictor(run_in_tmpdir): openmc.deplete.predictor(op, print_out=False) # Load the files - res = results.read_results(settings.output_dir + "/depletion_results.h5") + res = results.read_results(settings.output_dir / "depletion_results.h5") _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") From 484a0238888315242c5327d9a4ef7163ba806e64 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 07:03:01 -0600 Subject: [PATCH 18/68] Move more attributes to abc.Settings --- openmc/deplete/abc.py | 20 ++++++++++++++++++-- openmc/deplete/openmc_wrapper.py | 15 ++++++--------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 63a80b45c7..e2b286d6a4 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -21,12 +21,28 @@ class Settings(object): Array of time steps to take. output_dir : pathlib.Path Path to output directory to save results. + chain_file : str + Path to the depletion chain xml file. Defaults to the + :envvar:`OPENDEPLETE_CHAIN` environment variable if it exists. + dilute_initial : float + Initial atom density to add for nuclides that are zero in initial + condition to ensure they exist in the decay chain. Only done for + nuclides with reaction rates. Defaults to 1.0e3. + power : float + Power of the reactor in [W]. For a 2D problem, the power can be given in + W/cm as long as the "volume" assigned to a depletion material is + actually an area in cm^2. """ def __init__(self): - # Integrator specific + try: + self.chain_file = os.environ["OPENDEPLETE_CHAIN"] + except KeyError: + self.chain_file = None self.dt_vec = None - self.output_dir = Path('.') + self.output_dir = '.' + self.power = None + self.dilute_initial = 1.0e3 @property def output_dir(self): diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 701a35b7d3..b5b91f2ac4 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -56,13 +56,13 @@ class OpenMCSettings(Settings): Initial atom density to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for nuclides with reaction rates. Defaults to 1.0e3. - round_number : bool - Whether or not to round output to OpenMC to 8 digits. - Useful in testing, as OpenMC is incredibly sensitive to exact values. power : float Power of the reactor in [W]. For a 2D problem, the power can be given in W/cm as long as the "volume" assigned to a depletion material is actually an area in cm^2. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. settings : openmc.Settings Settings for OpenMC simulations @@ -73,24 +73,21 @@ class OpenMCSettings(Settings): def __init__(self): super().__init__() - try: - self.chain_file = os.environ["OPENDEPLETE_CHAIN"] - except KeyError: - self.chain_file = None - self.dilute_initial = 1.0e3 self.round_number = False - self.power = None # Avoid setattr to create OpenMC settings self.__dict__['settings'] = openmc.Settings() def __setattr__(self, name, value): if hasattr(self.__class__, name): + # Use properties when appropriate prop = getattr(self.__class__, name) prop.fset(self, value) elif name in self._depletion_attrs: + # For known attributes, store in dictionary self.__dict__[name] = value else: + # otherwise, delegate to openmc.Settings setattr(self.__dict__['settings'], name, value) def __getattr__(self, name): From 998a562a33b214fa41f62166ebd78c83eb77aed8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 09:47:37 -0600 Subject: [PATCH 19/68] Have Operator() return a namedtuple (simplifies integrators quite a bit) --- openmc/deplete/abc.py | 4 ++ openmc/deplete/integrator/cecm.py | 58 ++++++--------------- openmc/deplete/integrator/predictor.py | 42 +++++---------- openmc/deplete/integrator/save_results.py | 20 +++---- openmc/deplete/openmc_wrapper.py | 4 +- tests/dummy_geometry.py | 4 +- tests/unit_tests/test_deplete_integrator.py | 15 ++++-- 7 files changed, 55 insertions(+), 92 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index e2b286d6a4..155261742c 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -4,6 +4,7 @@ This module contains the Operator class, which is then passed to an integrator to run a full depletion simulation. """ +from collections import namedtuple import os from pathlib import Path @@ -53,6 +54,9 @@ class Settings(object): self._output_dir = Path(output_dir) +OperatorResult = namedtuple('OperatorResult', ['k', 'rates', 'seed']) + + class Operator(metaclass=ABCMeta): """Abstract class defining all methods needed for the integrator. diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 148b02b4ad..16fa299fd3 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -12,7 +12,7 @@ from .save_results import save_results def cecm(operator, print_out=True): - """The CE/CM integrator. + r"""The CE/CM integrator. Implements the second order CE/CM Predictor-Corrector algorithm [ref]_. This algorithm is mathematically defined as: @@ -22,11 +22,11 @@ def cecm(operator, print_out=True): A_p &= A(y_n, t_n) - y_m &= \\text{expm}(A_p h/2) y_n + y_m &= \text{expm}(A_p h/2) y_n A_c &= A(y_m, t_n + h/2) - y_{n+1} &= \\text{expm}(A_c h) y_n + y_{n+1} &= \text{expm}(A_c h) y_n .. [ref] Isotalo, Aarno. "Comparison of Neutronics-Depletion Coupling Schemes @@ -35,88 +35,64 @@ def cecm(operator, print_out=True): Parameters ---------- - operator : Operator + operator : openmc.deplete.Operator The operator object to simulate on. print_out : bool, optional Whether or not to print out time. - """ + """ # Generate initial conditions with operator as vec: n_mats = len(vec) t = 0.0 - for i, dt in enumerate(operator.settings.dt_vec): - # Create vectors + # Get beginning-of-timestep reaction rates x = [copy.deepcopy(vec)] - seeds = [] - eigvls = [] - rates_array = [] - - eigvl, rates, seed = operator(x[0]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) + results = [operator(x[0])] + # Deplete for first half of timestep t_start = time.time() - chains = repeat(operator.chain, n_mats) vecs = (x[0][i] for i in range(n_mats)) - rates = (rates_array[0][i, :, :] for i in range(n_mats)) + rates = (results[0].rates[i, :, :] for i in range(n_mats)) dts = repeat(dt/2, n_mats) - with Pool() as pool: iters = zip(chains, vecs, rates, dts) x_result = list(pool.starmap(cram_wrapper, iters)) - t_end = time.time() if comm.rank == 0: if print_out: print("Time to matexp: ", t_end - t_start) + # Get middle-of-timestep reaction rates x.append(x_result) + results.append(operator(x_result)) - eigvl, rates, seed = operator(x[1]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) - + # Deplete for second half of timestep t_start = time.time() - chains = repeat(operator.chain, n_mats) vecs = (x[0][i] for i in range(n_mats)) - rates = (rates_array[1][i, :, :] for i in range(n_mats)) + rates = (results[1].rates[i, :, :] for i in range(n_mats)) dts = repeat(dt, n_mats) - with Pool() as pool: iters = zip(chains, vecs, rates, dts) x_result = list(pool.starmap(cram_wrapper, iters)) - t_end = time.time() if comm.rank == 0: if print_out: print("Time to matexp: ", t_end - t_start) # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) + save_results(operator, x, results, [t, t + dt], i) + # Advance time, update vector t += dt vec = copy.deepcopy(x_result) # Perform one last simulation x = [copy.deepcopy(vec)] - seeds = [] - eigvls = [] - rates_array = [] - eigvl, rates, seed = operator(x[0]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) + results = [operator(x[0])] # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t], - len(operator.settings.dt_vec)) + save_results(operator, x, results, [t, t], len(operator.settings.dt_vec)) diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 8d499d1ff4..872b5ebb19 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -12,7 +12,7 @@ from .save_results import save_results def predictor(operator, print_out=True): - """The basic predictor integrator. + r"""The basic predictor integrator. Implements the first order predictor algorithm. This algorithm is mathematically defined as: @@ -22,68 +22,50 @@ def predictor(operator, print_out=True): A_p &= A(y_n, t_n) - y_{n+1} &= \\text{expm}(A_p h) y_n + y_{n+1} &= \text{expm}(A_p h) y_n Parameters ---------- - operator : Operator + operator : openmc.deplete.Operator The operator object to simulate on. print_out : bool, optional Whether or not to print out time. - """ + """ # Generate initial conditions with operator as vec: n_mats = len(vec) t = 0.0 - for i, dt in enumerate(operator.settings.dt_vec): - # Create vectors + # Get beginning-of-timestep reaction rates x = [copy.deepcopy(vec)] - seeds = [] - eigvls = [] - rates_array = [] - - eigvl, rates, seed = operator(x[0]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) + results = [operator(x[0])] # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t + dt], i) + save_results(operator, x, results, [t, t + dt], i) + # Deplete for full timestep t_start = time.time() - chains = repeat(operator.chain, n_mats) vecs = (x[0][i] for i in range(n_mats)) - rates = (rates_array[0][i, :, :] for i in range(n_mats)) + rates = (results[0].rates[i, :, :] for i in range(n_mats)) dts = repeat(dt, n_mats) - with Pool() as pool: iters = zip(chains, vecs, rates, dts) x_result = list(pool.starmap(cram_wrapper, iters)) - t_end = time.time() if comm.rank == 0: if print_out: print("Time to matexp: ", t_end - t_start) + # Advance time, update vector t += dt vec = copy.deepcopy(x_result) # Perform one last simulation x = [copy.deepcopy(vec)] - seeds = [] - eigvls = [] - rates_array = [] - eigvl, rates, seed = operator(x[0]) - - eigvls.append(eigvl) - seeds.append(seed) - rates_array.append(rates) + results = [operator(x[0])] # Create results, write to disk - save_results(operator, x, rates_array, eigvls, seeds, [t, t], - len(operator.settings.dt_vec)) + save_results(operator, x, results, [t, t], len(operator.settings.dt_vec)) diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py index f580af838c..31cd9b2880 100644 --- a/openmc/deplete/integrator/save_results.py +++ b/openmc/deplete/integrator/save_results.py @@ -4,8 +4,8 @@ from ..results import Results, write_results -def save_results(op, x, rates, eigvls, seeds, t, step_ind): - """ Creates and writes results to disk +def save_results(op, x, op_results, t, step_ind): + """Creates and writes depletion results to disk Parameters ---------- @@ -13,18 +13,14 @@ def save_results(op, x, rates, eigvls, seeds, t, step_ind): The operator used to generate these results. x : list of list of numpy.array The prior x vectors. Indexed [i][cell] using the above equation. - rates : list of ReactionRates - The reaction rates for each substep. - eigvls : list of float - Eigenvalue for each substep - seeds : list of int - Seeds for each substep. + op_results : list of openmc.deplete.OperatorResult + Results of applying transport operator t : list of float Time indices. step_ind : int Step index. - """ + """ # Get indexing terms vol_list, nuc_list, burn_list, full_burn_list = op.get_results_info() @@ -39,9 +35,9 @@ def save_results(op, x, rates, eigvls, seeds, t, step_ind): for mat_i in range(n_mat): results[i, mat_i, :] = x[i][mat_i][:] - results.k = eigvls - results.seeds = seeds + results.k = [r.k for r in op_results] + results.seeds = [r.seed for r in op_results] + results.rates = [r.rates for r in op_results] results.time = t - results.rates = rates write_results(results, "depletion_results.h5", step_ind) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index b5b91f2ac4..da397c0149 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -23,7 +23,7 @@ import openmc import openmc.capi from openmc.data import JOULE_PER_EV from . import comm -from .abc import Settings, Operator +from .abc import Settings, Operator, OperatorResult from .atom_number import AtomNumber from .chain import Chain from .reaction_rates import ReactionRates @@ -255,7 +255,7 @@ class OpenMCOperator(Operator): print("Time to openmc: ", time_openmc - time_start) print("Time to unpack: ", time_unpack - time_openmc) - return k, copy.deepcopy(self.reaction_rates), self.seed + return OperatorResult(k, copy.deepcopy(self.reaction_rates), self.seed) def extract_mat_ids(self): """Extracts materials and assigns them to processes. diff --git a/tests/dummy_geometry.py b/tests/dummy_geometry.py index aab396b85a..ceafc3fdc2 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_geometry.py @@ -1,7 +1,7 @@ import numpy as np import scipy.sparse as sp from openmc.deplete.reaction_rates import ReactionRates -from openmc.deplete.abc import Operator +from openmc.deplete.abc import Operator, OperatorResult class DummyGeometry(Operator): @@ -51,7 +51,7 @@ class DummyGeometry(Operator): reaction_rates[0, 1, 0] = vec[0][1] # Create a fake rates object - return 0.0, reaction_rates, 0 + return OperatorResult(0.0, reaction_rates, 0) @property def chain(self): diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 964f99eee3..faccd3392f 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -11,7 +11,8 @@ import os from unittest.mock import MagicMock import numpy as np -from openmc.deplete import integrator, ReactionRates, results, comm +from openmc.deplete import (integrator, ReactionRates, results, comm, + OperatorResult) def test_save_results(run_in_tmpdir): @@ -49,8 +50,8 @@ def test_save_results(run_in_tmpdir): x2.append([np.random.rand(2), np.random.rand(2)]) # Construct r - cell_dict = {s:i for i, s in enumerate(burn_list)} - r1 = ReactionRates(cell_dict, {"na":0, "nb":1}, {"ra":0, "rb":1}) + cell_dict = {s: i for i, s in enumerate(burn_list)} + r1 = ReactionRates(cell_dict, {"na": 0, "nb": 1}, {"ra": 0, "rb": 1}) r1.rates = np.random.rand(2, 2, 2) rate1 = [] @@ -76,8 +77,12 @@ def test_save_results(run_in_tmpdir): t1 = [0.0, 1.0] t2 = [1.0, 2.0] - integrator.save_results(op, x1, rate1, eigvl1, seed1, t1, 0) - integrator.save_results(op, x2, rate2, eigvl2, seed2, t2, 1) + op_result1 = [OperatorResult(k, rates, seed) + for k, rates, seed in zip(eigvl1, rate1, seed1)] + op_result2 = [OperatorResult(k, rates, seed) + for k, rates, seed in zip(eigvl2, rate2, seed2)] + integrator.save_results(op, x1, op_result1, t1, 0) + integrator.save_results(op, x2, op_result2, t2, 1) # Load the files res = results.read_results("depletion_results.h5") From b62e25bcf5f84e5178990519a5e6c2ec433cb7bd Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 10:39:16 -0600 Subject: [PATCH 20/68] Simplify integrator implementations by separating out function for depletion --- openmc/deplete/integrator/cecm.py | 46 +++++------------------- openmc/deplete/integrator/cram.py | 50 ++++++++++++++++++++++++++ openmc/deplete/integrator/predictor.py | 29 ++++----------- 3 files changed, 65 insertions(+), 60 deletions(-) diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 16fa299fd3..760e3c89d8 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -1,13 +1,8 @@ -""" The CE/CM integrator.""" +"""The CE/CM integrator.""" import copy -from itertools import repeat -import os -from multiprocessing import Pool -import time -from .. import comm -from .cram import CRAM48, cram_wrapper +from .cram import deplete from .save_results import save_results @@ -43,8 +38,7 @@ def cecm(operator, print_out=True): """ # Generate initial conditions with operator as vec: - n_mats = len(vec) - + chain = operator.chain t = 0.0 for i, dt in enumerate(operator.settings.dt_vec): # Get beginning-of-timestep reaction rates @@ -52,43 +46,21 @@ def cecm(operator, print_out=True): results = [operator(x[0])] # Deplete for first half of timestep - t_start = time.time() - chains = repeat(operator.chain, n_mats) - vecs = (x[0][i] for i in range(n_mats)) - rates = (results[0].rates[i, :, :] for i in range(n_mats)) - dts = repeat(dt/2, n_mats) - with Pool() as pool: - iters = zip(chains, vecs, rates, dts) - x_result = list(pool.starmap(cram_wrapper, iters)) - t_end = time.time() - if comm.rank == 0: - if print_out: - print("Time to matexp: ", t_end - t_start) + x_middle = deplete(chain, x[0], results[0], dt/2, print_out) # Get middle-of-timestep reaction rates - x.append(x_result) - results.append(operator(x_result)) + x.append(x_middle) + results.append(operator(x_middle)) - # Deplete for second half of timestep - t_start = time.time() - chains = repeat(operator.chain, n_mats) - vecs = (x[0][i] for i in range(n_mats)) - rates = (results[1].rates[i, :, :] for i in range(n_mats)) - dts = repeat(dt, n_mats) - with Pool() as pool: - iters = zip(chains, vecs, rates, dts) - x_result = list(pool.starmap(cram_wrapper, iters)) - t_end = time.time() - if comm.rank == 0: - if print_out: - print("Time to matexp: ", t_end - t_start) + # Deplete for full timestep using beginning-of-step materials + x_end = deplete(chain, x[0], results[1], dt, print_out) # Create results, write to disk save_results(operator, x, results, [t, t + dt], i) # Advance time, update vector t += dt - vec = copy.deepcopy(x_result) + vec = copy.deepcopy(x_end) # Perform one last simulation x = [copy.deepcopy(vec)] diff --git a/openmc/deplete/integrator/cram.py b/openmc/deplete/integrator/cram.py index 56476384c6..09207fbc6b 100644 --- a/openmc/deplete/integrator/cram.py +++ b/openmc/deplete/integrator/cram.py @@ -3,10 +3,60 @@ Implements two different forms of CRAM for use in openmc.deplete. """ +from itertools import repeat +from multiprocessing import Pool +import time + import numpy as np import scipy.sparse as sp import scipy.sparse.linalg as sla +from .. import comm + + +def deplete(chain, x, op_result, dt, print_out): + """Deplete materials using given reaction rates for a specified time + + Parameters + ---------- + chain : openmc.deplete.Chain + Depletion chain + x : list of numpy.ndarray + Atom number vectors for each material + op_result : openmc.deplete.OperatorResult + Result of applying transport operator (contains reaction rates) + dt : float + Time in [s] to deplete for + print_out : bool + Whether to show elapsed time + + Returns + ------- + x_result : list of numpy.ndarray + Updated atom number vectors for each material + + """ + t_start = time.time() + + # Set up iterators + n_mats = len(x) + chains = repeat(chain, n_mats) + vecs = (x[i] for i in range(n_mats)) + rates = (op_result.rates[i, :, :] for i in range(n_mats)) + dts = repeat(dt, n_mats) + + # Use multiprocessing pool to distribute work + with Pool() as pool: + iters = zip(chains, vecs, rates, dts) + x_result = list(pool.starmap(cram_wrapper, iters)) + + t_end = time.time() + if comm.rank == 0: + if print_out: + print("Time to matexp: ", t_end - t_start) + + return x_result + def cram_wrapper(chain, n0, rates, dt): """Wraps depletion matrix creation / CRAM solve for multiprocess execution diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 872b5ebb19..0640783318 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -1,20 +1,15 @@ -""" The Predictor algorithm.""" +"""The Predictor algorithm.""" import copy -from itertools import repeat -import os -from multiprocessing import Pool -import time -from .. import comm -from .cram import CRAM48, cram_wrapper +from .cram import deplete from .save_results import save_results def predictor(operator, print_out=True): r"""The basic predictor integrator. - Implements the first order predictor algorithm. This algorithm is + Implements the first-order predictor algorithm. This algorithm is mathematically defined as: .. math:: @@ -34,8 +29,7 @@ def predictor(operator, print_out=True): """ # Generate initial conditions with operator as vec: - n_mats = len(vec) - + chain = operator.chain t = 0.0 for i, dt in enumerate(operator.settings.dt_vec): # Get beginning-of-timestep reaction rates @@ -46,22 +40,11 @@ def predictor(operator, print_out=True): save_results(operator, x, results, [t, t + dt], i) # Deplete for full timestep - t_start = time.time() - chains = repeat(operator.chain, n_mats) - vecs = (x[0][i] for i in range(n_mats)) - rates = (results[0].rates[i, :, :] for i in range(n_mats)) - dts = repeat(dt, n_mats) - with Pool() as pool: - iters = zip(chains, vecs, rates, dts) - x_result = list(pool.starmap(cram_wrapper, iters)) - t_end = time.time() - if comm.rank == 0: - if print_out: - print("Time to matexp: ", t_end - t_start) + x_end = deplete(chain, x[0], results[0], dt, print_out) # Advance time, update vector t += dt - vec = copy.deepcopy(x_result) + vec = copy.deepcopy(x_end) # Perform one last simulation x = [copy.deepcopy(vec)] From bc4d631883032791249d8e63824a0fbe159d7af9 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 11:02:48 -0600 Subject: [PATCH 21/68] Get rid of deplete.Materials class that wasn't used --- openmc/deplete/openmc_wrapper.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index da397c0149..4668249b1f 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -97,25 +97,6 @@ class OpenMCSettings(Settings): return getattr(self.__dict__['settings'], name) -class Materials(object): - """The Materials class. - - Contains information about cross sections for a cell. - - Attributes - ---------- - temperature : float - Temperature in Kelvin for each region. - sab : str or list of str - ENDF S(a,b) name for a region that needs S(a,b) data. Not set if no - S(a,b) needed for region. - """ - - def __init__(self): - self.temperature = None - self.sab = None - - class OpenMCOperator(Operator): """The OpenMC Operator class. @@ -134,8 +115,6 @@ class OpenMCOperator(Operator): Settings object. (From Operator) geometry : openmc.Geometry The OpenMC geometry object. - materials : list of Materials - Materials to be used for this simulation. seed : int The RNG seed used in last OpenMC run. number : openmc.deplete.AtomNumber From 8a41bac17abdb16868dcbb6a4666e80a3f9b493c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 14:31:03 -0600 Subject: [PATCH 22/68] Removing a few attributes on OpenMCOperator --- openmc/deplete/abc.py | 2 +- openmc/deplete/integrator/save_results.py | 1 - openmc/deplete/openmc_wrapper.py | 27 +++------------------ openmc/deplete/results.py | 15 +----------- tests/dummy_geometry.py | 2 +- tests/unit_tests/test_deplete_integrator.py | 12 ++------- 6 files changed, 9 insertions(+), 50 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 155261742c..23322d91ca 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -54,7 +54,7 @@ class Settings(object): self._output_dir = Path(output_dir) -OperatorResult = namedtuple('OperatorResult', ['k', 'rates', 'seed']) +OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) class Operator(metaclass=ABCMeta): diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py index 31cd9b2880..8a0ae92040 100644 --- a/openmc/deplete/integrator/save_results.py +++ b/openmc/deplete/integrator/save_results.py @@ -36,7 +36,6 @@ def save_results(op, x, op_results, t, step_ind): results[i, mat_i, :] = x[i][mat_i][:] results.k = [r.k for r in op_results] - results.seeds = [r.seed for r in op_results] results.rates = [r.rates for r in op_results] results.time = t diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 4668249b1f..d9a5d405ad 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -115,8 +115,6 @@ class OpenMCOperator(Operator): Settings object. (From Operator) geometry : openmc.Geometry The OpenMC geometry object. - seed : int - The RNG seed used in last OpenMC run. number : openmc.deplete.AtomNumber Total number of atoms in simulation. participating_nuclides : set of str @@ -125,10 +123,6 @@ class OpenMCOperator(Operator): The depletion chain information necessary to form matrices and tallies. reaction_rates : openmc.deplete.ReactionRates Reaction rates from the last operator step. - power : OrderedDict of str to float - Material-by-Material power. Indexed by material ID. - mat_name : OrderedDict of str to int - The name of region each material is set to. Indexed by material ID. burn_mat_to_id : OrderedDict of str to int Dictionary mapping material ID (as a string) to an index in reaction_rates. burn_nuc_to_id : OrderedDict of str to int @@ -144,12 +138,9 @@ class OpenMCOperator(Operator): super().__init__(settings) self.geometry = geometry - self.seed = 0 self.number = None self.participating_nuclides = None self.reaction_rates = None - self.power = None - self.mat_name = OrderedDict() self.burn_mat_to_ind = OrderedDict() self.burn_nuc_to_ind = None @@ -196,16 +187,10 @@ class OpenMCOperator(Operator): Returns ------- - mat : list of scipy.sparse.csr_matrix - Matrices for the next step. - k : float - Eigenvalue of the problem. - rates : openmc.deplete.ReactionRates - Reaction rates from this simulation. - seed : int - Seed for this simulation. - """ + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator + """ # Prevent OpenMC from complaining about re-creating tallies openmc.reset_auto_ids() @@ -234,7 +219,7 @@ class OpenMCOperator(Operator): print("Time to openmc: ", time_openmc - time_start) print("Time to unpack: ", time_unpack - time_openmc) - return OperatorResult(k, copy.deepcopy(self.reaction_rates), self.seed) + return OperatorResult(k, copy.deepcopy(self.reaction_rates)) def extract_mat_ids(self): """Extracts materials and assigns them to processes. @@ -262,8 +247,6 @@ class OpenMCOperator(Operator): # Iterate once through the geometry to get dictionaries cells = self.geometry.get_all_material_cells() for cell in cells.values(): - name = cell.name - if isinstance(cell.fill, openmc.Material): mat = cell.fill for nuclide in mat.get_nuclide_densities(): @@ -273,7 +256,6 @@ class OpenMCOperator(Operator): volume[str(mat.id)] = mat.volume else: mat_not_burn.add(str(mat.id)) - self.mat_name[mat.id] = name else: for mat in cell.fill: for nuclide in mat.get_nuclide_densities(): @@ -283,7 +265,6 @@ class OpenMCOperator(Operator): volume[str(mat.id)] = mat.volume else: mat_not_burn.add(str(mat.id)) - self.mat_name[mat.id] = name need_vol = [] diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index fd48bb4df1..37c4b6e921 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -22,8 +22,6 @@ class Results(object): ---------- k : list of float Eigenvalue for each substep. - seeds : list of int - Seeds for each substep. time : list of float Time at beginning, end of step, in seconds. n_mat : int @@ -46,11 +44,10 @@ class Results(object): Number of stages in simulation. data : numpy.array Atom quantity, stored by stage, mat, then by nuclide. - """ + """ def __init__(self): self.k = None - self.seeds = None self.time = None self.rates = None self.volume = None @@ -227,8 +224,6 @@ class Results(object): handle.create_dataset("eigenvalues", (1, n_stages), maxshape=(None, n_stages), dtype='float64') - handle.create_dataset("seeds", (1, n_stages), maxshape=(None, n_stages), dtype='int64') - handle.create_dataset("time", (1, 2), maxshape=(None, 2), dtype='float64') def to_hdf5(self, handle, index): @@ -252,7 +247,6 @@ class Results(object): number_dset = handle["/number"] rxn_dset = handle["/reaction rates"] eigenvalues_dset = handle["/eigenvalues"] - seeds_dset = handle["/seeds"] time_dset = handle["/time"] # Get number of results stored @@ -274,10 +268,6 @@ class Results(object): eigenvalues_shape[0] = new_shape eigenvalues_dset.resize(eigenvalues_shape) - seeds_shape = list(seeds_dset.shape) - seeds_shape[0] = new_shape - seeds_dset.resize(seeds_shape) - time_shape = list(time_dset.shape) time_shape[0] = new_shape time_dset.resize(time_shape) @@ -297,7 +287,6 @@ class Results(object): rxn_dset[index, i, low:high+1, :, :] = self.rates[i][:, :, :] if comm.rank == 0: eigenvalues_dset[index, i] = self.k[i] - seeds_dset[index, i] = self.seeds[i] if comm.rank == 0: time_dset[index, :] = self.time @@ -317,12 +306,10 @@ class Results(object): # Grab handles number_dset = handle["/number"] eigenvalues_dset = handle["/eigenvalues"] - seeds_dset = handle["/seeds"] time_dset = handle["/time"] results.data = number_dset[index, :, :, :] results.k = eigenvalues_dset[index, :] - results.seeds = seeds_dset[index, :] results.time = time_dset[index, :] # Reconstruct dictionaries diff --git a/tests/dummy_geometry.py b/tests/dummy_geometry.py index ceafc3fdc2..c013bb0054 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_geometry.py @@ -51,7 +51,7 @@ class DummyGeometry(Operator): reaction_rates[0, 1, 0] = vec[0][1] # Create a fake rates object - return OperatorResult(0.0, reaction_rates, 0) + return OperatorResult(0.0, reaction_rates) @property def chain(self): diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index faccd3392f..59d08b4842 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -66,21 +66,15 @@ def test_save_results(run_in_tmpdir): # Create global terms eigvl1 = np.random.rand(stages) eigvl2 = np.random.rand(stages) - seed1 = [np.random.randint(100) for i in range(stages)] - seed2 = [np.random.randint(100) for i in range(stages)] eigvl1 = comm.bcast(eigvl1, root=0) eigvl2 = comm.bcast(eigvl2, root=0) - seed1 = comm.bcast(seed1, root=0) - seed2 = comm.bcast(seed2, root=0) t1 = [0.0, 1.0] t2 = [1.0, 2.0] - op_result1 = [OperatorResult(k, rates, seed) - for k, rates, seed in zip(eigvl1, rate1, seed1)] - op_result2 = [OperatorResult(k, rates, seed) - for k, rates, seed in zip(eigvl2, rate2, seed2)] + op_result1 = [OperatorResult(k, rates) for k, rates in zip(eigvl1, rate1)] + op_result2 = [OperatorResult(k, rates) for k, rates in zip(eigvl2, rate2)] integrator.save_results(op, x1, op_result1, t1, 0) integrator.save_results(op, x2, op_result2, t2, 1) @@ -98,9 +92,7 @@ def test_save_results(run_in_tmpdir): rate2[i][mat, nuc, :]) np.testing.assert_array_equal(res[0].k, eigvl1) - np.testing.assert_array_equal(res[0].seeds, seed1) np.testing.assert_array_equal(res[0].time, t1) np.testing.assert_array_equal(res[1].k, eigvl2) - np.testing.assert_array_equal(res[1].seeds, seed2) np.testing.assert_array_equal(res[1].time, t2) From 3bbd1774537bd2cfd5c0d775caefd7cfdd0f290f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 14:53:11 -0600 Subject: [PATCH 23/68] Have unpack_tallies_and_normalize return an OperatorResult --- openmc/deplete/abc.py | 11 ++++------- openmc/deplete/openmc_wrapper.py | 28 +++++++++++----------------- openmc/deplete/reaction_rates.py | 2 +- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 23322d91ca..b2594d2c84 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -58,7 +58,7 @@ OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) class Operator(metaclass=ABCMeta): - """Abstract class defining all methods needed for the integrator. + """Abstract class defining a transport operator Attributes ---------- @@ -82,12 +82,9 @@ class Operator(metaclass=ABCMeta): Returns ------- - k : float - Eigenvalue of the problem. - rates : ReactionRates - Reaction rates from this simulation. - seed : int - Seed for this simulation. + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator + """ pass diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index d9a5d405ad..65ed4793dd 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -98,9 +98,7 @@ class OpenMCSettings(Settings): class OpenMCOperator(Operator): - """The OpenMC Operator class. - - Provides Operator functions for OpenMC. + """OpenMC transport operator Parameters ---------- @@ -210,7 +208,7 @@ class OpenMCOperator(Operator): time_openmc = time.time() # Extract results - k = self.unpack_tallies_and_normalize() + op_result = self.unpack_tallies_and_normalize() if comm.rank == 0: time_unpack = time.time() @@ -219,7 +217,7 @@ class OpenMCOperator(Operator): print("Time to openmc: ", time_openmc - time_start) print("Time to unpack: ", time_unpack - time_openmc) - return OperatorResult(k, copy.deepcopy(self.reaction_rates)) + return copy.deepcopy(op_result) def extract_mat_ids(self): """Extracts materials and assigns them to processes. @@ -561,23 +559,19 @@ class OpenMCOperator(Operator): self.number.set_mat_slice(i, total_density[i]) def unpack_tallies_and_normalize(self): - """Unpack tallies from OpenMC + """Unpack tallies from OpenMC and return an operator result - This function reads the tallies generated by OpenMC (from the tally.xml - file generated in generate_tally_xml) normalizes them so that the total - power generated is new_power, and then stores them in the reaction rate - database. + This method uses OpenMC's C API bindings to determine the k-effective + value and reaction rates from the simulation. The reaction rates are + normalized by the user-specified power, summing the product of the + fission reaction rate times the fission Q value for each material. Returns ------- - k : float - Eigenvalue of the last simulation. + openmc.deplete.OperatorResult + Eigenvalue and reaction rates resulting from transport operator - Todo - ---- - Provide units for power """ - rates = self.reaction_rates rates[:, :, :] = 0.0 @@ -655,7 +649,7 @@ class OpenMCOperator(Operator): # Scale reaction rates to obtain units of reactions/sec rates[:, :, :] *= power / energy - return k_combined + return OperatorResult(k_combined, rates) def load_participating(self): """Loads a cross_sections.xml file to find participating nuclides. diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index de3a6a7280..e8bab101bf 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -23,7 +23,7 @@ class ReactionRates(object): Attributes ---------- mat_to_ind : OrderedDict of str to int - A dictionary mapping cell ID as string to index. + A dictionary mapping material ID as string to index. nuc_to_ind : OrderedDict of str to int A dictionary mapping nuclide name as string to index. react_to_ind : OrderedDict of str to int From 05afc55a88c11200d387519de6a71be9063dbbbc Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 15:34:00 -0600 Subject: [PATCH 24/68] Give deplete.Chain some special methods to make it more Pythonic --- openmc/deplete/chain.py | 34 +++++++++----------------- openmc/deplete/openmc_wrapper.py | 9 +++---- tests/unit_tests/test_deplete_chain.py | 25 ++++++++++--------- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 03decb72ab..846b3af564 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -113,8 +113,6 @@ class Chain(object): Attributes ---------- - n_nuclides : int - Number of nuclides in chain. nuclides : list of Nuclide List of nuclides in chain. nuclide_dict : OrderedDict of str to int @@ -132,8 +130,14 @@ class Chain(object): self.nuc_to_react_ind = OrderedDict() self.react_to_ind = OrderedDict() - @property - def n_nuclides(self): + def __contains__(self, nuclide): + return nuclide in self.nuclide_dict + + def __getitem__(self, name): + """Get a Nuclide by name.""" + return self.nuclides[self.nuclide_dict[name]] + + def __len__(self): """Number of nuclides in chain.""" return len(self.nuclides) @@ -381,14 +385,14 @@ class Chain(object): Parameters ---------- rates : numpy.ndarray - 2D array indexed by nuclide then by cell. + 2D array indexed by (nuclide, reaction) Returns ------- scipy.sparse.csr_matrix Sparse matrix representing depletion. - """ + """ matrix = defaultdict(float) reactions = set() @@ -450,21 +454,7 @@ class Chain(object): reactions.clear() # Use DOK matrix as intermediate representation, then convert to CSR and return - matrix_dok = sp.dok_matrix((self.n_nuclides, self.n_nuclides)) + n = len(self) + matrix_dok = sp.dok_matrix((n, n)) dict.update(matrix_dok, matrix) return matrix_dok.tocsr() - - def nuc_by_ind(self, ind): - """Extracts nuclides from the list by dictionary key. - - Parameters - ---------- - ind : str - Name of nuclide. - - Returns - ------- - Nuclide - Nuclide object that corresponds to ind. - """ - return self.nuclides[self.nuclide_dict[ind]] diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 65ed4793dd..b8b9065276 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -328,7 +328,7 @@ class OpenMCOperator(Operator): i += 1 n_mat_burn = len(mat_burn) - n_nuc_burn = len(self.chain.nuclide_dict) + n_nuc_burn = len(self.chain) self.number = AtomNumber(mat_dict, nuc_dict, volume, n_mat_burn, n_nuc_burn) @@ -500,8 +500,7 @@ class OpenMCOperator(Operator): # Store list of tally nuclides on each process nuc_list = comm.bcast(nuc_list, root=0) - tally_nuclides = [nuc for nuc in nuc_list - if nuc in self.chain.nuclide_dict] + tally_nuclides = [nuc for nuc in nuc_list if nuc in self.chain] return tally_nuclides @@ -690,14 +689,14 @@ class OpenMCOperator(Operator): # and nuclides in depletion chain. if name not in self.participating_nuclides: self.participating_nuclides.add(name) - if name in self.chain.nuclide_dict: + if name in self.chain: self.burn_nuc_to_ind[name] = nuc_ind nuc_ind += 1 @property def n_nuc(self): """Number of nuclides considered in the decay chain.""" - return len(self.chain.nuclides) + return len(self.chain) def get_results_info(self): """Returns volume list, cell lists, and nuc lists. diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 064b878afe..3a065fb46f 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -20,12 +20,12 @@ def test_init(): assert isinstance(dep.react_to_ind, Mapping) -def test_n_nuclides(): - """Test depletion chain n_nuclides parameter.""" +def test_len(): + """Test depletion chain length.""" dep = Chain() dep.nuclides = ["NucA", "NucB", "NucC"] - assert dep.n_nuclides == 3 + assert len(dep) == 3 def test_from_endf(): @@ -43,10 +43,10 @@ def test_from_xml(): dep = Chain.from_xml(_test_filename) # Basic checks - assert dep.n_nuclides == 3 + assert len(dep) == 3 # A tests - nuc = dep.nuclides[dep.nuclide_dict["A"]] + nuc = dep["A"] assert nuc.name == "A" assert nuc.half_life == 2.36520E+04 @@ -61,7 +61,7 @@ def test_from_xml(): assert [r.branching_ratio for r in nuc.reactions] == [1.0] # B tests - nuc = dep.nuclides[dep.nuclide_dict["B"]] + nuc = dep["B"] assert nuc.name == "B" assert nuc.half_life == 3.29040E+04 @@ -76,7 +76,7 @@ def test_from_xml(): assert [r.branching_ratio for r in nuc.reactions] == [1.0] # C tests - nuc = dep.nuclides[dep.nuclide_dict["C"]] + nuc = dep["C"] assert nuc.name == "C" assert nuc.n_decay_modes == 0 @@ -183,12 +183,13 @@ def test_form_matrix(): assert mat[2, 2] == mat22 -def test_nuc_by_ind(): +def test_getitem(): """ Test nuc_by_ind converter function. """ dep = Chain() dep.nuclides = ["NucA", "NucB", "NucC"] - dep.nuclide_dict = {"NucA" : 0, "NucB" : 1, "NucC" : 2} + dep.nuclide_dict = {nuc: dep.nuclides.index(nuc) + for nuc in dep.nuclides} - assert "NucA" == dep.nuc_by_ind("NucA") - assert "NucB" == dep.nuc_by_ind("NucB") - assert "NucC" == dep.nuc_by_ind("NucC") + assert "NucA" == dep["NucA"] + assert "NucB" == dep["NucB"] + assert "NucC" == dep["NucC"] From a6c095c4e9dd480b382d48ce0d3301648f8942b5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Feb 2018 15:37:41 -0600 Subject: [PATCH 25/68] Bugfix for Python 3.4 --- openmc/deplete/abc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index b2594d2c84..5441829b40 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -91,7 +91,8 @@ class Operator(metaclass=ABCMeta): def __enter__(self): # Save current directory and move to specific output directory self._orig_dir = os.getcwd() - self.settings.output_dir.mkdir(exist_ok=True) + if not self.settings.output_dir.exists(): + self.settings.output_dir.mkdir() # exist_ok parameter is 3.5+ # In Python 3.6+, chdir accepts a Path directly os.chdir(str(self.settings.output_dir)) From c9abcfc0a1890da0263642e22b3cd9675f28242c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 17 Feb 2018 10:28:24 -0600 Subject: [PATCH 26/68] Make ReactionRates a subclass of ndarray, simplifying mapping dictionaries --- openmc/deplete/chain.py | 41 +++++++-------- openmc/deplete/openmc_wrapper.py | 20 ++++---- openmc/deplete/reaction_rates.py | 86 +++++++++++--------------------- openmc/deplete/results.py | 12 ++--- 4 files changed, 63 insertions(+), 96 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 846b3af564..2187b04860 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -117,9 +117,7 @@ class Chain(object): List of nuclides in chain. nuclide_dict : OrderedDict of str to int Maps a nuclide name to an index in nuclides. - nuc_to_react_ind : OrderedDict of str to int - Dictionary mapping a nuclide name to an index in ReactionRates. - react_to_ind : OrderedDict of str to int + index_reaction : OrderedDict of str to int Dictionary mapping a reaction name to an index in ReactionRates. """ @@ -127,8 +125,7 @@ class Chain(object): def __init__(self): self.nuclides = [] self.nuclide_dict = OrderedDict() - self.nuc_to_react_ind = OrderedDict() - self.react_to_ind = OrderedDict() + self.index_reaction = OrderedDict() def __contains__(self, nuclide): return nuclide in self.nuclide_dict @@ -155,7 +152,7 @@ class Chain(object): List of ENDF neutron reaction sub-library files """ - depl_chain = cls() + chain = cls() # Create dictionary mapping target to filename reactions = {} @@ -200,8 +197,8 @@ class Chain(object): nuclide = Nuclide() nuclide.name = parent - depl_chain.nuclides.append(nuclide) - depl_chain.nuclide_dict[parent] = idx + chain.nuclides.append(nuclide) + chain.nuclide_dict[parent] = idx if not data.nuclide['stable'] and data.half_life.nominal_value != 0.0: nuclide.half_life = data.half_life.nominal_value @@ -235,8 +232,8 @@ class Chain(object): Z = data.nuclide['atomic_number'] + delta_Z daughter = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) - if name not in depl_chain.react_to_ind: - depl_chain.react_to_ind[name] = reaction_index + if name not in chain.index_reaction: + chain.index_reaction[name] = reaction_index reaction_index += 1 if daughter not in decay_data: @@ -259,8 +256,8 @@ class Chain(object): nuclide.reactions.append( ReactionTuple('fission', 0, q_value, 1.0)) - if 'fission' not in depl_chain.react_to_ind: - depl_chain.react_to_ind['fission'] = reaction_index + if 'fission' not in chain.index_reaction: + chain.index_reaction['fission'] = reaction_index reaction_index += 1 else: missing_fpy.append(parent) @@ -316,7 +313,7 @@ class Chain(object): for vals in missing_fp: print(' {}, E={} eV (total yield={})'.format(*vals)) - return depl_chain + return chain @classmethod def from_xml(cls, filename): @@ -331,7 +328,7 @@ class Chain(object): ---- Allow for branching on capture, etc. """ - depl_chain = cls() + chain = cls() # Load XML tree try: @@ -346,17 +343,17 @@ class Chain(object): reaction_index = 0 for i, nuclide_elem in enumerate(root.findall('nuclide_table')): nuc = Nuclide.from_xml(nuclide_elem) - depl_chain.nuclide_dict[nuc.name] = i + chain.nuclide_dict[nuc.name] = i # Check for reaction paths for rx in nuc.reactions: - if rx.type not in depl_chain.react_to_ind: - depl_chain.react_to_ind[rx.type] = reaction_index + if rx.type not in chain.index_reaction: + chain.index_reaction[rx.type] = reaction_index reaction_index += 1 - depl_chain.nuclides.append(nuc) + chain.nuclides.append(nuc) - return depl_chain + return chain def export_to_xml(self, filename): """Writes a depletion chain XML file. @@ -416,14 +413,14 @@ class Chain(object): k = self.nuclide_dict[target] matrix[k, i] += branch_val - if nuc.name in self.nuc_to_react_ind: + if nuc.name in rates.index_nuc: # Extract all reactions for this nuclide in this cell - nuc_ind = self.nuc_to_react_ind[nuc.name] + nuc_ind = rates.index_nuc[nuc.name] nuc_rates = rates[nuc_ind, :] for r_type, target, _, br in nuc.reactions: # Extract reaction index, and then final reaction rate - r_id = self.react_to_ind[r_type] + r_id = rates.index_rx[r_type] path_rate = nuc_rates[r_id] # Loss term -- make sure we only count loss once for diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index b8b9065276..46d07a23f7 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -369,9 +369,7 @@ class OpenMCOperator(Operator): self.reaction_rates = ReactionRates( self.burn_mat_to_ind, self.burn_nuc_to_ind, - self.chain.react_to_ind) - - self.chain.nuc_to_react_ind = self.burn_nuc_to_ind + self.chain.index_reaction) def form_matrix(self, y, mat): """Forms the depletion matrix. @@ -522,7 +520,7 @@ class OpenMCOperator(Operator): # transmutation. The nuclides for the tally are set later when eval() is # called. tally_dep = openmc.capi.Tally(1) - tally_dep.scores = self.chain.react_to_ind.keys() + tally_dep.scores = self.chain.index_reaction.keys() tally_dep.filters = [mat_filter] def total_density_list(self): @@ -579,11 +577,11 @@ class OpenMCOperator(Operator): # Extract tally bins materials = list(self.mat_tally_ind.keys()) nuclides = openmc.capi.tallies[1].nuclides - reactions = list(self.chain.react_to_ind.keys()) + reactions = list(self.chain.index_reaction.keys()) # Form fast map - nuc_ind = [rates.nuc_to_ind[nuc] for nuc in nuclides] - react_ind = [rates.react_to_ind[react] for react in reactions] + nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] + react_ind = [rates.index_rx[react] for react in reactions] # Compute fission power # TODO : improve this calculation @@ -598,13 +596,13 @@ class OpenMCOperator(Operator): rates_expanded = np.zeros((rates.n_nuc, rates.n_react)) number = np.zeros(rates.n_nuc) - fission_ind = rates.react_to_ind["fission"] + fission_ind = rates.index_rx["fission"] for nuclide in self.chain.nuclides: - if nuclide.name in rates.nuc_to_ind: + if nuclide.name in rates.index_nuc: for rx in nuclide.reactions: if rx.type == 'fission': - ind = rates.nuc_to_ind[nuclide.name] + ind = rates.index_nuc[nuclide.name] fission_Q[ind] = rx.Q break @@ -637,7 +635,7 @@ class OpenMCOperator(Operator): for react in react_ind: rates_expanded[i_nuc_results, react] /= number[i_nuc_results] - rates.rates[i, :, :] = rates_expanded + rates[i, :, :] = rates_expanded # Reduce energy produced from all processes energy = comm.allreduce(energy) diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index e8bab101bf..b437773826 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -6,7 +6,7 @@ An ndarray to store reaction rates with string, integer, or slice indexing. import numpy as np -class ReactionRates(object): +class ReactionRates(np.ndarray): """ReactionRates class. An ndarray to store reaction rates with string, integer, or slice indexing. @@ -38,76 +38,48 @@ class ReactionRates(object): Array storing rates indexed by the above dictionaries. """ - def __init__(self, mat_to_ind, nuc_to_ind, react_to_ind): + def __new__(cls, index_mat, index_nuc, index_rx): + # Create appropriately-sized zeroed-out ndarray + shape = (len(index_mat), len(index_nuc), len(index_rx)) + obj = super().__new__(cls, shape) + obj[:] = 0.0 - self.mat_to_ind = mat_to_ind - self.nuc_to_ind = nuc_to_ind - self.react_to_ind = react_to_ind + # Add mapping attributes + obj.index_mat = index_mat + obj.index_nuc = index_nuc + obj.index_rx = index_rx - self.rates = np.zeros((self.n_mat, self.n_nuc, self.n_react)) + return obj - def __getitem__(self, pos): - """Retrieves an item from reaction_rates. + def __array_finalize__(self, obj): + if obj is None: + return + self.index_mat = getattr(obj, 'index_mat', None) + self.index_nuc = getattr(obj, 'index_nuc', None) + self.index_rx = getattr(obj, 'index_rx', None) - Parameters - ---------- - pos : tuple - A three-length tuple containing a material index, a nuc index, and a - reaction index. These indexes can be strings (which get converted - to integers via the dictionaries), integers used directly, or - slices. + def __reduce__(self): + state = super().__reduce__() + new_state = state[2] + (self.index_mat, self.index_nuc, self.index_rx) + return (state[0], state[1], new_state) - Returns - ------- - numpy.array - The value indexed from self.rates. - """ - - mat, nuc, react = pos - if isinstance(mat, str): - mat = self.mat_to_ind[mat] - if isinstance(nuc, str): - nuc = self.nuc_to_ind[nuc] - if isinstance(react, str): - react = self.react_to_ind[react] - - return self.rates[mat, nuc, react] - - def __setitem__(self, pos, val): - """Sets an item from reaction_rates. - - Parameters - ---------- - pos : tuple - A three-length tuple containing a material index, a nuc index, and a - reaction index. These indexes can be strings (which get converted - to integers via the dictionaries), integers used directly, or - slices. - val : float - The value to set the array to. - """ - - mat, nuc, react = pos - if isinstance(mat, str): - mat = self.mat_to_ind[mat] - if isinstance(nuc, str): - nuc = self.nuc_to_ind[nuc] - if isinstance(react, str): - react = self.react_to_ind[react] - - self.rates[mat, nuc, react] = val + def __setstate__(self, state): + self.index_mat = state[-3] + self.index_nuc = state[-2] + self.index_rx = state[-1] + super().__setstate__(state[0:-3]) @property def n_mat(self): """Number of cells.""" - return len(self.mat_to_ind) + return len(self.index_mat) @property def n_nuc(self): """Number of nucs.""" - return len(self.nuc_to_ind) + return len(self.index_nuc) @property def n_react(self): """Number of reactions.""" - return len(self.react_to_ind) + return len(self.index_rx) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 37c4b6e921..4a5ca24352 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -180,11 +180,11 @@ class Results(object): mat_int = sorted([int(mat) for mat in self.mat_to_hdf5_ind]) mat_list = [str(mat) for mat in mat_int] nuc_list = sorted(self.nuc_to_ind.keys()) - rxn_list = sorted(self.rates[0].react_to_ind.keys()) + rxn_list = sorted(self.rates[0].index_rx.keys()) n_mats = self.n_hdf5_mats n_nuc_number = len(nuc_list) - n_nuc_rxn = len(self.rates[0].nuc_to_ind) + n_nuc_rxn = len(self.rates[0].index_nuc) n_rxn = len(rxn_list) n_stages = self.n_stages @@ -200,14 +200,14 @@ class Results(object): for nuc in nuc_list: nuc_single_group = nuc_group.create_group(nuc) nuc_single_group.attrs["atom number index"] = self.nuc_to_ind[nuc] - if nuc in self.rates[0].nuc_to_ind: - nuc_single_group.attrs["reaction rate index"] = self.rates[0].nuc_to_ind[nuc] + if nuc in self.rates[0].index_nuc: + nuc_single_group.attrs["reaction rate index"] = self.rates[0].index_nuc[nuc] rxn_group = handle.create_group("reactions") for rxn in rxn_list: rxn_single_group = rxn_group.create_group(rxn) - rxn_single_group.attrs["index"] = self.rates[0].react_to_ind[rxn] + rxn_single_group.attrs["index"] = self.rates[0].index_rx[rxn] # Construct array storage @@ -341,7 +341,7 @@ class Results(object): for i in range(results.n_stages): rate = ReactionRates(results.mat_to_ind, rxn_nuc_to_ind, rxn_to_ind) - rate.rates = handle["/reaction rates"][index, i, :, :, :] + rate[:] = handle["/reaction rates"][index, i, :, :, :] results.rates.append(rate) return results From 5c4ea0d640a41daa9da0a65d134525a7a6589a09 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 17 Feb 2018 14:31:04 -0600 Subject: [PATCH 27/68] Replace Chain.index_reaction with Chain.reactions. Now the ReactionRates controls all the indexing needed to build a depletion matrix. Got all tests fixed here too. Decided to add get/set methods on ReactionRates which take strings. --- openmc/deplete/chain.py | 27 ++++------ openmc/deplete/openmc_wrapper.py | 12 ++--- openmc/deplete/reaction_rates.py | 59 +++++++++++++++++---- openmc/deplete/utilities.py | 48 ++++++++--------- tests/unit_tests/test_deplete_chain.py | 57 ++++++++++---------- tests/unit_tests/test_deplete_integrator.py | 6 +-- tests/unit_tests/test_deplete_reaction.py | 51 +++++++++--------- 7 files changed, 147 insertions(+), 113 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 2187b04860..2af64e172a 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -113,19 +113,19 @@ class Chain(object): Attributes ---------- - nuclides : list of Nuclide - List of nuclides in chain. + nuclides : list of openmc.deplete.Nuclide + Nuclides present in the chain. + reactions : list of str + Reactions that are tracked in the depletion chain nuclide_dict : OrderedDict of str to int Maps a nuclide name to an index in nuclides. - index_reaction : OrderedDict of str to int - Dictionary mapping a reaction name to an index in ReactionRates. """ def __init__(self): self.nuclides = [] + self.reactions = [] self.nuclide_dict = OrderedDict() - self.index_reaction = OrderedDict() def __contains__(self, nuclide): return nuclide in self.nuclide_dict @@ -190,7 +190,6 @@ class Chain(object): missing_fpy = [] missing_fp = [] - reaction_index = 0 for idx, parent in enumerate(sorted(decay_data, key=_get_zai)): data = decay_data[parent] @@ -232,9 +231,8 @@ class Chain(object): Z = data.nuclide['atomic_number'] + delta_Z daughter = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) - if name not in chain.index_reaction: - chain.index_reaction[name] = reaction_index - reaction_index += 1 + if name not in chain.reactions: + chain.reactions.append(name) if daughter not in decay_data: missing_rx_product.append((parent, name, daughter)) @@ -256,9 +254,8 @@ class Chain(object): nuclide.reactions.append( ReactionTuple('fission', 0, q_value, 1.0)) - if 'fission' not in chain.index_reaction: - chain.index_reaction['fission'] = reaction_index - reaction_index += 1 + if 'fission' not in chain.reactions: + chain.reactions.append('fission') else: missing_fpy.append(parent) @@ -340,16 +337,14 @@ class Chain(object): print('Decay chain "', filename, '" is invalid.') raise - reaction_index = 0 for i, nuclide_elem in enumerate(root.findall('nuclide_table')): nuc = Nuclide.from_xml(nuclide_elem) chain.nuclide_dict[nuc.name] = i # Check for reaction paths for rx in nuc.reactions: - if rx.type not in chain.index_reaction: - chain.index_reaction[rx.type] = reaction_index - reaction_index += 1 + if rx.type not in chain.reactions: + chain.reactions.append(rx.type) chain.nuclides.append(nuc) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 46d07a23f7..c95170a461 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -366,10 +366,11 @@ class OpenMCOperator(Operator): def initialize_reaction_rates(self): """Create reaction rates object. """ + # Create dictionary to map reactions to indices + index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} + self.reaction_rates = ReactionRates( - self.burn_mat_to_ind, - self.burn_nuc_to_ind, - self.chain.index_reaction) + self.burn_mat_to_ind, self.burn_nuc_to_ind, index_rx) def form_matrix(self, y, mat): """Forms the depletion matrix. @@ -520,7 +521,7 @@ class OpenMCOperator(Operator): # transmutation. The nuclides for the tally are set later when eval() is # called. tally_dep = openmc.capi.Tally(1) - tally_dep.scores = self.chain.index_reaction.keys() + tally_dep.scores = self.chain.reactions tally_dep.filters = [mat_filter] def total_density_list(self): @@ -577,11 +578,10 @@ class OpenMCOperator(Operator): # Extract tally bins materials = list(self.mat_tally_ind.keys()) nuclides = openmc.capi.tallies[1].nuclides - reactions = list(self.chain.index_reaction.keys()) # Form fast map nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] - react_ind = [rates.index_rx[react] for react in reactions] + react_ind = [rates.index_rx[react] for react in self.chain.reactions] # Compute fission power # TODO : improve this calculation diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index b437773826..a479085174 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -13,20 +13,20 @@ class ReactionRates(np.ndarray): Parameters ---------- - mat_to_ind : OrderedDict of str to int + index_mat : OrderedDict of str to int A dictionary mapping material ID as string to index. - nuc_to_ind : OrderedDict of str to int + index_nuc : OrderedDict of str to int A dictionary mapping nuclide name as string to index. - react_to_ind : OrderedDict of str to int + index_rx : OrderedDict of str to int A dictionary mapping reaction name as string to index. Attributes ---------- - mat_to_ind : OrderedDict of str to int + index_mat : OrderedDict of str to int A dictionary mapping material ID as string to index. - nuc_to_ind : OrderedDict of str to int + index_nuc : OrderedDict of str to int A dictionary mapping nuclide name as string to index. - react_to_ind : OrderedDict of str to int + index_rx : OrderedDict of str to int A dictionary mapping reaction name as string to index. n_mat : int Number of materials. @@ -34,10 +34,8 @@ class ReactionRates(np.ndarray): Number of nucs. n_react : int Number of reactions. - rates : numpy.array - Array storing rates indexed by the above dictionaries. - """ + """ def __new__(cls, index_mat, index_nuc, index_rx): # Create appropriately-sized zeroed-out ndarray shape = (len(index_mat), len(index_nuc), len(index_rx)) @@ -83,3 +81,46 @@ class ReactionRates(np.ndarray): def n_react(self): """Number of reactions.""" return len(self.index_rx) + + def get(self, mat, nuc, rx): + """Get reaction rate by material/nuclide/reaction + + Parameters + ---------- + mat : str + Material ID as a string + nuc : str + Nuclide name + rx : str + Name of the reaction + + Returns + ------- + float + Reaction rate corresponding to given material, nuclide, and reaction + + """ + mat = self.index_mat[mat] + nuc = self.index_nuc[nuc] + rx = self.index_rx[rx] + return self[mat, nuc, rx] + + def set(self, mat, nuc, rx, value): + """Set reaction rate by material/nuclide/reaction + + Parameters + ---------- + mat : str + Material ID as a string + nuc : str + Nuclide name + rx : str + Name of the reaction + value : float + Corresponding reaction rate to set + + """ + mat = self.index_mat[mat] + nuc = self.index_nuc[nuc] + rx = self.index_rx[rx] + self[mat, nuc, rx] = value diff --git a/openmc/deplete/utilities.py b/openmc/deplete/utilities.py index 5433edce44..d155f321d9 100644 --- a/openmc/deplete/utilities.py +++ b/openmc/deplete/utilities.py @@ -7,26 +7,26 @@ the results module. import numpy as np -def evaluate_single_nuclide(results, cell, nuc): - """Evaluates a single nuclide in a single cell from a results list. +def evaluate_single_nuclide(results, mat, nuc): + """Evaluates a single nuclide in a single material from a results list. Parameters ---------- results : list of results The results to extract data from. Must be sorted and continuous. - cell : str - Cell name to evaluate + mat : str + Material name to evaluate nuc : str Nuclide name to evaluate Returns ------- - time : numpy.array - Time vector. - concentration : numpy.array - Total number of atoms in the cell. - """ + time : numpy.ndarray + Time vector + concentration : numpy.ndarray + Total number of atoms in the material + """ n_points = len(results) time = np.zeros(n_points) concentration = np.zeros(n_points) @@ -34,39 +34,39 @@ def evaluate_single_nuclide(results, cell, nuc): # Evaluate value in each region for i, result in enumerate(results): time[i] = result.time[0] - concentration[i] = result[0, cell, nuc] + concentration[i] = result[0, mat, nuc] return time, concentration -def evaluate_reaction_rate(results, cell, nuc, rxn): - """Evaluates a single nuclide reaction rate in a single cell from a results list. +def evaluate_reaction_rate(results, mat, nuc, rx): + """Return reaction rate in a single material/nuclide from a results list. Parameters ---------- - results : list of Results + results : list of openmc.deplete.Results The results to extract data from. Must be sorted and continuous. - cell : str - Cell name to evaluate + mat : str + Material name to evaluate nuc : str Nuclide name to evaluate - rxn : str + rx : str Reaction rate to evaluate Returns ------- - time : numpy.array + time : numpy.ndarray Time vector. - rate : numpy.array + rate : numpy.ndarray Reaction rate. - """ + """ n_points = len(results) time = np.zeros(n_points) rate = np.zeros(n_points) # Evaluate value in each region for i, result in enumerate(results): time[i] = result.time[0] - rate[i] = result.rates[0][cell, nuc, rxn] * result[0, cell, nuc] + rate[i] = result.rates[0].get(mat, nuc, rx) * result[0, mat, nuc] return time, rate @@ -76,17 +76,17 @@ def evaluate_eigenvalue(results): Parameters ---------- - results : list of Results + results : list of openmc.deplete.Results The results to extract data from. Must be sorted and continuous. Returns ------- - time : numpy.array + time : numpy.ndarray Time vector. - eigenvalue : numpy.array + eigenvalue : numpy.ndarray Eigenvalue. - """ + """ n_points = len(results) time = np.zeros(n_points) eigenvalue = np.zeros(n_points) diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 3a065fb46f..ae900e62d7 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -13,19 +13,18 @@ _test_filename = str(Path(__file__).parents[2] / 'chains' / 'chain_test.xml') def test_init(): """Test depletion chain initialization.""" - dep = Chain() + chain = Chain() - assert isinstance(dep.nuclides, list) - assert isinstance(dep.nuclide_dict, Mapping) - assert isinstance(dep.react_to_ind, Mapping) + assert isinstance(chain.nuclides, list) + assert isinstance(chain.nuclide_dict, Mapping) def test_len(): """Test depletion chain length.""" - dep = Chain() - dep.nuclides = ["NucA", "NucB", "NucC"] + chain = Chain() + chain.nuclides = ["NucA", "NucB", "NucC"] - assert len(dep) == 3 + assert len(chain) == 3 def test_from_endf(): @@ -40,13 +39,13 @@ def test_from_xml(): # the components external to depletion_chain.py are simple storage # types. - dep = Chain.from_xml(_test_filename) + chain = Chain.from_xml(_test_filename) # Basic checks - assert len(dep) == 3 + assert len(chain) == 3 # A tests - nuc = dep["A"] + nuc = chain["A"] assert nuc.name == "A" assert nuc.half_life == 2.36520E+04 @@ -61,7 +60,7 @@ def test_from_xml(): assert [r.branching_ratio for r in nuc.reactions] == [1.0] # B tests - nuc = dep["B"] + nuc = chain["B"] assert nuc.name == "B" assert nuc.half_life == 3.29040E+04 @@ -76,7 +75,7 @@ def test_from_xml(): assert [r.branching_ratio for r in nuc.reactions] == [1.0] # C tests - nuc = dep["C"] + nuc = chain["C"] assert nuc.name == "C" assert nuc.n_decay_modes == 0 @@ -135,22 +134,20 @@ def test_form_matrix(): """ Using chain_test, and a dummy reaction rate, compute the matrix. """ # Relies on test_from_xml passing. - dep = Chain.from_xml(_test_filename) + chain = Chain.from_xml(_test_filename) - cell_ind = {"10000": 0, "10001": 1} + mat_ind = {"10000": 0, "10001": 1} nuc_ind = {"A": 0, "B": 1, "C": 2} - react_ind = dep.react_to_ind + react_ind = {rx: i for i, rx in enumerate(chain.reactions)} - react = reaction_rates.ReactionRates(cell_ind, nuc_ind, react_ind) + react = reaction_rates.ReactionRates(mat_ind, nuc_ind, react_ind) - dep.nuc_to_react_ind = nuc_ind + react.set("10000", "C", "fission", 1.0) + react.set("10000", "A", "(n,gamma)", 2.0) + react.set("10000", "B", "(n,gamma)", 3.0) + react.set("10000", "C", "(n,gamma)", 4.0) - react["10000", "C", "fission"] = 1.0 - react["10000", "A", "(n,gamma)"] = 2.0 - react["10000", "B", "(n,gamma)"] = 3.0 - react["10000", "C", "(n,gamma)"] = 4.0 - - mat = dep.form_matrix(react[0, :, :]) + mat = chain.form_matrix(react[0, :, :]) # Loss A, decay, (n, gamma) mat00 = -np.log(2) / 2.36520E+04 - 2 # A -> B, decay, 0.6 branching ratio @@ -185,11 +182,11 @@ def test_form_matrix(): def test_getitem(): """ Test nuc_by_ind converter function. """ - dep = Chain() - dep.nuclides = ["NucA", "NucB", "NucC"] - dep.nuclide_dict = {nuc: dep.nuclides.index(nuc) - for nuc in dep.nuclides} + chain = Chain() + chain.nuclides = ["NucA", "NucB", "NucC"] + chain.nuclide_dict = {nuc: chain.nuclides.index(nuc) + for nuc in chain.nuclides} - assert "NucA" == dep["NucA"] - assert "NucB" == dep["NucB"] - assert "NucC" == dep["NucC"] + assert "NucA" == chain["NucA"] + assert "NucB" == chain["NucB"] + assert "NucC" == chain["NucC"] diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 59d08b4842..78dd6bcef2 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -86,10 +86,8 @@ def test_save_results(run_in_tmpdir): for nuc_i, nuc in enumerate(nuc_list): assert res[0][i, mat, nuc] == x1[i][mat_i][nuc_i] assert res[1][i, mat, nuc] == x2[i][mat_i][nuc_i] - np.testing.assert_array_equal(res[0].rates[i][mat, nuc, :], - rate1[i][mat, nuc, :]) - np.testing.assert_array_equal(res[1].rates[i][mat, nuc, :], - rate2[i][mat, nuc, :]) + np.testing.assert_array_equal(res[0].rates[i], rate1[i]) + np.testing.assert_array_equal(res[1].rates[i], rate2[i]) np.testing.assert_array_equal(res[0].k, eigvl1) np.testing.assert_array_equal(res[0].time, t1) diff --git a/tests/unit_tests/test_deplete_reaction.py b/tests/unit_tests/test_deplete_reaction.py index a98535e1d8..de628f8c66 100644 --- a/tests/unit_tests/test_deplete_reaction.py +++ b/tests/unit_tests/test_deplete_reaction.py @@ -1,35 +1,38 @@ """Tests for the openmc.deplete.ReactionRates class.""" -from openmc.deplete import reaction_rates +import numpy as np +from openmc.deplete import ReactionRates -def test_indexing(): - """Tests the __getitem__ and __setitem__ routines simultaneously.""" +def test_get_set(): + """Tests the get/set methods.""" mat_to_ind = {"10000" : 0, "10001" : 1} nuc_to_ind = {"U238" : 0, "U235" : 1} react_to_ind = {"fission" : 0, "(n,gamma)" : 1} - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + assert rates.shape == (2, 2, 2) + assert np.all(rates == 0.0) - rates["10000", "U238", "fission"] = 1.0 - rates["10001", "U238", "fission"] = 2.0 - rates["10000", "U235", "fission"] = 3.0 - rates["10001", "U235", "fission"] = 4.0 - rates["10000", "U238", "(n,gamma)"] = 5.0 - rates["10001", "U238", "(n,gamma)"] = 6.0 - rates["10000", "U235", "(n,gamma)"] = 7.0 - rates["10001", "U235", "(n,gamma)"] = 8.0 + rates.set("10000", "U238", "fission", 1.0) + rates.set("10001", "U238", "fission", 2.0) + rates.set("10000", "U235", "fission", 3.0) + rates.set("10001", "U235", "fission", 4.0) + rates.set("10000", "U238", "(n,gamma)", 5.0) + rates.set("10001", "U238", "(n,gamma)", 6.0) + rates.set("10000", "U235", "(n,gamma)", 7.0) + rates.set("10001", "U235", "(n,gamma)", 8.0) # String indexing - assert rates["10000", "U238", "fission"] == 1.0 - assert rates["10001", "U238", "fission"] == 2.0 - assert rates["10000", "U235", "fission"] == 3.0 - assert rates["10001", "U235", "fission"] == 4.0 - assert rates["10000", "U238", "(n,gamma)"] == 5.0 - assert rates["10001", "U238", "(n,gamma)"] == 6.0 - assert rates["10000", "U235", "(n,gamma)"] == 7.0 - assert rates["10001", "U235", "(n,gamma)"] == 8.0 + assert rates.get("10000", "U238", "fission") == 1.0 + assert rates.get("10001", "U238", "fission") == 2.0 + assert rates.get("10000", "U235", "fission") == 3.0 + assert rates.get("10001", "U235", "fission") == 4.0 + assert rates.get("10000", "U238", "(n,gamma)") == 5.0 + assert rates.get("10001", "U238", "(n,gamma)") == 6.0 + assert rates.get("10000", "U235", "(n,gamma)") == 7.0 + assert rates.get("10001", "U235", "(n,gamma)") == 8.0 # Int indexing assert rates[0, 0, 0] == 1.0 @@ -44,7 +47,7 @@ def test_indexing(): rates[0, 0, 0] = 5.0 assert rates[0, 0, 0] == 5.0 - assert rates["10000", "U238", "fission"] == 5.0 + assert rates.get("10000", "U238", "fission") == 5.0 def test_n_mat(): @@ -53,7 +56,7 @@ def test_n_mat(): nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) assert rates.n_mat == 2 @@ -64,7 +67,7 @@ def test_n_nuc(): nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) assert rates.n_nuc == 3 @@ -75,6 +78,6 @@ def test_n_react(): nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - rates = reaction_rates.ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) assert rates.n_react == 4 From 82fea220c40615bd44b61d120f7059b7efed3aaf Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 07:23:26 -0600 Subject: [PATCH 28/68] Use get_all_materials() to simplify logic --- openmc/deplete/openmc_wrapper.py | 71 ++++++++++---------------------- 1 file changed, 21 insertions(+), 50 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index c95170a461..9a895a5081 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -104,7 +104,7 @@ class OpenMCOperator(Operator): ---------- geometry : openmc.Geometry The OpenMC geometry object. - settings : OpenMCSettings + settings : openmc.deplete.OpenMCSettings Settings object. Attributes @@ -130,8 +130,8 @@ class OpenMCOperator(Operator): Number of nuclides considered in the decay chain. mat_tally_ind : OrderedDict of str to int Dictionary mapping material ID to index in tally. - """ + """ def __init__(self, geometry, settings): super().__init__(settings) @@ -170,8 +170,10 @@ class OpenMCOperator(Operator): # Extract number densities from the geometry self.extract_number(mat_burn, mat_not_burn, volume, nuc_dict) - # Create reaction rate tables - self.initialize_reaction_rates() + # Create reaction rates array + index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} + self.reaction_rates = ReactionRates( + self.burn_mat_to_ind, self.burn_nuc_to_ind, index_rx) def __call__(self, vec, print_out=True): """Runs a simulation. @@ -243,35 +245,17 @@ class OpenMCOperator(Operator): volume = OrderedDict() # Iterate once through the geometry to get dictionaries - cells = self.geometry.get_all_material_cells() - for cell in cells.values(): - if isinstance(cell.fill, openmc.Material): - mat = cell.fill - for nuclide in mat.get_nuclide_densities(): - nuc_set.add(nuclide) - if mat.depletable: - mat_burn.add(str(mat.id)) - volume[str(mat.id)] = mat.volume - else: - mat_not_burn.add(str(mat.id)) + for mat in self.geometry.get_all_materials().values(): + for nuclide in mat.get_nuclide_densities(): + nuc_set.add(nuclide) + if mat.depletable: + mat_burn.add(str(mat.id)) + if mat.volume is None: + raise RuntimeError("Volume not specified for depletable " + "material with ID={}.".format(mat.id)) + volume[str(mat.id)] = mat.volume else: - for mat in cell.fill: - for nuclide in mat.get_nuclide_densities(): - nuc_set.add(nuclide) - if mat.depletable: - mat_burn.add(str(mat.id)) - volume[str(mat.id)] = mat.volume - else: - mat_not_burn.add(str(mat.id)) - - need_vol = [] - - for mat_id in volume: - if volume[mat_id] is None: - need_vol.append(mat_id) - - if need_vol: - exit("Need volumes for materials: " + str(need_vol)) + mat_not_burn.add(str(mat.id)) # Sort the sets mat_burn = sorted(mat_burn, key=int) @@ -334,18 +318,13 @@ class OpenMCOperator(Operator): if self.settings.dilute_initial != 0.0: for nuc in self.burn_nuc_to_ind: - self.number.set_atom_density(np.s_[:], nuc, self.settings.dilute_initial) + self.number.set_atom_density(np.s_[:], nuc, + self.settings.dilute_initial) # Now extract the number densities and store - cells = self.geometry.get_all_material_cells() - for cell in cells.values(): - if isinstance(cell.fill, openmc.Material): - if str(cell.fill.id) in mat_dict: - self.set_number_from_mat(cell.fill) - else: - for mat in cell.fill: - if str(mat.id) in mat_dict: - self.set_number_from_mat(mat) + for mat in self.geometry.get_all_materials().values(): + if str(mat.id) in mat_dict: + self.set_number_from_mat(mat) def set_number_from_mat(self, mat): """Extracts material and number densities from openmc.Material @@ -364,14 +343,6 @@ class OpenMCOperator(Operator): number = nuc_dens[nuclide][1] * 1.0e24 self.number.set_atom_density(mat_id, nuclide, number) - def initialize_reaction_rates(self): - """Create reaction rates object. """ - # Create dictionary to map reactions to indices - index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} - - self.reaction_rates = ReactionRates( - self.burn_mat_to_ind, self.burn_nuc_to_ind, index_rx) - def form_matrix(self, y, mat): """Forms the depletion matrix. From afd4ad61b0717c3566053bc6ca294df4ee8cde59 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 08:53:39 -0600 Subject: [PATCH 29/68] Support --update for depletion regression test --- openmc/deplete/results.py | 2 +- tests/regression_tests/test_deplete_full.py | 27 +++++++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 4a5ca24352..539ce5dd67 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -419,7 +419,7 @@ def read_results(filename): The result objects. """ - with h5py.File(filename, "r") as fh: + with h5py.File(str(filename), "r") as fh: assert fh["version"].value == RESULTS_VERSION # Get number of results stored diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index c809ba0536..6033e3f75a 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -11,6 +11,7 @@ import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities +from tests.regression_tests import config from .example_geometry import generate_problem @@ -59,33 +60,39 @@ def test_full(run_in_tmpdir): # Perform simulation using the predictor algorithm openmc.deplete.integrator.predictor(op) - # Load the files - res_test = results.read_results(settings.output_dir / "depletion_results.h5") + # Get path to test and reference results + path_test = settings.output_dir / 'depletion_results.h5' + path_reference = Path(__file__).with_name('test_reference.h5') - # Load the reference - filename = str(Path(__file__).with_name('test_reference.h5')) - res_old = results.read_results(filename) + # If updating results, do so and return + if config['update']: + shutil.copyfile(str(path_test), str(path_reference)) + return + + # Load the reference/test results + res_test = results.read_results(path_test) + res_ref = results.read_results(path_reference) # Assert same mats - for mat in res_old[0].mat_to_ind: + for mat in res_ref[0].mat_to_ind: assert mat in res_test[0].mat_to_ind, \ "Material {} not in new results.".format(mat) - for nuc in res_old[0].nuc_to_ind: + for nuc in res_ref[0].nuc_to_ind: assert nuc in res_test[0].nuc_to_ind, \ "Nuclide {} not in new results.".format(nuc) for mat in res_test[0].mat_to_ind: - assert mat in res_old[0].mat_to_ind, \ + assert mat in res_ref[0].mat_to_ind, \ "Material {} not in old results.".format(mat) for nuc in res_test[0].nuc_to_ind: - assert nuc in res_old[0].nuc_to_ind, \ + assert nuc in res_ref[0].nuc_to_ind, \ "Nuclide {} not in old results.".format(nuc) tol = 1.0e-6 for mat in res_test[0].mat_to_ind: for nuc in res_test[0].nuc_to_ind: _, y_test = utilities.evaluate_single_nuclide(res_test, mat, nuc) - _, y_old = utilities.evaluate_single_nuclide(res_old, mat, nuc) + _, y_old = utilities.evaluate_single_nuclide(res_ref, mat, nuc) # Test each point correct = True From f113205ab91bb28976314d2cc0f306186410f9cd Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 09:15:09 -0600 Subject: [PATCH 30/68] Don't update non-depletable materials (changes test results) --- openmc/deplete/openmc_wrapper.py | 3 ++ .../test_deplete_utilities.py | 39 +++++++++--------- tests/regression_tests/test_reference.h5 | Bin 231608 -> 162120 bytes 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 9a895a5081..e5eaf52dc0 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -397,6 +397,9 @@ class OpenMCOperator(Operator): number_i = comm.bcast(self.number, root=rank) for mat in number_i.mat_to_ind: + if number_i.mat_to_ind[mat] >= number_i.n_mat_burn: + continue + nuclides = [] densities = [] for nuc in number_i.nuc_to_ind: diff --git a/tests/regression_tests/test_deplete_utilities.py b/tests/regression_tests/test_deplete_utilities.py index f9d34aa75a..f38129cc79 100644 --- a/tests/regression_tests/test_deplete_utilities.py +++ b/tests/regression_tests/test_deplete_utilities.py @@ -20,35 +20,36 @@ def res(): def test_evaluate_single_nuclide(res): """Tests evaluating single nuclide utility code.""" - x, y = utilities.evaluate_single_nuclide(res, "1", "Xe135") + t, n = utilities.evaluate_single_nuclide(res, "1", "Xe135") - x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - y_ref = [6.6747328233649218e+08, 3.5519299354458244e+14, - 3.4599104054580338e+14, 3.3821165110278112e+14] + t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + n_ref = [6.6747328233649218e+08, 3.5421791038348462e+14, + 3.6208592242443462e+14, 3.3799758969347038e+14] - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, y_ref) + np.testing.assert_array_equal(t, t_ref) + np.testing.assert_array_equal(n, n_ref) def test_evaluate_reaction_rate(res): """Tests evaluating reaction rate utility code.""" - x, y = utilities.evaluate_reaction_rate(res, "1", "Xe135", "(n,gamma)") + t, r = utilities.evaluate_reaction_rate(res, "1", "Xe135", "(n,gamma)") - x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - xe_ref = np.array([6.6747328233649218e+08, 3.5519299354458244e+14, - 3.4599104054580338e+14, 3.3821165110278112e+14]) - r_ref = np.array([4.0643598574337784e-05, 4.1457730544386974e-05, - 3.4121248544056681e-05, 3.9204686657643301e-05]) + t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + n_ref = np.array([6.6747328233649218e+08, 3.5421791038348462e+14, + 3.6208592242443462e+14, 3.3799758969347038e+14]) + xs_ref = np.array([4.0594392323131994e-05, 3.9249546927524987e-05, + 3.8394587728581798e-05, 4.1521845978371697e-05]) - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, xe_ref * r_ref) + np.testing.assert_array_equal(t, t_ref) + np.testing.assert_array_equal(r, n_ref * xs_ref) def test_evaluate_eigenvalue(res): """Tests evaluating eigenvalue.""" - x, y = utilities.evaluate_eigenvalue(res) + t, k = utilities.evaluate_eigenvalue(res) - x_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - y_ref = [1.1921986054449838, 1.1712785643938586, 1.1927099024502694, 1.2269183590698847] + t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] + k_ref = [1.181281798790367, 1.1798750921988739, 1.1965943696058159, + 1.2207119847790813] - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, y_ref) + np.testing.assert_array_equal(t, t_ref) + np.testing.assert_array_equal(k, k_ref) diff --git a/tests/regression_tests/test_reference.h5 b/tests/regression_tests/test_reference.h5 index f832e3e2635c8d25a09609c38830227a3a551958..6e724c597107560c4155a257f4e854210f36f08e 100644 GIT binary patch literal 162120 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 literal 231608 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 z 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 Wt^QlMVXT)}#SI@A@UPy#^8Wzjt8qmD From fe547feb74329804218084883de8edae76591b84 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 09:17:12 -0600 Subject: [PATCH 31/68] Don't keep track of non-burnable materials --- openmc/deplete/atom_number.py | 38 +++--------- openmc/deplete/openmc_wrapper.py | 64 ++++++-------------- tests/unit_tests/test_deplete_atom_number.py | 27 +++------ 3 files changed, 34 insertions(+), 95 deletions(-) diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 63c9af8364..f9d85091ae 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -19,8 +19,6 @@ class AtomNumber(object): A dictionary mapping nuclide name as string to index. volume : OrderedDict of int to float Volume of geometry. - n_mat_burn : int - Number of materials to be burned. n_nuc_burn : int Number of nuclides to be burned. @@ -33,8 +31,6 @@ class AtomNumber(object): volume : numpy.array Volume of geometry indexed by mat_to_ind. If a volume is not found, it defaults to 1 so that reading density still works correctly. - n_mat_burn : int - Number of materials to be burned. n_nuc_burn : int Number of nuclides to be burned. n_mat : int @@ -45,30 +41,26 @@ class AtomNumber(object): Array storing total atoms indexed by the above dictionaries. burn_nuc_list : list of str A list of all nuclide material names. Used for sorting the simulation. - burn_mat_list : list of str - A list of all burning material names. Used for sorting the simulation. - """ - def __init__(self, mat_to_ind, nuc_to_ind, volume, n_mat_burn, n_nuc_burn): + """ + def __init__(self, mat_to_ind, nuc_to_ind, volume, n_nuc_burn): self.mat_to_ind = mat_to_ind self.nuc_to_ind = nuc_to_ind - self.volume = np.ones(self.n_mat) + self.volume = np.ones(len(mat_to_ind)) for mat in volume: - if str(mat) in self.mat_to_ind: - ind = self.mat_to_ind[str(mat)] + if mat in self.mat_to_ind: + ind = self.mat_to_ind[mat] self.volume[ind] = volume[mat] - self.n_mat_burn = n_mat_burn self.n_nuc_burn = n_nuc_burn self.number = np.zeros((self.n_mat, self.n_nuc)) # For performance, create storage for burn_nuc_list, burn_mat_list self._burn_nuc_list = None - self._burn_mat_list = None def __getitem__(self, pos): """Retrieves total atom number from AtomNumber. @@ -175,7 +167,7 @@ class AtomNumber(object): if isinstance(mat, str): mat = self.mat_to_ind[mat] - return self[mat, 0:self.n_nuc_burn] + return self[mat, :self.n_nuc_burn] def set_mat_slice(self, mat, val): """Sets atom quantity indexed by mats for all burned nuclides @@ -191,7 +183,7 @@ class AtomNumber(object): if isinstance(mat, str): mat = self.mat_to_ind[mat] - self[mat, 0:self.n_nuc_burn] = val + self[mat, :self.n_nuc_burn] = val @property def n_mat(self): @@ -218,19 +210,3 @@ class AtomNumber(object): self._burn_nuc_list[ind] = nuc return self._burn_nuc_list - - @property - def burn_mat_list(self): - """burn_mat_list : list of str - A list of all burning material names. Used for sorting the simulation. - """ - - if self._burn_mat_list is None: - self._burn_mat_list = [None] * self.n_mat_burn - - for mat in self.mat_to_ind: - ind = self.mat_to_ind[mat] - if ind < self.n_mat_burn: - self._burn_mat_list[ind] = mat - - return self._burn_mat_list diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index e5eaf52dc0..b25afb79b3 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -121,9 +121,9 @@ class OpenMCOperator(Operator): The depletion chain information necessary to form matrices and tallies. reaction_rates : openmc.deplete.ReactionRates Reaction rates from the last operator step. - burn_mat_to_id : OrderedDict of str to int + burn_mat_to_ind : OrderedDict of str to int Dictionary mapping material ID (as a string) to an index in reaction_rates. - burn_nuc_to_id : OrderedDict of str to int + burn_nuc_to_ind : OrderedDict of str to int Dictionary mapping nuclide name (as a string) to an index in reaction_rates. n_nuc : int @@ -148,18 +148,16 @@ class OpenMCOperator(Operator): # Clear out OpenMC, create task lists, distribute if comm.rank == 0: openmc.reset_auto_ids() - mat_burn_list, mat_not_burn_list, volume, self.mat_tally_ind, \ + mat_burn_list, volume, self.mat_tally_ind, \ nuc_dict = self.extract_mat_ids() else: # Dummy variables mat_burn_list = None - mat_not_burn_list = None volume = None nuc_dict = None self.mat_tally_ind = None mat_burn = comm.scatter(mat_burn_list) - mat_not_burn = comm.scatter(mat_not_burn_list) nuc_dict = comm.bcast(nuc_dict) volume = comm.bcast(volume) self.mat_tally_ind = comm.bcast(self.mat_tally_ind) @@ -168,7 +166,7 @@ class OpenMCOperator(Operator): self.load_participating() # Extract number densities from the geometry - self.extract_number(mat_burn, mat_not_burn, volume, nuc_dict) + self.extract_number(mat_burn, volume, nuc_dict) # Create reaction rates array index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} @@ -239,9 +237,7 @@ class OpenMCOperator(Operator): """ mat_burn = set() - mat_not_burn = set() nuc_set = set() - volume = OrderedDict() # Iterate once through the geometry to get dictionaries @@ -254,19 +250,14 @@ class OpenMCOperator(Operator): raise RuntimeError("Volume not specified for depletable " "material with ID={}.".format(mat.id)) volume[str(mat.id)] = mat.volume - else: - mat_not_burn.add(str(mat.id)) # Sort the sets mat_burn = sorted(mat_burn, key=int) - mat_not_burn = sorted(mat_not_burn, key=int) nuc_set = sorted(nuc_set) # Construct a global nuclide dictionary, burned first nuc_dict = copy.deepcopy(self.chain.nuclide_dict) - i = len(nuc_dict) - for nuc in nuc_set: if nuc not in nuc_dict: nuc_dict[nuc] = i @@ -274,47 +265,35 @@ class OpenMCOperator(Operator): # Decompose geometry mat_burn_lists = _chunks(mat_burn, comm.size) - mat_not_burn_lists = _chunks(mat_not_burn, comm.size) mat_tally_ind = OrderedDict() - for i, mat in enumerate(mat_burn): mat_tally_ind[mat] = i - return mat_burn_lists, mat_not_burn_lists, volume, mat_tally_ind, nuc_dict + return mat_burn_lists, volume, mat_tally_ind, nuc_dict - def extract_number(self, mat_burn, mat_not_burn, volume, nuc_dict): + def extract_number(self, mat_burn, volume, nuc_dict): """Construct self.number read from geometry Parameters ---------- mat_burn : list of int Materials to be burned managed by this thread. - mat_not_burn - Materials not to be burned managed by this thread. volume : OrderedDict of str to float Volumes for the above materials. nuc_dict : OrderedDict of str to int Nuclides to be used in the simulation. + """ - # Same with materials - mat_dict = OrderedDict() self.burn_mat_to_ind = OrderedDict() - i = 0 - for mat in mat_burn: - mat_dict[mat] = i + for i, mat in enumerate(mat_burn): self.burn_mat_to_ind[mat] = i - i += 1 - for mat in mat_not_burn: - mat_dict[mat] = i - i += 1 - - n_mat_burn = len(mat_burn) n_nuc_burn = len(self.chain) - self.number = AtomNumber(mat_dict, nuc_dict, volume, n_mat_burn, n_nuc_burn) + self.number = AtomNumber(self.burn_mat_to_ind, nuc_dict, volume, + n_nuc_burn) if self.settings.dilute_initial != 0.0: for nuc in self.burn_nuc_to_ind: @@ -323,7 +302,7 @@ class OpenMCOperator(Operator): # Now extract the number densities and store for mat in self.geometry.get_all_materials().values(): - if str(mat.id) in mat_dict: + if str(mat.id) in self.burn_mat_to_ind: self.set_number_from_mat(mat) def set_number_from_mat(self, mat): @@ -397,9 +376,6 @@ class OpenMCOperator(Operator): number_i = comm.bcast(self.number, root=rank) for mat in number_i.mat_to_ind: - if number_i.mat_to_ind[mat] >= number_i.n_mat_burn: - continue - nuclides = [] densities = [] for nuc in number_i.nuc_to_ind: @@ -507,12 +483,10 @@ class OpenMCOperator(Operator): Returns ------- list of numpy.array - A list of np.arrays containing total atoms of each cell. + A list of arrays containing total atoms of each material + """ - - total_density = [self.number.get_mat_slice(i) for i in range(self.number.n_mat_burn)] - - return total_density + return list(self.number.get_mat_slice(np.s_[:])) def set_density(self, total_density): """Sets density. @@ -522,12 +496,12 @@ class OpenMCOperator(Operator): Parameters ---------- - total_density : list of numpy.array + total_density : list of numpy.ndarray Total atoms. - """ + """ # Fill in values - for i in range(self.number.n_mat_burn): + for i in range(self.number.n_mat): self.number.set_mat_slice(i, total_density[i]) def unpack_tallies_and_normalize(self): @@ -581,7 +555,7 @@ class OpenMCOperator(Operator): break # Extract results - for i, mat in enumerate(self.number.burn_mat_list): + for i, mat in enumerate(self.burn_mat_to_ind): # Get tally index slab = materials.index(mat) @@ -686,7 +660,7 @@ class OpenMCOperator(Operator): """ nuc_list = self.number.burn_nuc_list - burn_list = self.number.burn_mat_list + burn_list = list(self.burn_mat_to_ind) volume = {} for i, mat in enumerate(burn_list): diff --git a/tests/unit_tests/test_deplete_atom_number.py b/tests/unit_tests/test_deplete_atom_number.py index e3eb22aa55..887e9af1bc 100644 --- a/tests/unit_tests/test_deplete_atom_number.py +++ b/tests/unit_tests/test_deplete_atom_number.py @@ -7,11 +7,11 @@ from openmc.deplete import atom_number def test_indexing(): """Tests the __getitem__ and __setitem__ routines simultaneously.""" - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} + mat_to_ind = {"10000" : 0, "10001" : 1} nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) number["10000", "U238"] = 1.0 number["10001", "U238"] = 2.0 @@ -42,7 +42,7 @@ def test_n_mat(): nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) assert number.n_mat == 2 @@ -53,7 +53,7 @@ def test_n_nuc(): nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) assert number.n_nuc == 3 @@ -64,22 +64,11 @@ def test_burn_nuc_list(): nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) assert number.burn_nuc_list == ["U238", "U235"] -def test_burn_mat_list(): - """Test the list of burned nuclides property""" - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) - - assert number.burn_mat_list == ["10000", "10001"] - - def test_density_indexing(): """Tests the get and set_atom_density routines simultaneously.""" @@ -87,7 +76,7 @@ def test_density_indexing(): nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) number.set_atom_density("10000", "U238", 1.0) number.set_atom_density("10001", "U238", 2.0) @@ -144,7 +133,7 @@ def test_get_mat_slice(): nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) number.number = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) @@ -164,7 +153,7 @@ def test_set_mat_slice(): nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2, 2) + number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) number.set_mat_slice(0, [1.0, 2.0]) From 2cd9aecf21628cdda83d5fd5c9606c21ccaafb73 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 10:15:48 -0600 Subject: [PATCH 32/68] Get rid of Operator.mat_tally_ind --- openmc/deplete/openmc_wrapper.py | 42 +++++++++++--------------------- openmc/deplete/results.py | 17 ++++++------- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index b25afb79b3..7cdfbd4d8b 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -5,6 +5,7 @@ This module implements the depletion -> OpenMC linkage. import copy from collections import OrderedDict +from itertools import chain import os import random import sys @@ -126,10 +127,8 @@ class OpenMCOperator(Operator): burn_nuc_to_ind : OrderedDict of str to int Dictionary mapping nuclide name (as a string) to an index in reaction_rates. - n_nuc : int - Number of nuclides considered in the decay chain. - mat_tally_ind : OrderedDict of str to int - Dictionary mapping material ID to index in tally. + burnable_mats : list of str + All burnable material IDs """ def __init__(self, geometry, settings): @@ -138,7 +137,6 @@ class OpenMCOperator(Operator): self.geometry = geometry self.number = None self.participating_nuclides = None - self.reaction_rates = None self.burn_mat_to_ind = OrderedDict() self.burn_nuc_to_ind = None @@ -148,19 +146,18 @@ class OpenMCOperator(Operator): # Clear out OpenMC, create task lists, distribute if comm.rank == 0: openmc.reset_auto_ids() - mat_burn_list, volume, self.mat_tally_ind, \ - nuc_dict = self.extract_mat_ids() + mat_burn_list, volume, nuc_dict = self.extract_mat_ids() else: # Dummy variables mat_burn_list = None volume = None nuc_dict = None - self.mat_tally_ind = None - mat_burn = comm.scatter(mat_burn_list) + mat_burn_list = comm.bcast(mat_burn_list) nuc_dict = comm.bcast(nuc_dict) volume = comm.bcast(volume) - self.mat_tally_ind = comm.bcast(self.mat_tally_ind) + mat_burn = mat_burn_list[comm.rank] + self.burnable_mats = list(chain(*mat_burn_list)) # Load participating nuclides self.load_participating() @@ -230,8 +227,6 @@ class OpenMCOperator(Operator): List of non-burnable materials indexed by rank. volume : OrderedDict of str to float Volume of each cell - mat_tally_ind : OrderedDict of str to int - Dictionary mapping material ID to index in tally. nuc_dict : OrderedDict of str to int Nuclides in order of how they'll appear in the simulation. """ @@ -266,11 +261,7 @@ class OpenMCOperator(Operator): # Decompose geometry mat_burn_lists = _chunks(mat_burn, comm.size) - mat_tally_ind = OrderedDict() - for i, mat in enumerate(mat_burn): - mat_tally_ind[mat] = i - - return mat_burn_lists, volume, mat_tally_ind, nuc_dict + return mat_burn_lists, volume, nuc_dict def extract_number(self, mat_burn, volume, nuc_dict): """Construct self.number read from geometry @@ -463,7 +454,7 @@ class OpenMCOperator(Operator): """ # Create tallies for depleting regions materials = [openmc.capi.materials[int(i)] - for i in self.mat_tally_ind] + for i in self.burnable_mats] mat_filter = openmc.capi.MaterialFilter(materials, 1) # Set up a tally that has a material filter covering each depletable @@ -524,7 +515,7 @@ class OpenMCOperator(Operator): k_combined = openmc.capi.keff()[0] # Extract tally bins - materials = list(self.mat_tally_ind.keys()) + materials = self.burnable_mats nuclides = openmc.capi.tallies[1].nuclides # Form fast map @@ -639,11 +630,6 @@ class OpenMCOperator(Operator): self.burn_nuc_to_ind[name] = nuc_ind nuc_ind += 1 - @property - def n_nuc(self): - """Number of nuclides considered in the decay chain.""" - return len(self.chain) - def get_results_info(self): """Returns volume list, cell lists, and nuc lists. @@ -655,10 +641,10 @@ class OpenMCOperator(Operator): A list of all nuclide names. Used for sorting the simulation. burn_list : list of int A list of all cell IDs to be burned. Used for sorting the simulation. - full_burn_dict : OrderedDict of str to int - Maps cell name to index in global geometry. - """ + full_burn_list : list + List of all burnable material IDs + """ nuc_list = self.number.burn_nuc_list burn_list = list(self.burn_mat_to_ind) @@ -670,7 +656,7 @@ class OpenMCOperator(Operator): volume_list = comm.allgather(volume) volume = {k: v for d in volume_list for k, v in d.items()} - return volume, nuc_list, burn_list, self.mat_tally_ind + return volume, nuc_list, burn_list, self.burnable_mats def density_to_mat(dens_dict): diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 539ce5dd67..f7daca5b2e 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -58,7 +58,7 @@ class Results(object): self.data = None - def allocate(self, volume, nuc_list, burn_list, full_burn_dict, stages): + def allocate(self, volume, nuc_list, burn_list, full_burn_list, stages): """Allocates memory of Results. Parameters @@ -69,16 +69,16 @@ class Results(object): A list of all nuclide names. Used for sorting the simulation. burn_list : list of int A list of all mat IDs to be burned. Used for sorting the simulation. - full_burn_dict : dict of str to int - Map of material name to id in global geometry. + full_burn_list : list of str + List of all burnable material IDs stages : int Number of stages in simulation. - """ + """ self.volume = copy.deepcopy(volume) self.nuc_to_ind = OrderedDict() self.mat_to_ind = OrderedDict() - self.mat_to_hdf5_ind = copy.deepcopy(full_burn_dict) + self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} for i, mat in enumerate(burn_list): self.mat_to_ind[mat] = i @@ -177,10 +177,9 @@ class Results(object): handle.create_dataset("version", data=RESULTS_VERSION) - mat_int = sorted([int(mat) for mat in self.mat_to_hdf5_ind]) - mat_list = [str(mat) for mat in mat_int] - nuc_list = sorted(self.nuc_to_ind.keys()) - rxn_list = sorted(self.rates[0].index_rx.keys()) + mat_list = sorted(self.mat_to_hdf5_ind, key=int) + nuc_list = sorted(self.nuc_to_ind) + rxn_list = sorted(self.rates[0].index_rx) n_mats = self.n_hdf5_mats n_nuc_number = len(nuc_list) From 4e21e398c83f0997fc20b7c4a08952cfc73b482b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 10:19:02 -0600 Subject: [PATCH 33/68] Move density_to_mat to example_geometry.py --- openmc/deplete/openmc_wrapper.py | 21 --------------------- scripts/example_geometry.py | 23 ++++++++++++++++++++++- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 7cdfbd4d8b..c8a3bcca99 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -657,24 +657,3 @@ class OpenMCOperator(Operator): volume = {k: v for d in volume_list for k, v in d.items()} return volume, nuc_list, burn_list, self.burnable_mats - - -def density_to_mat(dens_dict): - """Generates an OpenMC material from a cell ID and self.number_density. - - Parameters - ---------- - m_id : int - Cell ID. - Returns - ------- - openmc.Material - The OpenMC material filled with nuclides. - """ - - mat = openmc.Material() - for key in dens_dict: - mat.add_nuclide(key, 1.0e-24*dens_dict[key]) - mat.set_density('sum') - - return mat diff --git a/scripts/example_geometry.py b/scripts/example_geometry.py index 09ce0576f1..ca10c1f725 100644 --- a/scripts/example_geometry.py +++ b/scripts/example_geometry.py @@ -9,7 +9,28 @@ import math import numpy as np import openmc -from openmc.deplete import density_to_mat + + +def density_to_mat(dens_dict): + """Generates an OpenMC material from a cell ID and self.number_density. + + Parameters + ---------- + dens_dict : dict + Dictionary mapping nuclide names to densities + + Returns + ------- + openmc.Material + The OpenMC material filled with nuclides. + + """ + mat = openmc.Material() + for key in dens_dict: + mat.add_nuclide(key, 1.0e-24*dens_dict[key]) + mat.set_density('sum') + + return mat def generate_initial_number_density(): From ea335e06961a94c6b680462e24224b998920f5dc Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 10:24:10 -0600 Subject: [PATCH 34/68] Change OPENDEPLETE_CHAIN -> OPENMC_DEPLETE_CHAIN --- openmc/deplete/abc.py | 6 +++--- openmc/deplete/chain.py | 4 ++-- openmc/deplete/openmc_wrapper.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 5441829b40..e8f65f8871 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -23,8 +23,8 @@ class Settings(object): output_dir : pathlib.Path Path to output directory to save results. chain_file : str - Path to the depletion chain xml file. Defaults to the - :envvar:`OPENDEPLETE_CHAIN` environment variable if it exists. + Path to the depletion chain XML file. Defaults to the + :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. dilute_initial : float Initial atom density to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for @@ -37,7 +37,7 @@ class Settings(object): """ def __init__(self): try: - self.chain_file = os.environ["OPENDEPLETE_CHAIN"] + self.chain_file = os.environ["OPENMC_DEPLETE_CHAIN"] except KeyError: self.chain_file = None self.dt_vec = None diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 2af64e172a..2d2e71a776 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -332,9 +332,9 @@ class Chain(object): root = ET.parse(filename) except Exception: if filename is None: - print("No chain specified, either manually or in environment variable OPENDEPLETE_CHAIN.") + print("No chain specified, either manually or in environment variable OPENMC_DEPLETE_CHAIN.") else: - print('Decay chain "', filename, '" is invalid.') + print('Decay chain "{}" is invalid.'.format(filename)) raise for i, nuclide_elem in enumerate(root.findall('nuclide_table')): diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index c8a3bcca99..a01949a911 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -51,8 +51,8 @@ class OpenMCSettings(Settings): output_dir : pathlib.Path Path to output directory to save results. chain_file : str - Path to the depletion chain xml file. Defaults to the - :envvar:`OPENDEPLETE_CHAIN` environment variable if it exists. + Path to the depletion chain XML file. Defaults to the + :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. dilute_initial : float Initial atom density to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for From 78e1afb0ffb6e07c32714967ea0001e120303b28 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 13:10:28 -0600 Subject: [PATCH 35/68] Rename participating_nuclides -> nuclides_with_data --- openmc/deplete/atom_number.py | 14 ++--- openmc/deplete/chain.py | 7 ++- openmc/deplete/openmc_wrapper.py | 103 +++++++++++++++---------------- 3 files changed, 60 insertions(+), 64 deletions(-) diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index f9d85091ae..fc9cb8433b 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -96,9 +96,9 @@ class AtomNumber(object): These indexes can be strings (which get converted to integers via the dictionaries), integers used directly, or slices. val : float - The value to set the array to. - """ + The value [atom] to set the array to. + """ mat, nuc = pos if isinstance(mat, str): mat = self.mat_to_ind[mat] @@ -119,10 +119,10 @@ class AtomNumber(object): Returns ------- - numpy.array - The density indexed. - """ + numpy.ndarray + Density in [atom/cm^3] + """ if isinstance(mat, str): mat = self.mat_to_ind[mat] if isinstance(nuc, str): @@ -140,9 +140,9 @@ class AtomNumber(object): nuc : str, int or slice Nuclide index. val : numpy.array - Array of values to set. - """ + Array of densities to set in [atom/cm^3] + """ if isinstance(mat, str): mat = self.mat_to_ind[mat] if isinstance(nuc, str): diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 2d2e71a776..ec8d79f819 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -332,10 +332,11 @@ class Chain(object): root = ET.parse(filename) except Exception: if filename is None: - print("No chain specified, either manually or in environment variable OPENMC_DEPLETE_CHAIN.") + msg = ("No chain specified, either manually or in environment " + "variable OPENMC_DEPLETE_CHAIN.") else: - print('Decay chain "{}" is invalid.'.format(filename)) - raise + msg = 'Decay chain "{}" is invalid.'.format(filename) + raise IOError(msg) for i, nuclide_elem in enumerate(root.findall('nuclide_table')): nuc = Nuclide.from_xml(nuclide_elem) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index a01949a911..7120926bcf 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -116,7 +116,7 @@ class OpenMCOperator(Operator): The OpenMC geometry object. number : openmc.deplete.AtomNumber Total number of atoms in simulation. - participating_nuclides : set of str + nuclides_with_data : set of str A set listing all unique nuclides available from cross_sections.xml. chain : openmc.deplete.Chain The depletion chain information necessary to form matrices and tallies. @@ -126,7 +126,8 @@ class OpenMCOperator(Operator): Dictionary mapping material ID (as a string) to an index in reaction_rates. burn_nuc_to_ind : OrderedDict of str to int Dictionary mapping nuclide name (as a string) to an index in - reaction_rates. + reaction_rates. Consists of all nuclides with neutron data and appearing + in the depletion chain. burnable_mats : list of str All burnable material IDs @@ -136,7 +137,6 @@ class OpenMCOperator(Operator): self.geometry = geometry self.number = None - self.participating_nuclides = None self.burn_mat_to_ind = OrderedDict() self.burn_nuc_to_ind = None @@ -146,7 +146,7 @@ class OpenMCOperator(Operator): # Clear out OpenMC, create task lists, distribute if comm.rank == 0: openmc.reset_auto_ids() - mat_burn_list, volume, nuc_dict = self.extract_mat_ids() + mat_burn_list, volume, nuc_dict = self._extract_mat_ids() else: # Dummy variables mat_burn_list = None @@ -160,10 +160,10 @@ class OpenMCOperator(Operator): self.burnable_mats = list(chain(*mat_burn_list)) # Load participating nuclides - self.load_participating() + self._load_participating() # Extract number densities from the geometry - self.extract_number(mat_burn, volume, nuc_dict) + self._extract_number(mat_burn, volume, nuc_dict) # Create reaction rates array index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} @@ -190,7 +190,7 @@ class OpenMCOperator(Operator): openmc.reset_auto_ids() # Update status - self.set_density(vec) + self._set_density(vec) time_start = time.time() @@ -205,7 +205,7 @@ class OpenMCOperator(Operator): time_openmc = time.time() # Extract results - op_result = self.unpack_tallies_and_normalize() + op_result = self._unpack_tallies_and_normalize() if comm.rank == 0: time_unpack = time.time() @@ -216,7 +216,7 @@ class OpenMCOperator(Operator): return copy.deepcopy(op_result) - def extract_mat_ids(self): + def _extract_mat_ids(self): """Extracts materials and assigns them to processes. Returns @@ -229,15 +229,15 @@ class OpenMCOperator(Operator): Volume of each cell nuc_dict : OrderedDict of str to int Nuclides in order of how they'll appear in the simulation. - """ + """ mat_burn = set() nuc_set = set() volume = OrderedDict() # Iterate once through the geometry to get dictionaries for mat in self.geometry.get_all_materials().values(): - for nuclide in mat.get_nuclide_densities(): + for nuclide in mat.get_nuclides(): nuc_set.add(nuclide) if mat.depletable: mat_burn.add(str(mat.id)) @@ -263,7 +263,7 @@ class OpenMCOperator(Operator): return mat_burn_lists, volume, nuc_dict - def extract_number(self, mat_burn, volume, nuc_dict): + def _extract_number(self, mat_burn, volume, nuc_dict): """Construct self.number read from geometry Parameters @@ -281,10 +281,8 @@ class OpenMCOperator(Operator): for i, mat in enumerate(mat_burn): self.burn_mat_to_ind[mat] = i - n_nuc_burn = len(self.chain) - self.number = AtomNumber(self.burn_mat_to_ind, nuc_dict, volume, - n_nuc_burn) + len(self.chain)) if self.settings.dilute_initial != 0.0: for nuc in self.burn_nuc_to_ind: @@ -294,23 +292,22 @@ class OpenMCOperator(Operator): # Now extract the number densities and store for mat in self.geometry.get_all_materials().values(): if str(mat.id) in self.burn_mat_to_ind: - self.set_number_from_mat(mat) + self._set_number_from_mat(mat) - def set_number_from_mat(self, mat): + def _set_number_from_mat(self, mat): """Extracts material and number densities from openmc.Material Parameters ---------- - mat : openmc.Materials + mat : openmc.Material The material to read from - """ + """ mat_id = str(mat.id) mat_ind = self.number.mat_to_ind[mat_id] - nuc_dens = mat.get_nuclide_atom_densities() - for nuclide in nuc_dens: - number = nuc_dens[nuclide][1] * 1.0e24 + for nuclide, density in mat.get_nuclide_atom_densities().values(): + number = density * 1.0e24 self.number.set_atom_density(mat_id, nuclide, number) def form_matrix(self, y, mat): @@ -344,17 +341,17 @@ class OpenMCOperator(Operator): if comm.rank == 0: self.geometry.export_to_xml() self.settings.settings.export_to_xml() - self.generate_materials_xml() + self._generate_materials_xml() # Initialize OpenMC library comm.barrier() openmc.capi.init(comm) # Generate tallies in memory - self.generate_tallies() + self._generate_tallies() # Return number density vector - return self.total_density_list() + return list(self.number.get_mat_slice(np.s_[:])) def finalize(self): """Finalize a depletion simulation and release resources.""" @@ -370,7 +367,7 @@ class OpenMCOperator(Operator): nuclides = [] densities = [] for nuc in number_i.nuc_to_ind: - if nuc in self.participating_nuclides: + if nuc in self.nuclides_with_data: val = 1.0e-24 * number_i.get_atom_density(mat, nuc) # If nuclide is zero, do not add to the problem. @@ -395,14 +392,14 @@ class OpenMCOperator(Operator): mat_internal = openmc.capi.materials[int(mat)] mat_internal.set_densities(nuclides, densities) - def generate_materials_xml(self): + def _generate_materials_xml(self): """Creates materials.xml from self.number. Due to uncertainty with how MPI interacts with OpenMC API, this constructs the XML manually. The long term goal is to do this through direct memory writing. - """ + """ materials = openmc.Materials(self.geometry.get_all_materials() .values()) @@ -414,12 +411,26 @@ class OpenMCOperator(Operator): materials.export_to_xml() def _get_tally_nuclides(self): + """Determine nuclides that should be tallied for reaction rates. + + This method returns a list of all nuclides that have neutron data and + are listed in the depletion chain. Technically, we should tally nuclides + that may not appear in the depletion chain because we still need to get + the fission reaction rate for these nuclides in order to normalize + power, but that is left as a future exercise. + + Returns + ------- + list of str + Tally nuclides + + """ nuc_set = set() # Create the set of all nuclides in the decay chain in cells marked for # burning in which the number density is greater than zero. for nuc in self.number.nuc_to_ind: - if nuc in self.participating_nuclides: + if nuc in self.nuclides_with_data: if np.sum(self.number[:, nuc]) > 0.0: nuc_set.add(nuc) @@ -439,12 +450,10 @@ class OpenMCOperator(Operator): nuc_list = None # Store list of tally nuclides on each process - nuc_list = comm.bcast(nuc_list, root=0) - tally_nuclides = [nuc for nuc in nuc_list if nuc in self.chain] + nuc_list = comm.bcast(nuc_list) + return [nuc for nuc in nuc_list if nuc in self.chain] - return tally_nuclides - - def generate_tallies(self): + def _generate_tallies(self): """Generates depletion tallies. Using information from the depletion chain as well as the nuclides @@ -465,21 +474,7 @@ class OpenMCOperator(Operator): tally_dep.scores = self.chain.reactions tally_dep.filters = [mat_filter] - def total_density_list(self): - """Returns a list of total density lists. - - This list is in the exact same order as depletion_matrix_list, so that - matrix exponentiation can be done easily. - - Returns - ------- - list of numpy.array - A list of arrays containing total atoms of each material - - """ - return list(self.number.get_mat_slice(np.s_[:])) - - def set_density(self, total_density): + def _set_density(self, total_density): """Sets density. Sets the density in the exact same order as total_density_list outputs, @@ -495,7 +490,7 @@ class OpenMCOperator(Operator): for i in range(self.number.n_mat): self.number.set_mat_slice(i, total_density[i]) - def unpack_tallies_and_normalize(self): + def _unpack_tallies_and_normalize(self): """Unpack tallies from OpenMC and return an operator result This method uses OpenMC's C API bindings to determine the k-effective @@ -587,7 +582,7 @@ class OpenMCOperator(Operator): return OperatorResult(k_combined, rates) - def load_participating(self): + def _load_participating(self): """Loads a cross_sections.xml file to find participating nuclides. This allows for nuclides that are important in the decay chain but not @@ -602,7 +597,7 @@ class OpenMCOperator(Operator): except KeyError: filename = None - self.participating_nuclides = set() + self.nuclides_with_data = set() try: tree = ET.parse(filename) @@ -624,8 +619,8 @@ class OpenMCOperator(Operator): for name in mats.split(): # Make a burn list of the union of nuclides in cross_sections.xml # and nuclides in depletion chain. - if name not in self.participating_nuclides: - self.participating_nuclides.add(name) + if name not in self.nuclides_with_data: + self.nuclides_with_data.add(name) if name in self.chain: self.burn_nuc_to_ind[name] = nuc_ind nuc_ind += 1 From 6832071b4cf8a5f1b01d20079a902b83abac7e4c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 13:35:51 -0600 Subject: [PATCH 36/68] Move set_density method to AtomNumber --- openmc/deplete/atom_number.py | 69 +++++++++++++++++++------------- openmc/deplete/openmc_wrapper.py | 18 +-------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index fc9cb8433b..f1b0155d73 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -107,6 +107,32 @@ class AtomNumber(object): self.number[mat, nuc] = val + @property + def n_mat(self): + """Number of materials.""" + return len(self.mat_to_ind) + + @property + def n_nuc(self): + """Number of nuclides.""" + return len(self.nuc_to_ind) + + @property + def burn_nuc_list(self): + """burn_nuc_list : list of str + A list of all nuclide material names. Used for sorting the simulation. + """ + + if self._burn_nuc_list is None: + self._burn_nuc_list = [None] * self.n_nuc_burn + + for nuc in self.nuc_to_ind: + ind = self.nuc_to_ind[nuc] + if ind < self.n_nuc_burn: + self._burn_nuc_list[ind] = nuc + + return self._burn_nuc_list + def get_atom_density(self, mat, nuc): """Accesses atom density instead of total number. @@ -160,10 +186,10 @@ class AtomNumber(object): Returns ------- - numpy.array - The slice requested. - """ + numpy.ndarray + The slice requested in [atom]. + """ if isinstance(mat, str): mat = self.mat_to_ind[mat] @@ -177,36 +203,25 @@ class AtomNumber(object): mat : str, int or slice Material index. val : numpy.array - The slice to set. - """ + The slice to set in [atom] + """ if isinstance(mat, str): mat = self.mat_to_ind[mat] self[mat, :self.n_nuc_burn] = val - @property - def n_mat(self): - """Number of materials.""" - return len(self.mat_to_ind) + def set_density(self, total_density): + """Sets density. - @property - def n_nuc(self): - """Number of nuclides.""" - return len(self.nuc_to_ind) + Sets the density in the exact same order as total_density_list outputs, + allowing for internal consistency + + Parameters + ---------- + total_density : list of numpy.ndarray + Total atoms. - @property - def burn_nuc_list(self): - """burn_nuc_list : list of str - A list of all nuclide material names. Used for sorting the simulation. """ - - if self._burn_nuc_list is None: - self._burn_nuc_list = [None] * self.n_nuc_burn - - for nuc in self.nuc_to_ind: - ind = self.nuc_to_ind[nuc] - if ind < self.n_nuc_burn: - self._burn_nuc_list[ind] = nuc - - return self._burn_nuc_list + for i in range(self.n_mat): + self.set_mat_slice(i, total_density[i]) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 7120926bcf..43a171afc5 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -190,7 +190,7 @@ class OpenMCOperator(Operator): openmc.reset_auto_ids() # Update status - self._set_density(vec) + self.number.set_density(vec) time_start = time.time() @@ -474,22 +474,6 @@ class OpenMCOperator(Operator): tally_dep.scores = self.chain.reactions tally_dep.filters = [mat_filter] - def _set_density(self, total_density): - """Sets density. - - Sets the density in the exact same order as total_density_list outputs, - allowing for internal consistency - - Parameters - ---------- - total_density : list of numpy.ndarray - Total atoms. - - """ - # Fill in values - for i in range(self.number.n_mat): - self.number.set_mat_slice(i, total_density[i]) - def _unpack_tallies_and_normalize(self): """Unpack tallies from OpenMC and return an operator result From 2a1c66913ad0a22c91757bf231e0023825f01110 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 13:47:49 -0600 Subject: [PATCH 37/68] Don't use hard-wired IDs for MaterialFilter and Tally --- openmc/deplete/openmc_wrapper.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 43a171afc5..42bd0e600e 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -196,7 +196,7 @@ class OpenMCOperator(Operator): # Update material compositions and tally nuclides self._update_materials() - openmc.capi.tallies[1].nuclides = self._get_tally_nuclides() + self._tally.nuclides = self._get_tally_nuclides() # Run OpenMC openmc.capi.reset() @@ -464,15 +464,15 @@ class OpenMCOperator(Operator): # Create tallies for depleting regions materials = [openmc.capi.materials[int(i)] for i in self.burnable_mats] - mat_filter = openmc.capi.MaterialFilter(materials, 1) + mat_filter = openmc.capi.MaterialFilter(materials) # Set up a tally that has a material filter covering each depletable # material and scores corresponding to all reactions that cause # transmutation. The nuclides for the tally are set later when eval() is # called. - tally_dep = openmc.capi.Tally(1) - tally_dep.scores = self.chain.reactions - tally_dep.filters = [mat_filter] + self._tally = openmc.capi.Tally() + self._tally.scores = self.chain.reactions + self._tally.filters = [mat_filter] def _unpack_tallies_and_normalize(self): """Unpack tallies from OpenMC and return an operator result @@ -495,7 +495,7 @@ class OpenMCOperator(Operator): # Extract tally bins materials = self.burnable_mats - nuclides = openmc.capi.tallies[1].nuclides + nuclides = self._tally.nuclides # Form fast map nuc_ind = [rates.index_nuc[nuc] for nuc in nuclides] @@ -530,7 +530,7 @@ class OpenMCOperator(Operator): slab = materials.index(mat) # Get material results hyperslab - results = openmc.capi.tallies[1].results[slab, :, 1] + results = self._tally.results[slab, :, 1] # Zero out reaction rates and nuclide numbers rates_expanded[:] = 0.0 From 945675aaaa5d27b7347c81fc17c97a945487f1f9 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 14:14:56 -0600 Subject: [PATCH 38/68] Simplification of OpenMCOperator.__init__ --- openmc/deplete/atom_number.py | 2 +- openmc/deplete/openmc_wrapper.py | 79 ++++++++++++-------------------- 2 files changed, 30 insertions(+), 51 deletions(-) diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index f1b0155d73..3ad4968a82 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -18,7 +18,7 @@ class AtomNumber(object): nuc_to_ind : OrderedDict of str to int A dictionary mapping nuclide name as string to index. volume : OrderedDict of int to float - Volume of geometry. + Volume of each material in [cm^3] n_nuc_burn : int Number of nuclides to be burned. diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 42bd0e600e..f3afedb298 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -30,15 +30,14 @@ from .chain import Chain from .reaction_rates import ReactionRates -def _chunks(items, n): - min_size, extra = divmod(len(items), n) +def _distribute(items): + min_size, extra = divmod(len(items), comm.size) j = 0 - chunk_list = [] - for i in range(n): + for i in range(comm.size): chunk_size = min_size + int(i < extra) - chunk_list.append(items[j:j + chunk_size]) + if comm.rank == i: + return items[j:j + chunk_size] j += chunk_size - return chunk_list class OpenMCSettings(Settings): @@ -134,36 +133,21 @@ class OpenMCOperator(Operator): """ def __init__(self, geometry, settings): super().__init__(settings) - self.geometry = geometry - self.number = None - self.burn_mat_to_ind = OrderedDict() - self.burn_nuc_to_ind = None # Read depletion chain self.chain = Chain.from_xml(settings.chain_file) # Clear out OpenMC, create task lists, distribute - if comm.rank == 0: - openmc.reset_auto_ids() - mat_burn_list, volume, nuc_dict = self._extract_mat_ids() - else: - # Dummy variables - mat_burn_list = None - volume = None - nuc_dict = None + openmc.reset_auto_ids() + self.burnable_mats, volume, nuc_dict = self._get_burnable_mats() + local_mats = _distribute(self.burnable_mats) - mat_burn_list = comm.bcast(mat_burn_list) - nuc_dict = comm.bcast(nuc_dict) - volume = comm.bcast(volume) - mat_burn = mat_burn_list[comm.rank] - self.burnable_mats = list(chain(*mat_burn_list)) - - # Load participating nuclides + # Determine which nuclides have incident neutron data self._load_participating() # Extract number densities from the geometry - self._extract_number(mat_burn, volume, nuc_dict) + self._extract_number(local_mats, volume, nuc_dict) # Create reaction rates array index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} @@ -216,69 +200,64 @@ class OpenMCOperator(Operator): return copy.deepcopy(op_result) - def _extract_mat_ids(self): - """Extracts materials and assigns them to processes. + def _get_burnable_mats(self): + """Determine depletable materials, volumes, and nuclids Returns ------- - mat_burn_lists : list of list of int - List of burnable materials indexed by rank. - mat_not_burn_lists : list of list of int - List of non-burnable materials indexed by rank. + burnable_mats : list of str + List of burnable material IDs volume : OrderedDict of str to float - Volume of each cell + Volume of each material in [cm^3] nuc_dict : OrderedDict of str to int Nuclides in order of how they'll appear in the simulation. """ - mat_burn = set() - nuc_set = set() + burnable_mats = set() + model_nuclides = set() volume = OrderedDict() # Iterate once through the geometry to get dictionaries for mat in self.geometry.get_all_materials().values(): for nuclide in mat.get_nuclides(): - nuc_set.add(nuclide) + model_nuclides.add(nuclide) if mat.depletable: - mat_burn.add(str(mat.id)) + burnable_mats.add(str(mat.id)) if mat.volume is None: raise RuntimeError("Volume not specified for depletable " "material with ID={}.".format(mat.id)) volume[str(mat.id)] = mat.volume # Sort the sets - mat_burn = sorted(mat_burn, key=int) - nuc_set = sorted(nuc_set) + burnable_mats = sorted(burnable_mats, key=int) + model_nuclides = sorted(model_nuclides) # Construct a global nuclide dictionary, burned first nuc_dict = copy.deepcopy(self.chain.nuclide_dict) i = len(nuc_dict) - for nuc in nuc_set: + for nuc in model_nuclides: if nuc not in nuc_dict: nuc_dict[nuc] = i i += 1 - # Decompose geometry - mat_burn_lists = _chunks(mat_burn, comm.size) + return burnable_mats, volume, nuc_dict - return mat_burn_lists, volume, nuc_dict - - def _extract_number(self, mat_burn, volume, nuc_dict): - """Construct self.number read from geometry + def _extract_number(self, local_mats, volume, nuc_dict): + """Construct AtomNumber using geometry Parameters ---------- - mat_burn : list of int - Materials to be burned managed by this thread. + local_mats : list of str + Material IDs to be managed by this process volume : OrderedDict of str to float - Volumes for the above materials. + Volumes for the above materials in [cm^3] nuc_dict : OrderedDict of str to int Nuclides to be used in the simulation. """ # Same with materials self.burn_mat_to_ind = OrderedDict() - for i, mat in enumerate(mat_burn): + for i, mat in enumerate(local_mats): self.burn_mat_to_ind[mat] = i self.number = AtomNumber(self.burn_mat_to_ind, nuc_dict, volume, From 65061bdadcb04904edb3775fb837bc1037cfc8bf Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 14:24:48 -0600 Subject: [PATCH 39/68] Change burn_nuc_to_ind to _burnable_nucs --- openmc/deplete/openmc_wrapper.py | 28 +++++++++++----------------- openmc/deplete/reaction_rates.py | 10 +++++----- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index f3afedb298..3c3b974c90 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -123,10 +123,6 @@ class OpenMCOperator(Operator): Reaction rates from the last operator step. burn_mat_to_ind : OrderedDict of str to int Dictionary mapping material ID (as a string) to an index in reaction_rates. - burn_nuc_to_ind : OrderedDict of str to int - Dictionary mapping nuclide name (as a string) to an index in - reaction_rates. Consists of all nuclides with neutron data and appearing - in the depletion chain. burnable_mats : list of str All burnable material IDs @@ -144,7 +140,9 @@ class OpenMCOperator(Operator): local_mats = _distribute(self.burnable_mats) # Determine which nuclides have incident neutron data - self._load_participating() + self.nuclides_with_data = self._get_nuclides_with_data() + self._burnable_nucs = [nuc for nuc in self.nuclides_with_data + if nuc in self.chain] # Extract number densities from the geometry self._extract_number(local_mats, volume, nuc_dict) @@ -152,7 +150,7 @@ class OpenMCOperator(Operator): # Create reaction rates array index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} self.reaction_rates = ReactionRates( - self.burn_mat_to_ind, self.burn_nuc_to_ind, index_rx) + self.burn_mat_to_ind, self._burnable_nucs, index_rx) def __call__(self, vec, print_out=True): """Runs a simulation. @@ -264,7 +262,7 @@ class OpenMCOperator(Operator): len(self.chain)) if self.settings.dilute_initial != 0.0: - for nuc in self.burn_nuc_to_ind: + for nuc in self._burnable_nucs: self.number.set_atom_density(np.s_[:], nuc, self.settings.dilute_initial) @@ -545,7 +543,7 @@ class OpenMCOperator(Operator): return OperatorResult(k_combined, rates) - def _load_participating(self): + def _get_nuclides_with_data(self): """Loads a cross_sections.xml file to find participating nuclides. This allows for nuclides that are important in the decay chain but not @@ -560,7 +558,7 @@ class OpenMCOperator(Operator): except KeyError: filename = None - self.nuclides_with_data = set() + nuclides = set() try: tree = ET.parse(filename) @@ -572,9 +570,6 @@ class OpenMCOperator(Operator): raise IOError(msg) root = tree.getroot() - self.burn_nuc_to_ind = OrderedDict() - nuc_ind = 0 - for nuclide_node in root.findall('library'): mats = nuclide_node.get('materials') if not mats: @@ -582,11 +577,10 @@ class OpenMCOperator(Operator): for name in mats.split(): # Make a burn list of the union of nuclides in cross_sections.xml # and nuclides in depletion chain. - if name not in self.nuclides_with_data: - self.nuclides_with_data.add(name) - if name in self.chain: - self.burn_nuc_to_ind[name] = nuc_ind - nuc_ind += 1 + if name not in nuclides: + nuclides.add(name) + + return nuclides def get_results_info(self): """Returns volume list, cell lists, and nuc lists. diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index a479085174..d1e1b0f1e2 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -15,8 +15,8 @@ class ReactionRates(np.ndarray): ---------- index_mat : OrderedDict of str to int A dictionary mapping material ID as string to index. - index_nuc : OrderedDict of str to int - A dictionary mapping nuclide name as string to index. + nuclides : list of str + Depletable nuclides index_rx : OrderedDict of str to int A dictionary mapping reaction name as string to index. @@ -36,15 +36,15 @@ class ReactionRates(np.ndarray): Number of reactions. """ - def __new__(cls, index_mat, index_nuc, index_rx): + def __new__(cls, index_mat, nuclides, index_rx): # Create appropriately-sized zeroed-out ndarray - shape = (len(index_mat), len(index_nuc), len(index_rx)) + shape = (len(index_mat), len(nuclides), len(index_rx)) obj = super().__new__(cls, shape) obj[:] = 0.0 # Add mapping attributes obj.index_mat = index_mat - obj.index_nuc = index_nuc + obj.index_nuc = {nuc: i for i, nuc in enumerate(nuclides)} obj.index_rx = index_rx return obj From 704a131f2d2039138e566216639011df5ddc102c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 16:38:45 -0600 Subject: [PATCH 40/68] Some refactoring of AtomNumber. Get rid of burn_mat_to_ind --- openmc/deplete/atom_number.py | 122 +++++++++---------- openmc/deplete/openmc_wrapper.py | 65 +++++----- openmc/deplete/results.py | 34 ------ tests/unit_tests/test_deplete_atom_number.py | 59 +++------ 4 files changed, 103 insertions(+), 177 deletions(-) diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 3ad4968a82..1a142676cb 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -7,60 +7,55 @@ import numpy as np class AtomNumber(object): - """AtomNumber module. - - An ndarray to store atom densities with string, integer, or slice indexing. + """Stores local material compositions (atoms of each nuclide). Parameters ---------- - mat_to_ind : OrderedDict of str to int - A dictionary mapping material ID as string to index. - nuc_to_ind : OrderedDict of str to int - A dictionary mapping nuclide name as string to index. - volume : OrderedDict of int to float + local_mats : list of str + Material IDs + nuclides : list of str + Nuclides to be tracked + volume : dict Volume of each material in [cm^3] n_nuc_burn : int Number of nuclides to be burned. Attributes ---------- - mat_to_ind : OrderedDict of str to int - A dictionary mapping cell ID as string to index. - nuc_to_ind : OrderedDict of str to int - A dictionary mapping nuclide name as string to index. - volume : numpy.array - Volume of geometry indexed by mat_to_ind. If a volume is not found, - it defaults to 1 so that reading density still works correctly. + index_mat : dict + A dictionary mapping material ID as string to index. + index_nuc : dict + A dictionary mapping nuclide name to index. + volume : numpy.ndarray + Volume of each material in [cm^3]. If a volume is not found, it defaults + to 1 so that reading density still works correctly. + number : numpy.ndarray + Array storing total atoms for each material/nuclide + materials : list of str + Material IDs as strings + nuclides : list of str + All nuclide names + burnable_nuclides : list of str + Burnable nuclides names. Used for sorting the simulation. n_nuc_burn : int - Number of nuclides to be burned. - n_mat : int - Number of materials. + Number of burnable nuclides. n_nuc : int - Number of nucs. - number : numpy.array - Array storing total atoms indexed by the above dictionaries. - burn_nuc_list : list of str - A list of all nuclide material names. Used for sorting the simulation. + Number of nuclidess. """ - def __init__(self, mat_to_ind, nuc_to_ind, volume, n_nuc_burn): + def __init__(self, local_mats, nuclides, volume, n_nuc_burn): + self.index_mat = {mat: i for i, mat in enumerate(local_mats)} + self.index_nuc = {nuc: i for i, nuc in enumerate(nuclides)} - self.mat_to_ind = mat_to_ind - self.nuc_to_ind = nuc_to_ind - - self.volume = np.ones(len(mat_to_ind)) - - for mat in volume: - if mat in self.mat_to_ind: - ind = self.mat_to_ind[mat] - self.volume[ind] = volume[mat] + self.volume = np.ones(len(local_mats)) + for mat, val in volume.items(): + if mat in self.index_mat: + ind = self.index_mat[mat] + self.volume[ind] = val self.n_nuc_burn = n_nuc_burn - self.number = np.zeros((self.n_mat, self.n_nuc)) - - # For performance, create storage for burn_nuc_list, burn_mat_list - self._burn_nuc_list = None + self.number = np.zeros((len(local_mats), self.n_nuc)) def __getitem__(self, pos): """Retrieves total atom number from AtomNumber. @@ -80,9 +75,9 @@ class AtomNumber(object): mat, nuc = pos if isinstance(mat, str): - mat = self.mat_to_ind[mat] + mat = self.index_mat[mat] if isinstance(nuc, str): - nuc = self.nuc_to_ind[nuc] + nuc = self.index_nuc[nuc] return self.number[mat, nuc] @@ -101,37 +96,30 @@ class AtomNumber(object): """ mat, nuc = pos if isinstance(mat, str): - mat = self.mat_to_ind[mat] + mat = self.index_mat[mat] if isinstance(nuc, str): - nuc = self.nuc_to_ind[nuc] + nuc = self.index_nuc[nuc] self.number[mat, nuc] = val @property - def n_mat(self): - """Number of materials.""" - return len(self.mat_to_ind) + def materials(self): + return self.index_mat.keys() + + @property + def nuclides(self): + return self.index_nuc.keys() @property def n_nuc(self): """Number of nuclides.""" - return len(self.nuc_to_ind) + return len(self.index_nuc) @property - def burn_nuc_list(self): - """burn_nuc_list : list of str - A list of all nuclide material names. Used for sorting the simulation. - """ - - if self._burn_nuc_list is None: - self._burn_nuc_list = [None] * self.n_nuc_burn - - for nuc in self.nuc_to_ind: - ind = self.nuc_to_ind[nuc] - if ind < self.n_nuc_burn: - self._burn_nuc_list[ind] = nuc - - return self._burn_nuc_list + def burnable_nuclides(self): + """All burnable nuclide names. Used for sorting the simulation.""" + return [nuc for nuc, ind in self.index_nuc.items() + if ind < self.n_nuc_burn] def get_atom_density(self, mat, nuc): """Accesses atom density instead of total number. @@ -150,9 +138,9 @@ class AtomNumber(object): """ if isinstance(mat, str): - mat = self.mat_to_ind[mat] + mat = self.index_mat[mat] if isinstance(nuc, str): - nuc = self.nuc_to_ind[nuc] + nuc = self.index_nuc[nuc] return self[mat, nuc] / self.volume[mat] @@ -170,9 +158,9 @@ class AtomNumber(object): """ if isinstance(mat, str): - mat = self.mat_to_ind[mat] + mat = self.index_mat[mat] if isinstance(nuc, str): - nuc = self.nuc_to_ind[nuc] + nuc = self.index_nuc[nuc] self[mat, nuc] = val * self.volume[mat] @@ -191,7 +179,7 @@ class AtomNumber(object): """ if isinstance(mat, str): - mat = self.mat_to_ind[mat] + mat = self.index_mat[mat] return self[mat, :self.n_nuc_burn] @@ -207,7 +195,7 @@ class AtomNumber(object): """ if isinstance(mat, str): - mat = self.mat_to_ind[mat] + mat = self.index_mat[mat] self[mat, :self.n_nuc_burn] = val @@ -223,5 +211,5 @@ class AtomNumber(object): Total atoms. """ - for i in range(self.n_mat): - self.set_mat_slice(i, total_density[i]) + for i, density_slice in enumerate(total_density): + self.set_mat_slice(i, density_slice) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 3c3b974c90..7d776825b0 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -121,10 +121,10 @@ class OpenMCOperator(Operator): The depletion chain information necessary to form matrices and tallies. reaction_rates : openmc.deplete.ReactionRates Reaction rates from the last operator step. - burn_mat_to_ind : OrderedDict of str to int - Dictionary mapping material ID (as a string) to an index in reaction_rates. burnable_mats : list of str All burnable material IDs + local_mats : list of str + All burnable material IDs being managed by a single process """ def __init__(self, geometry, settings): @@ -136,8 +136,8 @@ class OpenMCOperator(Operator): # Clear out OpenMC, create task lists, distribute openmc.reset_auto_ids() - self.burnable_mats, volume, nuc_dict = self._get_burnable_mats() - local_mats = _distribute(self.burnable_mats) + self.burnable_mats, volume, nuclides = self._get_burnable_mats() + self.local_mats = _distribute(self.burnable_mats) # Determine which nuclides have incident neutron data self.nuclides_with_data = self._get_nuclides_with_data() @@ -145,12 +145,12 @@ class OpenMCOperator(Operator): if nuc in self.chain] # Extract number densities from the geometry - self._extract_number(local_mats, volume, nuc_dict) + self._extract_number(self.local_mats, volume, nuclides) # Create reaction rates array index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} self.reaction_rates = ReactionRates( - self.burn_mat_to_ind, self._burnable_nucs, index_rx) + self.local_mats, self._burnable_nucs, index_rx) def __call__(self, vec, print_out=True): """Runs a simulation. @@ -207,7 +207,7 @@ class OpenMCOperator(Operator): List of burnable material IDs volume : OrderedDict of str to float Volume of each material in [cm^3] - nuc_dict : OrderedDict of str to int + nuclides : list of str Nuclides in order of how they'll appear in the simulation. """ @@ -231,16 +231,14 @@ class OpenMCOperator(Operator): model_nuclides = sorted(model_nuclides) # Construct a global nuclide dictionary, burned first - nuc_dict = copy.deepcopy(self.chain.nuclide_dict) - i = len(nuc_dict) + nuclides = list(self.chain.nuclide_dict) for nuc in model_nuclides: - if nuc not in nuc_dict: - nuc_dict[nuc] = i - i += 1 + if nuc not in nuclides: + nuclides.append(nuc) - return burnable_mats, volume, nuc_dict + return burnable_mats, volume, nuclides - def _extract_number(self, local_mats, volume, nuc_dict): + def _extract_number(self, local_mats, volume, nuclides): """Construct AtomNumber using geometry Parameters @@ -249,17 +247,11 @@ class OpenMCOperator(Operator): Material IDs to be managed by this process volume : OrderedDict of str to float Volumes for the above materials in [cm^3] - nuc_dict : OrderedDict of str to int + nuclides : list of str Nuclides to be used in the simulation. """ - # Same with materials - self.burn_mat_to_ind = OrderedDict() - for i, mat in enumerate(local_mats): - self.burn_mat_to_ind[mat] = i - - self.number = AtomNumber(self.burn_mat_to_ind, nuc_dict, volume, - len(self.chain)) + self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) if self.settings.dilute_initial != 0.0: for nuc in self._burnable_nucs: @@ -268,7 +260,7 @@ class OpenMCOperator(Operator): # Now extract the number densities and store for mat in self.geometry.get_all_materials().values(): - if str(mat.id) in self.burn_mat_to_ind: + if str(mat.id) in local_mats: self._set_number_from_mat(mat) def _set_number_from_mat(self, mat): @@ -281,7 +273,6 @@ class OpenMCOperator(Operator): """ mat_id = str(mat.id) - mat_ind = self.number.mat_to_ind[mat_id] for nuclide, density in mat.get_nuclide_atom_densities().values(): number = density * 1.0e24 @@ -293,7 +284,7 @@ class OpenMCOperator(Operator): Parameters ---------- y : numpy.ndarray - An array representing reaction rates for this cell. + An array representing reaction rates for this material. mat : int Material id. @@ -340,10 +331,10 @@ class OpenMCOperator(Operator): for rank in range(comm.size): number_i = comm.bcast(self.number, root=rank) - for mat in number_i.mat_to_ind: + for mat in number_i.materials: nuclides = [] densities = [] - for nuc in number_i.nuc_to_ind: + for nuc in number_i.nuclides: if nuc in self.nuclides_with_data: val = 1.0e-24 * number_i.get_atom_density(mat, nuc) @@ -381,7 +372,7 @@ class OpenMCOperator(Operator): .values()) # Sort nuclides according to order in AtomNumber object - nuclides = list(self.number.nuc_to_ind.keys()) + nuclides = list(self.number.nuclides) for mat in materials: mat._nuclides.sort(key=lambda x: nuclides.index(x[0])) @@ -404,9 +395,9 @@ class OpenMCOperator(Operator): """ nuc_set = set() - # Create the set of all nuclides in the decay chain in cells marked for - # burning in which the number density is greater than zero. - for nuc in self.number.nuc_to_ind: + # Create the set of all nuclides in the decay chain in materials marked + # for burning in which the number density is greater than zero. + for nuc in self.number.nuclides: if nuc in self.nuclides_with_data: if np.sum(self.number[:, nuc]) > 0.0: nuc_set.add(nuc) @@ -421,7 +412,7 @@ class OpenMCOperator(Operator): if comm.rank == 0: # Sort nuclides in the same order as self.number - nuc_list = [nuc for nuc in self.number.nuc_to_ind + nuc_list = [nuc for nuc in self.number.nuclides if nuc in nuc_set] else: nuc_list = None @@ -502,7 +493,7 @@ class OpenMCOperator(Operator): break # Extract results - for i, mat in enumerate(self.burn_mat_to_ind): + for i, mat in enumerate(self.local_mats): # Get tally index slab = materials.index(mat) @@ -583,7 +574,7 @@ class OpenMCOperator(Operator): return nuclides def get_results_info(self): - """Returns volume list, cell lists, and nuc lists. + """Returns volume list, material lists, and nuc lists. Returns ------- @@ -592,13 +583,13 @@ class OpenMCOperator(Operator): nuc_list : list of str A list of all nuclide names. Used for sorting the simulation. burn_list : list of int - A list of all cell IDs to be burned. Used for sorting the simulation. + A list of all material IDs to be burned. Used for sorting the simulation. full_burn_list : list List of all burnable material IDs """ - nuc_list = self.number.burn_nuc_list - burn_list = list(self.burn_mat_to_ind) + nuc_list = self.number.burnable_nuclides + burn_list = self.local_mats volume = {} for i, mat in enumerate(burn_list): diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index f7daca5b2e..e18051d9d8 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -346,40 +346,6 @@ class Results(object): return results -def get_dict(number): - """Given an operator nested dictionary, output indexing dictionaries. - - These indexing dictionaries map mat IDs and nuclide names to indices - inside of Results.data. - - Parameters - ---------- - number : AtomNumber - The object to extract dictionaries from - - Returns - ------- - mat_to_ind : OrderedDict of str to int - Maps mat strings to index in array. - nuc_to_ind : OrderedDict of str to int - Maps nuclide strings to index in array. - """ - mat_to_ind = OrderedDict() - nuc_to_ind = OrderedDict() - - for nuc in number.nuc_to_ind: - nuc_ind = number.nuc_to_ind[nuc] - if nuc_ind < number.n_nuc_burn: - nuc_to_ind[nuc] = nuc_ind - - for mat in number.mat_to_ind: - mat_ind = number.mat_to_ind[mat] - if mat_ind < number.n_mat_burn: - mat_to_ind[mat] = mat_ind - - return mat_to_ind, nuc_to_ind - - def write_results(result, filename, index): """Outputs result to an .hdf5 file. diff --git a/tests/unit_tests/test_deplete_atom_number.py b/tests/unit_tests/test_deplete_atom_number.py index 887e9af1bc..4cc9207ca9 100644 --- a/tests/unit_tests/test_deplete_atom_number.py +++ b/tests/unit_tests/test_deplete_atom_number.py @@ -7,11 +7,11 @@ from openmc.deplete import atom_number def test_indexing(): """Tests the __getitem__ and __setitem__ routines simultaneously.""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + local_mats = ["10000", "10001"] + nuclides = ["U238", "U235", "U234"] volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) + number = atom_number.AtomNumber(local_mats, nuclides, volume, 2) number["10000", "U238"] = 1.0 number["10001", "U238"] = 2.0 @@ -36,47 +36,28 @@ def test_indexing(): assert number["10000", "U238"] == 5.0 -def test_n_mat(): - """Test number of materials property. """ - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} +def test_properties(): + """Test properties. """ + local_mats = ["10000", "10001"] + nuclides = ["U238", "U235", "Gd157"] volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) - - assert number.n_mat == 2 - - -def test_n_nuc(): - """Test number of nuclides property.""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) + number = atom_number.AtomNumber(local_mats, nuclides, volume, 2) + assert list(number.materials) == ["10000", "10001"] assert number.n_nuc == 3 - - -def test_burn_nuc_list(): - """Test the list of burned nuclides property""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - volume = {"10000" : 0.38, "10001" : 0.21} - - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) - - assert number.burn_nuc_list == ["U238", "U235"] + assert list(number.nuclides) == ["U238", "U235", "Gd157"] + assert number.burnable_nuclides == ["U238", "U235"] def test_density_indexing(): """Tests the get and set_atom_density routines simultaneously.""" - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + local_mats = ["10000", "10001", "10002"] + nuclides = ["U238", "U235", "U234"] volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) + number = atom_number.AtomNumber(local_mats, nuclides, volume, 2) number.set_atom_density("10000", "U238", 1.0) number.set_atom_density("10001", "U238", 2.0) @@ -129,11 +110,11 @@ def test_density_indexing(): def test_get_mat_slice(): """Tests getting slices.""" - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + local_mats = ["10000", "10001", "10002"] + nuclides = ["U238", "U235", "U234"] volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) + number = atom_number.AtomNumber(local_mats, nuclides, volume, 2) number.number = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]) @@ -149,11 +130,11 @@ def test_get_mat_slice(): def test_set_mat_slice(): """Tests getting slices.""" - mat_to_ind = {"10000" : 0, "10001" : 1, "10002" : 2} - nuc_to_ind = {"U238" : 0, "U235" : 1, "U234" : 2} + local_mats = ["10000", "10001", "10002"] + nuclides = ["U238", "U235", "U234"] volume = {"10000" : 0.38, "10001" : 0.21} - number = atom_number.AtomNumber(mat_to_ind, nuc_to_ind, volume, 2) + number = atom_number.AtomNumber(local_mats, nuclides, volume, 2) number.set_mat_slice(0, [1.0, 2.0]) From c19f396aa77b78efe0658ba4d4cc7356b1d8d780 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 16:49:51 -0600 Subject: [PATCH 41/68] Remove Operator.form_matrix methods (Chain owns these) --- openmc/deplete/abc.py | 22 ---------------------- openmc/deplete/openmc_wrapper.py | 18 ------------------ 2 files changed, 40 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index e8f65f8871..6916066816 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -133,27 +133,5 @@ class Operator(metaclass=ABCMeta): pass - @abstractmethod - def form_matrix(self, y, mat): - """Forms the f(y) matrix in y' = f(y)y. - - Nominally a depletion matrix, this is abstracted on the off chance - that the function f has nothing to do with depletion at all. - - Parameters - ---------- - y : numpy.ndarray - An array representing y. - mat : int - Material id. - - Returns - ------- - scipy.sparse.csr_matrix - Sparse matrix representing f(y). - """ - - pass - def finalize(self): pass diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 7d776825b0..56a248c30c 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -278,24 +278,6 @@ class OpenMCOperator(Operator): number = density * 1.0e24 self.number.set_atom_density(mat_id, nuclide, number) - def form_matrix(self, y, mat): - """Forms the depletion matrix. - - Parameters - ---------- - y : numpy.ndarray - An array representing reaction rates for this material. - mat : int - Material id. - - Returns - ------- - scipy.sparse.csr_matrix - Sparse matrix representing the depletion matrix. - """ - - return copy.deepcopy(self.chain.form_matrix(y[mat, :, :])) - def initial_condition(self): """Performs final setup and returns initial condition. From ccfee55115a10144a04e63eac2e744b7411d1c82 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 17:26:44 -0600 Subject: [PATCH 42/68] Make timesteps and power specified in integrator functions --- openmc/deplete/abc.py | 8 ------- openmc/deplete/integrator/cecm.py | 24 ++++++++++++++----- openmc/deplete/integrator/predictor.py | 26 +++++++++++++++------ openmc/deplete/openmc_wrapper.py | 23 +++++++++++------- tests/dummy_geometry.py | 4 +++- tests/regression_tests/test_deplete_full.py | 17 +++++++------- tests/unit_tests/test_deplete_cecm.py | 5 ++-- tests/unit_tests/test_deplete_predictor.py | 5 ++-- 8 files changed, 68 insertions(+), 44 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 6916066816..7fda93aa28 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -18,8 +18,6 @@ class Settings(object): Attributes ---------- - dt_vec : numpy.array - Array of time steps to take. output_dir : pathlib.Path Path to output directory to save results. chain_file : str @@ -29,10 +27,6 @@ class Settings(object): Initial atom density to add for nuclides that are zero in initial condition to ensure they exist in the decay chain. Only done for nuclides with reaction rates. Defaults to 1.0e3. - power : float - Power of the reactor in [W]. For a 2D problem, the power can be given in - W/cm as long as the "volume" assigned to a depletion material is - actually an area in cm^2. """ def __init__(self): @@ -40,9 +34,7 @@ class Settings(object): self.chain_file = os.environ["OPENMC_DEPLETE_CHAIN"] except KeyError: self.chain_file = None - self.dt_vec = None self.output_dir = '.' - self.power = None self.dilute_initial = 1.0e3 @property diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 760e3c89d8..7d6c11190f 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -1,12 +1,13 @@ """The CE/CM integrator.""" import copy +from collections.abc import Iterable from .cram import deplete from .save_results import save_results -def cecm(operator, print_out=True): +def cecm(operator, timesteps, power, print_out=True): r"""The CE/CM integrator. Implements the second order CE/CM Predictor-Corrector algorithm [ref]_. @@ -32,25 +33,36 @@ def cecm(operator, print_out=True): ---------- operator : openmc.deplete.Operator The operator object to simulate on. + timesteps : iterable of float + Array of timesteps in units of [s] + power : float or iterable of float + Power of the reactor in [W]. A single value indicates that the power is + constant over all timesteps. An iterable indicates potentially different + power levels for each timestep. For a 2D problem, the power can be given + in [W/cm] as long as the "volume" assigned to a depletion material is + actually an area in [cm^2]. print_out : bool, optional Whether or not to print out time. """ + if not isinstance(power, Iterable): + power = [power]*len(timesteps) + # Generate initial conditions with operator as vec: chain = operator.chain t = 0.0 - for i, dt in enumerate(operator.settings.dt_vec): + for i, (dt, p) in enumerate(zip(timesteps, power)): # Get beginning-of-timestep reaction rates x = [copy.deepcopy(vec)] - results = [operator(x[0])] + results = [operator(x[0], p)] # Deplete for first half of timestep x_middle = deplete(chain, x[0], results[0], dt/2, print_out) # Get middle-of-timestep reaction rates x.append(x_middle) - results.append(operator(x_middle)) + results.append(operator(x_middle, p)) # Deplete for full timestep using beginning-of-step materials x_end = deplete(chain, x[0], results[1], dt, print_out) @@ -64,7 +76,7 @@ def cecm(operator, print_out=True): # Perform one last simulation x = [copy.deepcopy(vec)] - results = [operator(x[0])] + results = [operator(x[0], power[-1])] # Create results, write to disk - save_results(operator, x, results, [t, t], len(operator.settings.dt_vec)) + save_results(operator, x, results, [t, t], len(timesteps)) diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 0640783318..038c0778da 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -1,13 +1,14 @@ -"""The Predictor algorithm.""" +"""First-order predictor algorithm.""" import copy +from collections.abc import Iterable from .cram import deplete from .save_results import save_results -def predictor(operator, print_out=True): - r"""The basic predictor integrator. +def predictor(operator, timesteps, power, print_out=True): + r"""Deplete using a first-order predictor algorithm. Implements the first-order predictor algorithm. This algorithm is mathematically defined as: @@ -23,18 +24,29 @@ def predictor(operator, print_out=True): ---------- operator : openmc.deplete.Operator The operator object to simulate on. + timesteps : iterable of float + Array of timesteps in units of [s] + power : float or iterable of float + Power of the reactor in [W]. A single value indicates that the power is + constant over all timesteps. An iterable indicates potentially different + power levels for each timestep. For a 2D problem, the power can be given + in [W/cm] as long as the "volume" assigned to a depletion material is + actually an area in [cm^2]. print_out : bool, optional Whether or not to print out time. """ + if not isinstance(power, Iterable): + power = [power]*len(timesteps) + # Generate initial conditions with operator as vec: chain = operator.chain t = 0.0 - for i, dt in enumerate(operator.settings.dt_vec): + for i, (dt, p) in enumerate(zip(timesteps, power)): # Get beginning-of-timestep reaction rates x = [copy.deepcopy(vec)] - results = [operator(x[0])] + results = [operator(x[0], p)] # Create results, write to disk save_results(operator, x, results, [t, t + dt], i) @@ -48,7 +60,7 @@ def predictor(operator, print_out=True): # Perform one last simulation x = [copy.deepcopy(vec)] - results = [operator(x[0])] + results = [operator(x[0], power[-1])] # Create results, write to disk - save_results(operator, x, results, [t, t], len(operator.settings.dt_vec)) + save_results(operator, x, results, [t, t], len(timesteps)) diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 56a248c30c..e5898997bc 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -45,8 +45,6 @@ class OpenMCSettings(Settings): Attributes ---------- - dt_vec : numpy.array - Array of time steps to in units of [s] output_dir : pathlib.Path Path to output directory to save results. chain_file : str @@ -68,8 +66,8 @@ class OpenMCSettings(Settings): """ - _depletion_attrs = {'dt_vec', '_output_dir', 'chain_file', 'dilute_initial', - 'round_number', 'power'} + _depletion_attrs = {'_output_dir', 'chain_file', 'dilute_initial', + 'round_number'} def __init__(self): super().__init__() @@ -152,13 +150,15 @@ class OpenMCOperator(Operator): self.reaction_rates = ReactionRates( self.local_mats, self._burnable_nucs, index_rx) - def __call__(self, vec, print_out=True): + def __call__(self, vec, power, print_out=True): """Runs a simulation. Parameters ---------- vec : list of numpy.array Total atoms to be used in function. + power : float + Power of the reactor in [W] print_out : bool, optional Whether or not to print out time. @@ -187,7 +187,7 @@ class OpenMCOperator(Operator): time_openmc = time.time() # Extract results - op_result = self._unpack_tallies_and_normalize() + op_result = self._unpack_tallies_and_normalize(power) if comm.rank == 0: time_unpack = time.time() @@ -424,7 +424,7 @@ class OpenMCOperator(Operator): self._tally.scores = self.chain.reactions self._tally.filters = [mat_filter] - def _unpack_tallies_and_normalize(self): + def _unpack_tallies_and_normalize(self, power): """Unpack tallies from OpenMC and return an operator result This method uses OpenMC's C API bindings to determine the k-effective @@ -432,6 +432,11 @@ class OpenMCOperator(Operator): normalized by the user-specified power, summing the product of the fission reaction rate times the fission Q value for each material. + Parameters + ---------- + power : float + Power of the reactor in [W] + Returns ------- openmc.deplete.OperatorResult @@ -509,10 +514,10 @@ class OpenMCOperator(Operator): energy = comm.allreduce(energy) # Determine power in eV/s - power = self.settings.power / JOULE_PER_EV + power /= JOULE_PER_EV # Scale reaction rates to obtain units of reactions/sec - rates[:, :, :] *= power / energy + rates *= power / energy return OperatorResult(k_combined, rates) diff --git a/tests/dummy_geometry.py b/tests/dummy_geometry.py index c013bb0054..d662616698 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_geometry.py @@ -21,13 +21,15 @@ class DummyGeometry(Operator): def __init__(self, settings): super().__init__(settings) - def __call__(self, vec, print_out=False): + def __call__(self, vec, power, print_out=False): """Evaluates F(y) Parameters ---------- vec : list of numpy.array Total atoms to be used in function. + power : float + Power in [W] print_out : bool, optional, ignored Whether or not to print out time. diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index 6033e3f75a..1ae2cee1fa 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -32,18 +32,10 @@ def test_full(run_in_tmpdir): # Load geometry from example geometry, lower_left, upper_right = generate_problem(n_rings, n_wedges) - # Create dt vector for 3 steps with 15 day timesteps - dt1 = 15.*24*60*60 # 15 days - dt2 = 1.5*30*24*60*60 # 1.5 months - N = floor(dt2/dt1) - dt = np.full(N, dt1) - # Depletion settings settings = openmc.deplete.OpenMCSettings() settings.chain_file = str(Path(__file__).parents[2] / 'chains' / 'chain_simple.xml') - settings.power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO - settings.dt_vec = dt settings.round_number = True # Add OpenMC-specific settings @@ -57,8 +49,15 @@ def test_full(run_in_tmpdir): op = openmc.deplete.OpenMCOperator(geometry, settings) + # Power and timesteps + dt1 = 15.*24*60*60 # 15 days + dt2 = 1.5*30*24*60*60 # 1.5 months + N = floor(dt2/dt1) + dt = np.full(N, dt1) + power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO + # Perform simulation using the predictor algorithm - openmc.deplete.integrator.predictor(op) + openmc.deplete.integrator.predictor(op, dt, power) # Get path to test and reference results path_test = settings.output_dir / 'depletion_results.h5' diff --git a/tests/unit_tests/test_deplete_cecm.py b/tests/unit_tests/test_deplete_cecm.py index 66c3ee156c..3daa7a0489 100644 --- a/tests/unit_tests/test_deplete_cecm.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -15,13 +15,14 @@ def test_cecm(run_in_tmpdir): """Integral regression test of integrator algorithm using CE/CM.""" settings = openmc.deplete.Settings() - settings.dt_vec = [0.75, 0.75] settings.output_dir = "test_integrator_regression" op = dummy_geometry.DummyGeometry(settings) # Perform simulation using the MCNPX/MCNP6 algorithm - openmc.deplete.cecm(op, print_out=False) + dt = [0.75, 0.75] + power = 1.0 + openmc.deplete.cecm(op, dt, power, print_out=False) # Load the files res = results.read_results(settings.output_dir / "depletion_results.h5") diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index f1133f87d2..be8497f5f7 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -15,13 +15,14 @@ def test_predictor(run_in_tmpdir): """Integral regression test of integrator algorithm using predictor/corrector""" settings = openmc.deplete.Settings() - settings.dt_vec = [0.75, 0.75] settings.output_dir = "test_integrator_regression" op = dummy_geometry.DummyGeometry(settings) # Perform simulation using the predictor algorithm - openmc.deplete.predictor(op, print_out=False) + dt = [0.75, 0.75] + power = 1.0 + openmc.deplete.predictor(op, dt, power, print_out=False) # Load the files res = results.read_results(settings.output_dir / "depletion_results.h5") From a460782691b86bf932c2acbcdba76ceb6c914659 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 17:52:17 -0600 Subject: [PATCH 43/68] Get rid of extra Settings class --- docs/source/pythonapi/deplete/index.rst | 2 - openmc/deplete/abc.py | 75 +++++++-------- openmc/deplete/chain.py | 14 +-- openmc/deplete/openmc_wrapper.py | 100 ++++++-------------- tests/dummy_geometry.py | 5 +- tests/regression_tests/test_deplete_full.py | 16 ++-- tests/unit_tests/test_deplete_cecm.py | 8 +- tests/unit_tests/test_deplete_predictor.py | 8 +- 8 files changed, 77 insertions(+), 151 deletions(-) diff --git a/docs/source/pythonapi/deplete/index.rst b/docs/source/pythonapi/deplete/index.rst index 30d2d42611..f3212bd61d 100644 --- a/docs/source/pythonapi/deplete/index.rst +++ b/docs/source/pythonapi/deplete/index.rst @@ -29,7 +29,6 @@ Metaclasses :toctree: generated :nosignatures: - openmc.deplete.Settings openmc.deplete.Operator OpenMC Classes @@ -39,7 +38,6 @@ OpenMC Classes :toctree: generated :nosignatures: - openmc.deplete.OpenMCSettings openmc.deplete.Materials openmc.deplete.OpenMCOperator diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 7fda93aa28..9d53f40bfd 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -7,44 +7,9 @@ to run a full depletion simulation. from collections import namedtuple import os from pathlib import Path - from abc import ABCMeta, abstractmethod - -class Settings(object): - """The Settings class. - - Contains all parameters necessary for the integrator. - - Attributes - ---------- - output_dir : pathlib.Path - Path to output directory to save results. - chain_file : str - Path to the depletion chain XML file. Defaults to the - :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. - dilute_initial : float - Initial atom density to add for nuclides that are zero in initial - condition to ensure they exist in the decay chain. Only done for - nuclides with reaction rates. Defaults to 1.0e3. - - """ - def __init__(self): - try: - self.chain_file = os.environ["OPENMC_DEPLETE_CHAIN"] - except KeyError: - self.chain_file = None - self.output_dir = '.' - self.dilute_initial = 1.0e3 - - @property - def output_dir(self): - return self._output_dir - - @output_dir.setter - def output_dir(self, output_dir): - self._output_dir = Path(output_dir) - +from .chain import Chain OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) @@ -52,14 +17,30 @@ OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) class Operator(metaclass=ABCMeta): """Abstract class defining a transport operator + Parameters + ---------- + chain_file : str, optional + + Attributes ---------- - settings : Settings - Settings object. + dilute_initial : float + Initial atom density to add for nuclides that are zero in initial + condition to ensure they exist in the decay chain. Only done for + nuclides with reaction rates. Defaults to 1.0e3. """ - def __init__(self, settings): - self.settings = settings + def __init__(self, chain_file=None): + self.dilute_initial = 1.0e3 + self.output_dir = '.' + + # Read depletion chain + if chain_file is None: + chain_file = os.environ.get("OPENMC_DEPLETE_CHAIN", None) + if chain_file is None: + raise IOError("No chain specified, either manually or in " + "environment variable OPENMC_DEPLETE_CHAIN.") + self.chain = Chain.from_xml(chain_file) @abstractmethod def __call__(self, vec, print_out=True): @@ -83,11 +64,11 @@ class Operator(metaclass=ABCMeta): def __enter__(self): # Save current directory and move to specific output directory self._orig_dir = os.getcwd() - if not self.settings.output_dir.exists(): - self.settings.output_dir.mkdir() # exist_ok parameter is 3.5+ + if not self.output_dir.exists(): + self.output_dir.mkdir() # exist_ok parameter is 3.5+ # In Python 3.6+, chdir accepts a Path directly - os.chdir(str(self.settings.output_dir)) + os.chdir(str(self.output_dir)) return self.initial_condition() @@ -95,6 +76,14 @@ class Operator(metaclass=ABCMeta): self.finalize() os.chdir(self._orig_dir) + @property + def output_dir(self): + return self._output_dir + + @output_dir.setter + def output_dir(self, output_dir): + self._output_dir = Path(output_dir) + @abstractmethod def initial_condition(self): """Performs final setup and returns initial condition. diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index ec8d79f819..9ffc3d93ec 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -328,15 +328,7 @@ class Chain(object): chain = cls() # Load XML tree - try: - root = ET.parse(filename) - except Exception: - if filename is None: - msg = ("No chain specified, either manually or in environment " - "variable OPENMC_DEPLETE_CHAIN.") - else: - msg = 'Decay chain "{}" is invalid.'.format(filename) - raise IOError(msg) + root = ET.parse(str(filename)) for i, nuclide_elem in enumerate(root.findall('nuclide_table')): nuc = Nuclide.from_xml(nuclide_elem) @@ -367,10 +359,10 @@ class Chain(object): tree = ET.ElementTree(root_elem) if _have_lxml: - tree.write(filename, encoding='utf-8', pretty_print=True) + tree.write(str(filename), encoding='utf-8', pretty_print=True) else: clean_xml_indentation(root_elem) - tree.write(filename, encoding='utf-8') + tree.write(str(filename), encoding='utf-8') def form_matrix(self, rates): """Forms depletion matrix. diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index e5898997bc..12ed27f229 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -24,9 +24,8 @@ import openmc import openmc.capi from openmc.data import JOULE_PER_EV from . import comm -from .abc import Settings, Operator, OperatorResult +from .abc import Operator, OperatorResult from .atom_number import AtomNumber -from .chain import Chain from .reaction_rates import ReactionRates @@ -40,77 +39,34 @@ def _distribute(items): j += chunk_size -class OpenMCSettings(Settings): - """Extends Settings to provide information OpenMC needs to run. - - Attributes - ---------- - output_dir : pathlib.Path - Path to output directory to save results. - chain_file : str - Path to the depletion chain XML file. Defaults to the - :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. - dilute_initial : float - Initial atom density to add for nuclides that are zero in initial - condition to ensure they exist in the decay chain. Only done for - nuclides with reaction rates. Defaults to 1.0e3. - power : float - Power of the reactor in [W]. For a 2D problem, the power can be given in - W/cm as long as the "volume" assigned to a depletion material is - actually an area in cm^2. - round_number : bool - Whether or not to round output to OpenMC to 8 digits. - Useful in testing, as OpenMC is incredibly sensitive to exact values. - settings : openmc.Settings - Settings for OpenMC simulations - - """ - - _depletion_attrs = {'_output_dir', 'chain_file', 'dilute_initial', - 'round_number'} - - def __init__(self): - super().__init__() - self.round_number = False - - # Avoid setattr to create OpenMC settings - self.__dict__['settings'] = openmc.Settings() - - def __setattr__(self, name, value): - if hasattr(self.__class__, name): - # Use properties when appropriate - prop = getattr(self.__class__, name) - prop.fset(self, value) - elif name in self._depletion_attrs: - # For known attributes, store in dictionary - self.__dict__[name] = value - else: - # otherwise, delegate to openmc.Settings - setattr(self.__dict__['settings'], name, value) - - def __getattr__(self, name): - if name in self._depletion_attrs: - return self.__dict__[name] - else: - return getattr(self.__dict__['settings'], name) - - class OpenMCOperator(Operator): """OpenMC transport operator Parameters ---------- geometry : openmc.Geometry - The OpenMC geometry object. - settings : openmc.deplete.OpenMCSettings - Settings object. + OpenMC geometry object + settings : openmc.Settings + OpenMC Settings object + chain_file : str, optional + Path to the depletion chain XML file. Defaults to the + :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. Attributes ---------- - settings : OpenMCSettings - Settings object. (From Operator) geometry : openmc.Geometry - The OpenMC geometry object. + OpenMC geometry object + settings : openmc.Settings + OpenMC settings object + dilute_initial : float + Initial atom density to add for nuclides that are zero in initial + condition to ensure they exist in the decay chain. Only done for + nuclides with reaction rates. Defaults to 1.0e3. + output_dir : pathlib.Path + Path to output directory to save results. + round_number : bool + Whether or not to round output to OpenMC to 8 digits. + Useful in testing, as OpenMC is incredibly sensitive to exact values. number : openmc.deplete.AtomNumber Total number of atoms in simulation. nuclides_with_data : set of str @@ -125,13 +81,12 @@ class OpenMCOperator(Operator): All burnable material IDs being managed by a single process """ - def __init__(self, geometry, settings): - super().__init__(settings) + def __init__(self, geometry, settings, chain_file=None): + super().__init__(chain_file) + self.round_number = False + self.settings = settings self.geometry = geometry - # Read depletion chain - self.chain = Chain.from_xml(settings.chain_file) - # Clear out OpenMC, create task lists, distribute openmc.reset_auto_ids() self.burnable_mats, volume, nuclides = self._get_burnable_mats() @@ -253,10 +208,9 @@ class OpenMCOperator(Operator): """ self.number = AtomNumber(local_mats, nuclides, volume, len(self.chain)) - if self.settings.dilute_initial != 0.0: + if self.dilute_initial != 0.0: for nuc in self._burnable_nucs: - self.number.set_atom_density(np.s_[:], nuc, - self.settings.dilute_initial) + self.number.set_atom_density(np.s_[:], nuc, self.dilute_initial) # Now extract the number densities and store for mat in self.geometry.get_all_materials().values(): @@ -290,7 +244,7 @@ class OpenMCOperator(Operator): # Create XML files if comm.rank == 0: self.geometry.export_to_xml() - self.settings.settings.export_to_xml() + self.settings.export_to_xml() self._generate_materials_xml() # Initialize OpenMC library @@ -322,7 +276,7 @@ class OpenMCOperator(Operator): # If nuclide is zero, do not add to the problem. if val > 0.0: - if self.settings.round_number: + if self.round_number: val_magnitude = np.floor(np.log10(val)) val_scaled = val / 10**val_magnitude val_round = round(val_scaled, 8) diff --git a/tests/dummy_geometry.py b/tests/dummy_geometry.py index d662616698..ca2efd6630 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_geometry.py @@ -17,9 +17,8 @@ class DummyGeometry(Operator): y_2(1.5) ~ 3.1726475740397628 """ - - def __init__(self, settings): - super().__init__(settings) + def __init__(self): + pass def __call__(self, vec, power, print_out=False): """Evaluates F(y) diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index 1ae2cee1fa..22125d039d 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -32,13 +32,8 @@ def test_full(run_in_tmpdir): # Load geometry from example geometry, lower_left, upper_right = generate_problem(n_rings, n_wedges) - # Depletion settings - settings = openmc.deplete.OpenMCSettings() - settings.chain_file = str(Path(__file__).parents[2] / 'chains' / - 'chain_simple.xml') - settings.round_number = True - - # Add OpenMC-specific settings + # OpenMC-specific settings + settings = openmc.Settings() settings.particles = 100 settings.batches = 100 settings.inactive = 40 @@ -47,7 +42,10 @@ def test_full(run_in_tmpdir): settings.seed = 1 settings.verbosity = 3 - op = openmc.deplete.OpenMCOperator(geometry, settings) + # Create operator + chain_file = Path(__file__).parents[2] / 'chains' / 'chain_simple.xml' + op = openmc.deplete.OpenMCOperator(geometry, settings, chain_file) + op.round_number = True # Power and timesteps dt1 = 15.*24*60*60 # 15 days @@ -60,7 +58,7 @@ def test_full(run_in_tmpdir): openmc.deplete.integrator.predictor(op, dt, power) # Get path to test and reference results - path_test = settings.output_dir / 'depletion_results.h5' + path_test = op.output_dir / 'depletion_results.h5' path_reference = Path(__file__).with_name('test_reference.h5') # If updating results, do so and return diff --git a/tests/unit_tests/test_deplete_cecm.py b/tests/unit_tests/test_deplete_cecm.py index 3daa7a0489..97659b8923 100644 --- a/tests/unit_tests/test_deplete_cecm.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -14,10 +14,8 @@ from tests import dummy_geometry def test_cecm(run_in_tmpdir): """Integral regression test of integrator algorithm using CE/CM.""" - settings = openmc.deplete.Settings() - settings.output_dir = "test_integrator_regression" - - op = dummy_geometry.DummyGeometry(settings) + op = dummy_geometry.DummyGeometry() + op.output_dir = "test_integrator_regression" # Perform simulation using the MCNPX/MCNP6 algorithm dt = [0.75, 0.75] @@ -25,7 +23,7 @@ def test_cecm(run_in_tmpdir): openmc.deplete.cecm(op, dt, power, print_out=False) # Load the files - res = results.read_results(settings.output_dir / "depletion_results.h5") + res = results.read_results(op.output_dir / "depletion_results.h5") _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index be8497f5f7..42a3c13a3a 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -14,10 +14,8 @@ from tests import dummy_geometry def test_predictor(run_in_tmpdir): """Integral regression test of integrator algorithm using predictor/corrector""" - settings = openmc.deplete.Settings() - settings.output_dir = "test_integrator_regression" - - op = dummy_geometry.DummyGeometry(settings) + op = dummy_geometry.DummyGeometry() + op.output_dir = "test_integrator_regression" # Perform simulation using the predictor algorithm dt = [0.75, 0.75] @@ -25,7 +23,7 @@ def test_predictor(run_in_tmpdir): openmc.deplete.predictor(op, dt, power, print_out=False) # Load the files - res = results.read_results(settings.output_dir / "depletion_results.h5") + res = results.read_results(op.output_dir / "depletion_results.h5") _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") From a9df0465f0d7344d7156473657a575badf354c77 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 22:51:53 -0600 Subject: [PATCH 44/68] Start cleaning up documentation --- docs/source/conf.py | 14 +++-- docs/source/pythonapi/deplete.rst | 62 +++++++++++++++++++ docs/source/pythonapi/deplete/index.rst | 54 ---------------- .../pythonapi/deplete/integrator.CRAM16.rst | 6 -- .../pythonapi/deplete/integrator.CRAM48.rst | 6 -- .../pythonapi/deplete/integrator.cecm.rst | 6 -- .../deplete/integrator.predictor.rst | 6 -- .../deplete/integrator.save_results.rst | 6 -- docs/source/pythonapi/index.rst | 6 +- openmc/deplete/abc.py | 22 ++++++- openmc/deplete/chain.py | 3 - openmc/deplete/integrator/cecm.py | 16 ++--- openmc/deplete/integrator/cram.py | 28 +++------ openmc/deplete/integrator/predictor.py | 4 +- openmc/deplete/integrator/save_results.py | 2 +- openmc/deplete/openmc_wrapper.py | 14 +++-- .../{dummy_geometry.py => dummy_operator.py} | 6 +- tests/regression_tests/test_deplete_full.py | 2 +- .../test_deplete_utilities.py | 2 +- tests/unit_tests/test_deplete_cecm.py | 4 +- tests/unit_tests/test_deplete_predictor.py | 4 +- 21 files changed, 131 insertions(+), 142 deletions(-) create mode 100644 docs/source/pythonapi/deplete.rst delete mode 100644 docs/source/pythonapi/deplete/index.rst delete mode 100644 docs/source/pythonapi/deplete/integrator.CRAM16.rst delete mode 100644 docs/source/pythonapi/deplete/integrator.CRAM48.rst delete mode 100644 docs/source/pythonapi/deplete/integrator.cecm.rst delete mode 100644 docs/source/pythonapi/deplete/integrator.predictor.rst delete mode 100644 docs/source/pythonapi/deplete/integrator.save_results.rst rename tests/{dummy_geometry.py => dummy_operator.py} (95%) diff --git a/docs/source/conf.py b/docs/source/conf.py index db44e34f4d..b25d5abbed 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,12 +21,14 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' from unittest.mock import MagicMock -MOCK_MODULES = ['numpy', 'numpy.polynomial', 'numpy.polynomial.polynomial', - 'numpy.ctypeslib', 'scipy', 'scipy.sparse', 'scipy.interpolate', - 'scipy.integrate', 'scipy.optimize', 'scipy.special', - 'scipy.stats', 'scipy.spatial', 'h5py', 'pandas', 'uncertainties', - 'matplotlib', 'matplotlib.pyplot','openmoc', - 'openmc.data.reconstruct'] +MOCK_MODULES = [ + 'numpy', 'numpy.polynomial', 'numpy.polynomial.polynomial', + 'numpy.ctypeslib', 'scipy', 'scipy.sparse', 'scipy.sparse.linalg', + 'scipy.interpolate', 'scipy.integrate', 'scipy.optimize', 'scipy.special', + 'scipy.stats', 'scipy.spatial', 'h5py', 'pandas', 'uncertainties', + 'matplotlib', 'matplotlib.pyplot', 'tqdm', 'openmoc', + 'openmc.data.reconstruct' +] sys.modules.update((mod_name, MagicMock()) for mod_name in MOCK_MODULES) import numpy as np diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst new file mode 100644 index 0000000000..13f2fd1550 --- /dev/null +++ b/docs/source/pythonapi/deplete.rst @@ -0,0 +1,62 @@ +.. _pythonapi_deplete: + +---------------------------------- +:mod:`openmc.deplete` -- Depletion +---------------------------------- + +Integrators +----------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myfunction.rst + + openmc.deplete.integrator.predictor + openmc.deplete.integrator.cecm + +Integrator Helper Functions +--------------------------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myfunction.rst + + openmc.deplete.integrator.CRAM16 + openmc.deplete.integrator.CRAM48 + openmc.deplete.integrator.save_results + +Metaclasses +----------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + openmc.deplete.TransportOperator + +OpenMC Classes +-------------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + openmc.deplete.Operator + openmc.deplete.OperatorResult + +Data Classes +------------ +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + openmc.deplete.AtomNumber + openmc.deplete.Chain + openmc.deplete.Nuclide + openmc.deplete.ReactionRates + openmc.deplete.Results diff --git a/docs/source/pythonapi/deplete/index.rst b/docs/source/pythonapi/deplete/index.rst deleted file mode 100644 index f3212bd61d..0000000000 --- a/docs/source/pythonapi/deplete/index.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. _api: - -================= -API Documentation -================= - -Integrators ------------ - -.. toctree:: - :maxdepth: 2 - - integrator.predictor - integrator.cecm - -Integrator Helper Functions ---------------------------- -.. toctree:: - :maxdepth: 2 - - integrator.CRAM16 - integrator.CRAM48 - integrator.save_results - -Metaclasses ------------ - -.. autosummary:: - :toctree: generated - :nosignatures: - - openmc.deplete.Operator - -OpenMC Classes --------------- - -.. autosummary:: - :toctree: generated - :nosignatures: - - openmc.deplete.Materials - openmc.deplete.OpenMCOperator - -Data Classes ------------- -.. autosummary:: - :toctree: generated - :nosignatures: - - openmc.deplete.AtomNumber - openmc.deplete.Chain - openmc.deplete.Nuclide - openmc.deplete.ReactionRates - openmc.deplete.Results diff --git a/docs/source/pythonapi/deplete/integrator.CRAM16.rst b/docs/source/pythonapi/deplete/integrator.CRAM16.rst deleted file mode 100644 index a0dc648056..0000000000 --- a/docs/source/pythonapi/deplete/integrator.CRAM16.rst +++ /dev/null @@ -1,6 +0,0 @@ -integrator\.CRAM16 -================== - -.. currentmodule:: openmc.deplete.integrator - -.. autofunction:: CRAM16 diff --git a/docs/source/pythonapi/deplete/integrator.CRAM48.rst b/docs/source/pythonapi/deplete/integrator.CRAM48.rst deleted file mode 100644 index f9720f7ad9..0000000000 --- a/docs/source/pythonapi/deplete/integrator.CRAM48.rst +++ /dev/null @@ -1,6 +0,0 @@ -integrator\.CRAM48 -================== - -.. currentmodule:: openmc.deplete.integrator - -.. autofunction:: CRAM48 diff --git a/docs/source/pythonapi/deplete/integrator.cecm.rst b/docs/source/pythonapi/deplete/integrator.cecm.rst deleted file mode 100644 index 4851b20b34..0000000000 --- a/docs/source/pythonapi/deplete/integrator.cecm.rst +++ /dev/null @@ -1,6 +0,0 @@ -integrator\.cecm -================= - -.. currentmodule:: openmc.deplete.integrator - -.. autofunction:: cecm diff --git a/docs/source/pythonapi/deplete/integrator.predictor.rst b/docs/source/pythonapi/deplete/integrator.predictor.rst deleted file mode 100644 index 2243e77f71..0000000000 --- a/docs/source/pythonapi/deplete/integrator.predictor.rst +++ /dev/null @@ -1,6 +0,0 @@ -integrator\.predictor -===================== - -.. currentmodule:: openmc.deplete.integrator - -.. autofunction:: predictor diff --git a/docs/source/pythonapi/deplete/integrator.save_results.rst b/docs/source/pythonapi/deplete/integrator.save_results.rst deleted file mode 100644 index f9c830cd5b..0000000000 --- a/docs/source/pythonapi/deplete/integrator.save_results.rst +++ /dev/null @@ -1,6 +0,0 @@ -integrator\.save_results -======================== - -.. currentmodule:: openmc.deplete.integrator - -.. autofunction:: save_results diff --git a/docs/source/pythonapi/index.rst b/docs/source/pythonapi/index.rst index 862acb2384..3c28a21852 100644 --- a/docs/source/pythonapi/index.rst +++ b/docs/source/pythonapi/index.rst @@ -15,14 +15,15 @@ there are many substantial benefits to using the Python API, including: - The ability to define dimensions using variables. - Availability of standard-library modules for working with files. - An entire ecosystem of third-party packages for scientific computing. -- Ability to create materials based on natural elements or uranium enrichment - Automated multi-group cross section generation (:mod:`openmc.mgxs`) +- A fully-featured nuclear data interface (:mod:`openmc.data`) +- Depletion capability (:mod:`openmc.deplete`) - Convenience functions (e.g., a function returning a hexagonal region) - Ability to plot individual universes as geometry is being created - A :math:`k_\text{eff}` search function (:func:`openmc.search_for_keff`) - Random sphere packing for generating TRISO particle locations (:func:`openmc.model.pack_trisos`) -- A fully-featured nuclear data interface (:mod:`openmc.data`) +- Ability to create materials based on natural elements or uranium enrichment For those new to Python, there are many good tutorials available online. We recommend going through the modules from `Codecademy @@ -45,6 +46,7 @@ Modules base model examples + deplete mgxs stats data diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 9d53f40bfd..53e71c1959 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -12,15 +12,33 @@ from abc import ABCMeta, abstractmethod from .chain import Chain OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) +OperatorResult.__doc__ = """\ +Result of applying transport operator + +Parameters +---------- +k : float + Resulting eigenvalue +rates : openmc.deplete.ReactionRates + Resulting reaction rates + +""" -class Operator(metaclass=ABCMeta): +class TransportOperator(metaclass=ABCMeta): """Abstract class defining a transport operator + Each depletion integrator is written to work with a generic transport + operator that takes a vector of material compositions and returns an + eigenvalue and reaction rates. This abstract class sets the requirements for + such a transport operator. Users should instantiate + :class:`openmc.deplete.Operator` rather than this class. + Parameters ---------- chain_file : str, optional - + Path to the depletion chain XML file. Defaults to the + :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. Attributes ---------- diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 9ffc3d93ec..348103ea94 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -321,9 +321,6 @@ class Chain(object): filename : str The path to the depletion chain XML file. - Todo - ---- - Allow for branching on capture, etc. """ chain = cls() diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 7d6c11190f..db58d5d527 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -8,10 +8,11 @@ from .save_results import save_results def cecm(operator, timesteps, power, print_out=True): - r"""The CE/CM integrator. + r"""Deplete using the CE/CM algorithm. - Implements the second order CE/CM Predictor-Corrector algorithm [ref]_. - This algorithm is mathematically defined as: + Implements the second order `CE/CM Predictor-Corrector algorithm + `_. This algorithm is mathematically + defined as: .. math:: y' &= A(y, t) y(t) @@ -24,17 +25,12 @@ def cecm(operator, timesteps, power, print_out=True): y_{n+1} &= \text{expm}(A_c h) y_n - .. [ref] - Isotalo, Aarno. "Comparison of Neutronics-Depletion Coupling Schemes - for Burnup Calculations-Continued Study." Nuclear Science and - Engineering 180.3 (2015): 286-300. - Parameters ---------- - operator : openmc.deplete.Operator + operator : openmc.deplete.TransportOperator The operator object to simulate on. timesteps : iterable of float - Array of timesteps in units of [s] + Array of timesteps in units of [s]. Note that values are not cumulative. power : float or iterable of float Power of the reactor in [W]. A single value indicates that the power is constant over all timesteps. An iterable indicates potentially different diff --git a/openmc/deplete/integrator/cram.py b/openmc/deplete/integrator/cram.py index 09207fbc6b..85954603a1 100644 --- a/openmc/deplete/integrator/cram.py +++ b/openmc/deplete/integrator/cram.py @@ -48,7 +48,7 @@ def deplete(chain, x, op_result, dt, print_out): # Use multiprocessing pool to distribute work with Pool() as pool: iters = zip(chains, vecs, rates, dts) - x_result = list(pool.starmap(cram_wrapper, iters)) + x_result = list(pool.starmap(_cram_wrapper, iters)) t_end = time.time() if comm.rank == 0: @@ -58,7 +58,7 @@ def deplete(chain, x, op_result, dt, print_out): return x_result -def cram_wrapper(chain, n0, rates, dt): +def _cram_wrapper(chain, n0, rates, dt): """Wraps depletion matrix creation / CRAM solve for multiprocess execution Parameters @@ -82,16 +82,11 @@ def cram_wrapper(chain, n0, rates, dt): def CRAM16(A, n0, dt): - """ Chebyshev Rational Approximation Method, order 16 + """Chebyshev Rational Approximation Method, order 16 Algorithm is the 16th order Chebyshev Rational Approximation Method, - implemented in the more stable incomplete partial fraction (IPF) form - [cram16]_. - - .. [cram16] - Pusa, Maria. "Higher-Order Chebyshev Rational Approximation Method and - Application to Burnup Equations." Nuclear Science and Engineering 182.3 - (2016). + implemented in the more stable `incomplete partial fraction (IPF) + `_ form. Parameters ---------- @@ -106,6 +101,7 @@ def CRAM16(A, n0, dt): ------- numpy.array Results of the matrix exponent. + """ alpha = np.array([+2.124853710495224e-16, @@ -144,16 +140,11 @@ def CRAM16(A, n0, dt): def CRAM48(A, n0, dt): - """ Chebyshev Rational Approximation Method, order 48 + """Chebyshev Rational Approximation Method, order 48 Algorithm is the 48th order Chebyshev Rational Approximation Method, - implemented in the more stable incomplete partial fraction (IPF) form - [cram48]_. - - .. [cram48] - Pusa, Maria. "Higher-Order Chebyshev Rational Approximation Method and - Application to Burnup Equations." Nuclear Science and Engineering 182.3 - (2016). + implemented in the more stable `incomplete partial fraction (IPF) + `_ form. Parameters ---------- @@ -168,6 +159,7 @@ def CRAM48(A, n0, dt): ------- numpy.array Results of the matrix exponent. + """ theta_r = np.array([-4.465731934165702e+1, -5.284616241568964e+0, diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 038c0778da..444ca7baa7 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -22,10 +22,10 @@ def predictor(operator, timesteps, power, print_out=True): Parameters ---------- - operator : openmc.deplete.Operator + operator : openmc.deplete.TransportOperator The operator object to simulate on. timesteps : iterable of float - Array of timesteps in units of [s] + Array of timesteps in units of [s]. Note that values are not cumulative. power : float or iterable of float Power of the reactor in [W]. A single value indicates that the power is constant over all timesteps. An iterable indicates potentially different diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py index 8a0ae92040..40d2f70b63 100644 --- a/openmc/deplete/integrator/save_results.py +++ b/openmc/deplete/integrator/save_results.py @@ -9,7 +9,7 @@ def save_results(op, x, op_results, t, step_ind): Parameters ---------- - op : openmc.deplete.Operator + op : openmc.deplete.TransportOperator The operator used to generate these results. x : list of list of numpy.array The prior x vectors. Indexed [i][cell] using the above equation. diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/openmc_wrapper.py index 12ed27f229..5104ab2f17 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/openmc_wrapper.py @@ -1,6 +1,10 @@ -"""The OpenMC wrapper module. +"""OpenMC transport operator + +This module implements a transport operator for OpenMC so that it can be used by +depletion integrators. The implementation makes use of the Python bindings to +OpenMC's C API so that reading tally results and updating material number +densities is all done in-memory instead of through the filesystem. -This module implements the depletion -> OpenMC linkage. """ import copy @@ -24,7 +28,7 @@ import openmc import openmc.capi from openmc.data import JOULE_PER_EV from . import comm -from .abc import Operator, OperatorResult +from .abc import TransportOperator, OperatorResult from .atom_number import AtomNumber from .reaction_rates import ReactionRates @@ -39,8 +43,8 @@ def _distribute(items): j += chunk_size -class OpenMCOperator(Operator): - """OpenMC transport operator +class Operator(TransportOperator): + """OpenMC transport operator for depletion Parameters ---------- diff --git a/tests/dummy_geometry.py b/tests/dummy_operator.py similarity index 95% rename from tests/dummy_geometry.py rename to tests/dummy_operator.py index ca2efd6630..706793d93f 100644 --- a/tests/dummy_geometry.py +++ b/tests/dummy_operator.py @@ -1,11 +1,11 @@ import numpy as np import scipy.sparse as sp from openmc.deplete.reaction_rates import ReactionRates -from openmc.deplete.abc import Operator, OperatorResult +from openmc.deplete.abc import TransportOperator, OperatorResult -class DummyGeometry(Operator): - """This is a dummy geometry class with no statistical uncertainty. +class DummyOperator(TransportOperator): + """This is a dummy operator class with no statistical uncertainty. y_1' = sin(y_2) y_1 + cos(y_1) y_2 y_2' = -cos(y_2) y_1 + sin(y_1) y_2 diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index 22125d039d..2286af908a 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -44,7 +44,7 @@ def test_full(run_in_tmpdir): # Create operator chain_file = Path(__file__).parents[2] / 'chains' / 'chain_simple.xml' - op = openmc.deplete.OpenMCOperator(geometry, settings, chain_file) + op = openmc.deplete.Operator(geometry, settings, chain_file) op.round_number = True # Power and timesteps diff --git a/tests/regression_tests/test_deplete_utilities.py b/tests/regression_tests/test_deplete_utilities.py index f38129cc79..82d4d56a4c 100644 --- a/tests/regression_tests/test_deplete_utilities.py +++ b/tests/regression_tests/test_deplete_utilities.py @@ -14,7 +14,7 @@ from openmc.deplete import utilities @pytest.fixture def res(): """Load the reference results""" - filename = str(Path(__file__).with_name('test_reference.h5')) + filename = Path(__file__).with_name('test_reference.h5') return results.read_results(filename) diff --git a/tests/unit_tests/test_deplete_cecm.py b/tests/unit_tests/test_deplete_cecm.py index 97659b8923..6deb6cd3dd 100644 --- a/tests/unit_tests/test_deplete_cecm.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -8,13 +8,13 @@ import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities -from tests import dummy_geometry +from tests import dummy_operator def test_cecm(run_in_tmpdir): """Integral regression test of integrator algorithm using CE/CM.""" - op = dummy_geometry.DummyGeometry() + op = dummy_operator.DummyOperator() op.output_dir = "test_integrator_regression" # Perform simulation using the MCNPX/MCNP6 algorithm diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index 42a3c13a3a..0d283855cb 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -8,13 +8,13 @@ import openmc.deplete from openmc.deplete import results from openmc.deplete import utilities -from tests import dummy_geometry +from tests import dummy_operator def test_predictor(run_in_tmpdir): """Integral regression test of integrator algorithm using predictor/corrector""" - op = dummy_geometry.DummyGeometry() + op = dummy_operator.DummyOperator() op.output_dir = "test_integrator_regression" # Perform simulation using the predictor algorithm From c9f8daa0aba77274bdd7cf29c8c3a6ecb66970f6 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Feb 2018 23:17:50 -0600 Subject: [PATCH 45/68] Fix in ReactionRates class. More documentation updates --- openmc/deplete/__init__.py | 2 +- openmc/deplete/abc.py | 4 +- openmc/deplete/atom_number.py | 8 ++-- .../{openmc_wrapper.py => operator.py} | 26 ++++++++----- openmc/deplete/reaction_rates.py | 31 +++++++++++---- openmc/deplete/results.py | 2 +- openmc/deplete/utilities.py | 1 + tests/unit_tests/test_deplete_reaction.py | 38 +++++-------------- 8 files changed, 57 insertions(+), 55 deletions(-) rename openmc/deplete/{openmc_wrapper.py => operator.py} (98%) diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 2467b973a9..33b6d9af14 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -16,7 +16,7 @@ except ImportError: from .nuclide import * from .chain import * -from .openmc_wrapper import * +from .operator import * from .reaction_rates import * from .abc import * from .results import * diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 53e71c1959..f2fa736508 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -66,7 +66,7 @@ class TransportOperator(metaclass=ABCMeta): Parameters ---------- - vec : list of numpy.array + vec : list of numpy.ndarray Total atoms to be used in function. print_out : bool, optional Whether or not to print out time. @@ -108,7 +108,7 @@ class TransportOperator(metaclass=ABCMeta): Returns ------- - list of numpy.array + list of numpy.ndarray Total density for initial conditions. """ diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 1a142676cb..5179224c24 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -55,7 +55,7 @@ class AtomNumber(object): self.n_nuc_burn = n_nuc_burn - self.number = np.zeros((len(local_mats), self.n_nuc)) + self.number = np.zeros((len(local_mats), len(nuclides))) def __getitem__(self, pos): """Retrieves total atom number from AtomNumber. @@ -69,7 +69,7 @@ class AtomNumber(object): Returns ------- - numpy.array + numpy.ndarray The value indexed from self.number. """ @@ -153,7 +153,7 @@ class AtomNumber(object): Material index. nuc : str, int or slice Nuclide index. - val : numpy.array + val : numpy.ndarray Array of densities to set in [atom/cm^3] """ @@ -190,7 +190,7 @@ class AtomNumber(object): ---------- mat : str, int or slice Material index. - val : numpy.array + val : numpy.ndarray The slice to set in [atom] """ diff --git a/openmc/deplete/openmc_wrapper.py b/openmc/deplete/operator.py similarity index 98% rename from openmc/deplete/openmc_wrapper.py rename to openmc/deplete/operator.py index 5104ab2f17..af08727eaa 100644 --- a/openmc/deplete/openmc_wrapper.py +++ b/openmc/deplete/operator.py @@ -11,15 +11,8 @@ import copy from collections import OrderedDict from itertools import chain import os -import random -import sys import time -try: - import lxml.etree as ET - _have_lxml = True -except ImportError: - import xml.etree.ElementTree as ET - _have_lxml = False +import xml.etree.ElementTree as ET import h5py import numpy as np @@ -34,6 +27,19 @@ from .reaction_rates import ReactionRates def _distribute(items): + """Distribute items across MPI communicator + + Parameters + ---------- + items : list + List of items of distribute + + Returns + ------- + list + Items assigned to process that called + + """ min_size, extra = divmod(len(items), comm.size) j = 0 for i in range(comm.size): @@ -114,7 +120,7 @@ class Operator(TransportOperator): Parameters ---------- - vec : list of numpy.array + vec : list of numpy.ndarray Total atoms to be used in function. power : float Power of the reactor in [W] @@ -241,7 +247,7 @@ class Operator(TransportOperator): Returns ------- - list of numpy.array + list of numpy.ndarray Total density for initial conditions. """ diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index d1e1b0f1e2..c66003530c 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -7,14 +7,16 @@ import numpy as np class ReactionRates(np.ndarray): - """ReactionRates class. + """Reaction rates resulting from a transport operator call - An ndarray to store reaction rates with string, integer, or slice indexing. + This class is a subclass of :class:`numpy.ndarray` with a few custom + attributes that make it easy to determine what index corresponds to a given + material, nuclide, and reaction rate. Parameters ---------- - index_mat : OrderedDict of str to int - A dictionary mapping material ID as string to index. + local_mats : list of str + Material IDs nuclides : list of str Depletable nuclides index_rx : OrderedDict of str to int @@ -36,14 +38,22 @@ class ReactionRates(np.ndarray): Number of reactions. """ - def __new__(cls, index_mat, nuclides, index_rx): + + # NumPy arrays can be created 1) explicitly 2) using view casting, and 3) by + # slicing an existing array. Because of these possibilities, it's necessary + # to put initialization logic in __new__ rather than __init__. Additionally, + # subclasses need to handle the multiple ways of creating arrays by using + # the __array_finalize__ method (discussed here: + # https://docs.scipy.org/doc/numpy/user/basics.subclassing.html) + + def __new__(cls, local_mats, nuclides, index_rx): # Create appropriately-sized zeroed-out ndarray - shape = (len(index_mat), len(nuclides), len(index_rx)) + shape = (len(local_mats), len(nuclides), len(index_rx)) obj = super().__new__(cls, shape) obj[:] = 0.0 # Add mapping attributes - obj.index_mat = index_mat + obj.index_mat = {mat: i for i, mat in enumerate(local_mats)} obj.index_nuc = {nuc: i for i, nuc in enumerate(nuclides)} obj.index_rx = index_rx @@ -56,6 +66,11 @@ class ReactionRates(np.ndarray): self.index_nuc = getattr(obj, 'index_nuc', None) self.index_rx = getattr(obj, 'index_rx', None) + # Reaction rates are distributed to other processes via multiprocessing, + # which entails pickling the objects. In order to preserve the custom + # attributes, we have to modify how the ndarray is pickled as described + # here: https://stackoverflow.com/a/26599346/1572453 + def __reduce__(self): state = super().__reduce__() new_state = state[2] + (self.index_mat, self.index_nuc, self.index_rx) @@ -69,7 +84,7 @@ class ReactionRates(np.ndarray): @property def n_mat(self): - """Number of cells.""" + """Number of materials.""" return len(self.index_mat) @property diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index e18051d9d8..177158dbe2 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -42,7 +42,7 @@ class Results(object): Number of materials in entire geometry. n_stages : int Number of stages in simulation. - data : numpy.array + data : numpy.ndarray Atom quantity, stored by stage, mat, then by nuclide. """ diff --git a/openmc/deplete/utilities.py b/openmc/deplete/utilities.py index d155f321d9..59d5305465 100644 --- a/openmc/deplete/utilities.py +++ b/openmc/deplete/utilities.py @@ -38,6 +38,7 @@ def evaluate_single_nuclide(results, mat, nuc): return time, concentration + def evaluate_reaction_rate(results, mat, nuc, rx): """Return reaction rate in a single material/nuclide from a results list. diff --git a/tests/unit_tests/test_deplete_reaction.py b/tests/unit_tests/test_deplete_reaction.py index de628f8c66..e46a7b13d7 100644 --- a/tests/unit_tests/test_deplete_reaction.py +++ b/tests/unit_tests/test_deplete_reaction.py @@ -7,11 +7,11 @@ from openmc.deplete import ReactionRates def test_get_set(): """Tests the get/set methods.""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1} + local_mats = ["10000", "10001"] + nuclides = ["U238", "U235"] + react_to_ind = {"fission": 0, "(n,gamma)": 1} - rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + rates = ReactionRates(local_mats, nuclides, react_to_ind) assert rates.shape == (2, 2, 2) assert np.all(rates == 0.0) @@ -50,34 +50,14 @@ def test_get_set(): assert rates.get("10000", "U238", "fission") == 5.0 -def test_n_mat(): +def test_properties(): """Test number of materials property.""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} + local_mats = ["10000", "10001"] + nuclides = ["U238", "U235", "Gd157"] + react_to_ind = {"fission": 0, "(n,gamma)": 1, "(n,2n)": 2, "(n,3n)": 3} - rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) + rates = ReactionRates(local_mats, nuclides, react_to_ind) assert rates.n_mat == 2 - - -def test_n_nuc(): - """Test number of nuclides property.""" - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - - rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) - assert rates.n_nuc == 3 - - -def test_n_react(): - """ Test number of reactions property. """ - mat_to_ind = {"10000" : 0, "10001" : 1} - nuc_to_ind = {"U238" : 0, "U235" : 1, "Gd157" : 2} - react_to_ind = {"fission" : 0, "(n,gamma)" : 1, "(n,2n)" : 2, "(n,3n)" : 3} - - rates = ReactionRates(mat_to_ind, nuc_to_ind, react_to_ind) - assert rates.n_react == 4 From 236e7f8b9e626347eff4395508c0b95efe97493a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 20 Feb 2018 00:05:53 -0600 Subject: [PATCH 46/68] Add format spec for depletion chain XML file --- chains/chain_simple.xml | 54 +++++------ chains/chain_test.xml | 32 +++---- docs/source/io_formats/depletion_chain.rst | 102 +++++++++++++++++++++ docs/source/io_formats/index.rst | 1 + openmc/deplete/chain.py | 4 +- openmc/deplete/nuclide.py | 14 +-- tests/unit_tests/test_deplete_nuclide.py | 22 ++--- 7 files changed, 166 insertions(+), 63 deletions(-) create mode 100644 docs/source/io_formats/depletion_chain.rst diff --git a/chains/chain_simple.xml b/chains/chain_simple.xml index 345da2237d..c2e50a370f 100644 --- a/chains/chain_simple.xml +++ b/chains/chain_simple.xml @@ -1,23 +1,23 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + 2.53000e-02 @@ -25,9 +25,9 @@ 1.093250e-04 2.087260e-04 2.780820e-02 6.759540e-03 2.392300e-02 4.356330e-05 - - - + + + 2.53000e-02 @@ -35,9 +35,9 @@ 6.142710e-5 1.483250e-04 0.0292737 0.002566345 0.0219242 4.9097e-6 - - - + + + 2.53000e-02 @@ -45,5 +45,5 @@ 4.141120e-04 7.605360e-04 0.0135457 0.00026864 0.0024432 3.7100E-07 - - + + diff --git a/chains/chain_test.xml b/chains/chain_test.xml index 5985704063..c8c75ad7b3 100644 --- a/chains/chain_test.xml +++ b/chains/chain_test.xml @@ -1,17 +1,17 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + 0.0253 @@ -19,5 +19,5 @@ 0.0292737 0.002566345 - - + + diff --git a/docs/source/io_formats/depletion_chain.rst b/docs/source/io_formats/depletion_chain.rst new file mode 100644 index 0000000000..00d95bdf58 --- /dev/null +++ b/docs/source/io_formats/depletion_chain.rst @@ -0,0 +1,102 @@ +.. _io_chain: + +============================ +Depletion Chain -- chain.xml +============================ + +A depletion chain file has a ```` root element with one or more +```` child elements. The decay, reaction, and fission product data for +each nuclide appears as child elements of ````. + +--------------------- +```` Element +--------------------- + +The ```` element contains information on the decay modes, reactions, +and fission product yields for a given nuclide in the depletion chain. This +element may have the following attributes: + + :name: + Name of the nuclide + + :half_life: + Half-life of the nuclide in [s] + + :decay_modes: + Number of decay modes present + + :decay_energy: + Decay energy released in [eV] + + :reactions: + Number of reactions present + +For each decay mode, a :ref:`io_chain_decay` appears as a child of +````. For each reaction present, a :ref:`io_chain_reaction` appears as +a child of ````. If the nuclide is fissionable, a :ref:`io_chain_nfy` +appears as well. + +.. _io_chain_decay: + +------------------- +```` Element +------------------- + +The ```` element represents a single decay mode and has the following +attributes: + + :type: + The type of the decay, e.g. 'ec/beta+' + + :target: + The daughter nuclide produced from the decay + + :branching_ratio: + The branching ratio for this decay mode + +.. _io_chain_reaction: + +---------------------- +```` Element +---------------------- + +The ```` element represents a single transmutation reaction. This +element has the following attributes: + + :type: + The type of the reaction, e.g., '(n,gamma)' + + :Q: + The Q value of the reaction in [eV] + + :target: + The nuclide produced in the reaction (absent if the type is 'fission') + + :branching_ratio: + The branching ratio for the reaction + +.. _io_chain_nfy: + +------------------------------------ +```` Element +------------------------------------ + +The ```` element provides yields of fission products for +fissionable nuclides. It has the follow sub-elements: + + :energies: + Energies in [eV] at which yields for products are tabulated + + :fission_yields: + + Fission product yields for a single energy point. This element itself has a + number of attributes/sub-elements: + + :energy: + Energy in [eV] at which yields are tabulated + + :products: + Names of fission products + + :data: + Independent yields for each fission product diff --git a/docs/source/io_formats/index.rst b/docs/source/io_formats/index.rst index f3eea21def..1068973332 100644 --- a/docs/source/io_formats/index.rst +++ b/docs/source/io_formats/index.rst @@ -30,6 +30,7 @@ Data Files :maxdepth: 2 cross_sections + depletion_chain nuclear_data mgxs_library data_wmp diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 348103ea94..0cd3fb0d7e 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -327,7 +327,7 @@ class Chain(object): # Load XML tree root = ET.parse(str(filename)) - for i, nuclide_elem in enumerate(root.findall('nuclide_table')): + for i, nuclide_elem in enumerate(root.findall('nuclide')): nuc = Nuclide.from_xml(nuclide_elem) chain.nuclide_dict[nuc.name] = i @@ -350,7 +350,7 @@ class Chain(object): """ - root_elem = ET.Element('depletion') + root_elem = ET.Element('depletion_chain') for nuclide in self.nuclides: root_elem.append(nuclide.to_xml_element()) diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index 17cf4d9b8f..4a7b86f029 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -23,9 +23,9 @@ class Nuclide(object): name : str Name of nuclide. half_life : float - Half life of nuclide in s^-1. + Half life of nuclide in [s]. decay_energy : float - Energy deposited from decay in eV. + Energy deposited from decay in [eV]. n_decay_modes : int Number of decay pathways. decay_modes : list of DecayTuple @@ -94,14 +94,14 @@ class Nuclide(object): nuc.decay_energy = float(element.get('decay_energy', '0')) # Check for decay paths - for decay_elem in element.iter('decay_type'): + for decay_elem in element.iter('decay'): d_type = decay_elem.get('type') target = decay_elem.get('target') branching_ratio = float(decay_elem.get('branching_ratio')) nuc.decay_modes.append(DecayTuple(d_type, target, branching_ratio)) # Check for reaction paths - for reaction_elem in element.iter('reaction_type'): + for reaction_elem in element.iter('reaction'): r_type = reaction_elem.get('type') Q = float(reaction_elem.get('Q', '0')) branching_ratio = float(reaction_elem.get('branching_ratio', '1')) @@ -138,7 +138,7 @@ class Nuclide(object): XML element to write nuclide data to """ - elem = ET.Element('nuclide_table') + elem = ET.Element('nuclide') elem.set('name', self.name) if self.half_life is not None: @@ -146,14 +146,14 @@ class Nuclide(object): elem.set('decay_modes', str(len(self.decay_modes))) elem.set('decay_energy', str(self.decay_energy)) for mode, daughter, br in self.decay_modes: - mode_elem = ET.SubElement(elem, 'decay_type') + mode_elem = ET.SubElement(elem, 'decay') mode_elem.set('type', mode) mode_elem.set('target', daughter) mode_elem.set('branching_ratio', str(br)) elem.set('reactions', str(len(self.reactions))) for rx, daughter, Q, br in self.reactions: - rx_elem = ET.SubElement(elem, 'reaction_type') + rx_elem = ET.SubElement(elem, 'reaction') rx_elem.set('type', rx) rx_elem.set('Q', str(Q)) if rx != 'fission': diff --git a/tests/unit_tests/test_deplete_nuclide.py b/tests/unit_tests/test_deplete_nuclide.py index 2add13f866..f2a101d2a9 100644 --- a/tests/unit_tests/test_deplete_nuclide.py +++ b/tests/unit_tests/test_deplete_nuclide.py @@ -37,14 +37,14 @@ def test_from_xml(): """Test reading nuclide data from an XML element.""" data = """ - - - - - - - - + + + + + + + + 0.0253 @@ -52,7 +52,7 @@ def test_from_xml(): 0.062155 0.0497641 0.0481413 - + """ element = ET.fromstring(data) @@ -96,7 +96,7 @@ def test_to_xml_element(): assert element.get("half_life") == "0.123" - decay_elems = element.findall("decay_type") + decay_elems = element.findall("decay") assert len(decay_elems) == 2 assert decay_elems[0].get("type") == "beta-" assert decay_elems[0].get("target") == "B" @@ -105,7 +105,7 @@ def test_to_xml_element(): assert decay_elems[1].get("target") == "D" assert decay_elems[1].get("branching_ratio") == "0.01" - rx_elems = element.findall("reaction_type") + rx_elems = element.findall("reaction") assert len(rx_elems) == 2 assert rx_elems[0].get("type") == "fission" assert float(rx_elems[0].get("Q")) == 2.0e8 From f3758e5330490e4f90df99938bb76efd2e9cebb6 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 20 Feb 2018 07:41:50 -0600 Subject: [PATCH 47/68] Fix broken tests on Py3.4, 3.5 --- openmc/deplete/atom_number.py | 5 +++-- openmc/deplete/integrator/save_results.py | 4 ++-- openmc/deplete/operator.py | 3 +-- openmc/deplete/reaction_rates.py | 11 ++++++----- openmc/deplete/results.py | 21 ++++++++------------- tests/unit_tests/test_deplete_chain.py | 7 +++---- tests/unit_tests/test_deplete_integrator.py | 21 +++++++++------------ tests/unit_tests/test_deplete_reaction.py | 8 ++++---- 8 files changed, 36 insertions(+), 44 deletions(-) diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 5179224c24..8f6a419113 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -2,6 +2,7 @@ An ndarray to store atom densities with string, integer, or slice indexing. """ +from collections import OrderedDict import numpy as np @@ -44,8 +45,8 @@ class AtomNumber(object): """ def __init__(self, local_mats, nuclides, volume, n_nuc_burn): - self.index_mat = {mat: i for i, mat in enumerate(local_mats)} - self.index_nuc = {nuc: i for i, nuc in enumerate(nuclides)} + self.index_mat = OrderedDict((mat, i) for i, mat in enumerate(local_mats)) + self.index_nuc = OrderedDict((nuc, i) for i, nuc in enumerate(nuclides)) self.volume = np.ones(len(local_mats)) for mat, val in volume.items(): diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py index 40d2f70b63..98245fdeae 100644 --- a/openmc/deplete/integrator/save_results.py +++ b/openmc/deplete/integrator/save_results.py @@ -22,12 +22,12 @@ def save_results(op, x, op_results, t, step_ind): """ # Get indexing terms - vol_list, nuc_list, burn_list, full_burn_list = op.get_results_info() + vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info() # Create results stages = len(x) results = Results() - results.allocate(vol_list, nuc_list, burn_list, full_burn_list, stages) + results.allocate(vol_dict, nuc_list, burn_list, full_burn_list, stages) n_mat = len(burn_list) diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index af08727eaa..d0a64a06d3 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -111,9 +111,8 @@ class Operator(TransportOperator): self._extract_number(self.local_mats, volume, nuclides) # Create reaction rates array - index_rx = {rx: i for i, rx in enumerate(self.chain.reactions)} self.reaction_rates = ReactionRates( - self.local_mats, self._burnable_nucs, index_rx) + self.local_mats, self._burnable_nucs, self.chain.reactions) def __call__(self, vec, power, print_out=True): """Runs a simulation. diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index c66003530c..482c357a29 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -2,6 +2,7 @@ An ndarray to store reaction rates with string, integer, or slice indexing. """ +from collections import OrderedDict import numpy as np @@ -19,8 +20,8 @@ class ReactionRates(np.ndarray): Material IDs nuclides : list of str Depletable nuclides - index_rx : OrderedDict of str to int - A dictionary mapping reaction name as string to index. + reactions : list of str + Transmutation reactions being tracked Attributes ---------- @@ -46,16 +47,16 @@ class ReactionRates(np.ndarray): # the __array_finalize__ method (discussed here: # https://docs.scipy.org/doc/numpy/user/basics.subclassing.html) - def __new__(cls, local_mats, nuclides, index_rx): + def __new__(cls, local_mats, nuclides, reactions): # Create appropriately-sized zeroed-out ndarray - shape = (len(local_mats), len(nuclides), len(index_rx)) + shape = (len(local_mats), len(nuclides), len(reactions)) obj = super().__new__(cls, shape) obj[:] = 0.0 # Add mapping attributes obj.index_mat = {mat: i for i, mat in enumerate(local_mats)} obj.index_nuc = {nuc: i for i, nuc in enumerate(nuclides)} - obj.index_rx = index_rx + obj.index_rx = {rx: i for i, rx in enumerate(reactions)} return obj diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 177158dbe2..307d18a405 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -76,16 +76,10 @@ class Results(object): """ self.volume = copy.deepcopy(volume) - self.nuc_to_ind = OrderedDict() - self.mat_to_ind = OrderedDict() + self.nuc_to_ind = {nuc: i for i, nuc in enumerate(nuc_list)} + self.mat_to_ind = {mat: i for i, mat in enumerate(burn_list)} self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} - for i, mat in enumerate(burn_list): - self.mat_to_ind[mat] = i - - for i, nuc in enumerate(nuc_list): - self.nuc_to_ind[nuc] = i - # Create storage array self.data = np.zeros((stages, self.n_mat, self.n_nuc)) @@ -123,8 +117,8 @@ class Results(object): ------- float The atoms for stage, mat, nuc - """ + """ stage, mat, nuc = pos if isinstance(mat, str): mat = self.mat_to_ind[mat] @@ -145,8 +139,8 @@ class Results(object): val : float The value to set data to. - """ + """ stage, mat, nuc = pos if isinstance(mat, str): mat = self.mat_to_ind[mat] @@ -162,8 +156,8 @@ class Results(object): ---------- handle : h5py.File or h5py.Group An hdf5 file or group type to store this in. - """ + """ # Create and save the 5 dictionaries: # quantities # self.mat_to_ind -> self.volume (TODO: support for changing volumes) @@ -234,8 +228,8 @@ class Results(object): An hdf5 file or group type to store this in. index : int What step is this? - """ + """ if "/number" not in handle: comm.barrier() self.create_hdf5(handle) @@ -299,6 +293,7 @@ class Results(object): An hdf5 file or group type to load from. index : int What step is this? + """ results = cls() @@ -357,8 +352,8 @@ def write_results(result, filename, index): Target filename. index : int What step is this? - """ + """ if have_mpi and h5py.get_config().mpi: kwargs = {'driver': 'mpio', 'comm': comm} else: diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index ae900e62d7..7ac3e2f8dd 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -136,11 +136,10 @@ def test_form_matrix(): chain = Chain.from_xml(_test_filename) - mat_ind = {"10000": 0, "10001": 1} - nuc_ind = {"A": 0, "B": 1, "C": 2} - react_ind = {rx: i for i, rx in enumerate(chain.reactions)} + mats = ["10000", "10001"] + nuclides = ["A", "B", "C"] - react = reaction_rates.ReactionRates(mat_ind, nuc_ind, react_ind) + react = reaction_rates.ReactionRates(mats, nuclides, chain.reactions) react.set("10000", "C", "fission", 1.0) react.set("10000", "A", "(n,gamma)", 2.0) diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 78dd6bcef2..b209900889 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -26,20 +26,18 @@ def test_save_results(run_in_tmpdir): op = MagicMock() vol_dict = {} - full_burn_dict = {} + full_burn_list = [] - j = 0 for i in range(comm.size): vol_dict[str(2*i)] = 1.2 vol_dict[str(2*i + 1)] = 1.2 - full_burn_dict[str(2*i)] = j - full_burn_dict[str(2*i + 1)] = j + 1 - j += 2 + full_burn_list.append(str(2*i)) + full_burn_list.append(str(2*i + 1)) - burn_list = [str(i) for i in range(2*comm.rank, 2*comm.rank + 2)] + burn_list = full_burn_list[2*comm.rank : 2*comm.rank + 2] nuc_list = ["na", "nb"] - op.get_results_info.return_value = vol_dict, nuc_list, burn_list, full_burn_dict + op.get_results_info.return_value = vol_dict, nuc_list, burn_list, full_burn_list # Construct x x1 = [] @@ -50,18 +48,17 @@ def test_save_results(run_in_tmpdir): x2.append([np.random.rand(2), np.random.rand(2)]) # Construct r - cell_dict = {s: i for i, s in enumerate(burn_list)} - r1 = ReactionRates(cell_dict, {"na": 0, "nb": 1}, {"ra": 0, "rb": 1}) - r1.rates = np.random.rand(2, 2, 2) + r1 = ReactionRates(burn_list, ["na", "nb"], ["ra", "rb"]) + r1[:] = np.random.rand(2, 2, 2) rate1 = [] rate2 = [] for i in range(stages): rate1.append(copy.deepcopy(r1)) - r1.rates = np.random.rand(2, 2, 2) + r1[:] = np.random.rand(2, 2, 2) rate2.append(copy.deepcopy(r1)) - r1.rates = np.random.rand(2, 2, 2) + r1[:] = np.random.rand(2, 2, 2) # Create global terms eigvl1 = np.random.rand(stages) diff --git a/tests/unit_tests/test_deplete_reaction.py b/tests/unit_tests/test_deplete_reaction.py index e46a7b13d7..18639a27a1 100644 --- a/tests/unit_tests/test_deplete_reaction.py +++ b/tests/unit_tests/test_deplete_reaction.py @@ -9,9 +9,9 @@ def test_get_set(): local_mats = ["10000", "10001"] nuclides = ["U238", "U235"] - react_to_ind = {"fission": 0, "(n,gamma)": 1} + reactions = ["fission", "(n,gamma)"] - rates = ReactionRates(local_mats, nuclides, react_to_ind) + rates = ReactionRates(local_mats, nuclides, reactions) assert rates.shape == (2, 2, 2) assert np.all(rates == 0.0) @@ -54,9 +54,9 @@ def test_properties(): """Test number of materials property.""" local_mats = ["10000", "10001"] nuclides = ["U238", "U235", "Gd157"] - react_to_ind = {"fission": 0, "(n,gamma)": 1, "(n,2n)": 2, "(n,3n)": 3} + reactions = ["fission", "(n,gamma)", "(n,2n)", "(n,3n)"] - rates = ReactionRates(local_mats, nuclides, react_to_ind) + rates = ReactionRates(local_mats, nuclides, reactions) assert rates.n_mat == 2 assert rates.n_nuc == 3 From 6fac1367ec15fc6ee45fcfd220be2205de2b5f88 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 20 Feb 2018 14:43:26 -0600 Subject: [PATCH 48/68] Improve depletion reference documentation --- docs/source/conf.py | 1 + docs/source/pythonapi/deplete.rst | 98 +++++++++++++++++-------------- openmc/deplete/abc.py | 2 + openmc/deplete/atom_number.py | 2 - openmc/deplete/chain.py | 7 +++ openmc/deplete/integrator/cecm.py | 2 +- openmc/deplete/nuclide.py | 50 +++++++++++++--- openmc/deplete/operator.py | 7 ++- openmc/deplete/reaction_rates.py | 3 - openmc/deplete/results.py | 86 +++++++++++++-------------- 10 files changed, 155 insertions(+), 103 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b25d5abbed..a2fec39b75 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,6 +32,7 @@ MOCK_MODULES = [ sys.modules.update((mod_name, MagicMock()) for mod_name in MOCK_MODULES) import numpy as np +np.ndarray = MagicMock np.polynomial.Polynomial = MagicMock diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 13f2fd1550..1d1a1c0ee5 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -4,59 +4,71 @@ :mod:`openmc.deplete` -- Depletion ---------------------------------- -Integrators ------------ +.. module:: openmc.deplete + +Two functions are provided that implement different time-integration algorithms +for depletion calculations. .. autosummary:: :toctree: generated :nosignatures: :template: myfunction.rst - openmc.deplete.integrator.predictor - openmc.deplete.integrator.cecm + integrator.predictor + integrator.cecm -Integrator Helper Functions ---------------------------- +Each of these functions expects a "transport operator" to be passed. An operator +specific to OpenMC is available using the following class: + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + Operator + +Internal Classes and Functions +------------------------------ + +During a depletion calculation, the depletion chain, reaction rates, and number +densities are managed through a series of internal classes that are not normally +visible to a user. However, should you find yourself wondering about these +classes (e.g., if you want to know what decay modes or reactions are present in +a depletion chain), they are documented here. The following classes store data +for a depletion chain: + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + Chain + DecayTuple + Nuclide + ReactionTuple + +The following classes are used during a depletion simulation and store auxiliary +data, such as number densities and reaction rates for each material. + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + AtomNumber + OperatorResult + ReactionRates + Results + TransportOperator + +Each of the integrator functions also relies on a number of "helper" functions +as follows: .. autosummary:: :toctree: generated :nosignatures: :template: myfunction.rst - openmc.deplete.integrator.CRAM16 - openmc.deplete.integrator.CRAM48 - openmc.deplete.integrator.save_results - -Metaclasses ------------ - -.. autosummary:: - :toctree: generated - :nosignatures: - :template: myclass.rst - - openmc.deplete.TransportOperator - -OpenMC Classes --------------- - -.. autosummary:: - :toctree: generated - :nosignatures: - :template: myclass.rst - - openmc.deplete.Operator - openmc.deplete.OperatorResult - -Data Classes ------------- -.. autosummary:: - :toctree: generated - :nosignatures: - :template: myclass.rst - - openmc.deplete.AtomNumber - openmc.deplete.Chain - openmc.deplete.Nuclide - openmc.deplete.ReactionRates - openmc.deplete.Results + integrator.CRAM16 + integrator.CRAM48 + integrator.save_results diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index f2fa736508..8b42ee800e 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -23,6 +23,8 @@ rates : openmc.deplete.ReactionRates Resulting reaction rates """ +OperatorResult.k.__doc__ = None +OperatorResult.rates.__doc__ = None class TransportOperator(metaclass=ABCMeta): diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 8f6a419113..9a32dfa3a0 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -113,12 +113,10 @@ class AtomNumber(object): @property def n_nuc(self): - """Number of nuclides.""" return len(self.index_nuc) @property def burnable_nuclides(self): - """All burnable nuclide names. Used for sorting the simulation.""" return [nuc for nuc, ind in self.index_nuc.items() if ind < self.n_nuc_burn] diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 0cd3fb0d7e..23395329eb 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -111,6 +111,13 @@ def replace_missing(product, decay_data): class Chain(object): """Full representation of a depletion chain. + A depletion chain can be created by using the :meth:`from_endf` method which + requires a list of ENDF incident neutron, decay, and neutron fission product + yield sublibrary files. The depletion chain used during a depletion + simulation is indicated by either an argument to + :class:`openmc.deplete.Operator` or through the + :envvar:`OPENMC_DEPLETE_CHAIN` environment variable. + Attributes ---------- nuclides : list of openmc.deplete.Nuclide diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index db58d5d527..9a07cd198f 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -10,7 +10,7 @@ from .save_results import save_results def cecm(operator, timesteps, power, print_out=True): r"""Deplete using the CE/CM algorithm. - Implements the second order `CE/CM Predictor-Corrector algorithm + Implements the second order `CE/CM predictor-corrector algorithm `_. This algorithm is mathematically defined as: diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index 4a7b86f029..af10e5c2f9 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -9,14 +9,50 @@ try: except ImportError: import xml.etree.ElementTree as ET + DecayTuple = namedtuple('DecayTuple', 'type target branching_ratio') +DecayTuple.__doc__ = """\ +Decay mode information + +Parameters +---------- +type : str + Type of the decay mode, e.g., 'beta-' +target : str + Nuclide resulting from decay +branching_ratio : float + Branching ratio of the decay mode + +""" +DecayTuple.type.__doc__ = None +DecayTuple.target.__doc__ = None +DecayTuple.branching_ratio.__doc__ = None + + ReactionTuple = namedtuple('ReactionTuple', 'type target Q branching_ratio') +ReactionTuple.__doc__ = """\ +Transmutation reaction information + +Parameters +---------- +type : str + Type of the reaction, e.g., 'fission' +target : str + nuclide resulting from reaction +Q : float + Q value of the reaction in [eV] +branching_ratio : float + Branching ratio of the reaction + +""" +ReactionTuple.type.__doc__ = None +ReactionTuple.target.__doc__ = None +ReactionTuple.Q.__doc__ = None +ReactionTuple.branching_ratio.__doc__ = None class Nuclide(object): - """The Nuclide class. - - Contains everything in a depletion chain relating to a single nuclide. + """Decay modes, reactions, and fission yields for a single nuclide. Attributes ---------- @@ -28,12 +64,12 @@ class Nuclide(object): Energy deposited from decay in [eV]. n_decay_modes : int Number of decay pathways. - decay_modes : list of DecayTuple + decay_modes : list of openmc.deplete.DecayTuple Decay mode information. Each element of the list is a named tuple with attributes 'type', 'target', and 'branching_ratio'. n_reaction_paths : int Number of possible reaction pathways. - reactions : list of ReactionTuple + reactions : list of openmc.deplete.ReactionTuple Reaction information. Each element of the list is a named tuple with attribute 'type', 'target', 'Q', and 'branching_ratio'. yield_data : dict of float to list @@ -62,12 +98,10 @@ class Nuclide(object): @property def n_decay_modes(self): - """Number of decay modes.""" return len(self.decay_modes) @property def n_reaction_paths(self): - """Number of possible reaction pathways.""" return len(self.reactions) @classmethod @@ -81,7 +115,7 @@ class Nuclide(object): Returns ------- - nuc : Nuclide + nuc : openmc.deplete.Nuclide Instance of a nuclide """ diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index d0a64a06d3..1c62bdd401 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -50,7 +50,12 @@ def _distribute(items): class Operator(TransportOperator): - """OpenMC transport operator for depletion + """OpenMC transport operator for depletion. + + Instances of this class can be used to perform depletion using OpenMC as the + transport operator. Normally, a user needn't call methods of this class + directly. Instead, an instance of this class is passed to an integrator + function, such as :func:`openmc.deplete.integrator.cecm`. Parameters ---------- diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index 482c357a29..fddb88b19d 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -85,17 +85,14 @@ class ReactionRates(np.ndarray): @property def n_mat(self): - """Number of materials.""" return len(self.index_mat) @property def n_nuc(self): - """Number of nucs.""" return len(self.index_nuc) @property def n_react(self): - """Number of reactions.""" return len(self.index_rx) def get(self, mat, nuc, rx): diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 307d18a405..f27353a4e3 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -58,51 +58,6 @@ class Results(object): self.data = None - def allocate(self, volume, nuc_list, burn_list, full_burn_list, stages): - """Allocates memory of Results. - - Parameters - ---------- - volume : dict of str float - Volumes corresponding to materials in full_burn_dict - nuc_list : list of str - A list of all nuclide names. Used for sorting the simulation. - burn_list : list of int - A list of all mat IDs to be burned. Used for sorting the simulation. - full_burn_list : list of str - List of all burnable material IDs - stages : int - Number of stages in simulation. - - """ - self.volume = copy.deepcopy(volume) - self.nuc_to_ind = {nuc: i for i, nuc in enumerate(nuc_list)} - self.mat_to_ind = {mat: i for i, mat in enumerate(burn_list)} - self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} - - # Create storage array - self.data = np.zeros((stages, self.n_mat, self.n_nuc)) - - @property - def n_mat(self): - """Number of mats.""" - return len(self.mat_to_ind) - - @property - def n_nuc(self): - """Number of nuclides.""" - return len(self.nuc_to_ind) - - @property - def n_hdf5_mats(self): - """Number of materials in entire geometry.""" - return len(self.mat_to_hdf5_ind) - - @property - def n_stages(self): - """Number of stages in simulation.""" - return self.data.shape[0] - def __getitem__(self, pos): """Retrieves an item from results. @@ -149,6 +104,47 @@ class Results(object): self.data[stage, mat, nuc] = val + @property + def n_mat(self): + return len(self.mat_to_ind) + + @property + def n_nuc(self): + return len(self.nuc_to_ind) + + @property + def n_hdf5_mats(self): + return len(self.mat_to_hdf5_ind) + + @property + def n_stages(self): + return self.data.shape[0] + + def allocate(self, volume, nuc_list, burn_list, full_burn_list, stages): + """Allocates memory of Results. + + Parameters + ---------- + volume : dict of str float + Volumes corresponding to materials in full_burn_dict + nuc_list : list of str + A list of all nuclide names. Used for sorting the simulation. + burn_list : list of int + A list of all mat IDs to be burned. Used for sorting the simulation. + full_burn_list : list of str + List of all burnable material IDs + stages : int + Number of stages in simulation. + + """ + self.volume = copy.deepcopy(volume) + self.nuc_to_ind = {nuc: i for i, nuc in enumerate(nuc_list)} + self.mat_to_ind = {mat: i for i, mat in enumerate(burn_list)} + self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} + + # Create storage array + self.data = np.zeros((stages, self.n_mat, self.n_nuc)) + def create_hdf5(self, handle): """Creates file structure for a blank HDF5 file. From 33dec88bd089876b36a73d0850ee594e1a7a49d6 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 20 Feb 2018 15:35:39 -0600 Subject: [PATCH 49/68] Add format spec for depletion results HDF5 file (add necessary attributes too) --- docs/source/io_formats/depletion_chain.rst | 2 +- docs/source/io_formats/depletion_results.rst | 42 ++++++++++++++++++ docs/source/io_formats/index.rst | 7 +-- openmc/checkvalue.py | 4 +- openmc/deplete/results.py | 44 ++++++++++--------- tests/regression_tests/test_reference.h5 | Bin 162120 -> 162320 bytes 6 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 docs/source/io_formats/depletion_results.rst diff --git a/docs/source/io_formats/depletion_chain.rst b/docs/source/io_formats/depletion_chain.rst index 00d95bdf58..b0dd72eb9a 100644 --- a/docs/source/io_formats/depletion_chain.rst +++ b/docs/source/io_formats/depletion_chain.rst @@ -1,4 +1,4 @@ -.. _io_chain: +.. _io_depletion_chain: ============================ Depletion Chain -- chain.xml diff --git a/docs/source/io_formats/depletion_results.rst b/docs/source/io_formats/depletion_results.rst new file mode 100644 index 0000000000..fcbc4fb99b --- /dev/null +++ b/docs/source/io_formats/depletion_results.rst @@ -0,0 +1,42 @@ +.. _io_depletion_results: + +============================= +Depletion Results File Format +============================= + +The current version of the depletion results file format is 1.0. + +**/** + +:Attributes: - **filetype** (*char[]*) -- String indicating the type of file. + - **version** (*int[2]*) -- Major and minor version of the + statepoint file format. + +:Datasets: - **eigenvalues** (*float[][]*) -- k-eigenvalues at each + time/stage. This array has shape (number of timesteps, number of + stages). + - **number** (*float[][][][]*) -- Total number of atoms. This array + has shape (number of timesteps, number of stages, number of + materials, number of nuclides). + - **reaction rates** (*float[][][][][]*) -- Reaction rates used to + build depletion matrices. This array has shape (number of + timesteps, number of stages, number of materials, number of + nuclides, number of reactions). + - **time** (*float[][2]*) -- Time in [s] at beginning/end of each + step. + +**/materials//** + +:Attributes: - **index** (*int*) -- Index used in results for this material + - **volume** (*float*) -- Volume of this material in [cm^3] + +**/nuclides//** + +:Attributes: - **atom number index** (*int*) -- Index in array of total atoms + for this nuclide + - **reaction rate index** (*int*) -- Index in array of reaction + rates for this nuclide + +**/reactions//** + +:Attributes: - **index** (*int*) -- Index user in results for this reaction diff --git a/docs/source/io_formats/index.rst b/docs/source/io_formats/index.rst index 1068973332..c1bb76a29a 100644 --- a/docs/source/io_formats/index.rst +++ b/docs/source/io_formats/index.rst @@ -12,7 +12,7 @@ Input Files .. toctree:: :numbered: - :maxdepth: 2 + :maxdepth: 1 geometry materials @@ -27,7 +27,7 @@ Data Files .. toctree:: :numbered: - :maxdepth: 2 + :maxdepth: 1 cross_sections depletion_chain @@ -42,11 +42,12 @@ Output Files .. toctree:: :numbered: - :maxdepth: 2 + :maxdepth: 1 statepoint source summary + depletion_results particle_restart track voxel diff --git a/openmc/checkvalue.py b/openmc/checkvalue.py index dd32aa566c..2f80ee4c0c 100644 --- a/openmc/checkvalue.py +++ b/openmc/checkvalue.py @@ -246,9 +246,9 @@ def check_filetype_version(obj, expected_type, expected_version): ---------- obj : h5py.File HDF5 file to check - expected_type + expected_type : str Expected file type, e.g. 'statepoint' - expected_version + expected_version : int Expected major version number. """ diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index f27353a4e3..b7e5623c7d 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -11,12 +11,13 @@ import h5py from . import comm, have_mpi from .reaction_rates import ReactionRates +from openmc.checkvalue import check_filetype_version -RESULTS_VERSION = 2 +_VERSION_RESULTS = (1, 0) class Results(object): - """Contains output of a depletion run. + """Output of a depletion run Attributes ---------- @@ -145,8 +146,8 @@ class Results(object): # Create storage array self.data = np.zeros((stages, self.n_mat, self.n_nuc)) - def create_hdf5(self, handle): - """Creates file structure for a blank HDF5 file. + def _write_hdf5_metadata(self, handle): + """Writes result metadata in HDF5 file Parameters ---------- @@ -165,7 +166,8 @@ class Results(object): # Store concentration mat and nuclide dictionaries (along with volumes) - handle.create_dataset("version", data=RESULTS_VERSION) + handle.attrs['version'] = np.array(_VERSION_RESULTS) + handle.attrs['filetype'] = np.string_('depletion results') mat_list = sorted(self.mat_to_hdf5_ind, key=int) nuc_list = sorted(self.nuc_to_ind) @@ -221,14 +223,14 @@ class Results(object): Parameters ---------- handle : h5py.File or h5py.Group - An hdf5 file or group type to store this in. + An HDF5 file or group type to store this in. index : int What step is this? """ if "/number" not in handle: comm.barrier() - self.create_hdf5(handle) + self._write_hdf5_metadata(handle) comm.barrier() @@ -280,14 +282,14 @@ class Results(object): time_dset[index, :] = self.time @classmethod - def from_hdf5(cls, handle, index): + def from_hdf5(cls, handle, step): """Loads results object from HDF5. Parameters ---------- handle : h5py.File or h5py.Group - An hdf5 file or group type to load from. - index : int + An HDF5 file or group type to load from. + step : int What step is this? """ @@ -298,9 +300,9 @@ class Results(object): eigenvalues_dset = handle["/eigenvalues"] time_dset = handle["/time"] - results.data = number_dset[index, :, :, :] - results.k = eigenvalues_dset[index, :] - results.time = time_dset[index, :] + results.data = number_dset[step, :, :, :] + results.k = eigenvalues_dset[step, :] + results.time = time_dset[step, :] # Reconstruct dictionaries results.volume = OrderedDict() @@ -331,14 +333,14 @@ class Results(object): for i in range(results.n_stages): rate = ReactionRates(results.mat_to_ind, rxn_nuc_to_ind, rxn_to_ind) - rate[:] = handle["/reaction rates"][index, i, :, :, :] + rate[:] = handle["/reaction rates"][step, i, :, :, :] results.rates.append(rate) return results -def write_results(result, filename, index): - """Outputs result to an .hdf5 file. +def write_results(result, filename, step): + """Outputs result to an HDF5 file. Parameters ---------- @@ -346,7 +348,7 @@ def write_results(result, filename, index): Object to be stored in a file. filename : String Target filename. - index : int + step : int What step is this? """ @@ -355,10 +357,10 @@ def write_results(result, filename, index): else: kwargs = {} - kwargs['mode'] = "w" if index == 0 else "a" + kwargs['mode'] = "w" if step == 0 else "a" with h5py.File(filename, **kwargs) as handle: - result.to_hdf5(handle, index) + result.to_hdf5(handle, step) def read_results(filename): @@ -371,12 +373,12 @@ def read_results(filename): Returns ------- - results : list of Results + results : list of openmc.deplete.Results The result objects. """ with h5py.File(str(filename), "r") as fh: - assert fh["version"].value == RESULTS_VERSION + check_filetype_version(fh, 'depletion results', _VERSION_RESULTS[0]) # Get number of results stored n = fh["number"].value.shape[0] diff --git a/tests/regression_tests/test_reference.h5 b/tests/regression_tests/test_reference.h5 index 6e724c597107560c4155a257f4e854210f36f08e..5323a9a904e0fce1f806fbc49ad90324b4fe1aa9 100644 GIT binary patch delta 188 zcmX@{iF3ji&IuY!0#y^WEG9o<7vuD(WMTk;6VqQhGpaYPXkEd$bp_LciirghmOKm| z3@ku7Mg|TB9tH`9vecsD%=|nC0S*SB2naZUNk&FSFby$@fq`lI#X?4LZyumDL^~%? zIR`^pW=?8JWkD)fEszif>JkLf5X}q>DX9fO1wacFic*V9b4rR~3K;|@ZWILo?m{Cr delta 45 xcmbR6h4aKG&IuY!9+eZdET$_dGKz6^FhIZxrs=Po8PytBw60*>x`Jsz1prXj4y6D9 From 4a500df455aba407bb69d8c9980a834cccd09f3b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 20 Feb 2018 16:40:18 -0600 Subject: [PATCH 50/68] Add Model.deplete method to make user's life easier --- openmc/deplete/operator.py | 5 +++++ openmc/model/model.py | 39 +++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index 1c62bdd401..a1d8ffb0b0 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -195,6 +195,11 @@ class Operator(TransportOperator): "material with ID={}.".format(mat.id)) volume[str(mat.id)] = mat.volume + # Make sure there are burnable materials + if not burnable_mats: + raise RuntimeError( + "No depletable materials were found in the model.") + # Sort the sets burnable_mats = sorted(burnable_mats, key=int) model_nuclides = sorted(model_nuclides) diff --git a/openmc/model/model.py b/openmc/model/model.py index 89fa84e604..d6e6ddce38 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2,6 +2,10 @@ from collections.abc import Iterable import openmc from openmc.checkvalue import check_type +import openmc.deplete as dep + +_DEPLETE_METHODS = {'predictor': dep.integrator.predictor, + 'cecm': dep.integrator.cecm} class Model(object): @@ -136,9 +140,38 @@ class Model(object): for plot in plots: self._plots.append(plot) - def export_to_xml(self): - """Export model to XML files. + def deplete(self, timesteps, power, chain_file=None, method='cecm', **kwargs): + """Deplete model using specified timesteps/power + + Parameters + ---------- + timesteps : iterable of float + Array of timesteps in units of [s]. Note that values are not + cumulative. + power : float or iterable of float + Power of the reactor in [W]. A single value indicates that the power + is constant over all timesteps. An iterable indicates potentially + different power levels for each timestep. For a 2D problem, the + power can be given in [W/cm] as long as the "volume" assigned to a + depletion material is actually an area in [cm^2]. + chain_file : str, optional + Path to the depletion chain XML file. Defaults to the + :envvar:`OPENMC_DEPLETE_CHAIN` environment variable if it exists. + method : {'cecm', 'predictor'} + Integration method used for depletion + **kwargs + Keyword arguments passed to integration function (e.g., + :func:`openmc.deplete.integrator.cecm`) + """ + # Create OpenMC transport operator + op = dep.Operator(self.geometry, self.settings, chain_file) + + # Perform depletion + _DEPLETE_METHODS[method](op, timesteps, power, **kwargs) + + def export_to_xml(self): + """Export model to XML files.""" self.settings.export_to_xml() self.geometry.export_to_xml() @@ -166,7 +199,7 @@ class Model(object): Parameters ---------- **kwargs - All keyword arguments are passed to openmc.run + All keyword arguments are passed to :func:`openmc.run` Returns ------- From b4719cf53fbabc9f1aa60dba9608f4f101f71674 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 20 Feb 2018 22:46:26 -0600 Subject: [PATCH 51/68] Fix for Python 3.4 (setting __doc__ on properties) --- openmc/deplete/abc.py | 8 ++++++-- openmc/deplete/nuclide.py | 21 ++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 8b42ee800e..8502909a21 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -23,8 +23,12 @@ rates : openmc.deplete.ReactionRates Resulting reaction rates """ -OperatorResult.k.__doc__ = None -OperatorResult.rates.__doc__ = None +try: + OperatorResult.k.__doc__ = None + OperatorResult.rates.__doc__ = None +except AttributeError: + # Can't set __doc__ on properties on Python 3.4 + pass class TransportOperator(metaclass=ABCMeta): diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index af10e5c2f9..8a30214c2c 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -24,9 +24,13 @@ branching_ratio : float Branching ratio of the decay mode """ -DecayTuple.type.__doc__ = None -DecayTuple.target.__doc__ = None -DecayTuple.branching_ratio.__doc__ = None +try: + DecayTuple.type.__doc__ = None + DecayTuple.target.__doc__ = None + DecayTuple.branching_ratio.__doc__ = None +except AttributeError: + # Can't set __doc__ on properties on Python 3.4 + pass ReactionTuple = namedtuple('ReactionTuple', 'type target Q branching_ratio') @@ -45,10 +49,13 @@ branching_ratio : float Branching ratio of the reaction """ -ReactionTuple.type.__doc__ = None -ReactionTuple.target.__doc__ = None -ReactionTuple.Q.__doc__ = None -ReactionTuple.branching_ratio.__doc__ = None +try: + ReactionTuple.type.__doc__ = None + ReactionTuple.target.__doc__ = None + ReactionTuple.Q.__doc__ = None + ReactionTuple.branching_ratio.__doc__ = None +except AttributeError: + pass class Nuclide(object): From 82d6c34d27e0021280581398be350e3f0df1a234 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 21 Feb 2018 06:51:51 -0600 Subject: [PATCH 52/68] Move make_chain.py to openmc-make-depletion-chain --- .gitignore | 7 +--- openmc/_utils.py | 49 +++++++++++++++++++++++ scripts/example_run.py | 16 ++++---- scripts/make_chain.py | 60 ----------------------------- scripts/openmc-make-depletion-chain | 33 ++++++++++++++++ 5 files changed, 91 insertions(+), 74 deletions(-) create mode 100644 openmc/_utils.py delete mode 100644 scripts/make_chain.py create mode 100755 scripts/openmc-make-depletion-chain diff --git a/.gitignore b/.gitignore index 8c7cc3e366..65c7285af1 100644 --- a/.gitignore +++ b/.gitignore @@ -42,11 +42,8 @@ results_error.dat inputs_error.dat results_test.dat -# Test build files -tests/build/ -tests/coverage/ -tests/memcheck/ -tests/ctestscript.run +# Test +.pytest_cache/ # HDF5 files *.h5 diff --git a/openmc/_utils.py b/openmc/_utils.py new file mode 100644 index 0000000000..83455c17aa --- /dev/null +++ b/openmc/_utils.py @@ -0,0 +1,49 @@ +import os.path +from pathlib import Path +from urllib.parse import urlparse +from urllib.request import urlopen + +_BLOCK_SIZE = 16384 + + +def download(url): + """Download file from a URL + + Parameters + ---------- + url : str + URL from which to download + + Returns + ------- + basename : str + Name of file written locally + + """ + req = urlopen(url) + + # Get file size from header + file_size = req.length + + # Check if file already downloaded + basename = Path(urlparse(url).path).name + if os.path.exists(basename): + if os.path.getsize(basename) == file_size: + print('Skipping {}, already downloaded'.format(basename)) + return basename + + # Copy file to disk in chunks + print('Downloading {}... '.format(basename), end='') + downloaded = 0 + with open(basename, 'wb') as fh: + while True: + chunk = req.read(_BLOCK_SIZE) + if not chunk: + break + fh.write(chunk) + downloaded += len(chunk) + status = '{:10} [{:3.2f}%]'.format( + downloaded, downloaded * 100. / file_size) + print(status + '\b'*len(status), end='') + print('') + return basename diff --git a/scripts/example_run.py b/scripts/example_run.py index 30b6bdc2ee..36b6cce1a4 100644 --- a/scripts/example_run.py +++ b/scripts/example_run.py @@ -14,22 +14,20 @@ geometry, lower_left, upper_right = example_geometry.generate_problem() dt1 = 15*24*60*60 # 15 days dt2 = 5.5*30*24*60*60 # 5.5 months N = np.floor(dt2/dt1) - dt = np.repeat([dt1], N) -# Depletion settings -settings = openmc.deplete.OpenMCSettings() -settings.power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO -settings.dt_vec = dt -settings.output_dir = 'test' +# Power for simulation +power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO -# OpenMC-delegated settings +# OpenMC settings +settings = openmc.Settings() settings.particles = 1000 settings.batches = 100 settings.inactive = 40 settings.source = openmc.Source(space=openmc.stats.Box(lower_left, upper_right)) -op = openmc.deplete.OpenMCOperator(geometry, settings) +op = openmc.deplete.Operator(geometry, settings) +op.output_dir = 'test' # Perform simulation using the MCNPX/MCNP6 algorithm -openmc.deplete.integrator.cecm(op) +openmc.deplete.integrator.cecm(op, dt, power) diff --git a/scripts/make_chain.py b/scripts/make_chain.py deleted file mode 100644 index 6d64f34b3c..0000000000 --- a/scripts/make_chain.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python - -import glob -import os -from zipfile import ZipFile - -import requests -from tqdm import tqdm -import openmc.deplete - - -urls = [ - 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-neutrons.zip', - 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-decay.zip', - 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-nfy.zip' -] - - -def download_file(url): - response = requests.get(url, stream=True) - filesize = int(response.headers.get('content-length')) - - # Check if file already downloaded - basename = url.split('/')[-1] - if os.path.exists(basename): - if os.path.getsize(basename) == filesize: - return basename - else: - overwrite = input('Overwrite {}? ([y]/n) '.format(basename)) - if overwrite.lower().startswith('n'): - return basename - - with open(basename, 'wb') as f: - with tqdm(desc='Downloading {}'.format(basename), - total=filesize, unit='B', unit_scale=True) as pbar: - for i, chunk in enumerate(response.iter_content(chunk_size=4096)): - pbar.update(4096) - if chunk: - f.write(chunk) - - return basename - - -def main(): - for url in urls: - basename = download_file(url) - with ZipFile(basename, 'r') as zf: - print('Extracting {}...'.format(basename)) - zf.extractall() - - decay_files = glob.glob(os.path.join('decay', '*.endf')) - nfy_files = glob.glob(os.path.join('nfy', '*.endf')) - neutron_files = glob.glob(os.path.join('neutrons', '*.endf')) - - chain = openmc.deplete.Chain.from_endf(decay_files, nfy_files, neutron_files) - chain.export_to_xml('chain_endfb71.xml') - - -if __name__ == '__main__': - main() diff --git a/scripts/openmc-make-depletion-chain b/scripts/openmc-make-depletion-chain new file mode 100755 index 0000000000..7c08f984e9 --- /dev/null +++ b/scripts/openmc-make-depletion-chain @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import glob +import os +from zipfile import ZipFile + +from openmc._utils import download +import openmc.deplete + + +URLS = [ + 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-neutrons.zip', + 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-decay.zip', + 'http://www.nndc.bnl.gov/endf/b7.1/zips/ENDF-B-VII.1-nfy.zip' +] + +def main(): + for url in URLS: + basename = download(url) + with ZipFile(basename, 'r') as zf: + print('Extracting {}...'.format(basename)) + zf.extractall() + + decay_files = glob.glob(os.path.join('decay', '*.endf')) + nfy_files = glob.glob(os.path.join('nfy', '*.endf')) + neutron_files = glob.glob(os.path.join('neutrons', '*.endf')) + + chain = openmc.deplete.Chain.from_endf(decay_files, nfy_files, neutron_files) + chain.export_to_xml('chain_endfb71.xml') + + +if __name__ == '__main__': + main() From d981b34dc18236cf857d1249629b6437005e073f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 21 Feb 2018 07:00:20 -0600 Subject: [PATCH 53/68] Remove FutureWarning for capi import --- openmc/capi/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openmc/capi/__init__.py b/openmc/capi/__init__.py index d302001c99..bc173f9946 100644 --- a/openmc/capi/__init__.py +++ b/openmc/capi/__init__.py @@ -15,7 +15,6 @@ objects in the :mod:`openmc.capi` subpackage, for example: from ctypes import CDLL import os import sys -from warnings import warn import pkg_resources @@ -36,10 +35,7 @@ else: # available. Instead, we create a mock object so that when the modules # within the openmc.capi package try to configure arguments and return # values for symbols, no errors occur - try: - from unittest.mock import Mock - except ImportError: - from mock import Mock + from unittest.mock import Mock _dll = Mock() from .error import * @@ -50,6 +46,3 @@ from .cell import * from .filter import * from .tally import * from .settings import settings - -warn("The Python bindings to OpenMC's C API are still unstable " - "and may change substantially in future releases.", FutureWarning) From 81b859ad4ff3995ac84e27b8bf15ba2f87cc4cc4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 21 Feb 2018 07:33:18 -0600 Subject: [PATCH 54/68] Get rid of example_run.py and example_plot.py --- scripts/example_geometry.py | 378 -------------------- scripts/example_plot.py | 43 --- scripts/example_run.py | 33 -- tests/regression_tests/example_geometry.py | 379 ++++++++++++++++++++- 4 files changed, 378 insertions(+), 455 deletions(-) delete mode 100644 scripts/example_geometry.py delete mode 100644 scripts/example_plot.py delete mode 100644 scripts/example_run.py mode change 120000 => 100644 tests/regression_tests/example_geometry.py diff --git a/scripts/example_geometry.py b/scripts/example_geometry.py deleted file mode 100644 index ca10c1f725..0000000000 --- a/scripts/example_geometry.py +++ /dev/null @@ -1,378 +0,0 @@ -"""An example file showing how to make a geometry. - -This particular example creates a 3x3 geometry, with 8 regular pins and one -Gd-157 2 wt-percent enriched. All pins are segmented. -""" - -from collections import OrderedDict -import math - -import numpy as np -import openmc - - -def density_to_mat(dens_dict): - """Generates an OpenMC material from a cell ID and self.number_density. - - Parameters - ---------- - dens_dict : dict - Dictionary mapping nuclide names to densities - - Returns - ------- - openmc.Material - The OpenMC material filled with nuclides. - - """ - mat = openmc.Material() - for key in dens_dict: - mat.add_nuclide(key, 1.0e-24*dens_dict[key]) - mat.set_density('sum') - - return mat - - -def generate_initial_number_density(): - """ Generates initial number density. - - These results were from a CASMO5 run in which the gadolinium pin was - loaded with 2 wt percent of Gd-157. - """ - - # Concentration to be used for all fuel pins - fuel_dict = OrderedDict() - fuel_dict['U235'] = 1.05692e21 - fuel_dict['U234'] = 1.00506e19 - fuel_dict['U238'] = 2.21371e22 - fuel_dict['O16'] = 4.62954e22 - fuel_dict['O17'] = 1.127684e20 - fuel_dict['I135'] = 1.0e10 - fuel_dict['Xe135'] = 1.0e10 - fuel_dict['Xe136'] = 1.0e10 - fuel_dict['Cs135'] = 1.0e10 - fuel_dict['Gd156'] = 1.0e10 - fuel_dict['Gd157'] = 1.0e10 - # fuel_dict['O18'] = 9.51352e19 # Does not exist in ENDF71, merged into 17 - - # Concentration to be used for the gadolinium fuel pin - fuel_gd_dict = OrderedDict() - fuel_gd_dict['U235'] = 1.03579e21 - fuel_gd_dict['U238'] = 2.16943e22 - fuel_gd_dict['Gd156'] = 3.95517E+10 - fuel_gd_dict['Gd157'] = 1.08156e20 - fuel_gd_dict['O16'] = 4.64035e22 - fuel_dict['I135'] = 1.0e10 - fuel_dict['Xe136'] = 1.0e10 - fuel_dict['Xe135'] = 1.0e10 - fuel_dict['Cs135'] = 1.0e10 - # There are a whole bunch of 1e-10 stuff here. - - # Concentration to be used for cladding - clad_dict = OrderedDict() - clad_dict['O16'] = 3.07427e20 - clad_dict['O17'] = 7.48868e17 - clad_dict['Cr50'] = 3.29620e18 - clad_dict['Cr52'] = 6.35639e19 - clad_dict['Cr53'] = 7.20763e18 - clad_dict['Cr54'] = 1.79413e18 - clad_dict['Fe54'] = 5.57350e18 - clad_dict['Fe56'] = 8.74921e19 - clad_dict['Fe57'] = 2.02057e18 - clad_dict['Fe58'] = 2.68901e17 - clad_dict['Cr50'] = 3.29620e18 - clad_dict['Cr52'] = 6.35639e19 - clad_dict['Cr53'] = 7.20763e18 - clad_dict['Cr54'] = 1.79413e18 - clad_dict['Ni58'] = 2.51631e19 - clad_dict['Ni60'] = 9.69278e18 - clad_dict['Ni61'] = 4.21338e17 - clad_dict['Ni62'] = 1.34341e18 - clad_dict['Ni64'] = 3.43127e17 - clad_dict['Zr90'] = 2.18320e22 - clad_dict['Zr91'] = 4.76104e21 - clad_dict['Zr92'] = 7.27734e21 - clad_dict['Zr94'] = 7.37494e21 - clad_dict['Zr96'] = 1.18814e21 - clad_dict['Sn112'] = 4.67352e18 - clad_dict['Sn114'] = 3.17992e18 - clad_dict['Sn115'] = 1.63814e18 - clad_dict['Sn116'] = 7.00546e19 - clad_dict['Sn117'] = 3.70027e19 - clad_dict['Sn118'] = 1.16694e20 - clad_dict['Sn119'] = 4.13872e19 - clad_dict['Sn120'] = 1.56973e20 - clad_dict['Sn122'] = 2.23076e19 - clad_dict['Sn124'] = 2.78966e19 - - # Gap concentration - # Funny enough, the example problem uses air. - gap_dict = OrderedDict() - gap_dict['O16'] = 7.86548e18 - gap_dict['O17'] = 2.99548e15 - gap_dict['N14'] = 3.38646e19 - gap_dict['N15'] = 1.23717e17 - - # Concentration to be used for coolant - # No boron - cool_dict = OrderedDict() - cool_dict['H1'] = 4.68063e22 - cool_dict['O16'] = 2.33427e22 - cool_dict['O17'] = 8.89086e18 - - # Store these dictionaries in the initial conditions dictionary - initial_density = OrderedDict() - initial_density['fuel_gd'] = fuel_gd_dict - initial_density['fuel'] = fuel_dict - initial_density['gap'] = gap_dict - initial_density['clad'] = clad_dict - initial_density['cool'] = cool_dict - - # Set up libraries to use - temperature = OrderedDict() - sab = OrderedDict() - - # Toggle betweeen MCNP and NNDC data - MCNP = False - - if MCNP: - temperature['fuel_gd'] = 900.0 - temperature['fuel'] = 900.0 - # We approximate temperature of everything as 600K, even though it was - # actually 580K. - temperature['gap'] = 600.0 - temperature['clad'] = 600.0 - temperature['cool'] = 600.0 - else: - temperature['fuel_gd'] = 293.6 - temperature['fuel'] = 293.6 - temperature['gap'] = 293.6 - temperature['clad'] = 293.6 - temperature['cool'] = 293.6 - - sab['cool'] = 'c_H_in_H2O' - - # Set up burnable materials - burn = OrderedDict() - burn['fuel_gd'] = True - burn['fuel'] = True - burn['gap'] = False - burn['clad'] = False - burn['cool'] = False - - return temperature, sab, initial_density, burn - -def segment_pin(n_rings, n_wedges, r_fuel, r_gap, r_clad): - """ Calculates a segmented pin. - - Separates a pin with n_rings and n_wedges. All cells have equal volume. - Pin is centered at origin. - """ - - # Calculate all the volumes of interest - v_fuel = math.pi * r_fuel**2 - v_gap = math.pi * r_gap**2 - v_fuel - v_clad = math.pi * r_clad**2 - v_fuel - v_gap - v_ring = v_fuel / n_rings - v_segment = v_ring / n_wedges - - # Compute ring radiuses - r_rings = np.zeros(n_rings) - - for i in range(n_rings): - r_rings[i] = math.sqrt(1.0/(math.pi) * v_ring * (i+1)) - - # Compute thetas - theta = np.linspace(0, 2*math.pi, n_wedges + 1) - - # Compute surfaces - fuel_rings = [openmc.ZCylinder(x0=0, y0=0, R=r_rings[i]) - for i in range(n_rings)] - - fuel_wedges = [openmc.Plane(A=math.cos(theta[i]), B=math.sin(theta[i])) - for i in range(n_wedges)] - - gap_ring = openmc.ZCylinder(x0=0, y0=0, R=r_gap) - clad_ring = openmc.ZCylinder(x0=0, y0=0, R=r_clad) - - # Create cells - fuel_cells = [] - if n_wedges == 1: - for i in range(n_rings): - cell = openmc.Cell(name='fuel') - if i == 0: - cell.region = -fuel_rings[0] - else: - cell.region = +fuel_rings[i-1] & -fuel_rings[i] - fuel_cells.append(cell) - else: - for i in range(n_rings): - for j in range(n_wedges): - cell = openmc.Cell(name='fuel') - if i == 0: - if j != n_wedges-1: - cell.region = (-fuel_rings[0] - & +fuel_wedges[j] - & -fuel_wedges[j+1]) - else: - cell.region = (-fuel_rings[0] - & +fuel_wedges[j] - & -fuel_wedges[0]) - else: - if j != n_wedges-1: - cell.region = (+fuel_rings[i-1] - & -fuel_rings[i] - & +fuel_wedges[j] - & -fuel_wedges[j+1]) - else: - cell.region = (+fuel_rings[i-1] - & -fuel_rings[i] - & +fuel_wedges[j] - & -fuel_wedges[0]) - fuel_cells.append(cell) - - # Gap ring - gap_cell = openmc.Cell(name='gap') - gap_cell.region = +fuel_rings[-1] & -gap_ring - fuel_cells.append(gap_cell) - - # Clad ring - clad_cell = openmc.Cell(name='clad') - clad_cell.region = +gap_ring & -clad_ring - fuel_cells.append(clad_cell) - - # Moderator - mod_cell = openmc.Cell(name='cool') - mod_cell.region = +clad_ring - fuel_cells.append(mod_cell) - - # Form universe - fuel_u = openmc.Universe() - fuel_u.add_cells(fuel_cells) - - return fuel_u, v_segment, v_gap, v_clad - -def generate_geometry(n_rings, n_wedges): - """ Generates example geometry. - - This function creates the initial geometry, a 9 pin reflective problem. - One pin, containing gadolinium, is discretized into sectors. - - In addition to what one would do with the general OpenMC geometry code, it - is necessary to create a dictionary, volume, that maps a cell ID to a - volume. Further, by naming cells the same as the above materials, the code - can automatically handle the mapping. - - Parameters - ---------- - n_rings : int - Number of rings to generate for the geometry - n_wedges : int - Number of wedges to generate for the geometry - """ - - pitch = 1.26197 - r_fuel = 0.412275 - r_gap = 0.418987 - r_clad = 0.476121 - - n_pin = 3 - - # This table describes the 'fuel' to actual type mapping - # It's not necessary to do it this way. Just adjust the initial conditions - # below. - mapping = ['fuel', 'fuel', 'fuel', - 'fuel', 'fuel_gd', 'fuel', - 'fuel', 'fuel', 'fuel'] - - # Form pin cell - fuel_u, v_segment, v_gap, v_clad = segment_pin(n_rings, n_wedges, r_fuel, r_gap, r_clad) - - # Form lattice - all_water_c = openmc.Cell(name='cool') - all_water_u = openmc.Universe(cells=(all_water_c, )) - - lattice = openmc.RectLattice() - lattice.pitch = [pitch]*2 - lattice.lower_left = [-pitch*n_pin/2, -pitch*n_pin/2] - lattice_array = [[fuel_u for i in range(n_pin)] for j in range(n_pin)] - lattice.universes = lattice_array - lattice.outer = all_water_u - - # Bound universe - x_low = openmc.XPlane(x0=-pitch*n_pin/2, boundary_type='reflective') - x_high = openmc.XPlane(x0=pitch*n_pin/2, boundary_type='reflective') - y_low = openmc.YPlane(y0=-pitch*n_pin/2, boundary_type='reflective') - y_high = openmc.YPlane(y0=pitch*n_pin/2, boundary_type='reflective') - z_low = openmc.ZPlane(z0=-10, boundary_type='reflective') - z_high = openmc.ZPlane(z0=10, boundary_type='reflective') - - # Compute bounding box - lower_left = [-pitch*n_pin/2, -pitch*n_pin/2, -10] - upper_right = [pitch*n_pin/2, pitch*n_pin/2, 10] - - root_c = openmc.Cell(fill=lattice) - root_c.region = (+x_low & -x_high - & +y_low & -y_high - & +z_low & -z_high) - root_u = openmc.Universe(universe_id=0, cells=(root_c, )) - geometry = openmc.Geometry(root_u) - - v_cool = pitch**2 - (v_gap + v_clad + n_rings * n_wedges * v_segment) - - # Store volumes for later usage - volume = {'fuel': v_segment, 'gap':v_gap, 'clad':v_clad, 'cool':v_cool} - - return geometry, volume, mapping, lower_left, upper_right - -def generate_problem(n_rings=5, n_wedges=8): - """ Merges geometry and materials. - - This function initializes the materials for each cell using the dictionaries - provided by generate_initial_number_density. It is assumed a cell named - 'fuel' will have further region differentiation (see mapping). - - Parameters - ---------- - n_rings : int, optional - Number of rings to generate for the geometry - n_wedges : int, optional - Number of wedges to generate for the geometry - """ - - # Get materials dictionary, geometry, and volumes - temperature, sab, initial_density, burn = generate_initial_number_density() - geometry, volume, mapping, lower_left, upper_right = generate_geometry(n_rings, n_wedges) - - # Apply distribmats, fill geometry - cells = geometry.root_universe.get_all_cells() - for cell_id in cells: - cell = cells[cell_id] - if cell.name == 'fuel': - - omc_mats = [] - - for cell_type in mapping: - omc_mat = density_to_mat(initial_density[cell_type]) - - if cell_type in sab: - omc_mat.add_s_alpha_beta(sab[cell_type]) - omc_mat.temperature = temperature[cell_type] - omc_mat.depletable = burn[cell_type] - omc_mat.volume = volume['fuel'] - - omc_mats.append(omc_mat) - - cell.fill = omc_mats - elif cell.name != '': - omc_mat = density_to_mat(initial_density[cell.name]) - - if cell.name in sab: - omc_mat.add_s_alpha_beta(sab[cell.name]) - omc_mat.temperature = temperature[cell.name] - omc_mat.depletable = burn[cell.name] - omc_mat.volume = volume[cell.name] - - cell.fill = omc_mat - - return geometry, lower_left, upper_right diff --git a/scripts/example_plot.py b/scripts/example_plot.py deleted file mode 100644 index ab5ac204d8..0000000000 --- a/scripts/example_plot.py +++ /dev/null @@ -1,43 +0,0 @@ -"""An example file showing how to plot data from a simulation.""" - -import matplotlib.pyplot as plt -from openmc.deplete import (read_results, evaluate_single_nuclide, - evaluate_reaction_rate, evaluate_eigenvalue) - -# Set variables for where the data is, and what we want to read out. -result_folder = "test" - -# Load data -results = read_results(result_folder + "/deplete_results.h5") - -cell = "5" -nuc = "Gd157" -rxn = "(n,gamma)" - -# Total number of nuclides -plt.figure() -# Pointwise data -x, y = evaluate_single_nuclide(results, cell, nuc) -plt.semilogy(x, y) - -plt.xlabel("Time, s") -plt.ylabel("Total Number") -plt.savefig("number.pdf") - -# Reaction rate -plt.figure() -x, y = evaluate_reaction_rate(results, cell, nuc, rxn) -plt.plot(x, y) -plt.xlabel("Time, s") -plt.ylabel("Reaction Rate, 1/s") - -plt.savefig("rate.pdf") - -# Eigenvalue -plt.figure() -x, y = evaluate_eigenvalue(results) -plt.plot(x, y) -plt.xlabel("Time, s") -plt.ylabel("Eigenvalue") - -plt.savefig("eigvl.pdf") diff --git a/scripts/example_run.py b/scripts/example_run.py deleted file mode 100644 index 36b6cce1a4..0000000000 --- a/scripts/example_run.py +++ /dev/null @@ -1,33 +0,0 @@ -"""An example file showing how to run a simulation.""" - -import numpy as np -import openmc -from openmc.data import JOULE_PER_EV -import openmc.deplete - -import example_geometry - -# Load geometry from example -geometry, lower_left, upper_right = example_geometry.generate_problem() - -# Create dt vector for 5.5 months with 15 day timesteps -dt1 = 15*24*60*60 # 15 days -dt2 = 5.5*30*24*60*60 # 5.5 months -N = np.floor(dt2/dt1) -dt = np.repeat([dt1], N) - -# Power for simulation -power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO - -# OpenMC settings -settings = openmc.Settings() -settings.particles = 1000 -settings.batches = 100 -settings.inactive = 40 -settings.source = openmc.Source(space=openmc.stats.Box(lower_left, upper_right)) - -op = openmc.deplete.Operator(geometry, settings) -op.output_dir = 'test' - -# Perform simulation using the MCNPX/MCNP6 algorithm -openmc.deplete.integrator.cecm(op, dt, power) diff --git a/tests/regression_tests/example_geometry.py b/tests/regression_tests/example_geometry.py deleted file mode 120000 index 1071aabc05..0000000000 --- a/tests/regression_tests/example_geometry.py +++ /dev/null @@ -1 +0,0 @@ -../../scripts/example_geometry.py \ No newline at end of file diff --git a/tests/regression_tests/example_geometry.py b/tests/regression_tests/example_geometry.py new file mode 100644 index 0000000000..ca10c1f725 --- /dev/null +++ b/tests/regression_tests/example_geometry.py @@ -0,0 +1,378 @@ +"""An example file showing how to make a geometry. + +This particular example creates a 3x3 geometry, with 8 regular pins and one +Gd-157 2 wt-percent enriched. All pins are segmented. +""" + +from collections import OrderedDict +import math + +import numpy as np +import openmc + + +def density_to_mat(dens_dict): + """Generates an OpenMC material from a cell ID and self.number_density. + + Parameters + ---------- + dens_dict : dict + Dictionary mapping nuclide names to densities + + Returns + ------- + openmc.Material + The OpenMC material filled with nuclides. + + """ + mat = openmc.Material() + for key in dens_dict: + mat.add_nuclide(key, 1.0e-24*dens_dict[key]) + mat.set_density('sum') + + return mat + + +def generate_initial_number_density(): + """ Generates initial number density. + + These results were from a CASMO5 run in which the gadolinium pin was + loaded with 2 wt percent of Gd-157. + """ + + # Concentration to be used for all fuel pins + fuel_dict = OrderedDict() + fuel_dict['U235'] = 1.05692e21 + fuel_dict['U234'] = 1.00506e19 + fuel_dict['U238'] = 2.21371e22 + fuel_dict['O16'] = 4.62954e22 + fuel_dict['O17'] = 1.127684e20 + fuel_dict['I135'] = 1.0e10 + fuel_dict['Xe135'] = 1.0e10 + fuel_dict['Xe136'] = 1.0e10 + fuel_dict['Cs135'] = 1.0e10 + fuel_dict['Gd156'] = 1.0e10 + fuel_dict['Gd157'] = 1.0e10 + # fuel_dict['O18'] = 9.51352e19 # Does not exist in ENDF71, merged into 17 + + # Concentration to be used for the gadolinium fuel pin + fuel_gd_dict = OrderedDict() + fuel_gd_dict['U235'] = 1.03579e21 + fuel_gd_dict['U238'] = 2.16943e22 + fuel_gd_dict['Gd156'] = 3.95517E+10 + fuel_gd_dict['Gd157'] = 1.08156e20 + fuel_gd_dict['O16'] = 4.64035e22 + fuel_dict['I135'] = 1.0e10 + fuel_dict['Xe136'] = 1.0e10 + fuel_dict['Xe135'] = 1.0e10 + fuel_dict['Cs135'] = 1.0e10 + # There are a whole bunch of 1e-10 stuff here. + + # Concentration to be used for cladding + clad_dict = OrderedDict() + clad_dict['O16'] = 3.07427e20 + clad_dict['O17'] = 7.48868e17 + clad_dict['Cr50'] = 3.29620e18 + clad_dict['Cr52'] = 6.35639e19 + clad_dict['Cr53'] = 7.20763e18 + clad_dict['Cr54'] = 1.79413e18 + clad_dict['Fe54'] = 5.57350e18 + clad_dict['Fe56'] = 8.74921e19 + clad_dict['Fe57'] = 2.02057e18 + clad_dict['Fe58'] = 2.68901e17 + clad_dict['Cr50'] = 3.29620e18 + clad_dict['Cr52'] = 6.35639e19 + clad_dict['Cr53'] = 7.20763e18 + clad_dict['Cr54'] = 1.79413e18 + clad_dict['Ni58'] = 2.51631e19 + clad_dict['Ni60'] = 9.69278e18 + clad_dict['Ni61'] = 4.21338e17 + clad_dict['Ni62'] = 1.34341e18 + clad_dict['Ni64'] = 3.43127e17 + clad_dict['Zr90'] = 2.18320e22 + clad_dict['Zr91'] = 4.76104e21 + clad_dict['Zr92'] = 7.27734e21 + clad_dict['Zr94'] = 7.37494e21 + clad_dict['Zr96'] = 1.18814e21 + clad_dict['Sn112'] = 4.67352e18 + clad_dict['Sn114'] = 3.17992e18 + clad_dict['Sn115'] = 1.63814e18 + clad_dict['Sn116'] = 7.00546e19 + clad_dict['Sn117'] = 3.70027e19 + clad_dict['Sn118'] = 1.16694e20 + clad_dict['Sn119'] = 4.13872e19 + clad_dict['Sn120'] = 1.56973e20 + clad_dict['Sn122'] = 2.23076e19 + clad_dict['Sn124'] = 2.78966e19 + + # Gap concentration + # Funny enough, the example problem uses air. + gap_dict = OrderedDict() + gap_dict['O16'] = 7.86548e18 + gap_dict['O17'] = 2.99548e15 + gap_dict['N14'] = 3.38646e19 + gap_dict['N15'] = 1.23717e17 + + # Concentration to be used for coolant + # No boron + cool_dict = OrderedDict() + cool_dict['H1'] = 4.68063e22 + cool_dict['O16'] = 2.33427e22 + cool_dict['O17'] = 8.89086e18 + + # Store these dictionaries in the initial conditions dictionary + initial_density = OrderedDict() + initial_density['fuel_gd'] = fuel_gd_dict + initial_density['fuel'] = fuel_dict + initial_density['gap'] = gap_dict + initial_density['clad'] = clad_dict + initial_density['cool'] = cool_dict + + # Set up libraries to use + temperature = OrderedDict() + sab = OrderedDict() + + # Toggle betweeen MCNP and NNDC data + MCNP = False + + if MCNP: + temperature['fuel_gd'] = 900.0 + temperature['fuel'] = 900.0 + # We approximate temperature of everything as 600K, even though it was + # actually 580K. + temperature['gap'] = 600.0 + temperature['clad'] = 600.0 + temperature['cool'] = 600.0 + else: + temperature['fuel_gd'] = 293.6 + temperature['fuel'] = 293.6 + temperature['gap'] = 293.6 + temperature['clad'] = 293.6 + temperature['cool'] = 293.6 + + sab['cool'] = 'c_H_in_H2O' + + # Set up burnable materials + burn = OrderedDict() + burn['fuel_gd'] = True + burn['fuel'] = True + burn['gap'] = False + burn['clad'] = False + burn['cool'] = False + + return temperature, sab, initial_density, burn + +def segment_pin(n_rings, n_wedges, r_fuel, r_gap, r_clad): + """ Calculates a segmented pin. + + Separates a pin with n_rings and n_wedges. All cells have equal volume. + Pin is centered at origin. + """ + + # Calculate all the volumes of interest + v_fuel = math.pi * r_fuel**2 + v_gap = math.pi * r_gap**2 - v_fuel + v_clad = math.pi * r_clad**2 - v_fuel - v_gap + v_ring = v_fuel / n_rings + v_segment = v_ring / n_wedges + + # Compute ring radiuses + r_rings = np.zeros(n_rings) + + for i in range(n_rings): + r_rings[i] = math.sqrt(1.0/(math.pi) * v_ring * (i+1)) + + # Compute thetas + theta = np.linspace(0, 2*math.pi, n_wedges + 1) + + # Compute surfaces + fuel_rings = [openmc.ZCylinder(x0=0, y0=0, R=r_rings[i]) + for i in range(n_rings)] + + fuel_wedges = [openmc.Plane(A=math.cos(theta[i]), B=math.sin(theta[i])) + for i in range(n_wedges)] + + gap_ring = openmc.ZCylinder(x0=0, y0=0, R=r_gap) + clad_ring = openmc.ZCylinder(x0=0, y0=0, R=r_clad) + + # Create cells + fuel_cells = [] + if n_wedges == 1: + for i in range(n_rings): + cell = openmc.Cell(name='fuel') + if i == 0: + cell.region = -fuel_rings[0] + else: + cell.region = +fuel_rings[i-1] & -fuel_rings[i] + fuel_cells.append(cell) + else: + for i in range(n_rings): + for j in range(n_wedges): + cell = openmc.Cell(name='fuel') + if i == 0: + if j != n_wedges-1: + cell.region = (-fuel_rings[0] + & +fuel_wedges[j] + & -fuel_wedges[j+1]) + else: + cell.region = (-fuel_rings[0] + & +fuel_wedges[j] + & -fuel_wedges[0]) + else: + if j != n_wedges-1: + cell.region = (+fuel_rings[i-1] + & -fuel_rings[i] + & +fuel_wedges[j] + & -fuel_wedges[j+1]) + else: + cell.region = (+fuel_rings[i-1] + & -fuel_rings[i] + & +fuel_wedges[j] + & -fuel_wedges[0]) + fuel_cells.append(cell) + + # Gap ring + gap_cell = openmc.Cell(name='gap') + gap_cell.region = +fuel_rings[-1] & -gap_ring + fuel_cells.append(gap_cell) + + # Clad ring + clad_cell = openmc.Cell(name='clad') + clad_cell.region = +gap_ring & -clad_ring + fuel_cells.append(clad_cell) + + # Moderator + mod_cell = openmc.Cell(name='cool') + mod_cell.region = +clad_ring + fuel_cells.append(mod_cell) + + # Form universe + fuel_u = openmc.Universe() + fuel_u.add_cells(fuel_cells) + + return fuel_u, v_segment, v_gap, v_clad + +def generate_geometry(n_rings, n_wedges): + """ Generates example geometry. + + This function creates the initial geometry, a 9 pin reflective problem. + One pin, containing gadolinium, is discretized into sectors. + + In addition to what one would do with the general OpenMC geometry code, it + is necessary to create a dictionary, volume, that maps a cell ID to a + volume. Further, by naming cells the same as the above materials, the code + can automatically handle the mapping. + + Parameters + ---------- + n_rings : int + Number of rings to generate for the geometry + n_wedges : int + Number of wedges to generate for the geometry + """ + + pitch = 1.26197 + r_fuel = 0.412275 + r_gap = 0.418987 + r_clad = 0.476121 + + n_pin = 3 + + # This table describes the 'fuel' to actual type mapping + # It's not necessary to do it this way. Just adjust the initial conditions + # below. + mapping = ['fuel', 'fuel', 'fuel', + 'fuel', 'fuel_gd', 'fuel', + 'fuel', 'fuel', 'fuel'] + + # Form pin cell + fuel_u, v_segment, v_gap, v_clad = segment_pin(n_rings, n_wedges, r_fuel, r_gap, r_clad) + + # Form lattice + all_water_c = openmc.Cell(name='cool') + all_water_u = openmc.Universe(cells=(all_water_c, )) + + lattice = openmc.RectLattice() + lattice.pitch = [pitch]*2 + lattice.lower_left = [-pitch*n_pin/2, -pitch*n_pin/2] + lattice_array = [[fuel_u for i in range(n_pin)] for j in range(n_pin)] + lattice.universes = lattice_array + lattice.outer = all_water_u + + # Bound universe + x_low = openmc.XPlane(x0=-pitch*n_pin/2, boundary_type='reflective') + x_high = openmc.XPlane(x0=pitch*n_pin/2, boundary_type='reflective') + y_low = openmc.YPlane(y0=-pitch*n_pin/2, boundary_type='reflective') + y_high = openmc.YPlane(y0=pitch*n_pin/2, boundary_type='reflective') + z_low = openmc.ZPlane(z0=-10, boundary_type='reflective') + z_high = openmc.ZPlane(z0=10, boundary_type='reflective') + + # Compute bounding box + lower_left = [-pitch*n_pin/2, -pitch*n_pin/2, -10] + upper_right = [pitch*n_pin/2, pitch*n_pin/2, 10] + + root_c = openmc.Cell(fill=lattice) + root_c.region = (+x_low & -x_high + & +y_low & -y_high + & +z_low & -z_high) + root_u = openmc.Universe(universe_id=0, cells=(root_c, )) + geometry = openmc.Geometry(root_u) + + v_cool = pitch**2 - (v_gap + v_clad + n_rings * n_wedges * v_segment) + + # Store volumes for later usage + volume = {'fuel': v_segment, 'gap':v_gap, 'clad':v_clad, 'cool':v_cool} + + return geometry, volume, mapping, lower_left, upper_right + +def generate_problem(n_rings=5, n_wedges=8): + """ Merges geometry and materials. + + This function initializes the materials for each cell using the dictionaries + provided by generate_initial_number_density. It is assumed a cell named + 'fuel' will have further region differentiation (see mapping). + + Parameters + ---------- + n_rings : int, optional + Number of rings to generate for the geometry + n_wedges : int, optional + Number of wedges to generate for the geometry + """ + + # Get materials dictionary, geometry, and volumes + temperature, sab, initial_density, burn = generate_initial_number_density() + geometry, volume, mapping, lower_left, upper_right = generate_geometry(n_rings, n_wedges) + + # Apply distribmats, fill geometry + cells = geometry.root_universe.get_all_cells() + for cell_id in cells: + cell = cells[cell_id] + if cell.name == 'fuel': + + omc_mats = [] + + for cell_type in mapping: + omc_mat = density_to_mat(initial_density[cell_type]) + + if cell_type in sab: + omc_mat.add_s_alpha_beta(sab[cell_type]) + omc_mat.temperature = temperature[cell_type] + omc_mat.depletable = burn[cell_type] + omc_mat.volume = volume['fuel'] + + omc_mats.append(omc_mat) + + cell.fill = omc_mats + elif cell.name != '': + omc_mat = density_to_mat(initial_density[cell.name]) + + if cell.name in sab: + omc_mat.add_s_alpha_beta(sab[cell.name]) + omc_mat.temperature = temperature[cell.name] + omc_mat.depletable = burn[cell.name] + omc_mat.volume = volume[cell.name] + + cell.fill = omc_mat + + return geometry, lower_left, upper_right From d7f1904e41cb8e00578c476c7ec75a62d9174101 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 21 Feb 2018 10:15:42 -0600 Subject: [PATCH 55/68] Move simple chains into tests directory --- {chains => tests}/chain_simple.xml | 0 {chains => tests}/chain_test.xml | 0 tests/regression_tests/test_deplete_full.py | 2 +- tests/unit_tests/test_deplete_chain.py | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename {chains => tests}/chain_simple.xml (100%) rename {chains => tests}/chain_test.xml (100%) diff --git a/chains/chain_simple.xml b/tests/chain_simple.xml similarity index 100% rename from chains/chain_simple.xml rename to tests/chain_simple.xml diff --git a/chains/chain_test.xml b/tests/chain_test.xml similarity index 100% rename from chains/chain_test.xml rename to tests/chain_test.xml diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index 2286af908a..fb31ac0c03 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -43,7 +43,7 @@ def test_full(run_in_tmpdir): settings.verbosity = 3 # Create operator - chain_file = Path(__file__).parents[2] / 'chains' / 'chain_simple.xml' + chain_file = Path(__file__).parents[1] / 'chain_simple.xml' op = openmc.deplete.Operator(geometry, settings, chain_file) op.round_number = True diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 7ac3e2f8dd..8fc01b4270 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -8,7 +8,7 @@ import numpy as np from openmc.deplete import comm, Chain, reaction_rates, nuclide -_test_filename = str(Path(__file__).parents[2] / 'chains' / 'chain_test.xml') +_test_filename = str(Path(__file__).parents[1] / 'chain_test.xml') def test_init(): From 7ef5174274fb4fb034d0fb710db6e5b5a1756858 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 21 Feb 2018 10:31:57 -0600 Subject: [PATCH 56/68] Move test chain directly into test_deplete_chain --- tests/chain_test.xml | 23 ------------ tests/unit_tests/test_deplete_chain.py | 48 ++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 30 deletions(-) delete mode 100644 tests/chain_test.xml diff --git a/tests/chain_test.xml b/tests/chain_test.xml deleted file mode 100644 index c8c75ad7b3..0000000000 --- a/tests/chain_test.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - 0.0253 - - A B - 0.0292737 0.002566345 - - - - diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 8fc01b4270..f7e7899a30 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -6,9 +6,44 @@ from pathlib import Path import numpy as np from openmc.deplete import comm, Chain, reaction_rates, nuclide +import pytest + +from tests import cdtemp -_test_filename = str(Path(__file__).parents[1] / 'chain_test.xml') +_TEST_CHAIN = """\ + + + + + + + + + + + + + + + + 0.0253 + + A B + 0.0292737 0.002566345 + + + + +""" + + +@pytest.fixture(scope='module') +def simple_chain(): + with cdtemp(): + with open('chain_test.xml', 'w') as fh: + fh.write(_TEST_CHAIN) + yield Chain.from_xml('chain_test.xml') def test_init(): @@ -33,13 +68,13 @@ def test_from_endf(): pass -def test_from_xml(): +def test_from_xml(simple_chain): """Read chain_test.xml and ensure all values are correct.""" # Unfortunately, this routine touches a lot of the code, but most of # the components external to depletion_chain.py are simple storage # types. - chain = Chain.from_xml(_test_filename) + chain = simple_chain # Basic checks assert len(chain) == 3 @@ -125,16 +160,15 @@ def test_export_to_xml(run_in_tmpdir): chain.nuclides = [A, B, C] chain.export_to_xml(filename) - original = open(_test_filename, 'r').read() chain_xml = open(filename, 'r').read() - assert original == chain_xml + assert _TEST_CHAIN == chain_xml -def test_form_matrix(): +def test_form_matrix(simple_chain): """ Using chain_test, and a dummy reaction rate, compute the matrix. """ # Relies on test_from_xml passing. - chain = Chain.from_xml(_test_filename) + chain = simple_chain mats = ["10000", "10001"] nuclides = ["A", "B", "C"] From 92697ec06eae659cfd3a801d7bca52efee81cb2d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 21 Feb 2018 15:36:27 -0600 Subject: [PATCH 57/68] Introduce ResultsList class to replace scattered functions --- docs/source/pythonapi/deplete.rst | 2 +- openmc/deplete/__init__.py | 2 +- openmc/deplete/integrator/__init__.py | 1 - openmc/deplete/integrator/cecm.py | 16 +-- openmc/deplete/integrator/predictor.py | 12 +- openmc/deplete/integrator/save_results.py | 42 ------- openmc/deplete/results.py | 93 +++++++++------- openmc/deplete/results_list.py | 105 ++++++++++++++++++ openmc/deplete/utilities.py | 101 ----------------- tests/dummy_operator.py | 42 ++++--- tests/regression_tests/test_deplete_full.py | 10 +- tests/unit_tests/test_deplete_cecm.py | 8 +- tests/unit_tests/test_deplete_integrator.py | 12 +- tests/unit_tests/test_deplete_predictor.py | 8 +- .../test_deplete_resultslist.py} | 28 ++--- 15 files changed, 220 insertions(+), 262 deletions(-) delete mode 100644 openmc/deplete/integrator/save_results.py create mode 100644 openmc/deplete/results_list.py delete mode 100644 openmc/deplete/utilities.py rename tests/{regression_tests/test_deplete_utilities.py => unit_tests/test_deplete_resultslist.py} (62%) diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 1d1a1c0ee5..61c5dd18c4 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -59,6 +59,7 @@ data, such as number densities and reaction rates for each material. OperatorResult ReactionRates Results + ResultsList TransportOperator Each of the integrator functions also relies on a number of "helper" functions @@ -71,4 +72,3 @@ as follows: integrator.CRAM16 integrator.CRAM48 - integrator.save_results diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 33b6d9af14..e49c4e69cf 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -20,5 +20,5 @@ from .operator import * from .reaction_rates import * from .abc import * from .results import * +from .results_list import * from .integrator import * -from .utilities import * diff --git a/openmc/deplete/integrator/__init__.py b/openmc/deplete/integrator/__init__.py index 607650dc69..cf8caffdfe 100644 --- a/openmc/deplete/integrator/__init__.py +++ b/openmc/deplete/integrator/__init__.py @@ -8,4 +8,3 @@ The integrator subcomponents. from .cecm import * from .cram import * from .predictor import * -from .save_results import * diff --git a/openmc/deplete/integrator/cecm.py b/openmc/deplete/integrator/cecm.py index 9a07cd198f..61b58d0b95 100644 --- a/openmc/deplete/integrator/cecm.py +++ b/openmc/deplete/integrator/cecm.py @@ -4,7 +4,7 @@ import copy from collections.abc import Iterable from .cram import deplete -from .save_results import save_results +from ..results import Results def cecm(operator, timesteps, power, print_out=True): @@ -51,20 +51,20 @@ def cecm(operator, timesteps, power, print_out=True): for i, (dt, p) in enumerate(zip(timesteps, power)): # Get beginning-of-timestep reaction rates x = [copy.deepcopy(vec)] - results = [operator(x[0], p)] + op_results = [operator(x[0], p)] # Deplete for first half of timestep - x_middle = deplete(chain, x[0], results[0], dt/2, print_out) + x_middle = deplete(chain, x[0], op_results[0], dt/2, print_out) # Get middle-of-timestep reaction rates x.append(x_middle) - results.append(operator(x_middle, p)) + op_results.append(operator(x_middle, p)) # Deplete for full timestep using beginning-of-step materials - x_end = deplete(chain, x[0], results[1], dt, print_out) + x_end = deplete(chain, x[0], op_results[1], dt, print_out) # Create results, write to disk - save_results(operator, x, results, [t, t + dt], i) + Results.save(operator, x, op_results, [t, t + dt], i) # Advance time, update vector t += dt @@ -72,7 +72,7 @@ def cecm(operator, timesteps, power, print_out=True): # Perform one last simulation x = [copy.deepcopy(vec)] - results = [operator(x[0], power[-1])] + op_results = [operator(x[0], power[-1])] # Create results, write to disk - save_results(operator, x, results, [t, t], len(timesteps)) + Results.save(operator, x, op_results, [t, t], len(timesteps)) diff --git a/openmc/deplete/integrator/predictor.py b/openmc/deplete/integrator/predictor.py index 444ca7baa7..9be992c16a 100644 --- a/openmc/deplete/integrator/predictor.py +++ b/openmc/deplete/integrator/predictor.py @@ -4,7 +4,7 @@ import copy from collections.abc import Iterable from .cram import deplete -from .save_results import save_results +from ..results import Results def predictor(operator, timesteps, power, print_out=True): @@ -46,13 +46,13 @@ def predictor(operator, timesteps, power, print_out=True): for i, (dt, p) in enumerate(zip(timesteps, power)): # Get beginning-of-timestep reaction rates x = [copy.deepcopy(vec)] - results = [operator(x[0], p)] + op_results = [operator(x[0], p)] # Create results, write to disk - save_results(operator, x, results, [t, t + dt], i) + Results.save(operator, x, op_results, [t, t + dt], i) # Deplete for full timestep - x_end = deplete(chain, x[0], results[0], dt, print_out) + x_end = deplete(chain, x[0], op_results[0], dt, print_out) # Advance time, update vector t += dt @@ -60,7 +60,7 @@ def predictor(operator, timesteps, power, print_out=True): # Perform one last simulation x = [copy.deepcopy(vec)] - results = [operator(x[0], power[-1])] + op_results = [operator(x[0], power[-1])] # Create results, write to disk - save_results(operator, x, results, [t, t], len(timesteps)) + Results.save(operator, x, op_results, [t, t], len(timesteps)) diff --git a/openmc/deplete/integrator/save_results.py b/openmc/deplete/integrator/save_results.py deleted file mode 100644 index 98245fdeae..0000000000 --- a/openmc/deplete/integrator/save_results.py +++ /dev/null @@ -1,42 +0,0 @@ -""" Generic result saving code for integrators. - -""" -from ..results import Results, write_results - - -def save_results(op, x, op_results, t, step_ind): - """Creates and writes depletion results to disk - - Parameters - ---------- - op : openmc.deplete.TransportOperator - The operator used to generate these results. - x : list of list of numpy.array - The prior x vectors. Indexed [i][cell] using the above equation. - op_results : list of openmc.deplete.OperatorResult - Results of applying transport operator - t : list of float - Time indices. - step_ind : int - Step index. - - """ - # Get indexing terms - vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info() - - # Create results - stages = len(x) - results = Results() - results.allocate(vol_dict, nuc_list, burn_list, full_burn_list, stages) - - n_mat = len(burn_list) - - for i in range(stages): - for mat_i in range(n_mat): - results[i, mat_i, :] = x[i][mat_i][:] - - results.k = [r.k for r in op_results] - results.rates = [r.rates for r in op_results] - results.time = t - - write_results(results, "depletion_results.h5", step_ind) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index b7e5623c7d..ab740e61ec 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -11,7 +11,6 @@ import h5py from . import comm, have_mpi from .reaction_rates import ReactionRates -from openmc.checkvalue import check_filetype_version _VERSION_RESULTS = (1, 0) @@ -146,6 +145,27 @@ class Results(object): # Create storage array self.data = np.zeros((stages, self.n_mat, self.n_nuc)) + def export_to_hdf5(self, filename, step): + """Export results to an HDF5 file + + Parameters + ---------- + filename : str + The filename to write to + step : int + What step is this? + + """ + if have_mpi and h5py.get_config().mpi: + kwargs = {'driver': 'mpio', 'comm': comm} + else: + kwargs = {} + + kwargs['mode'] = "w" if step == 0 else "a" + + with h5py.File(filename, **kwargs) as handle: + self._to_hdf5(handle, step) + def _write_hdf5_metadata(self, handle): """Writes result metadata in HDF5 file @@ -217,7 +237,7 @@ class Results(object): handle.create_dataset("time", (1, 2), maxshape=(None, 2), dtype='float64') - def to_hdf5(self, handle, index): + def _to_hdf5(self, handle, index): """Converts results object into an hdf5 object. Parameters @@ -338,49 +358,40 @@ class Results(object): return results + @staticmethod + def save(op, x, op_results, t, step_ind): + """Creates and writes depletion results to disk -def write_results(result, filename, step): - """Outputs result to an HDF5 file. + Parameters + ---------- + op : openmc.deplete.TransportOperator + The operator used to generate these results. + x : list of list of numpy.array + The prior x vectors. Indexed [i][cell] using the above equation. + op_results : list of openmc.deplete.OperatorResult + Results of applying transport operator + t : list of float + Time indices. + step_ind : int + Step index. - Parameters - ---------- - result : Results - Object to be stored in a file. - filename : String - Target filename. - step : int - What step is this? + """ + # Get indexing terms + vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info() - """ - if have_mpi and h5py.get_config().mpi: - kwargs = {'driver': 'mpio', 'comm': comm} - else: - kwargs = {} + # Create results + stages = len(x) + results = Results() + results.allocate(vol_dict, nuc_list, burn_list, full_burn_list, stages) - kwargs['mode'] = "w" if step == 0 else "a" + n_mat = len(burn_list) - with h5py.File(filename, **kwargs) as handle: - result.to_hdf5(handle, step) + for i in range(stages): + for mat_i in range(n_mat): + results[i, mat_i, :] = x[i][mat_i][:] + results.k = [r.k for r in op_results] + results.rates = [r.rates for r in op_results] + results.time = t -def read_results(filename): - """Return a list of Results objects from an HDF5 file. - - Parameters - ---------- - filename : str - The filename to read from. - - Returns - ------- - results : list of openmc.deplete.Results - The result objects. - - """ - with h5py.File(str(filename), "r") as fh: - check_filetype_version(fh, 'depletion results', _VERSION_RESULTS[0]) - - # Get number of results stored - n = fh["number"].value.shape[0] - - return [Results.from_hdf5(fh, i) for i in range(n)] + results.export_to_hdf5("depletion_results.h5", step_ind) diff --git a/openmc/deplete/results_list.py b/openmc/deplete/results_list.py new file mode 100644 index 0000000000..d3d45e955b --- /dev/null +++ b/openmc/deplete/results_list.py @@ -0,0 +1,105 @@ +import h5py +import numpy as np + +from .results import Results, _VERSION_RESULTS +from openmc.checkvalue import check_filetype_version + + +class ResultsList(list): + """A list of openmc.deplete.Results objects + + Parameters + ---------- + filename : str + The filename to read from. + + """ + def __init__(self, filename): + super().__init__() + with h5py.File(str(filename), "r") as fh: + check_filetype_version(fh, 'depletion results', _VERSION_RESULTS[0]) + + # Get number of results stored + n = fh["number"].value.shape[0] + + for i in range(n): + self.append(Results.from_hdf5(fh, i)) + + def get_atoms(self, mat, nuc): + """Get nuclide concentration over time from a single material + + Parameters + ---------- + mat : str + Material name to evaluate + nuc : str + Nuclide name to evaluate + + Returns + ------- + time : numpy.ndarray + Array of times in [s] + concentration : numpy.ndarray + Total number of atoms for specified nuclide + + """ + time = np.empty_like(self) + concentration = np.empty_like(self) + + # Evaluate value in each region + for i, result in enumerate(self): + time[i] = result.time[0] + concentration[i] = result[0, mat, nuc] + + return time, concentration + + def get_reaction_rate(self, mat, nuc, rx): + """Get reaction rate in a single material/nuclide over time + + Parameters + ---------- + mat : str + Material name to evaluate + nuc : str + Nuclide name to evaluate + rx : str + Reaction rate to evaluate + + Returns + ------- + time : numpy.ndarray + Array of times in [s] + rate : numpy.ndarray + Array of reaction rates + + """ + time = np.empty_like(self) + rate = np.empty_like(self) + + # Evaluate value in each region + for i, result in enumerate(self): + time[i] = result.time[0] + rate[i] = result.rates[0].get(mat, nuc, rx) * result[0, mat, nuc] + + return time, rate + + def get_eigenvalue(self): + """Evaluates the eigenvalue from a results list. + + Returns + ------- + time : numpy.ndarray + Array of times in [s] + eigenvalue : numpy.ndarray + k-eigenvalue at each time + + """ + time = np.empty_like(self) + eigenvalue = np.empty_like(self) + + # Get time/eigenvalue at each point + for i, result in enumerate(self): + time[i] = result.time[0] + eigenvalue[i] = result.k[0] + + return time, eigenvalue diff --git a/openmc/deplete/utilities.py b/openmc/deplete/utilities.py deleted file mode 100644 index 59d5305465..0000000000 --- a/openmc/deplete/utilities.py +++ /dev/null @@ -1,101 +0,0 @@ -"""The utilities module. - -Contains functions that can be used to post-process objects that come out of -the results module. -""" - -import numpy as np - - -def evaluate_single_nuclide(results, mat, nuc): - """Evaluates a single nuclide in a single material from a results list. - - Parameters - ---------- - results : list of results - The results to extract data from. Must be sorted and continuous. - mat : str - Material name to evaluate - nuc : str - Nuclide name to evaluate - - Returns - ------- - time : numpy.ndarray - Time vector - concentration : numpy.ndarray - Total number of atoms in the material - - """ - n_points = len(results) - time = np.zeros(n_points) - concentration = np.zeros(n_points) - - # Evaluate value in each region - for i, result in enumerate(results): - time[i] = result.time[0] - concentration[i] = result[0, mat, nuc] - - return time, concentration - - -def evaluate_reaction_rate(results, mat, nuc, rx): - """Return reaction rate in a single material/nuclide from a results list. - - Parameters - ---------- - results : list of openmc.deplete.Results - The results to extract data from. Must be sorted and continuous. - mat : str - Material name to evaluate - nuc : str - Nuclide name to evaluate - rx : str - Reaction rate to evaluate - - Returns - ------- - time : numpy.ndarray - Time vector. - rate : numpy.ndarray - Reaction rate. - - """ - n_points = len(results) - time = np.zeros(n_points) - rate = np.zeros(n_points) - # Evaluate value in each region - for i, result in enumerate(results): - time[i] = result.time[0] - rate[i] = result.rates[0].get(mat, nuc, rx) * result[0, mat, nuc] - - return time, rate - - -def evaluate_eigenvalue(results): - """Evaluates the eigenvalue from a results list. - - Parameters - ---------- - results : list of openmc.deplete.Results - The results to extract data from. Must be sorted and continuous. - - Returns - ------- - time : numpy.ndarray - Time vector. - eigenvalue : numpy.ndarray - Eigenvalue. - - """ - n_points = len(results) - time = np.zeros(n_points) - eigenvalue = np.zeros(n_points) - - # Evaluate value in each region - for i, result in enumerate(results): - - time[i] = result.time[0] - eigenvalue[i] = result.k[0] - - return time, eigenvalue diff --git a/tests/dummy_operator.py b/tests/dummy_operator.py index 706793d93f..05fe97a950 100644 --- a/tests/dummy_operator.py +++ b/tests/dummy_operator.py @@ -34,19 +34,15 @@ class DummyOperator(TransportOperator): Returns ------- - k : float - Zero. - rates : ReactionRates - Reaction rates from this simulation. - seed : int - Zero. + openmc.deplete.OperatorResult + Result of transport operator + """ + mats = ["1"] + nuclides = ["1", "2"] + reactions = ["1"] - cell_to_ind = {"1" : 0} - nuc_to_ind = {"1" : 0, "2" : 1} - react_to_ind = {"1" : 0} - - reaction_rates = ReactionRates(cell_to_ind, nuc_to_ind, react_to_ind) + reaction_rates = ReactionRates(mats, nuclides, reactions) reaction_rates[0, 0, 0] = vec[0][0] reaction_rates[0, 1, 0] = vec[0][1] @@ -105,18 +101,18 @@ class DummyOperator(TransportOperator): return ["1", "2"] @property - def burn_list(self): + def local_mats(self): """ - burn_list : list of str - A list of all cell IDs to be burned. Used for sorting the simulation. + local_mats : list of str + A list of all material IDs to be burned. Used for sorting the simulation. """ return ["1"] @property - def mat_tally_ind(self): + def burnable_mats(self): """Maps cell name to index in global geometry.""" - return {"1": 0} + return self.local_mats @property @@ -125,11 +121,11 @@ class DummyOperator(TransportOperator): reaction_rates : ReactionRates Reaction rates from the last operator step. """ - cell_to_ind = {"1" : 0} - nuc_to_ind = {"1" : 0, "2" : 1} - react_to_ind = {"1" : 0} + mats = ["1"] + nuclides = ["1", "2"] + reactions = ["1"] - return ReactionRates(cell_to_ind, nuc_to_ind, react_to_ind) + return ReactionRates(mats, nuclides, reactions) def initial_condition(self): """Returns initial vector. @@ -153,8 +149,8 @@ class DummyOperator(TransportOperator): A list of all nuclide names. Used for sorting the simulation. burn_list : list of int A list of all cell IDs to be burned. Used for sorting the simulation. - full_burn_dict : OrderedDict of str to int + full_burn_list : OrderedDict of str to int Maps cell name to index in global geometry. - """ - return self.volume, self.nuc_list, self.burn_list, self.mat_tally_ind + """ + return self.volume, self.nuc_list, self.local_mats, self.burnable_mats diff --git a/tests/regression_tests/test_deplete_full.py b/tests/regression_tests/test_deplete_full.py index fb31ac0c03..19e2c7664b 100644 --- a/tests/regression_tests/test_deplete_full.py +++ b/tests/regression_tests/test_deplete_full.py @@ -8,8 +8,6 @@ import numpy as np import openmc from openmc.data import JOULE_PER_EV import openmc.deplete -from openmc.deplete import results -from openmc.deplete import utilities from tests.regression_tests import config from .example_geometry import generate_problem @@ -67,8 +65,8 @@ def test_full(run_in_tmpdir): return # Load the reference/test results - res_test = results.read_results(path_test) - res_ref = results.read_results(path_reference) + res_test = openmc.deplete.ResultsList(path_test) + res_ref = openmc.deplete.ResultsList(path_reference) # Assert same mats for mat in res_ref[0].mat_to_ind: @@ -88,8 +86,8 @@ def test_full(run_in_tmpdir): tol = 1.0e-6 for mat in res_test[0].mat_to_ind: for nuc in res_test[0].nuc_to_ind: - _, y_test = utilities.evaluate_single_nuclide(res_test, mat, nuc) - _, y_old = utilities.evaluate_single_nuclide(res_ref, mat, nuc) + _, y_test = res_test.get_atoms(mat, nuc) + _, y_old = res_ref.get_atoms(mat, nuc) # Test each point correct = True diff --git a/tests/unit_tests/test_deplete_cecm.py b/tests/unit_tests/test_deplete_cecm.py index 6deb6cd3dd..466a2eec74 100644 --- a/tests/unit_tests/test_deplete_cecm.py +++ b/tests/unit_tests/test_deplete_cecm.py @@ -5,8 +5,6 @@ These tests integrate a simple test problem described in dummy_geometry.py. from pytest import approx import openmc.deplete -from openmc.deplete import results -from openmc.deplete import utilities from tests import dummy_operator @@ -23,10 +21,10 @@ def test_cecm(run_in_tmpdir): openmc.deplete.cecm(op, dt, power, print_out=False) # Load the files - res = results.read_results(op.output_dir / "depletion_results.h5") + res = openmc.deplete.ResultsList(op.output_dir / "depletion_results.h5") - _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") - _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") + _, y1 = res.get_atoms("1", "1") + _, y2 = res.get_atoms("1", "2") # Mathematica solution s1 = [1.86872629872102, 1.395525772416039] diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index b209900889..a1768625b2 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -1,4 +1,4 @@ -"""Tests for integrator.py +"""Tests for saving results It is worth noting that openmc.deplete.integrate is extremely complex, to the point I am unsure if it can be reasonably unit-tested. For the time being, it @@ -11,11 +11,11 @@ import os from unittest.mock import MagicMock import numpy as np -from openmc.deplete import (integrator, ReactionRates, results, comm, +from openmc.deplete import (ReactionRates, Results, ResultsList, comm, OperatorResult) -def test_save_results(run_in_tmpdir): +def test_results_save(run_in_tmpdir): """Test data save module""" stages = 3 @@ -72,11 +72,11 @@ def test_save_results(run_in_tmpdir): op_result1 = [OperatorResult(k, rates) for k, rates in zip(eigvl1, rate1)] op_result2 = [OperatorResult(k, rates) for k, rates in zip(eigvl2, rate2)] - integrator.save_results(op, x1, op_result1, t1, 0) - integrator.save_results(op, x2, op_result2, t2, 1) + Results.save(op, x1, op_result1, t1, 0) + Results.save(op, x2, op_result2, t2, 1) # Load the files - res = results.read_results("depletion_results.h5") + res = ResultsList("depletion_results.h5") for i in range(stages): for mat_i, mat in enumerate(burn_list): diff --git a/tests/unit_tests/test_deplete_predictor.py b/tests/unit_tests/test_deplete_predictor.py index 0d283855cb..50803e5085 100644 --- a/tests/unit_tests/test_deplete_predictor.py +++ b/tests/unit_tests/test_deplete_predictor.py @@ -5,8 +5,6 @@ These tests integrate a simple test problem described in dummy_geometry.py. from pytest import approx import openmc.deplete -from openmc.deplete import results -from openmc.deplete import utilities from tests import dummy_operator @@ -23,10 +21,10 @@ def test_predictor(run_in_tmpdir): openmc.deplete.predictor(op, dt, power, print_out=False) # Load the files - res = results.read_results(op.output_dir / "depletion_results.h5") + res = openmc.deplete.ResultsList(op.output_dir / "depletion_results.h5") - _, y1 = utilities.evaluate_single_nuclide(res, "1", "1") - _, y2 = utilities.evaluate_single_nuclide(res, "1", "2") + _, y1 = res.get_atoms("1", "1") + _, y2 = res.get_atoms("1", "2") # Mathematica solution s1 = [2.46847546272295, 0.986431226850467] diff --git a/tests/regression_tests/test_deplete_utilities.py b/tests/unit_tests/test_deplete_resultslist.py similarity index 62% rename from tests/regression_tests/test_deplete_utilities.py rename to tests/unit_tests/test_deplete_resultslist.py index 82d4d56a4c..aad8cd9f68 100644 --- a/tests/regression_tests/test_deplete_utilities.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -1,26 +1,22 @@ -""" Tests the utilities classes. - -This also tests the results read/write code. -""" +"""Tests the ResultsList class""" from pathlib import Path import numpy as np import pytest -from openmc.deplete import results -from openmc.deplete import utilities +import openmc.deplete @pytest.fixture def res(): """Load the reference results""" - filename = Path(__file__).with_name('test_reference.h5') - return results.read_results(filename) + filename = Path(__file__).parents[1] / 'regression_tests' / 'test_reference.h5' + return openmc.deplete.ResultsList(filename) -def test_evaluate_single_nuclide(res): - """Tests evaluating single nuclide utility code.""" - t, n = utilities.evaluate_single_nuclide(res, "1", "Xe135") +def test_get_atoms(res): + """Tests evaluating single nuclide concentration.""" + t, n = res.get_atoms("1", "Xe135") t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] n_ref = [6.6747328233649218e+08, 3.5421791038348462e+14, @@ -29,9 +25,9 @@ def test_evaluate_single_nuclide(res): np.testing.assert_array_equal(t, t_ref) np.testing.assert_array_equal(n, n_ref) -def test_evaluate_reaction_rate(res): - """Tests evaluating reaction rate utility code.""" - t, r = utilities.evaluate_reaction_rate(res, "1", "Xe135", "(n,gamma)") +def test_get_reaction_rate(res): + """Tests evaluating reaction rate.""" + t, r = res.get_reaction_rate("1", "Xe135", "(n,gamma)") t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] n_ref = np.array([6.6747328233649218e+08, 3.5421791038348462e+14, @@ -43,9 +39,9 @@ def test_evaluate_reaction_rate(res): np.testing.assert_array_equal(r, n_ref * xs_ref) -def test_evaluate_eigenvalue(res): +def test_get_eigenvalue(res): """Tests evaluating eigenvalue.""" - t, k = utilities.evaluate_eigenvalue(res) + t, k = res.get_eigenvalue() t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] k_ref = [1.181281798790367, 1.1798750921988739, 1.1965943696058159, From 7622a1a39472a64fac950930423b6bedd2b8ff9a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 22 Feb 2018 16:00:32 -0600 Subject: [PATCH 58/68] Add methods on Material to get mass (if volume specified) --- docs/source/pythonapi/data.rst | 1 + openmc/data/data.py | 20 ++++++++++ openmc/deplete/chain.py | 13 +----- openmc/material.py | 64 +++++++++++++++++++++++++++++- tests/unit_tests/test_data_misc.py | 8 ++++ tests/unit_tests/test_material.py | 17 ++++++++ 6 files changed, 110 insertions(+), 13 deletions(-) diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 3c221906db..52dc5173b0 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -36,6 +36,7 @@ Core Functions openmc.data.thin openmc.data.water_density openmc.data.write_compact_458_library + openmc.data.zam Angle-Energy Distributions -------------------------- diff --git a/openmc/data/data.py b/openmc/data/data.py index 523ac9769d..70bc00bd92 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -313,6 +313,26 @@ def water_density(temperature, pressure=0.1013): return coeff / pi / gamma1_pi +def zam(name): + """Return tuple of (atomic number, mass number, metastable state) + + Parameters + ---------- + name : str + Name of nuclide using GND convention, e.g., 'Am242m1' + + Returns + ------- + 3-tuple of int + Atomic number, mass number, and metastable state + + """ + symbol, A, state = re.match(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)', + name).groups() + metastable = int(state[2:]) if state else 0 + return (ATOMIC_NUMBER[symbol], int(A), metastable) + + # Values here are from the Committee on Data for Science and Technology # (CODATA) 2014 recommendation (doi:10.1103/RevModPhys.88.035009). diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 23395329eb..bcef5fff11 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -40,15 +40,6 @@ _REACTIONS = [ ] -def _get_zai(s): - """Get ZAI value (10000*z + 10*A + metastable state) for sorting purposes""" - symbol, A, state = re.match(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)', s).groups() - Z = openmc.data.ATOMIC_NUMBER[symbol] - A = int(A) - state = int(state[2:]) if state else 0 - return 10000*Z + 10*A + state - - def replace_missing(product, decay_data): """Replace missing product with suitable decay daughter. @@ -197,7 +188,7 @@ class Chain(object): missing_fpy = [] missing_fp = [] - for idx, parent in enumerate(sorted(decay_data, key=_get_zai)): + for idx, parent in enumerate(sorted(decay_data, key=openmc.data.zam)): data = decay_data[parent] nuclide = Nuclide() @@ -290,7 +281,7 @@ class Chain(object): missing_fp.append((parent, E, yield_replace)) nuclide.yield_data[E] = [] - for k in sorted(yields, key=_get_zai): + for k in sorted(yields, key=openmc.data.zam): nuclide.yield_data[E].append((k, yields[k])) # Display warnings diff --git a/openmc/material.py b/openmc/material.py index e409d6536c..351e2db272 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -52,8 +52,7 @@ class Material(IDManagerMixin): 'atom/b-cm', 'atom/cm3', 'sum', or 'macro'. The 'macro' unit only applies in the case of a multi-group calculation. depletable : bool - Indicate whether the material is depletable. This attribute can be used - by downstream depletion applications. + Indicate whether the material is depletable. nuclides : list of tuple List in which each item is a 3-tuple consisting of a nuclide string, the percent density, and the percent type ('ao' or 'wo'). @@ -74,6 +73,9 @@ class Material(IDManagerMixin): :meth:`Geometry.determine_paths` method. num_instances : int The number of instances of this material throughout the geometry. + fissionable_mass : float + Mass of fissionable nuclides in the material in [g]. Requires that the + :attr:`volume` attribute is set. """ @@ -245,6 +247,18 @@ class Material(IDManagerMixin): str) self._isotropic = list(isotropic) + @property + def fissionable_mass(self): + if self.volume is None: + raise ValueError("Volume must be set in order to determine mass.") + density = 0.0 + for nuc, atoms_per_cc in self.get_nuclide_atom_densities().values(): + Z = openmc.data.zam(nuc)[0] + if Z >= 90: + density += 1e24 * atoms_per_cc * openmc.data.atomic_mass(nuc) \ + / openmc.data.AVOGADRO + return density*self.volume + @classmethod def from_hdf5(cls, group): """Create material from HDF5 group @@ -687,7 +701,53 @@ class Material(IDManagerMixin): return nuclides + def get_mass_density(self, nuclide=None): + """Return mass density of one or all nuclides + + Parameters + ---------- + nuclides : str, optional + Nuclide for which density is desired. If not specified, the density + for the entire material is given. + + Returns + ------- + float + Density of the nuclide/material in [g/cm^3] + + """ + mass_density = 0.0 + for nuc, atoms_per_cc in self.get_nuclide_atom_densities().values(): + density_i = 1e24 * atoms_per_cc * openmc.data.atomic_mass(nuc) \ + / openmc.data.AVOGADRO + if nuclide is None or nuclide == nuc: + mass_density += density_i + return mass_density + + def get_mass(self, nuclide=None): + """Return mass of one or all nuclides. + + Note that this method requires that the :attr:`Material.volume` has + already been set. + + Parameters + ---------- + nuclides : str, optional + Nuclide for which mass is desired. If not specified, the density + for the entire material is given. + + Returns + ------- + float + Mass of the nuclide/material in [g] + + """ + if self.volume is None: + raise ValueError("Volume must be set in order to determine mass.") + return self.volume*self.get_mass_density(nuclide) + def clone(self, memo=None): + """Create a copy of this material with a new unique ID. Parameters diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index 433d34adb9..7d00b7bc77 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -58,3 +58,11 @@ def test_water_density(): assert dens(300.0, 3.0) == pytest.approx(1e-3/0.100215168e-2, 1e-6) assert dens(300.0, 80.0) == pytest.approx(1e-3/0.971180894e-3, 1e-6) assert dens(500.0, 3.0) == pytest.approx(1e-3/0.120241800e-2, 1e-6) + + +def test_zam(): + assert openmc.data.zam('H1') == (1, 1, 0) + assert openmc.data.zam('Zr90') == (40, 90, 0) + assert openmc.data.zam('Am242') == (95, 242, 0) + assert openmc.data.zam('Am242_m1') == (95, 242, 1) + assert openmc.data.zam('Am242_m10') == (95, 242, 10) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 2265215417..c251df3a6d 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -121,6 +121,23 @@ def test_get_nuclide_atom_densities(uo2): assert density > 0 +def test_mass(): + m = openmc.Material() + m.add_nuclide('Zr90', 1.0, 'wo') + m.add_nuclide('U235', 1.0, 'wo') + m.set_density('g/cm3', 2.0) + m.volume = 10.0 + + assert m.get_mass_density('Zr90') == pytest.approx(1.0) + assert m.get_mass_density('U235') == pytest.approx(1.0) + assert m.get_mass_density() == pytest.approx(2.0) + + assert m.get_mass('Zr90') == pytest.approx(10.0) + assert m.get_mass('U235') == pytest.approx(10.0) + assert m.get_mass() == pytest.approx(20.0) + assert m.fissionable_mass == pytest.approx(10.0) + + def test_materials(run_in_tmpdir): m1 = openmc.Material() m1.add_nuclide('U235', 1.0, 'wo') From ab00421c0eda42adb29a90e49e08230cbebaa9a5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 23 Feb 2018 11:37:55 -0600 Subject: [PATCH 59/68] Add test for Chain.from_endf. Remove tqdm dependence. --- docs/source/conf.py | 2 +- openmc/data/endf.py | 5 +-- openmc/deplete/chain.py | 42 ++++++++++++-------------- setup.py | 2 +- tests/unit_tests/test_deplete_chain.py | 14 +++++++-- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index a2fec39b75..eeecba23e4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ MOCK_MODULES = [ 'numpy.ctypeslib', 'scipy', 'scipy.sparse', 'scipy.sparse.linalg', 'scipy.interpolate', 'scipy.integrate', 'scipy.optimize', 'scipy.special', 'scipy.stats', 'scipy.spatial', 'h5py', 'pandas', 'uncertainties', - 'matplotlib', 'matplotlib.pyplot', 'tqdm', 'openmoc', + 'matplotlib', 'matplotlib.pyplot', 'openmoc', 'openmc.data.reconstruct' ] sys.modules.update((mod_name, MagicMock()) for mod_name in MOCK_MODULES) diff --git a/openmc/data/endf.py b/openmc/data/endf.py index 160ab61513..db94e15bea 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -10,6 +10,7 @@ import io import re import os from math import pi +from pathlib import PurePath from collections import OrderedDict from collections.abc import Iterable @@ -299,8 +300,8 @@ class Evaluation(object): """ def __init__(self, filename_or_obj): - if isinstance(filename_or_obj, str): - fh = open(filename_or_obj, 'r') + if isinstance(filename_or_obj, (str, PurePath)): + fh = open(str(filename_or_obj), 'r') else: fh = filename_or_obj self.section = {} diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index bcef5fff11..612ec29cb9 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -20,7 +20,6 @@ try: except ImportError: import xml.etree.ElementTree as ET _have_lxml = False -from tqdm import tqdm import scipy.sparse as sp import openmc.data @@ -153,34 +152,31 @@ class Chain(object): chain = cls() # Create dictionary mapping target to filename + print('Processing neutron sub-library files...') reactions = {} - with tqdm(neutron_files) as pbar: - for f in pbar: - pbar.set_description('Processing {}'.format(os.path.basename(f))) - evaluation = openmc.data.endf.Evaluation(f) - name = evaluation.gnd_name - reactions[name] = {} - for mf, mt, nc, mod in evaluation.reaction_list: - if mf == 3: - file_obj = StringIO(evaluation.section[3, mt]) - openmc.data.endf.get_head_record(file_obj) - q_value = openmc.data.endf.get_cont_record(file_obj)[1] - reactions[name][mt] = q_value + for f in neutron_files: + evaluation = openmc.data.endf.Evaluation(f) + name = evaluation.gnd_name + reactions[name] = {} + for mf, mt, nc, mod in evaluation.reaction_list: + if mf == 3: + file_obj = StringIO(evaluation.section[3, mt]) + openmc.data.endf.get_head_record(file_obj) + q_value = openmc.data.endf.get_cont_record(file_obj)[1] + reactions[name][mt] = q_value # Determine what decay and FPY nuclides are available + print('Processing decay sub-library files...') decay_data = {} - with tqdm(decay_files) as pbar: - for f in pbar: - pbar.set_description('Processing {}'.format(os.path.basename(f))) - data = openmc.data.Decay(f) - decay_data[data.nuclide['name']] = data + for f in decay_files: + data = openmc.data.Decay(f) + decay_data[data.nuclide['name']] = data + print('Processing fission product yield sub-library files...') fpy_data = {} - with tqdm(fpy_files) as pbar: - for f in pbar: - pbar.set_description('Processing {}'.format(os.path.basename(f))) - data = openmc.data.FissionProductYields(f) - fpy_data[data.nuclide['name']] = data + for f in fpy_files: + data = openmc.data.FissionProductYields(f) + fpy_data[data.nuclide['name']] = data print('Creating depletion_chain...') missing_daughter = [] diff --git a/setup.py b/setup.py index ee11f414b6..2a42dd65dd 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ kwargs = { # Required dependencies 'install_requires': [ 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', - 'pandas', 'lxml', 'uncertainties', 'tqdm' + 'pandas', 'lxml', 'uncertainties' ], # Optional dependencies diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index f7e7899a30..4ec5415ee9 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -5,11 +5,13 @@ import os from pathlib import Path import numpy as np +from openmc.data import zam, ATOMIC_SYMBOL from openmc.deplete import comm, Chain, reaction_rates, nuclide import pytest from tests import cdtemp +_ENDF_DATA = Path(os.environ['OPENMC_ENDF_DATA']) _TEST_CHAIN = """\ @@ -63,9 +65,15 @@ def test_len(): def test_from_endf(): - """Test depletion chain building from ENDF. Empty at the moment until we figure - out a good way to unit-test this.""" - pass + """Test depletion chain building from ENDF files""" + decay_data = (_ENDF_DATA / 'decay').glob('*.endf') + fpy_data = (_ENDF_DATA / 'nfy').glob('*.endf') + neutron_data = (_ENDF_DATA / 'neutrons').glob('*.endf') + chain = Chain.from_endf(decay_data, fpy_data, neutron_data) + + assert len(chain) == len(chain.nuclides) == len(chain.nuclide_dict) == 3821 + for nuc in chain.nuclides: + assert nuc == chain[nuc.name] def test_from_xml(simple_chain): From fc73f195a520bf39772d86fd8d6260f40c5842fc Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 23 Feb 2018 11:54:16 -0600 Subject: [PATCH 60/68] Mention mpi4py as optional dependency in docs --- docs/source/usersguide/install.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 9b11d1dce4..24bcc7164d 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -452,6 +452,11 @@ distributions. .. admonition:: Optional :class: note + `mpi4py `_ + mpi4py provides Python bindings to MPI for running distributed-memory + parallel runs. This package is needed if you plan on running depletion + simulations in parallel using MPI. + `Cython `_ Cython is used for resonance reconstruction for ENDF data converted to :class:`openmc.data.IncidentNeutron`. From 13a167393d9bccb173e0c1e6584cbecf5b6e8ef0 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 23 Feb 2018 14:59:13 -0600 Subject: [PATCH 61/68] Add archive destination in CMakeLists.txt for building static --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 71934a47cc..d3673df26c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -496,7 +496,8 @@ add_custom_command(TARGET libopenmc POST_BUILD install(TARGETS ${program} libopenmc RUNTIME DESTINATION bin - LIBRARY DESTINATION lib) + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib) install(DIRECTORY src/relaxng DESTINATION share/openmc) install(FILES man/man1/openmc.1 DESTINATION share/man/man1) install(FILES LICENSE DESTINATION "share/doc/${program}" RENAME copyright) From 09a63ec3e71734daf1ae88f5be1b286b45396f04 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 24 Feb 2018 15:07:58 -0600 Subject: [PATCH 62/68] Remove blank line that was added --- openmc/material.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 351e2db272..5fdcb76894 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -747,7 +747,6 @@ class Material(IDManagerMixin): return self.volume*self.get_mass_density(nuclide) def clone(self, memo=None): - """Create a copy of this material with a new unique ID. Parameters From cc57551bd3746f6e1a26a85f309193031474f27f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 25 Feb 2018 15:22:30 -0600 Subject: [PATCH 63/68] Fix bug in Model.run() --- openmc/model/model.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index d6e6ddce38..72a2c50dd7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -209,9 +209,7 @@ class Model(object): """ self.export_to_xml() - return_code = openmc.run(**kwargs) - - assert (return_code == 0), "OpenMC did not execute successfully" + openmc.run(**kwargs) n = self.settings.batches if self.settings.statepoint is not None: From e3d6189cfa163579396377df7a91d0f76a2fc043 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 26 Feb 2018 22:12:14 -0600 Subject: [PATCH 64/68] Make sure Decay.half_life is set, even for stable nuclides --- openmc/data/decay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index d83338d028..fa18759396 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -457,6 +457,7 @@ class Decay(EqualityMixin): items, values = get_list_record(file_obj) self.nuclide['spin'] = items[0] self.nuclide['parity'] = items[1] + self.half_life = ufloat(float('inf'), float('inf')) @property def decay_constant(self): From 5993a59196119db37ec9b7183887055480a26808 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 27 Feb 2018 07:22:27 -0600 Subject: [PATCH 65/68] Address first round of comments on #976 --- docs/source/io_formats/depletion_results.rst | 10 +++++----- docs/source/pythonapi/deplete.rst | 11 +++++++++++ openmc/data/data.py | 8 ++++++-- openmc/deplete/atom_number.py | 2 +- openmc/deplete/chain.py | 9 +++------ tests/unit_tests/test_data_misc.py | 2 ++ 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/docs/source/io_formats/depletion_results.rst b/docs/source/io_formats/depletion_results.rst index fcbc4fb99b..d35e251469 100644 --- a/docs/source/io_formats/depletion_results.rst +++ b/docs/source/io_formats/depletion_results.rst @@ -12,23 +12,23 @@ The current version of the depletion results file format is 1.0. - **version** (*int[2]*) -- Major and minor version of the statepoint file format. -:Datasets: - **eigenvalues** (*float[][]*) -- k-eigenvalues at each +:Datasets: - **eigenvalues** (*double[][]*) -- k-eigenvalues at each time/stage. This array has shape (number of timesteps, number of stages). - - **number** (*float[][][][]*) -- Total number of atoms. This array + - **number** (*double[][][][]*) -- Total number of atoms. This array has shape (number of timesteps, number of stages, number of materials, number of nuclides). - - **reaction rates** (*float[][][][][]*) -- Reaction rates used to + - **reaction rates** (*double[][][][][]*) -- Reaction rates used to build depletion matrices. This array has shape (number of timesteps, number of stages, number of materials, number of nuclides, number of reactions). - - **time** (*float[][2]*) -- Time in [s] at beginning/end of each + - **time** (*double[][2]*) -- Time in [s] at beginning/end of each step. **/materials//** :Attributes: - **index** (*int*) -- Index used in results for this material - - **volume** (*float*) -- Volume of this material in [cm^3] + - **volume** (*double*) -- Volume of this material in [cm^3] **/nuclides//** diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 61c5dd18c4..d4055f0fdf 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -27,6 +27,17 @@ specific to OpenMC is available using the following class: Operator +When running in parallel using `mpi4py `_, the MPI +intercommunicator used can be changed by modifying the following module +variable. If it is not explicitly modified, it defaults to +``mpi4py.MPI.COMM_WORLD``. + +.. data:: comm + + MPI intercommunicator used to call OpenMC library + + :type: mpi4py.MPI.Comm + Internal Classes and Functions ------------------------------ diff --git a/openmc/data/data.py b/openmc/data/data.py index 70bc00bd92..d0bb65648c 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -327,8 +327,12 @@ def zam(name): Atomic number, mass number, and metastable state """ - symbol, A, state = re.match(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)', - name).groups() + try: + symbol, A, state = re.match(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)', + name).groups() + except AttributeError: + raise ValueError("'{}' does not appear to be a nuclide name in GND " + "format.".format(name)) metastable = int(state[2:]) if state else 0 return (ATOMIC_NUMBER[symbol], int(A), metastable) diff --git a/openmc/deplete/atom_number.py b/openmc/deplete/atom_number.py index 9a32dfa3a0..b5357280c0 100644 --- a/openmc/deplete/atom_number.py +++ b/openmc/deplete/atom_number.py @@ -41,7 +41,7 @@ class AtomNumber(object): n_nuc_burn : int Number of burnable nuclides. n_nuc : int - Number of nuclidess. + Number of nuclides. """ def __init__(self, local_mats, nuclides, volume, n_nuc_burn): diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 612ec29cb9..fbe6792227 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -55,15 +55,12 @@ def replace_missing(product, decay_data): Replacement for missing product in GND format. """ - - symbol, A, state = re.match(r'([A-Zn][a-z]*)(\d+)((?:_m\d+)?)', - product).groups() - Z = openmc.data.ATOMIC_NUMBER[symbol] - A = int(A) + # Determine atomic number, mass number, and metastable state + Z, A, state = openmc.data.zam(product) + symbol = openmc.data.ATOMIC_SYMBOL[Z] # First check if ground state is available if state: - metastable_state = int(state[2:]) product = '{}{}'.format(symbol, A) # Find isotope with longest half-life diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index 7d00b7bc77..6167449268 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -66,3 +66,5 @@ def test_zam(): assert openmc.data.zam('Am242') == (95, 242, 0) assert openmc.data.zam('Am242_m1') == (95, 242, 1) assert openmc.data.zam('Am242_m10') == (95, 242, 10) + with pytest.raises(ValueError): + openmc.data.zam('garbage') From 7ee08d5280c3195ba74102027f127f6ac18f26cb Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 1 Mar 2018 23:08:43 -0600 Subject: [PATCH 66/68] Add gnd_name function --- docs/source/pythonapi/data.rst | 1 + openmc/data/data.py | 26 +++++++++++++++++++++++++- openmc/data/endf.py | 13 +++++-------- openmc/material.py | 2 +- tests/unit_tests/test_data_misc.py | 8 ++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 52dc5173b0..7feaa8d608 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -32,6 +32,7 @@ Core Functions :template: myfunction.rst openmc.data.atomic_mass + openmc.data.gnd_name openmc.data.linearize openmc.data.thin openmc.data.water_density diff --git a/openmc/data/data.py b/openmc/data/data.py index d0bb65648c..fd13299617 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -313,13 +313,37 @@ def water_density(temperature, pressure=0.1013): return coeff / pi / gamma1_pi +def gnd_name(Z, A, m=0): + """Return nuclide name using GND convention + + Parameters + ---------- + Z : int + Atomic number + A : int + Mass number + m : int, optional + Metastable state + + Returns + ------- + str + Nuclide name in GND convention, e.g., 'Am242_m1' + + """ + if m > 0: + return '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A, m) + else: + return '{}{}'.format(ATOMIC_SYMBOL[Z], A) + + def zam(name): """Return tuple of (atomic number, mass number, metastable state) Parameters ---------- name : str - Name of nuclide using GND convention, e.g., 'Am242m1' + Name of nuclide using GND convention, e.g., 'Am242_m1' Returns ------- diff --git a/openmc/data/endf.py b/openmc/data/endf.py index db94e15bea..c44c66be0c 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -17,7 +17,7 @@ from collections.abc import Iterable import numpy as np from numpy.polynomial.polynomial import Polynomial -from .data import ATOMIC_SYMBOL +from .data import ATOMIC_SYMBOL, gnd_name from .function import Tabulated1D, INTERPOLATION_SCHEME from openmc.stats.univariate import Uniform, Tabular, Legendre @@ -249,6 +249,7 @@ def get_tab2_record(file_obj): return params, Tabulated2D(breakpoints, interpolation) + def get_evaluations(filename): """Return a list of all evaluations within an ENDF file. @@ -424,13 +425,9 @@ class Evaluation(object): @property def gnd_name(self): - symbol = ATOMIC_SYMBOL[self.target['atomic_number']] - A = self.target['mass_number'] - m = self.target['isomeric_state'] - if m > 0: - return '{}{}_m{}'.format(symbol, A, m) - else: - return '{}{}'.format(symbol, A) + return gnd_name(self.target['atomic_number'], + self.target['mass_number'], + self.target['isomeric_state']) class Tabulated2D(object): diff --git a/openmc/material.py b/openmc/material.py index 5fdcb76894..d52d8a27b4 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -24,7 +24,7 @@ class Material(IDManagerMixin): To create a material, one should create an instance of this class, add nuclides or elements with :meth:`Material.add_nuclide` or `Material.add_element`, respectively, and set the total material density - with `Material.export_to_xml()`. The material can then be assigned to a cell + with `Material.set_density()`. The material can then be assigned to a cell using the :attr:`Cell.fill` attribute. Parameters diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index 6167449268..04ca0f1031 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -60,6 +60,14 @@ def test_water_density(): assert dens(500.0, 3.0) == pytest.approx(1e-3/0.120241800e-2, 1e-6) +def test_gnd_name(): + assert openmc.data.gnd_name(1, 1) == 'H1' + assert openmc.data.gnd_name(40, 90) == ('Zr90') + assert openmc.data.gnd_name(95, 242, 0) == ('Am242') + assert openmc.data.gnd_name(95, 242, 1) == ('Am242_m1') + assert openmc.data.gnd_name(95, 242, 10) == ('Am242_m10') + + def test_zam(): assert openmc.data.zam('H1') == (1, 1, 0) assert openmc.data.zam('Zr90') == (40, 90, 0) From b9ff205090520b718bbec7a26a07fa7d9361374a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 1 Mar 2018 23:09:10 -0600 Subject: [PATCH 67/68] Don't put free neutron in depletion chain --- openmc/deplete/chain.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index fbe6792227..1826ca9ca0 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -59,6 +59,10 @@ def replace_missing(product, decay_data): Z, A, state = openmc.data.zam(product) symbol = openmc.data.ATOMIC_SYMBOL[Z] + # Replace neutron with proton + if Z == 0 and A == 1: + return 'H1' + # First check if ground state is available if state: product = '{}{}'.format(symbol, A) @@ -167,6 +171,9 @@ class Chain(object): decay_data = {} for f in decay_files: data = openmc.data.Decay(f) + # Skip decay data for neutron itself + if data.nuclide['atomic_number'] == 0: + continue decay_data[data.nuclide['name']] = data print('Processing fission product yield sub-library files...') From efb264da605b1b818e1e62b62decdbe10e8fa03d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 2 Mar 2018 06:20:16 -0600 Subject: [PATCH 68/68] Fix depletion chain unit test --- tests/unit_tests/test_deplete_chain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 4ec5415ee9..1fe83ad98a 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -71,7 +71,7 @@ def test_from_endf(): neutron_data = (_ENDF_DATA / 'neutrons').glob('*.endf') chain = Chain.from_endf(decay_data, fpy_data, neutron_data) - assert len(chain) == len(chain.nuclides) == len(chain.nuclide_dict) == 3821 + assert len(chain) == len(chain.nuclides) == len(chain.nuclide_dict) == 3820 for nuc in chain.nuclides: assert nuc == chain[nuc.name]