From 3e827530baef93dabcfa99a7141bba4db9997660 Mon Sep 17 00:00:00 2001 From: yardasol Date: Thu, 28 Jul 2022 15:55:07 -0500 Subject: [PATCH] make changes from @paulromano's 3rd review - syntax fixes and adjustments - change name of FluxDepletionOperator to IndependentOperator - flux_operator.py -> independent_operator.py - new class, MicroXS, for creating (for now) one-group microscopic cross section DataFrames. This class takes the functionality that was previously in static functions in IndependentOperator - Associated changes to the test suite and online docs --- docs/source/pythonapi/deplete.rst | 5 +- docs/source/usersguide/depletion.rst | 64 +++-- openmc/deplete/__init__.py | 3 +- openmc/deplete/abc.py | 4 +- ...ux_operator.py => independent_operator.py} | 234 +++--------------- openmc/deplete/microxs.py | 178 +++++++++++++ openmc/deplete/openmc_operator.py | 8 +- openmc/deplete/operator.py | 4 +- .../deplete_no_transport/test.py | 18 +- tests/regression_tests/microxs/__init.py__ | 0 tests/regression_tests/microxs/test.py | 52 ++++ .../microxs/test_reference.csv | 7 + .../unit_tests/test_deplete_flux_operator.py | 83 ------- .../test_deplete_independent_operator.py | 36 +++ tests/unit_tests/test_deplete_microxs.py | 60 +++++ 15 files changed, 437 insertions(+), 319 deletions(-) rename openmc/deplete/{flux_operator.py => independent_operator.py} (67%) create mode 100644 openmc/deplete/microxs.py create mode 100644 tests/regression_tests/microxs/__init.py__ create mode 100644 tests/regression_tests/microxs/test.py create mode 100644 tests/regression_tests/microxs/test_reference.csv delete mode 100644 tests/unit_tests/test_deplete_flux_operator.py create mode 100644 tests/unit_tests/test_deplete_independent_operator.py create mode 100644 tests/unit_tests/test_deplete_microxs.py diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index cb9adfaaf..246cea59f 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -49,9 +49,9 @@ specific to OpenMC are available using the following classes: :template: mycallable.rst Operator - FluxDepletionOperator + IndependentOperator -The :class:`Operator` and :class:`FluxDepletionOperator` classes must also have +The :class:`Operator` and :class:`IndependentOperator` classes must also have some knowledge of how nuclides transmute and decay. This is handled by the :class:`Chain`. @@ -134,6 +134,7 @@ data, such as number densities and reaction rates for each material. :template: myclass.rst AtomNumber + MicroXS OperatorResult ReactionRates Results diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index cfb373e13..0bc2a5af9 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -97,7 +97,7 @@ Energy Deposition ----------------- The default energy deposition mode, ``"fission-q"``, instructs the -:class:`openmc.deplete.Operator` to normalize reaction rates using the product +:class:`~openmc.deplete.Operator` to normalize reaction rates using the product of fission reaction rates and fission Q values taken from the depletion chain. This approach does not consider indirect contributions to energy deposition, such as neutron heating and energy from secondary photons. In doing this, the @@ -113,7 +113,7 @@ should be, including indirect components. Some examples are provided below:: fission_q = {"U235": 202e+6} # energy in eV # create a Model object - model = openmc.Model(geometry, settings) + model = openmc.Model(geometry, settings) # create a modified chain and write it to a new file chain = openmc.deplete.Chain.from_xml("chain.xml", fission_q) @@ -133,7 +133,7 @@ to normalize reaction rates instead of using the fission reaction rates with:: normalization_mode="energy-deposition") These modified heating libraries can be generated by running the latest version -of :meth:`openmc.data.IncidentNeutron.from_njoy`, and will eventually be bundled +of :meth:`openmc.data.IncidentNeutron.from_njoy()`, and will eventually be bundled into the distributed libraries. Local Spectra and Repeated Materials @@ -188,31 +188,57 @@ Transport-independent depletion possible and likely in the near future. OpenMC supports running depletion calculations independent of the OpenMC -transport solver using the :class:`~openmc.deplete.FluxDepletionOperator` class. +transport solver using the :class:`~openmc.deplete.IndependentOperator` class. This class supports both constant-flux (``source-rate`` normalization) and constant-power depletion (``fission-q`` normalization). .. important:: - Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` class. Use the ``source_rates`` parameter when ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` when ``normalization_mode == fission-q``. + Make sure you set the correct parameter in the :class:`openmc.abc.Integrator` + class. Use the ``source_rates`` parameter when + ``normalization_mode == source-rate``, and use ``power`` or ``power_density`` + when ``normalization_mode == fission-q``. .. warning:: - The accuracy of results when using ``fission-q`` is entirely dependent on your depletion chain. Make sure it has sufficient data to resolve the dynamics of your particular scenario. + The accuracy of results when using ``fission-q`` is entirely dependent on + your depletion chain. Make sure it has sufficient data to resolve the + dynamics of your particular scenario. -This class has two ways to initialize it: the default constructor accepts an -:class:`openmc.Materials` object and one-group microscopic -cross sections as a :class:`pandas.DataFrame`, while the ``from_nuclides`` -method accepts a volume and dictionary of nuclide concentrations in place of -the :class:`openmc.Materials` object in addition to the other parameters. -The class includes helper functions to construct the dataframe from a csv file -or from data arrays:: +:class:`~openmc.deplete.IndependentOperator` class uses one-group microscopic cross sections to calculate reaction +rates. Users can generate one-group microscopic cross sections using the +:class:`~openmc.deplete.MicroXS` class:: + + import openmc + from openmc.deplete import MicroXS + + model = openmc.Model.from_xml() + + micro_xs = MicroXS.from_model(model, model.materials[0]) + + micro_xs.to_csv(micro_xs_path) + +:class:`~openmc.deplete.MicroXS` also includes functions to read in cross +section data directly from a ``.csv`` file or from data arrays:: + + micro_xs = MicroXS.from_csv(micro_xs_path) + + nuclides = ['U234', 'U235', 'U238'] + reactions = ['fission', '(n,gamma)'] + data = np.array([[0.1, 0.2], + [0.3, 0.4], + [0.01, 0.5]]) + micro_xs = MicroXS.from_array(nuclides, reactions, data) + +:class:`~openmc.deplete.IndependentOperator` has two ways to initialize it: +the default constructor accepts an :class:`openmc.Materials` object and +one-group microscopic cross sections as a :class:`~openmc.deplete.MicroXS` +object, while the :meth:`~openmc.deplete.IndependentOperator.from_nuclides` +method accepts a volume and dictionary of nuclide concentrations in place of the +:class:`openmc.Materials` object in addition to the other parameters:: - ... # load in the microscopic cross sections - micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_path) - flux = 1.16e15 - op = FluxDepletionOperator(materials, micro_xs, chain_file) + op = IndependentOperator(materials, micro_xs, chain_file) # alternate construtor nuclides = {'U234': 8.92e18, @@ -222,8 +248,8 @@ or from data arrays:: 'O16': 4.64e22, 'O17': 1.76e19} volume = 0.5 - op = FluxDepletionOperator.from_nuclides(volume, nuclides, 'atom/cm3', - micro_xs, flux, chain_file) + op = IndependentOperator.from_nuclides(volume, nuclides, micro_xs, + chain_file, nuc_units='atom/cm3') A user can then define an integrator class as they would for a coupled transport-depletion calculation and follow the same steps from there. diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index deddf1100..12e4abe87 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -9,7 +9,8 @@ from .nuclide import * from .chain import * from .openmc_operator import * from .operator import * -from .flux_operator import * +from .independent_operator import * +from .microxs import * from .reaction_rates import * from .atom_number import * from .stepresult import * diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 90a353d93..cdbd1361a 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -526,7 +526,7 @@ class Integrator(ABC): initial heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] or neutron flux in [neut/s-cm^2] for each + Source rate in [neutron/sec] or neutron flux in [neut/cm^2-s] for each interval in :attr:`timesteps` .. versionadded:: 0.12.1 @@ -864,7 +864,7 @@ class SIIntegrator(Integrator): initial heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional - Source rate in [neutron/sec] or neutron flux in [neut/s-cm^2] for each + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for each interval in :attr:`timesteps` .. versionadded:: 0.12.1 diff --git a/openmc/deplete/flux_operator.py b/openmc/deplete/independent_operator.py similarity index 67% rename from openmc/deplete/flux_operator.py rename to openmc/deplete/independent_operator.py index 410552671..98ef6656d 100644 --- a/openmc/deplete/flux_operator.py +++ b/openmc/deplete/independent_operator.py @@ -1,7 +1,7 @@ -"""Pure depletion operator +"""Independent depletion operator -This module implements a pure depletion operator that user-provided one-group -cross sections. +This module implements a depletion operator that runs independently of any +transport solver by using user-provided one-group cross sections. """ @@ -11,110 +11,33 @@ from warnings import warn from itertools import product import numpy as np -import pandas as pd from uncertainties import ufloat import openmc -from openmc.checkvalue import check_type, check_value, check_iterable_type +from openmc.checkvalue import check_type from openmc.mpi import comm -from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS from .abc import ReactionRateHelper, OperatorResult -from .chain import REACTIONS from .openmc_operator import OpenMCOperator, _distribute +from .microxs import MicroXS from .results import Results from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateHelper -_valid_rxns = list(REACTIONS) -_valid_rxns.append('fission') - -def generate_1g_cross_sections(model, - reaction_domain, - reactions=['(n,gamma)', - '(n,2n)', - '(n,p)', - '(n,a)', - '(n,3n)', - '(n,4n)', - 'fission'], - energy_bounds=(0, 20e6), - write_to_csv=False, - filename='micro_xs.csv'): - """Helper function to generate a one-group cross-section dataframe using - OpenMC. Note that the ``openmc`` executable must be compiled. - - Parameters - ---------- - model : openmc.model.Model - OpenMC model object. Must contain geometry, materials, and settings. - reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh - Domain in which to tally reaction rates. - reactions : list of str, optional - Reaction names to tally - energy_bound : 2-tuple of float, optional - Bounds for the energy group. - write_to_csv : bool, optional - Option to write the DataFrame to a `.csv` file. - filename : str - Name for csv file. Only applicable if ``write_to_csv == True`` - - Returns - ------- - None or pandas.DataFrame - - """ - groups = EnergyGroups(energy_bounds) - - # Set up the reaction tallies - original_tallies = model.tallies - tallies = openmc.Tallies() - xs = {} - for rxn in reactions: - if rxn == 'fission': - xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) - else: - xs[rxn] = ArbitraryXS(rxn, domain=reaction_domain, groups=groups, by_nuclide=True) - tallies += xs[rxn].tallies.values() - - model.tallies = tallies - statepoint_path = model.run() - - # Revert to the original tallies - model.tallies = old_tallies - - with openmc.StatePoint(statepoint_path) as sp: - for rxn in xs: - xs[rxn].load_from_statepoint(sp) - - # Build the DataFrame - micro_xs = pd.DataFrame() - for rxn in xs: - df = xs[rxn].get_pandas_dataframe(xs_type='micro') - df.index = df['nuclide'] - df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) - df.rename({'mean':rxn}, axis=1, inplace=True) - micro_xs = pd.concat([micro_xs, df], axis=1) - - if write_to_csv: - micro_xs.to_csv(filename) - - return micro_xs - -class FluxDepletionOperator(OpenMCOperator): - """Depletion operator that uses one-group - cross sections to calculate reaction rates. +class IndependentOperator(OpenMCOperator): + """Depletion operator that uses one-group cross sections to calculate + reaction rates. Instances of this class can be used to perform depletion using one-group - cross sections and constant flux or constant power. Normally, a user needn't call methods of - this class directly. Instead, an instance of this class is passed to an - integrator class, such as :class:`openmc.deplete.CECMIntegrator`. + cross sections and constant flux or constant power. Normally, a user needn't + call methods of this class directly. Instead, an instance of this class is + passed to an integrator class, such as + :class:`openmc.deplete.CECMIntegrator`. Parameters ---------- materials : openmc.Materials Materials to deplete. - micro_xs : pandas.DataFrame - DataFrame with nuclides names as index and microscopic cross section - data in the columns. Cross section units are [cm^-2]. + micro_xs : MicroXS + One-group microscopic cross sections in [b] . chain_file : str Path to the depletion chain XML file. keff : 2-tuple of float, optional @@ -134,23 +57,23 @@ class FluxDepletionOperator(OpenMCOperator): if ``"normalization_mode" == "fission-q"``. reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. + depletion chain up to ``reduce_chain_level``. reduce_chain_level : int, optional Depth of the search when reducing the depletion chain. Only used if ``reduce_chain`` evaluates to true. The default value of ``None`` implies no limit on the depth. fission_yield_opts : dict of str to option, optional - Optional arguments to pass to the `FissionYieldHelper`. Will be + Optional arguments to pass to the + :class:`openmc.deplete.helpers.FissionYieldHelper` object. Will be passed directly on to the helper. Passing a value of None will use the defaults for the associated helper. - Attributes ---------- materials : openmc.Materials All materials present in the model - cross_sections : pandas.DataFrame - Object containing one-group cross-sections. + cross_sections : MicroXS + Object containing one-group cross-sections in [cm^2]. dilute_initial : float Initial atom density [atoms/cm^3] to add for nuclides that are zero in initial condition to ensure they exist in the decay @@ -193,7 +116,7 @@ class FluxDepletionOperator(OpenMCOperator): fission_yield_opts=None): # Validate micro-xs parameters check_type('materials', materials, openmc.Materials) - check_type('micro_xs', micro_xs, pd.DataFrame) + check_type('micro_xs', micro_xs, MicroXS) if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(*keff) @@ -205,9 +128,11 @@ class FluxDepletionOperator(OpenMCOperator): helper_kwargs = {'normalization_mode': normalization_mode, 'fission_yield_opts': fission_yield_opts} + cross_sections = micro_xs * 1e-24 + cross_sections._units = 'cm^2' super().__init__( materials, - micro_xs, + cross_sections, chain_file, prev_results, fission_q=fission_q, @@ -216,9 +141,10 @@ class FluxDepletionOperator(OpenMCOperator): reduce_chain_level=reduce_chain_level) @classmethod - def from_nuclides(cls, volume, nuclides, nuc_units, + def from_nuclides(cls, volume, nuclides, micro_xs, chain_file, + nuc_units='atom/b-cm', keff=None, normalization_mode='source-rate', fission_q=None, @@ -234,13 +160,12 @@ class FluxDepletionOperator(OpenMCOperator): nuclides : dict of str to float Dictionary with nuclide names as keys and nuclide concentrations as values. - nuc_units : {'atom/cm3', 'atom/b-cm'} - Units for nuclide concentration. - micro_xs : pandas.DataFrame - DataFrame with nuclides names as index and microscopic cross section - data in the columns. Cross section units are [cm^-2]. + micro_xs : MicroXS + One-group microscopic cross sections. chain_file : str Path to the depletion chain XML file. + nuc_units : {'atom/cm3', 'atom/b-cm'} + Units for nuclide concentration. keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. Default is None. @@ -325,17 +250,16 @@ class FluxDepletionOperator(OpenMCOperator): """Finds nuclides with cross section data""" return set(cross_sections.index) - class _FluxDepletionNormalizationHelper(ChainFissionHelper): - """Class for calculating one-group flux - based on a power. + class _IndependentNormalizationHelper(ChainFissionHelper): + """Class for calculating one-group flux based on a power. flux = Power / X, where X = volume * sum_i(Q_i * fission_micro_xs_i * density_i) Parameters ---------- - op : openmc.deplete.FluxDepletionOperator - Reference to the object encapsulate _FluxDepletionNormalizationHelper. - We pass this so we don't have to duplicate the ``number`` object. + op : openmc.deplete.IndependentOperator + Reference to the object encapsulating _IndependentNormalizationHelper. + We pass this so we don't have to duplicate :attr:`IndependentOperator.number`. """ @@ -367,7 +291,7 @@ class FluxDepletionOperator(OpenMCOperator): super().update(fission_rates) - class _FluxDepletionRateHelper(ReactionRateHelper): + class _IndependentRateHelper(ReactionRateHelper): """Class for generating one-group reaction rates with flux and one-group cross sections. @@ -378,9 +302,9 @@ class FluxDepletionOperator(OpenMCOperator): Parameters ---------- - op : openmc.deplete.FluxDepletionOperator - Reference to the object encapsulate _FluxDepletionRateHelper. - We pass this so we don't have to duplicate the ``number`` object. + op : openmc.deplete.IndependentOperator + Reference to the object encapsulate _IndependentRateHelper. + We pass this so we don't have to duplicate the :attr:`IndependentOperator.number` object. Attributes @@ -441,9 +365,9 @@ class FluxDepletionOperator(OpenMCOperator): normalization_mode = helper_kwargs['normalization_mode'] fission_yield_opts = helper_kwargs['fission_yield_opts'] - self._rate_helper = self._FluxDepletionRateHelper(self) + self._rate_helper = self._IndependentRateHelper(self) if normalization_mode == "fission-q": - self._normalization_helper = self._FluxDepletionNormalizationHelper(self) + self._normalization_helper = self._IndependentNormalizationHelper(self) else: self._normalization_helper = SourceRateHelper() @@ -472,7 +396,7 @@ class FluxDepletionOperator(OpenMCOperator): vec : list of numpy.ndarray Total atoms to be used in function. source_rate : float - Power in [W] or flux in [neut/s-cm^2] + Power in [W] or flux in [neutron/cm^2-s] Returns ------- @@ -523,79 +447,3 @@ class FluxDepletionOperator(OpenMCOperator): ' atom/b-cm)') number_i[mat, nuc] = 0.0 - - @staticmethod - def create_micro_xs_from_data_array(nuclides, reactions, data): - """ - Creates a ``micro_xs`` parameter from a dictionary. - - Parameters - ---------- - nuclides : list of str - List of nuclide symbols for that have data for at least one - reaction. - reactions : list of str - List of reactions. All reactions must match those in ``chain.REACTIONS`` - data : ndarray of floats - Array containing one-group microscopic cross section values, in - [barn], for each nuclide and reaction. - - Returns - ------- - micro_xs : pandas.DataFrame - A DataFrame object correctly formatted for use in ``FluxOperator``. - Cross section data is in [cm^2] - """ - - # Validate inputs - if data.shape != (len(nuclides), len(reactions)): - raise ValueError( - f'Nuclides list of length {len(nuclides)} and ' - f'reactions array of length {len(reactions)} do not ' - f'match dimensions of data array of shape {data.shape}') - - FluxDepletionOperator._validate_micro_xs_inputs( - nuclides, reactions, data) - - # Convert to cm^2 - data *= 1e-24 - - return pd.DataFrame(index=nuclides, columns=reactions, data=data) - - @staticmethod - def create_micro_xs_from_csv(csv_file, units='barn'): - """ - Create the ``micro_xs`` parameter from a ``.csv`` file. - - Parameters - ---------- - csv_file : str - Relative path to csv-file containing microscopic cross section - data. Cross section values should be in [barn]. - - Returns - ------- - micro_xs : pandas.DataFrame - A DataFrame object correctly formatted for use in ``FluxOperator``. - Cross section data is in [cm^2] - - """ - micro_xs = pd.read_csv(csv_file, index_col=0) - - FluxDepletionOperator._validate_micro_xs_inputs(list(micro_xs.index), - list(micro_xs.columns), - micro_xs.to_numpy()) - - micro_xs *= 1e-24 - - return micro_xs - - # Convenience function for the micro_xs static methods - @staticmethod - def _validate_micro_xs_inputs(nuclides, reactions, data): - check_iterable_type('nuclides', nuclides, str) - check_iterable_type('reactions', reactions, str) - check_type('data', data, np.ndarray, expected_iter_type=float) - for reaction in reactions: - check_value('reactions', reaction, _valid_rxns) - diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py new file mode 100644 index 000000000..6abf8836f --- /dev/null +++ b/openmc/deplete/microxs.py @@ -0,0 +1,178 @@ +"""MicroXS module + +A pandas.DataFrame storing microscopic cross section data with +nuclides names as row indices and reaction names as column indices. +""" + +import tempfile +from pathlib import Path +from os import chdir + +from pandas import DataFrame, read_csv, concat +from numpy import ndarray + +from openmc.checkvalue import check_type, check_value, check_iterable_type +from openmc.mgxs import EnergyGroups, ArbitraryXS, FissionXS +from openmc import Tallies, StatePoint + +from .chain import REACTIONS + +_valid_rxns = list(REACTIONS) +_valid_rxns.append('fission') + + +class MicroXS(DataFrame): + """Stores microscopic cross section data for use in + independent depletion. + """ + + @classmethod + def from_model(cls, + model, + reaction_domain, + reactions=['(n,gamma)', + '(n,2n)', + '(n,p)', + '(n,a)', + '(n,3n)', + '(n,4n)', + 'fission'], + energy_bounds=(0, 20e6)): + """Generate a one-group cross-section dataframe using + OpenMC. Note that the ``openmc`` executable must be compiled. + + Parameters + ---------- + model : openmc.Model + OpenMC model object. Must contain geometry, materials, and settings. + reaction_domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh + Domain in which to tally reaction rates. + reactions : list of str, optional + Reaction names to tally + energy_bound : 2-tuple of float, optional + Bounds for the energy group. + + Returns + ------- + MicroXS, cross section data in [b] + + """ + groups = EnergyGroups(energy_bounds) + + # Set up the reaction tallies + original_tallies = model.tallies + tallies = Tallies() + xs = {} + for rxn in reactions: + if rxn == 'fission': + xs[rxn] = FissionXS(domain=reaction_domain, groups=groups, by_nuclide=True) + else: + xs[rxn] = ArbitraryXS(rxn, domain=reaction_domain, groups=groups, by_nuclide=True) + tallies += xs[rxn].tallies.values() + + model.tallies = tallies + + # create temporary run + original_dir = Path.cwd() + temp_dir = tempfile.TemporaryDirectory() + chdir(tempfile.gettempdir()) + statepoint_path = model.run() + chdir(original_dir) + + with StatePoint(statepoint_path) as sp: + for rxn in xs: + xs[rxn].load_from_statepoint(sp) + + # Build the DataFrame + micro_xs = cls() + for rxn in xs: + df = xs[rxn].get_pandas_dataframe(xs_type='micro') + df.index = df['nuclide'] + df.drop(['nuclide', xs[rxn].domain_type, 'group in', 'std. dev.'], axis=1, inplace=True) + df.rename({'mean':rxn}, axis=1, inplace=True) + micro_xs = concat([micro_xs, df], axis=1) + + micro_xs._units = 'b' + + # Revert to the original tallies + model.tallies = original_tallies + + return micro_xs + + @classmethod + def from_array(cls, nuclides, reactions, data, units='b'): + """ + Creates a ``MicroXS`` object from arrays. + + Parameters + ---------- + nuclides : list of str + List of nuclide symbols for that have data for at least one + reaction. + reactions : list of str + List of reactions. All reactions must match those in + :data:`openmc.deplete.chain.REACTIONS` + data : ndarray of floats + Array containing one-group microscopic cross section values for + each nuclide and reaction + units : {'b', 'cm^2'} + Units of the cross section values in ``data`` + + Returns + ------- + MicroXS + """ + + # Validate inputs + if data.shape != (len(nuclides), len(reactions)): + raise ValueError( + f'Nuclides list of length {len(nuclides)} and ' + f'reactions array of length {len(reactions)} do not ' + f'match dimensions of data array of shape {data.shape}') + + cls._validate_micro_xs_inputs( + nuclides, reactions, data) + micro_xs = cls(index=nuclides, columns=reactions, data=data) + micro_xs._units = units + + return micro_xs + + @classmethod + def from_csv(cls, csv_file, units='b', **kwargs): + """ + Load a ``MicroXS`` object from a ``.csv`` file. + + Parameters + ---------- + csv_file : str + Relative path to csv-file containing microscopic cross section + data. + units : {'b', 'cm^2'} + Units of the cross section values in the ``.csv`` file + **kwargs : dict + Keyword arguments to pass to :func:`pandas.read_csv()`. + + Returns + ------- + MicroXS + + """ + if 'float_precision' not in kwargs: + kwargs['float_precision'] = 'round_trip' + + micro_xs = cls(read_csv(csv_file, index_col=0, **kwargs)) + + cls._validate_micro_xs_inputs(list(micro_xs.index), + list(micro_xs.columns), + micro_xs.to_numpy()) + micro_xs._units = units + + return micro_xs + + @staticmethod + def _validate_micro_xs_inputs(nuclides, reactions, data): + check_iterable_type('nuclides', nuclides, str) + check_iterable_type('reactions', reactions, str) + check_type('data', data, ndarray, expected_iter_type=float) + for reaction in reactions: + check_value('reactions', reaction, _valid_rxns) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 7c0bb6678..7235ab9bf 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -1,6 +1,6 @@ """OpenMC transport operator -This module implements functions used by both OpenMC transport operators as well as pure depletion operators. +This module implements functions shared by both OpenMC transport operators as well as indepenedent depletion operators. """ @@ -75,8 +75,8 @@ class OpenMCOperator(TransportOperator): helper_kwargs : dict Keyword arguments for helper classes reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. + If True, use :meth:`openmc.deplete.Chain.reduce()` to reduce the + depletion chain up to ``reduce_chain_level``. reduce_chain_level : int, optional Depth of the search when reducing the depletion chain. Only used if ``reduce_chain`` evaluates to true. The default value of @@ -87,7 +87,7 @@ class OpenMCOperator(TransportOperator): ---------- materials : openmc.Materials All materials present in the model - cross_sections : str or pandas.DataFrame + cross_sections : str or MicroXS Path to continuous energy cross section library, or object containing one-group cross-sections. dilute_initial : float diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py index db4e80b19..53a1db3e9 100644 --- a/openmc/deplete/operator.py +++ b/openmc/deplete/operator.py @@ -131,7 +131,7 @@ class Operator(OpenMCOperator): .. versionadded:: 0.12.1 reduce_chain : bool, optional If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. + depletion chain up to ``reduce_chain_level``. .. versionadded:: 0.12 reduce_chain_level : int, optional @@ -226,7 +226,7 @@ class Operator(OpenMCOperator): self.cleanup_when_done = True if reaction_rate_opts is None: - reaction_rate_opts = {} + reaction_rate_opts = {} if fission_yield_opts is None: fission_yield_opts = {} helper_kwargs = { diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 38d4aaf2e..95b842537 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -1,19 +1,12 @@ """ Transport-free depletion test suite """ -from math import floor -import shutil from pathlib import Path -from difflib import unified_diff import numpy as np import pytest import openmc -from openmc.data import JOULE_PER_EV import openmc.deplete -from openmc.deplete import FluxDepletionOperator - -from tests.regression_tests import config - +from openmc.deplete import IndependentOperator, MicroXS @pytest.fixture(scope="module") def fuel(): @@ -27,7 +20,6 @@ def fuel(): return fuel - @pytest.mark.parametrize("multiproc, from_nuclides, normalization_mode, power, flux", [ (True, True,'source-rate', None, 1164719970082145.0), (False, True, 'source-rate', None, 1164719970082145.0), @@ -46,7 +38,7 @@ def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclide """ # Create operator micro_xs_file = Path(__file__).parents[2] / 'micro_xs_simple.csv' - micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(micro_xs_file) + micro_xs = MicroXS.from_csv(micro_xs_file) chain_file = Path(__file__).parents[2] / 'chain_simple.xml' if from_nuclides: @@ -54,11 +46,11 @@ def test_no_transport_from_nuclides(run_in_tmpdir, fuel, multiproc, from_nuclide for nuc, dens in fuel.get_nuclide_atom_densities().items(): nuclides[nuc] = dens - op = FluxDepletionOperator.from_nuclides( - fuel.volume, nuclides,'atom/b-cm', micro_xs, chain_file, normalization_mode=normalization_mode) + op = IndependentOperator.from_nuclides( + fuel.volume, nuclides, micro_xs, chain_file, normalization_mode=normalization_mode) else: - op = FluxDepletionOperator(openmc.Materials([fuel]), micro_xs, chain_file, normalization_mode=normalization_mode) + op = IndependentOperator(openmc.Materials([fuel]), micro_xs, chain_file, normalization_mode=normalization_mode) # Power and timesteps dt = [30] # single step diff --git a/tests/regression_tests/microxs/__init.py__ b/tests/regression_tests/microxs/__init.py__ new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py new file mode 100644 index 000000000..bebe1c302 --- /dev/null +++ b/tests/regression_tests/microxs/test.py @@ -0,0 +1,52 @@ +"""Test one-group cross section generation""" +import numpy as np +import pytest +import openmc + +from openmc.deplete import MicroXS + +@pytest.fixture(scope="module") +def model(): + fuel = openmc.Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + + clad = openmc.Material(name="clad") + clad.add_element("Zr", 1) + clad.set_density("g/cc", 6) + + water = openmc.Material(name="water") + water.add_element("O", 1) + water.add_element("H", 2) + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + + radii = [0.42, 0.45] + fuel.volume = np.pi * radii[0] ** 2 + clad.volume = np.pi * (radii[1]**2 - radii[0]**2) + water.volume = 1.24**2 - (np.pi * radii[1]**2) + + materials = openmc.Materials([fuel, clad, water]) + + pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] + pin_univ = openmc.model.pin(pin_surfaces, materials) + bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + geometry = openmc.Geometry([root_cell]) + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + + +def test_from_model(model): + ref_xs = MicroXS.from_csv('test_reference.csv') + test_xs = MicroXS.from_model(model, model.materials[0]) + + assert ref_xs._units == test_xs._units + np.testing.assert_allclose(test_xs, ref_xs, rtol=1e-11) + diff --git a/tests/regression_tests/microxs/test_reference.csv b/tests/regression_tests/microxs/test_reference.csv new file mode 100644 index 000000000..0c47409b0 --- /dev/null +++ b/tests/regression_tests/microxs/test_reference.csv @@ -0,0 +1,7 @@ +nuclide,"(n,gamma)","(n,2n)","(n,p)","(n,a)","(n,3n)","(n,4n)",fission +U234,23.518634203050674,0.0008255330840536984,0.0,0.0,9.404554397521455e-07,0.0,0.49531930067650964 +U235,10.621118186344795,0.004359401013759254,0.0,0.0,7.2974901306692395e-06,0.0,49.10955932965902 +U238,0.8652742788116055,0.005661917442209096,0.0,0.0,4.92273921631416e-05,0.0,0.10579281644765708 +U236,9.095623870006163,0.0024373322002926834,0.0,0.0,1.966889146690413e-05,0.0,0.3231539233923791 +O16,7.511380881289377e-05,0.0,1.3764104470470622e-05,0.002862620940027927,0.0,0.0,0.0 +O17,0.00041221042693945085,1.9826700699084533e-05,7.764409141772239e-06,0.05938517754333328,0.0,0.0,0.0 diff --git a/tests/unit_tests/test_deplete_flux_operator.py b/tests/unit_tests/test_deplete_flux_operator.py deleted file mode 100644 index 8f17538ed..000000000 --- a/tests/unit_tests/test_deplete_flux_operator.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Basic unit tests for openmc.deplete.FluxDepletionOperator instantiation - -Modifies and resets environment variable OPENMC_CROSS_SECTIONS -to a custom file with new depletion_chain node -""" - -from pathlib import Path - -import pytest -from openmc.deplete.flux_operator import FluxDepletionOperator -from openmc import Material, Materials -import pandas as pd -import numpy as np - -CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" -ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" - - -def test_create_micro_xs_from_data_array(): - nuclides = [ - 'U234', - 'U235', - 'U238', - 'U236', - 'O16', - 'O17', - 'I135', - 'Xe135', - 'Xe136', - 'Cs135', - 'Gd157', - 'Gd156'] - reactions = ['fission', '(n,gamma)'] - # These values are placeholders and are not at all - # physically meaningful. - data = np.array([[0.1, 0.], - [0.1, 0.], - [0.9, 0.], - [0.4, 0.], - [0., 0.], - [0., 0.], - [0., 0.1], - [0., 0.9], - [0., 0.], - [0., 0.], - [0., 0.1], - [0., 0.1]]) - - FluxDepletionOperator.create_micro_xs_from_data_array( - nuclides, reactions, data) - with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' - r'reactions array of length \d* do not ' - r'match dimensions of data array of shape \(\d*\,d*\)'): - FluxDepletionOperator.create_micro_xs_from_data_array( - nuclides, reactions, data[:, 0]) - - -def test_create_micro_xs_from_csv(): - FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) - - -def test_operator_init(): - """The test uses a temporary dummy chain. This file will be removed - at the end of the test, and only contains a depletion_chain node.""" - volume = 1 - nuclides = {'U234': 8.922411359424315e+18, - 'U235': 9.98240191860822e+20, - 'U238': 2.2192386373095893e+22, - 'U236': 4.5724195495061115e+18, - 'O16': 4.639065406771322e+22, - 'O17': 1.7588724018066158e+19} - micro_xs = FluxDepletionOperator.create_micro_xs_from_csv(ONE_GROUP_XS) - nuclide_flux_operator = FluxDepletionOperator.from_nuclides( - volume, nuclides, 'atom/cm3', micro_xs, CHAIN_PATH) - - fuel = Material(name="uo2") - fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) - fuel.add_element("O", 2) - fuel.set_density("g/cc", 10.4) - fuel.depletable=True - fuel.volume = 1 - materials = Materials([fuel]) - nuclide_flux_operator = FluxDepletionOperator(materials, micro_xs, CHAIN_PATH) diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py new file mode 100644 index 000000000..f6cc7e200 --- /dev/null +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -0,0 +1,36 @@ +"""Basic unit tests for openmc.deplete.IndependentOperator instantiation + +""" + +from pathlib import Path + +import pytest +from openmc.deplete import IndependentOperator, MicroXS +from openmc import Material, Materials +import numpy as np + +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" +ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" + +def test_operator_init(): + """The test uses a temporary dummy chain. This file will be removed + at the end of the test, and only contains a depletion_chain node.""" + volume = 1 + nuclides = {'U234': 8.922411359424315e+18, + 'U235': 9.98240191860822e+20, + 'U238': 2.2192386373095893e+22, + 'U236': 4.5724195495061115e+18, + 'O16': 4.639065406771322e+22, + 'O17': 1.7588724018066158e+19} + micro_xs = MicroXS.from_csv(ONE_GROUP_XS) + IndependentOperator.from_nuclides( + volume, nuclides, micro_xs, CHAIN_PATH, nuc_units='atom/cm3') + + fuel = Material(name="uo2") + fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) + fuel.add_element("O", 2) + fuel.set_density("g/cc", 10.4) + fuel.depletable=True + fuel.volume = 1 + materials = Materials([fuel]) + IndependentOperator(materials, micro_xs, CHAIN_PATH) diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py new file mode 100644 index 000000000..17d9cb38f --- /dev/null +++ b/tests/unit_tests/test_deplete_microxs.py @@ -0,0 +1,60 @@ +"""Basic unit tests for openmc.deplete.IndependentOperator instantiation + +Modifies and resets environment variable OPENMC_CROSS_SECTIONS +to a custom file with new depletion_chain node +""" + +from os import remove +from pathlib import Path + +import pytest +from openmc.deplete import MicroXS +import numpy as np + +ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" + + +def test_from_array(): + nuclides = [ + 'U234', + 'U235', + 'U238', + 'U236', + 'O16', + 'O17', + 'I135', + 'Xe135', + 'Xe136', + 'Cs135', + 'Gd157', + 'Gd156'] + reactions = ['fission', '(n,gamma)'] + # These values are placeholders and are not at all + # physically meaningful. + data = np.array([[0.1, 0.], + [0.1, 0.], + [0.9, 0.], + [0.4, 0.], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.9], + [0., 0.], + [0., 0.], + [0., 0.1], + [0., 0.1]]) + + MicroXS.from_array(nuclides, reactions, data) + with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' + r'reactions array of length \d* do not ' + r'match dimensions of data array of shape \(\d*\,d*\)'): + MicroXS.from_array(nuclides, reactions, data[:, 0]) + +def test_csv(): + ref_xs = MicroXS.from_csv(ONE_GROUP_XS) + ref_xs.to_csv('temp_xs.csv') + temp_xs = MicroXS.from_csv('temp_xs.csv') + assert ref_xs._units == temp_xs._units + assert np.all(ref_xs == temp_xs) + remove('temp_xs.csv') +