mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-26 21:25:36 -04:00
Co-authored-by: Jin Whan Bae <baej@ornl.gov> Co-authored-by: shimwell <mail@jshimwell.com> Co-authored-by: Jonathan Shimwell <drshimwell@gmail.com> Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
375 lines
14 KiB
Python
375 lines
14 KiB
Python
"""MicroXS module
|
|
|
|
A class for storing microscopic cross section data that can be used with the
|
|
IndependentOperator class for depletion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
from tempfile import TemporaryDirectory
|
|
from typing import List, Tuple, Iterable, Optional, Union, Sequence
|
|
|
|
import pandas as pd
|
|
import numpy as np
|
|
|
|
from openmc.checkvalue import check_type, check_value, check_iterable_type, PathLike
|
|
from openmc.exceptions import DataError
|
|
from openmc import StatePoint
|
|
from openmc.mgxs import GROUP_STRUCTURES
|
|
from openmc.data import REACTION_MT
|
|
import openmc
|
|
from .abc import change_directory
|
|
from .chain import Chain, REACTIONS
|
|
from .coupled_operator import _find_cross_sections, _get_nuclides_with_data
|
|
import openmc.lib
|
|
|
|
_valid_rxns = list(REACTIONS)
|
|
_valid_rxns.append('fission')
|
|
|
|
|
|
def _resolve_chain_file_path(chain_file: str):
|
|
# Determine what reactions and nuclides are available in chain
|
|
if chain_file is None:
|
|
chain_file = openmc.config.get('chain_file')
|
|
if 'chain_file' in openmc.config:
|
|
raise DataError(
|
|
"No depletion chain specified and could not find depletion "
|
|
"chain in openmc.config['chain_file']"
|
|
)
|
|
return chain_file
|
|
|
|
|
|
def get_microxs_and_flux(
|
|
model: openmc.Model,
|
|
domains,
|
|
nuclides: Optional[Iterable[str]] = None,
|
|
reactions: Optional[Iterable[str]] = None,
|
|
energies: Optional[Union[Iterable[float], str]] = None,
|
|
chain_file: Optional[PathLike] = None,
|
|
run_kwargs=None
|
|
) -> Tuple[List[np.ndarray], List[MicroXS]]:
|
|
"""Generate a microscopic cross sections and flux from a Model
|
|
|
|
.. versionadded:: 0.14.0
|
|
|
|
Parameters
|
|
----------
|
|
model : openmc.Model
|
|
OpenMC model object. Must contain geometry, materials, and settings.
|
|
domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase
|
|
Domains in which to tally reaction rates.
|
|
nuclides : list of str
|
|
Nuclides to get cross sections for. If not specified, all burnable
|
|
nuclides from the depletion chain file are used.
|
|
reactions : list of str
|
|
Reactions to get cross sections for. If not specified, all neutron
|
|
reactions listed in the depletion chain file are used.
|
|
energies : iterable of float or str
|
|
Energy group boundaries in [eV] or the name of the group structure
|
|
chain_file : str, optional
|
|
Path to the depletion chain XML file that will be used in depletion
|
|
simulation. Used to determine cross sections for materials not
|
|
present in the inital composition. Defaults to
|
|
``openmc.config['chain_file']``.
|
|
run_kwargs : dict, optional
|
|
Keyword arguments passed to :meth:`openmc.Model.run`
|
|
|
|
Returns
|
|
-------
|
|
list of numpy.ndarray
|
|
Flux in each group in [n-cm/src] for each domain
|
|
list of MicroXS
|
|
Cross section data in [b] for each domain
|
|
|
|
"""
|
|
# Save any original tallies on the model
|
|
original_tallies = model.tallies
|
|
|
|
# Determine what reactions and nuclides are available in chain
|
|
chain_file = _resolve_chain_file_path(chain_file)
|
|
chain = Chain.from_xml(chain_file)
|
|
if reactions is None:
|
|
reactions = chain.reactions
|
|
if not nuclides:
|
|
cross_sections = _find_cross_sections(model)
|
|
nuclides_with_data = _get_nuclides_with_data(cross_sections)
|
|
nuclides = [nuc.name for nuc in chain.nuclides
|
|
if nuc.name in nuclides_with_data]
|
|
|
|
# Set up the reaction rate and flux tallies
|
|
if energies is None:
|
|
energy_filter = openmc.EnergyFilter([0.0, 100.0e6])
|
|
elif isinstance(energies, str):
|
|
energy_filter = openmc.EnergyFilter.from_group_structure(energies)
|
|
else:
|
|
energy_filter = openmc.EnergyFilter(energies)
|
|
if isinstance(domains, openmc.MeshBase):
|
|
domain_filter = openmc.MeshFilter(domains)
|
|
elif isinstance(domains[0], openmc.Material):
|
|
domain_filter = openmc.MaterialFilter(domains)
|
|
elif isinstance(domains[0], openmc.Cell):
|
|
domain_filter = openmc.CellFilter(domains)
|
|
elif isinstance(domains[0], openmc.Universe):
|
|
domain_filter = openmc.UniverseFilter(domains)
|
|
else:
|
|
raise ValueError(f"Unsupported domain type: {type(domains[0])}")
|
|
|
|
rr_tally = openmc.Tally(name='MicroXS RR')
|
|
rr_tally.filters = [domain_filter, energy_filter]
|
|
rr_tally.nuclides = nuclides
|
|
rr_tally.multiply_density = False
|
|
rr_tally.scores = reactions
|
|
|
|
flux_tally = openmc.Tally(name='MicroXS flux')
|
|
flux_tally.filters = [domain_filter, energy_filter]
|
|
flux_tally.scores = ['flux']
|
|
model.tallies = openmc.Tallies([rr_tally, flux_tally])
|
|
|
|
# create temporary run
|
|
with TemporaryDirectory() as temp_dir:
|
|
if run_kwargs is None:
|
|
run_kwargs = {}
|
|
else:
|
|
run_kwargs = dict(run_kwargs)
|
|
run_kwargs.setdefault('cwd', temp_dir)
|
|
statepoint_path = model.run(**run_kwargs)
|
|
|
|
with StatePoint(statepoint_path) as sp:
|
|
rr_tally = sp.tallies[rr_tally.id]
|
|
rr_tally._read_results()
|
|
flux_tally = sp.tallies[flux_tally.id]
|
|
flux_tally._read_results()
|
|
|
|
# Get reaction rates and flux values
|
|
reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions)
|
|
flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1)
|
|
|
|
# Make energy groups last dimension
|
|
reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups)
|
|
flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups)
|
|
|
|
# Divide RR by flux to get microscopic cross sections
|
|
xs = np.empty_like(reaction_rates) # (domains, nuclides, reactions, groups)
|
|
d, _, _, g = np.nonzero(flux)
|
|
xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g]
|
|
|
|
# Reset tallies
|
|
model.tallies = original_tallies
|
|
|
|
# Create lists where each item corresponds to one domain
|
|
fluxes = list(flux.squeeze((1, 2)))
|
|
micros = [MicroXS(xs_i, nuclides, reactions) for xs_i in xs]
|
|
return fluxes, micros
|
|
|
|
|
|
class MicroXS:
|
|
"""Microscopic cross section data for use in transport-independent depletion.
|
|
|
|
.. versionadded:: 0.13.1
|
|
|
|
.. versionchanged:: 0.14.0
|
|
Class was heavily refactored and no longer subclasses pandas.DataFrame.
|
|
|
|
Parameters
|
|
----------
|
|
data : numpy.ndarray of floats
|
|
3D array containing microscopic cross section values for each
|
|
nuclide, reaction, and energy group. Cross section values are assumed to
|
|
be in [b], and indexed by [nuclide, reaction, energy group]
|
|
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`
|
|
|
|
"""
|
|
def __init__(self, data: np.ndarray, nuclides: List[str], reactions: List[str]):
|
|
# Validate inputs
|
|
if data.shape[:2] != (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}')
|
|
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)
|
|
|
|
self.data = data
|
|
self.nuclides = nuclides
|
|
self.reactions = reactions
|
|
self._index_nuc = {nuc: i for i, nuc in enumerate(nuclides)}
|
|
self._index_rx = {rx: i for i, rx in enumerate(reactions)}
|
|
|
|
@classmethod
|
|
def from_multigroup_flux(
|
|
cls,
|
|
energies: Union[Sequence[float], str],
|
|
multigroup_flux: Sequence[float],
|
|
chain_file: Optional[PathLike] = None,
|
|
temperature: float = 293.6,
|
|
nuclides: Optional[Sequence[str]] = None,
|
|
reactions: Optional[Sequence[str]] = None,
|
|
**init_kwargs: dict,
|
|
) -> MicroXS:
|
|
"""Generated microscopic cross sections from a known flux.
|
|
|
|
The size of the MicroXS matrix depends on the chain file and cross
|
|
sections available. MicroXS entry will be 0 if the nuclide cross section
|
|
is not found.
|
|
|
|
.. versionadded:: 0.14.1
|
|
|
|
Parameters
|
|
----------
|
|
energies : iterable of float or str
|
|
Energy group boundaries in [eV] or the name of the group structure
|
|
multi_group_flux : iterable of float
|
|
Energy-dependent multigroup flux values
|
|
chain_file : str, optional
|
|
Path to the depletion chain XML file that will be used in depletion
|
|
simulation. Defaults to ``openmc.config['chain_file']``.
|
|
temperature : int, optional
|
|
Temperature for cross section evaluation in [K].
|
|
nuclides : list of str, optional
|
|
Nuclides to get cross sections for. If not specified, all burnable
|
|
nuclides from the depletion chain file are used.
|
|
reactions : list of str, optional
|
|
Reactions to get cross sections for. If not specified, all neutron
|
|
reactions listed in the depletion chain file are used.
|
|
**init_kwargs : dict
|
|
Keyword arguments passed to :func:`openmc.lib.init`
|
|
|
|
Returns
|
|
-------
|
|
MicroXS
|
|
"""
|
|
|
|
check_type("temperature", temperature, (int, float))
|
|
# if energy is string then use group structure of that name
|
|
if isinstance(energies, str):
|
|
energies = GROUP_STRUCTURES[energies]
|
|
else:
|
|
# if user inputs energies check they are ascending (low to high) as
|
|
# some depletion codes use high energy to low energy.
|
|
if not np.all(np.diff(energies) > 0):
|
|
raise ValueError('Energy group boundaries must be in ascending order')
|
|
|
|
# check dimension consistency
|
|
if len(multigroup_flux) != len(energies) - 1:
|
|
raise ValueError('Length of flux array should be len(energies)-1')
|
|
|
|
chain_file_path = _resolve_chain_file_path(chain_file)
|
|
chain = Chain.from_xml(chain_file_path)
|
|
|
|
cross_sections = _find_cross_sections(model=None)
|
|
nuclides_with_data = _get_nuclides_with_data(cross_sections)
|
|
|
|
# If no nuclides were specified, default to all nuclides from the chain
|
|
if not nuclides:
|
|
nuclides = chain.nuclides
|
|
nuclides = [nuc.name for nuc in nuclides]
|
|
|
|
# Get reaction MT values. If no reactions specified, default to the
|
|
# reactions available in the chain file
|
|
if reactions is None:
|
|
reactions = chain.reactions
|
|
mts = [REACTION_MT[name] for name in reactions]
|
|
|
|
# Normalize multigroup flux
|
|
multigroup_flux = np.asarray(multigroup_flux)
|
|
multigroup_flux /= multigroup_flux.sum()
|
|
|
|
# Create 2D array for microscopic cross sections
|
|
microxs_arr = np.zeros((len(nuclides), len(mts)))
|
|
|
|
with TemporaryDirectory() as tmpdir:
|
|
# Create a material with all nuclides
|
|
mat_all_nucs = openmc.Material()
|
|
for nuc in nuclides:
|
|
if nuc in nuclides_with_data:
|
|
mat_all_nucs.add_nuclide(nuc, 1.0)
|
|
mat_all_nucs.set_density("atom/b-cm", 1.0)
|
|
|
|
# Create simple model containing the above material
|
|
surf1 = openmc.Sphere(boundary_type="vacuum")
|
|
surf1_cell = openmc.Cell(fill=mat_all_nucs, region=-surf1)
|
|
model = openmc.Model()
|
|
model.geometry = openmc.Geometry([surf1_cell])
|
|
model.settings = openmc.Settings(
|
|
particles=1, batches=1, output={'summary': False})
|
|
|
|
with change_directory(tmpdir):
|
|
# Export model within temporary directory
|
|
model.export_to_model_xml()
|
|
|
|
with openmc.lib.run_in_memory(**init_kwargs):
|
|
# For each nuclide and reaction, compute the flux-averaged
|
|
# cross section
|
|
for nuc_index, nuc in enumerate(nuclides):
|
|
if nuc not in nuclides_with_data:
|
|
continue
|
|
lib_nuc = openmc.lib.nuclides[nuc]
|
|
for mt_index, mt in enumerate(mts):
|
|
xs = lib_nuc.collapse_rate(
|
|
mt, temperature, energies, multigroup_flux
|
|
)
|
|
microxs_arr[nuc_index, mt_index] = xs
|
|
|
|
return cls(microxs_arr, nuclides, reactions)
|
|
|
|
@classmethod
|
|
def from_csv(cls, csv_file, **kwargs):
|
|
"""Load data from a comma-separated values (csv) file.
|
|
|
|
Parameters
|
|
----------
|
|
csv_file : str
|
|
Relative path to csv-file containing microscopic cross section
|
|
data. Cross section values are assumed to be in [b]
|
|
**kwargs : dict
|
|
Keyword arguments to pass to :func:`pandas.read_csv()`.
|
|
|
|
Returns
|
|
-------
|
|
MicroXS
|
|
|
|
"""
|
|
if 'float_precision' not in kwargs:
|
|
kwargs['float_precision'] = 'round_trip'
|
|
|
|
df = pd.read_csv(csv_file, **kwargs)
|
|
df.set_index(['nuclides', 'reactions', 'groups'], inplace=True)
|
|
nuclides = list(df.index.unique(level='nuclides'))
|
|
reactions = list(df.index.unique(level='reactions'))
|
|
groups = list(df.index.unique(level='groups'))
|
|
shape = (len(nuclides), len(reactions), len(groups))
|
|
data = df.values.reshape(shape)
|
|
return cls(data, nuclides, reactions)
|
|
|
|
def __getitem__(self, index):
|
|
nuc, rx = index
|
|
i_nuc = self._index_nuc[nuc]
|
|
i_rx = self._index_rx[rx]
|
|
return self.data[i_nuc, i_rx]
|
|
|
|
def to_csv(self, *args, **kwargs):
|
|
"""Write data to a comma-separated values (csv) file
|
|
|
|
Parameters
|
|
----------
|
|
*args
|
|
Positional arguments passed to :meth:`pandas.DataFrame.to_csv`
|
|
**kwargs
|
|
Keyword arguments passed to :meth:`pandas.DataFrame.to_csv`
|
|
|
|
"""
|
|
groups = self.data.shape[2]
|
|
multi_index = pd.MultiIndex.from_product(
|
|
[self.nuclides, self.reactions, range(1, groups + 1)],
|
|
names=['nuclides', 'reactions', 'groups']
|
|
)
|
|
df = pd.DataFrame({'xs': self.data.flatten()}, index=multi_index)
|
|
df.to_csv(*args, **kwargs)
|
|
|