2022-07-30 15:39:47 -05:00
|
|
|
|
"""abc module.
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
2025-03-05 19:07:50 -06:00
|
|
|
|
This module contains Abstract Base Classes for implementing operator,
|
|
|
|
|
|
integrator, depletion system solver, and operator helper classes
|
2018-02-09 13:29:04 -06:00
|
|
|
|
"""
|
|
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
from __future__ import annotations
|
2020-07-13 22:53:59 -05:00
|
|
|
|
from abc import ABC, abstractmethod
|
2020-05-09 14:54:03 -04:00
|
|
|
|
from collections import namedtuple, defaultdict
|
|
|
|
|
|
from collections.abc import Iterable, Callable
|
2019-08-07 10:51:19 -05:00
|
|
|
|
from copy import deepcopy
|
2020-05-09 14:54:03 -04:00
|
|
|
|
from inspect import signature
|
2020-07-13 22:53:59 -05:00
|
|
|
|
from numbers import Real, Integral
|
|
|
|
|
|
from pathlib import Path
|
2025-09-18 23:27:41 -05:00
|
|
|
|
from textwrap import dedent
|
2020-05-09 14:54:03 -04:00
|
|
|
|
import time
|
2024-02-15 07:30:46 +00:00
|
|
|
|
from typing import Optional, Union, Sequence
|
2020-07-13 22:53:59 -05:00
|
|
|
|
from warnings import warn
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
import numpy as np
|
2019-08-07 10:51:19 -05:00
|
|
|
|
from uncertainties import ufloat
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
2023-12-12 19:32:09 +00:00
|
|
|
|
from openmc.checkvalue import check_type, check_greater_than, PathLike
|
2021-10-01 06:09:24 -05:00
|
|
|
|
from openmc.mpi import comm
|
2024-05-02 12:47:10 -05:00
|
|
|
|
from openmc.utility_funcs import change_directory
|
2024-02-15 07:30:46 +00:00
|
|
|
|
from openmc import Material
|
2022-04-28 17:23:37 -05:00
|
|
|
|
from .stepresult import StepResult
|
2025-06-12 23:02:35 +02:00
|
|
|
|
from .chain import _get_chain
|
2025-03-05 19:07:50 -06:00
|
|
|
|
from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \
|
|
|
|
|
|
_SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR
|
2020-05-09 14:54:03 -04:00
|
|
|
|
from .pool import deplete
|
2024-02-15 07:30:46 +00:00
|
|
|
|
from .reaction_rates import ReactionRates
|
2025-05-12 22:48:21 +02:00
|
|
|
|
from .transfer_rates import TransferRates, ExternalSourceRates
|
2026-04-20 14:13:53 +02:00
|
|
|
|
from .keff_search_control import _KeffSearchControl
|
2018-02-19 17:52:17 -06:00
|
|
|
|
|
2019-09-17 15:13:19 -05:00
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
2022-08-01 12:44:28 -05:00
|
|
|
|
"OperatorResult", "TransportOperator",
|
2022-07-30 18:37:51 -05:00
|
|
|
|
"ReactionRateHelper", "NormalizationHelper", "FissionYieldHelper",
|
2020-07-15 21:28:35 -05:00
|
|
|
|
"Integrator", "SIIntegrator", "DepSystemSolver", "add_params"]
|
2019-09-17 15:13:19 -05:00
|
|
|
|
|
|
|
|
|
|
|
2025-02-21 11:47:38 -06:00
|
|
|
|
def _normalize_timesteps(
|
|
|
|
|
|
timesteps: Sequence[float] | Sequence[tuple[float, str]],
|
|
|
|
|
|
source_rates: float | Sequence[float],
|
|
|
|
|
|
timestep_units: str = 's',
|
|
|
|
|
|
operator: TransportOperator | None = None,
|
|
|
|
|
|
):
|
|
|
|
|
|
if not isinstance(source_rates, Sequence):
|
|
|
|
|
|
# Ensure that rate is single value if that is the case
|
|
|
|
|
|
source_rates = [source_rates] * len(timesteps)
|
|
|
|
|
|
|
|
|
|
|
|
if len(source_rates) != len(timesteps):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"Number of time steps ({}) != number of powers ({})".format(
|
|
|
|
|
|
len(timesteps), len(source_rates)))
|
|
|
|
|
|
|
|
|
|
|
|
# Get list of times / units
|
|
|
|
|
|
if isinstance(timesteps[0], Sequence):
|
|
|
|
|
|
times, units = zip(*timesteps)
|
|
|
|
|
|
else:
|
|
|
|
|
|
times = timesteps
|
|
|
|
|
|
units = [timestep_units] * len(timesteps)
|
|
|
|
|
|
|
|
|
|
|
|
# Determine number of seconds for each timestep
|
|
|
|
|
|
seconds = []
|
|
|
|
|
|
for timestep, unit, rate in zip(times, units, source_rates):
|
|
|
|
|
|
# Make sure values passed make sense
|
|
|
|
|
|
check_type('timestep', timestep, Real)
|
|
|
|
|
|
check_greater_than('timestep', timestep, 0.0, False)
|
|
|
|
|
|
check_type('timestep units', unit, str)
|
|
|
|
|
|
check_type('source rate', rate, Real)
|
|
|
|
|
|
check_greater_than('source rate', rate, 0.0, True)
|
|
|
|
|
|
|
|
|
|
|
|
if unit in ('s', 'sec'):
|
|
|
|
|
|
seconds.append(timestep)
|
|
|
|
|
|
elif unit in ('min', 'minute'):
|
|
|
|
|
|
seconds.append(timestep*_SECONDS_PER_MINUTE)
|
|
|
|
|
|
elif unit in ('h', 'hr', 'hour'):
|
|
|
|
|
|
seconds.append(timestep*_SECONDS_PER_HOUR)
|
|
|
|
|
|
elif unit in ('d', 'day'):
|
|
|
|
|
|
seconds.append(timestep*_SECONDS_PER_DAY)
|
|
|
|
|
|
elif unit in ('a', 'year'):
|
|
|
|
|
|
seconds.append(timestep*_SECONDS_PER_JULIAN_YEAR)
|
|
|
|
|
|
elif unit.lower() == 'mwd/kg':
|
|
|
|
|
|
watt_days_per_kg = 1e6*timestep
|
|
|
|
|
|
kilograms = 1e-3*operator.heavy_metal
|
|
|
|
|
|
if rate == 0.0:
|
|
|
|
|
|
raise ValueError("Cannot specify a timestep in [MWd/kg] when"
|
|
|
|
|
|
" the power is zero.")
|
|
|
|
|
|
days = watt_days_per_kg * kilograms / rate
|
|
|
|
|
|
seconds.append(days*_SECONDS_PER_DAY)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(f"Invalid timestep unit '{unit}'")
|
|
|
|
|
|
|
|
|
|
|
|
return (np.asarray(seconds), np.asarray(source_rates))
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-02-19 17:52:17 -06:00
|
|
|
|
OperatorResult = namedtuple('OperatorResult', ['k', 'rates'])
|
2018-02-19 22:51:53 -06:00
|
|
|
|
OperatorResult.__doc__ = """\
|
|
|
|
|
|
Result of applying transport operator
|
2018-02-19 17:52:17 -06:00
|
|
|
|
|
2018-02-19 22:51:53 -06:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2019-07-26 10:31:24 -05:00
|
|
|
|
k : uncertainties.ufloat
|
|
|
|
|
|
Resulting eigenvalue and standard deviation
|
2018-02-19 22:51:53 -06:00
|
|
|
|
rates : openmc.deplete.ReactionRates
|
|
|
|
|
|
Resulting reaction rates
|
2018-02-19 17:52:17 -06:00
|
|
|
|
|
2018-02-19 22:51:53 -06:00
|
|
|
|
"""
|
2018-02-20 22:46:26 -06:00
|
|
|
|
try:
|
|
|
|
|
|
OperatorResult.k.__doc__ = None
|
|
|
|
|
|
OperatorResult.rates.__doc__ = None
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
# Can't set __doc__ on properties on Python 3.4
|
|
|
|
|
|
pass
|
2018-02-19 22:51:53 -06:00
|
|
|
|
|
|
|
|
|
|
|
2022-08-01 12:44:28 -05:00
|
|
|
|
class TransportOperator(ABC):
|
|
|
|
|
|
"""Abstract class defining a transport operator
|
2018-02-09 14:01:59 -06:00
|
|
|
|
|
2022-08-02 12:28:00 -05:00
|
|
|
|
Each depletion integrator is written to work with a generic transport
|
2018-02-19 22:51:53 -06:00
|
|
|
|
operator that takes a vector of material compositions and returns an
|
2019-08-07 10:51:19 -05:00
|
|
|
|
eigenvalue and reaction rates. This abstract class sets the requirements
|
2022-08-01 12:44:28 -05:00
|
|
|
|
for such a transport operator. Users should instantiate
|
2022-07-30 16:46:31 -05:00
|
|
|
|
:class:`openmc.deplete.CoupledOperator` or
|
|
|
|
|
|
:class:`openmc.deplete.IndependentOperator` rather than this class.
|
2018-02-19 22:51:53 -06:00
|
|
|
|
|
2018-02-19 17:52:17 -06:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2025-06-12 23:02:35 +02:00
|
|
|
|
chain_file : PathLike or Chain
|
|
|
|
|
|
Path to the depletion chain XML file or instance of openmc.deplete.Chain.
|
2019-06-28 10:21:57 -05:00
|
|
|
|
fission_q : dict, optional
|
|
|
|
|
|
Dictionary of nuclides and their fission Q values [eV]. If not given,
|
2019-06-27 11:42:18 -05:00
|
|
|
|
values will be pulled from the ``chain_file``.
|
2022-04-28 17:20:55 -05:00
|
|
|
|
prev_results : Results, optional
|
2019-08-06 17:00:41 -05:00
|
|
|
|
Results from a previous depletion calculation.
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
2022-07-12 16:07:59 -05:00
|
|
|
|
output_dir : pathlib.Path
|
|
|
|
|
|
Path to output directory to save results.
|
2022-04-28 17:20:55 -05:00
|
|
|
|
prev_res : Results or None
|
2019-08-06 17:00:41 -05:00
|
|
|
|
Results from a previous depletion calculation. ``None`` if no
|
|
|
|
|
|
results are to be used.
|
2022-07-12 16:07:59 -05:00
|
|
|
|
chain : openmc.deplete.Chain
|
|
|
|
|
|
The depletion chain information necessary to form matrices and tallies.
|
|
|
|
|
|
|
2018-02-15 06:37:46 -06:00
|
|
|
|
"""
|
2025-06-12 23:02:35 +02:00
|
|
|
|
def __init__(self, chain_file=None, fission_q=None, prev_results=None):
|
2018-02-19 17:52:17 -06:00
|
|
|
|
self.output_dir = '.'
|
2018-02-15 06:37:46 -06:00
|
|
|
|
|
2018-02-19 17:52:17 -06:00
|
|
|
|
# Read depletion chain
|
2025-06-12 23:02:35 +02:00
|
|
|
|
self.chain = _get_chain(chain_file, fission_q)
|
|
|
|
|
|
|
2019-08-06 17:00:41 -05:00
|
|
|
|
if prev_results is None:
|
|
|
|
|
|
self.prev_res = None
|
|
|
|
|
|
else:
|
2022-04-28 17:20:55 -05:00
|
|
|
|
check_type("previous results", prev_results, Results)
|
2020-07-16 11:00:10 +08:00
|
|
|
|
self.prev_res = prev_results
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
2026-04-20 14:13:53 +02:00
|
|
|
|
def __call__(self, vec, source_rate) -> OperatorResult:
|
2018-02-09 14:01:59 -06:00
|
|
|
|
"""Runs a simulation.
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2018-02-19 23:17:50 -06:00
|
|
|
|
vec : list of numpy.ndarray
|
2018-02-09 13:29:04 -06:00
|
|
|
|
Total atoms to be used in function.
|
2020-07-15 08:09:37 -05:00
|
|
|
|
source_rate : float
|
|
|
|
|
|
Power in [W] or source rate in [neutron/sec]
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2018-02-15 14:53:11 -06:00
|
|
|
|
openmc.deplete.OperatorResult
|
|
|
|
|
|
Eigenvalue and reaction rates resulting from transport operator
|
|
|
|
|
|
|
2018-02-09 13:29:04 -06:00
|
|
|
|
"""
|
2018-02-14 12:57:56 -06:00
|
|
|
|
|
2018-02-19 17:52:17 -06:00
|
|
|
|
@property
|
|
|
|
|
|
def output_dir(self):
|
|
|
|
|
|
return self._output_dir
|
|
|
|
|
|
|
|
|
|
|
|
@output_dir.setter
|
|
|
|
|
|
def output_dir(self, output_dir):
|
|
|
|
|
|
self._output_dir = Path(output_dir)
|
|
|
|
|
|
|
2018-02-14 12:57:56 -06:00
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def initial_condition(self):
|
|
|
|
|
|
"""Performs final setup and returns initial condition.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2018-02-19 23:17:50 -06:00
|
|
|
|
list of numpy.ndarray
|
2018-02-14 12:57:56 -06:00
|
|
|
|
Total density for initial conditions.
|
|
|
|
|
|
"""
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def get_results_info(self):
|
2018-02-09 14:01:59 -06:00
|
|
|
|
"""Returns volume list, cell lists, and nuc lists.
|
2018-02-09 13:29:04 -06:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2020-03-21 10:07:40 -04:00
|
|
|
|
volume : dict of str to float
|
2026-04-02 12:30:09 -04:00
|
|
|
|
Volumes corresponding to materials in full_burn_list
|
2018-02-09 13:29:04 -06:00
|
|
|
|
nuc_list : list of str
|
|
|
|
|
|
A list of all nuclide names. Used for sorting the simulation.
|
|
|
|
|
|
burn_list : list of int
|
2019-08-07 10:51:19 -05:00
|
|
|
|
A list of all cell IDs to be burned. Used for sorting the
|
|
|
|
|
|
simulation.
|
2018-02-09 13:29:04 -06:00
|
|
|
|
full_burn_list : list of int
|
|
|
|
|
|
All burnable materials in the geometry.
|
2026-01-27 07:06:36 +01:00
|
|
|
|
name_list : list of str
|
2026-04-02 12:30:09 -04:00
|
|
|
|
Material names corresponding to materials in full_burn_list
|
2018-02-09 13:29:04 -06:00
|
|
|
|
"""
|
|
|
|
|
|
|
2018-02-14 12:57:56 -06:00
|
|
|
|
def finalize(self):
|
|
|
|
|
|
pass
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
2019-08-06 17:36:22 -05:00
|
|
|
|
@abstractmethod
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def write_bos_data(self, step: int):
|
2019-08-06 17:36:22 -05:00
|
|
|
|
"""Document beginning of step data for a given step
|
|
|
|
|
|
|
|
|
|
|
|
Called at the beginning of a depletion step and at
|
|
|
|
|
|
the final point in the simulation.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
step : int
|
|
|
|
|
|
Current depletion step including restarts
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
|
|
|
|
|
class ReactionRateHelper(ABC):
|
|
|
|
|
|
"""Abstract class for generating reaction rates for operators
|
|
|
|
|
|
|
2020-08-04 14:35:12 -05:00
|
|
|
|
Responsible for generating reaction rate tallies for burnable materials,
|
|
|
|
|
|
given nuclides and scores from the operator.
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
2020-09-17 22:18:20 -05:00
|
|
|
|
Reaction rates are passed back to the operator to be used by an
|
2020-08-04 14:35:12 -05:00
|
|
|
|
:class:`openmc.deplete.OperatorResult` instance.
|
2019-07-15 11:13:45 -05:00
|
|
|
|
|
Fix bug when reaction rate nuclides change through depletion
The size of the Operator.reaction_rates does not change through
depletion, but the number of tallied nuclides for reaction
rates may or may not. The DirectReactionRateHelper returns
reaction rates according to the number of nuclides tallied,
a potential subset of all nuclides designated as burnable
by the Operator. This causes IndexErrors if a nuclide is not
tallied at a later step.
Example: 10 nuclides [0-9] are originally tracked by the Operator
and tallied by DirectReactionRateHelper. The Operator.reaction_rates
array will be of shape (n_mat, 10, n_react). Initially, the
DirectReactionRateHelper returns an array of size (10, n_react)
for each material. Then, if nuclide 5 is not in the list of nuclides
passed to the DirectReactionRateHelper at the next step, [decayed to
zero], DirectReactionRateHelper will return an array (9, n_react) and
try to pass tally data for nuclide 9 into row 9 of the reaction rate
array, causing an IndexError.
This commit instructs requires two integers, n_nucs and n_react, to be
passed to the initialization of any ReactionRateHelper, allocating
a single array for storing material-reaction rates. The method
get_material_rates uses this directly and does not re-allocate storage
if len(nuc_index) has changed [like if nuclide 9 has dropped out].
2019-07-25 09:32:13 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
n_nucs : int
|
2022-07-30 16:46:31 -05:00
|
|
|
|
Number of burnable nuclides tracked by
|
2022-08-01 12:44:28 -05:00
|
|
|
|
:class:`openmc.deplete.abc.TransportOperator`
|
Fix bug when reaction rate nuclides change through depletion
The size of the Operator.reaction_rates does not change through
depletion, but the number of tallied nuclides for reaction
rates may or may not. The DirectReactionRateHelper returns
reaction rates according to the number of nuclides tallied,
a potential subset of all nuclides designated as burnable
by the Operator. This causes IndexErrors if a nuclide is not
tallied at a later step.
Example: 10 nuclides [0-9] are originally tracked by the Operator
and tallied by DirectReactionRateHelper. The Operator.reaction_rates
array will be of shape (n_mat, 10, n_react). Initially, the
DirectReactionRateHelper returns an array of size (10, n_react)
for each material. Then, if nuclide 5 is not in the list of nuclides
passed to the DirectReactionRateHelper at the next step, [decayed to
zero], DirectReactionRateHelper will return an array (9, n_react) and
try to pass tally data for nuclide 9 into row 9 of the reaction rate
array, causing an IndexError.
This commit instructs requires two integers, n_nucs and n_react, to be
passed to the initialization of any ReactionRateHelper, allocating
a single array for storing material-reaction rates. The method
get_material_rates uses this directly and does not re-allocate storage
if len(nuc_index) has changed [like if nuclide 9 has dropped out].
2019-07-25 09:32:13 -05:00
|
|
|
|
n_react : int
|
2022-07-30 16:46:31 -05:00
|
|
|
|
Number of reactions tracked by
|
2022-08-01 12:44:28 -05:00
|
|
|
|
:class:`openmc.deplete.abc.TransportOperator`
|
Fix bug when reaction rate nuclides change through depletion
The size of the Operator.reaction_rates does not change through
depletion, but the number of tallied nuclides for reaction
rates may or may not. The DirectReactionRateHelper returns
reaction rates according to the number of nuclides tallied,
a potential subset of all nuclides designated as burnable
by the Operator. This causes IndexErrors if a nuclide is not
tallied at a later step.
Example: 10 nuclides [0-9] are originally tracked by the Operator
and tallied by DirectReactionRateHelper. The Operator.reaction_rates
array will be of shape (n_mat, 10, n_react). Initially, the
DirectReactionRateHelper returns an array of size (10, n_react)
for each material. Then, if nuclide 5 is not in the list of nuclides
passed to the DirectReactionRateHelper at the next step, [decayed to
zero], DirectReactionRateHelper will return an array (9, n_react) and
try to pass tally data for nuclide 9 into row 9 of the reaction rate
array, causing an IndexError.
This commit instructs requires two integers, n_nucs and n_react, to be
passed to the initialization of any ReactionRateHelper, allocating
a single array for storing material-reaction rates. The method
get_material_rates uses this directly and does not re-allocate storage
if len(nuc_index) has changed [like if nuclide 9 has dropped out].
2019-07-25 09:32:13 -05:00
|
|
|
|
|
2019-07-15 11:13:45 -05:00
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
|
|
|
|
|
nuclides : list of str
|
Fix bug when reaction rate nuclides change through depletion
The size of the Operator.reaction_rates does not change through
depletion, but the number of tallied nuclides for reaction
rates may or may not. The DirectReactionRateHelper returns
reaction rates according to the number of nuclides tallied,
a potential subset of all nuclides designated as burnable
by the Operator. This causes IndexErrors if a nuclide is not
tallied at a later step.
Example: 10 nuclides [0-9] are originally tracked by the Operator
and tallied by DirectReactionRateHelper. The Operator.reaction_rates
array will be of shape (n_mat, 10, n_react). Initially, the
DirectReactionRateHelper returns an array of size (10, n_react)
for each material. Then, if nuclide 5 is not in the list of nuclides
passed to the DirectReactionRateHelper at the next step, [decayed to
zero], DirectReactionRateHelper will return an array (9, n_react) and
try to pass tally data for nuclide 9 into row 9 of the reaction rate
array, causing an IndexError.
This commit instructs requires two integers, n_nucs and n_react, to be
passed to the initialization of any ReactionRateHelper, allocating
a single array for storing material-reaction rates. The method
get_material_rates uses this directly and does not re-allocate storage
if len(nuc_index) has changed [like if nuclide 9 has dropped out].
2019-07-25 09:32:13 -05:00
|
|
|
|
All nuclides with desired reaction rates.
|
2019-07-05 13:09:30 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
Fix bug when reaction rate nuclides change through depletion
The size of the Operator.reaction_rates does not change through
depletion, but the number of tallied nuclides for reaction
rates may or may not. The DirectReactionRateHelper returns
reaction rates according to the number of nuclides tallied,
a potential subset of all nuclides designated as burnable
by the Operator. This causes IndexErrors if a nuclide is not
tallied at a later step.
Example: 10 nuclides [0-9] are originally tracked by the Operator
and tallied by DirectReactionRateHelper. The Operator.reaction_rates
array will be of shape (n_mat, 10, n_react). Initially, the
DirectReactionRateHelper returns an array of size (10, n_react)
for each material. Then, if nuclide 5 is not in the list of nuclides
passed to the DirectReactionRateHelper at the next step, [decayed to
zero], DirectReactionRateHelper will return an array (9, n_react) and
try to pass tally data for nuclide 9 into row 9 of the reaction rate
array, causing an IndexError.
This commit instructs requires two integers, n_nucs and n_react, to be
passed to the initialization of any ReactionRateHelper, allocating
a single array for storing material-reaction rates. The method
get_material_rates uses this directly and does not re-allocate storage
if len(nuc_index) has changed [like if nuclide 9 has dropped out].
2019-07-25 09:32:13 -05:00
|
|
|
|
def __init__(self, n_nucs, n_react):
|
2019-07-05 13:09:30 -05:00
|
|
|
|
self._nuclides = None
|
2024-02-15 07:30:46 +00:00
|
|
|
|
self._results_cache = np.empty((n_nucs, n_react))
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def generate_tallies(self, materials, scores):
|
2019-07-23 08:29:37 -05:00
|
|
|
|
"""Use the C API to build tallies needed for reaction rates"""
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def nuclides(self):
|
|
|
|
|
|
"""List of nuclides with requested reaction rates"""
|
|
|
|
|
|
return self._nuclides
|
|
|
|
|
|
|
|
|
|
|
|
@nuclides.setter
|
|
|
|
|
|
def nuclides(self, nuclides):
|
|
|
|
|
|
check_type("nuclides", nuclides, list, str)
|
|
|
|
|
|
self._nuclides = nuclides
|
|
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def get_material_rates(
|
|
|
|
|
|
self,
|
|
|
|
|
|
mat_id: int,
|
|
|
|
|
|
nuc_index: Sequence[str],
|
|
|
|
|
|
react_index: Sequence[str]
|
|
|
|
|
|
):
|
2019-07-05 13:09:30 -05:00
|
|
|
|
"""Return 2D array of [nuclide, reaction] reaction rates
|
|
|
|
|
|
|
2019-07-15 11:39:05 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2019-07-23 08:29:37 -05:00
|
|
|
|
mat_id : int
|
|
|
|
|
|
Unique ID for the requested material
|
2019-07-15 11:39:05 -05:00
|
|
|
|
nuc_index : list of str
|
|
|
|
|
|
Ordering of desired nuclides
|
|
|
|
|
|
react_index : list of str
|
|
|
|
|
|
Ordering of reactions
|
|
|
|
|
|
"""
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def divide_by_atoms(self, number: Sequence[float]):
|
2022-11-21 14:51:40 -06:00
|
|
|
|
"""Normalize reaction rates by number of atoms
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
2020-08-04 14:35:12 -05:00
|
|
|
|
Acts on the current material examined by :meth:`get_material_rates`
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2019-07-23 11:28:58 -05:00
|
|
|
|
number : iterable of float
|
2023-06-15 00:15:22 -05:00
|
|
|
|
Number of each nuclide in [atom] tracked in the calculation.
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2019-07-23 08:29:37 -05:00
|
|
|
|
results : numpy.ndarray
|
|
|
|
|
|
Array of reactions rates of shape ``(n_nuclides, n_rxns)``
|
|
|
|
|
|
normalized by the number of nuclides
|
2019-07-05 13:09:30 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
mask = np.nonzero(number)
|
2019-07-05 13:09:30 -05:00
|
|
|
|
results = self._results_cache
|
|
|
|
|
|
for col in range(results.shape[1]):
|
|
|
|
|
|
results[mask, col] /= number[mask]
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-07-13 16:15:14 -05:00
|
|
|
|
class NormalizationHelper(ABC):
|
|
|
|
|
|
"""Abstract class for obtaining normalization factor on tallies
|
2019-07-18 14:35:39 -05:00
|
|
|
|
|
2020-07-13 16:15:14 -05:00
|
|
|
|
This helper class determines how reaction rates calculated by an instance of
|
2022-08-01 12:44:28 -05:00
|
|
|
|
:class:`openmc.deplete.abc.TransportOperator` should be normalized for the
|
2022-07-30 16:46:31 -05:00
|
|
|
|
purpose of constructing a burnup matrix. Based on the method chosen, the
|
|
|
|
|
|
power or source rate provided by the user, and reaction rates from a
|
2020-07-13 16:15:14 -05:00
|
|
|
|
:class:`ReactionRateHelper`, this class will scale reaction rates to the
|
2019-07-18 14:35:39 -05:00
|
|
|
|
correct values.
|
2019-07-15 11:13:45 -05:00
|
|
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
|
|
|
|
|
nuclides : list of str
|
|
|
|
|
|
All nuclides with desired reaction rates. Ordered to be
|
2022-08-01 12:44:28 -05:00
|
|
|
|
consistent with :class:`openmc.deplete.abc.TransportOperator`
|
2020-07-13 22:53:59 -05:00
|
|
|
|
|
2019-07-05 13:09:30 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self._nuclides = None
|
2019-07-18 14:35:39 -05:00
|
|
|
|
|
|
|
|
|
|
def reset(self):
|
2020-08-03 14:25:58 -05:00
|
|
|
|
"""Reset state for normalization"""
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def prepare(self, chain_nucs: Sequence[str], rate_index: dict):
|
2019-07-18 14:35:39 -05:00
|
|
|
|
"""Perform work needed to obtain energy produced
|
|
|
|
|
|
|
2022-07-30 16:46:31 -05:00
|
|
|
|
This method is called prior to calculating the reaction rates
|
2022-08-01 12:44:28 -05:00
|
|
|
|
in :meth:`openmc.deplete.abc.TransportOperator.initial_condition`. Only
|
2022-07-30 16:46:31 -05:00
|
|
|
|
used for energy-based normalization.
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
2019-07-15 11:39:05 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
chain_nucs : list of str
|
|
|
|
|
|
All nuclides to be tracked in this problem
|
|
|
|
|
|
rate_index : dict of str to int
|
|
|
|
|
|
Mapping from nuclide name to index in the
|
2019-07-18 14:35:39 -05:00
|
|
|
|
`fission_rates` for :meth:`update`.
|
2019-07-15 11:39:05 -05:00
|
|
|
|
"""
|
2019-07-05 13:09:30 -05:00
|
|
|
|
|
2022-08-10 15:53:40 -05:00
|
|
|
|
def update(self, fission_rates):
|
2020-08-03 14:25:58 -05:00
|
|
|
|
"""Update the normalization based on fission rates (only used for
|
|
|
|
|
|
energy-based normalization)
|
2019-07-15 11:39:05 -05:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
fission_rates : numpy.ndarray
|
|
|
|
|
|
fission reaction rate for each isotope in the specified
|
|
|
|
|
|
material. Should be ordered corresponding to initial
|
|
|
|
|
|
``rate_index`` used in :meth:`prepare`
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2019-07-05 13:09:30 -05:00
|
|
|
|
@property
|
|
|
|
|
|
def nuclides(self):
|
|
|
|
|
|
"""List of nuclides with requested reaction rates"""
|
|
|
|
|
|
return self._nuclides
|
|
|
|
|
|
|
|
|
|
|
|
@nuclides.setter
|
|
|
|
|
|
def nuclides(self, nuclides):
|
|
|
|
|
|
check_type("nuclides", nuclides, list, str)
|
|
|
|
|
|
self._nuclides = nuclides
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2020-08-03 14:25:58 -05:00
|
|
|
|
@abstractmethod
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def factor(self, source_rate: float):
|
2020-08-03 14:25:58 -05:00
|
|
|
|
"""Return normalization factor
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
source_rate : float
|
|
|
|
|
|
Power in [W] or source rate in [neutron/sec]
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
float
|
|
|
|
|
|
Normalization factor for tallies
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
2020-07-13 22:53:59 -05:00
|
|
|
|
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2019-08-13 15:23:19 -05:00
|
|
|
|
class FissionYieldHelper(ABC):
|
|
|
|
|
|
"""Abstract class for processing energy dependent fission yields
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
chain_nuclides : iterable of openmc.deplete.Nuclide
|
2019-08-21 09:34:44 -05:00
|
|
|
|
Nuclides tracked in the depletion chain. All nuclides are
|
|
|
|
|
|
not required to have fission yield data.
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
2019-09-11 10:26:45 -05:00
|
|
|
|
constant_yields : collections.defaultdict
|
2019-08-13 15:23:19 -05:00
|
|
|
|
Fission yields for all nuclides that only have one set of
|
2019-09-11 10:26:45 -05:00
|
|
|
|
fission yield data. Dictionary of form ``{str: {str: float}}``
|
|
|
|
|
|
representing yields for ``{parent: {product: yield}}``. Default
|
|
|
|
|
|
return object is an empty dictionary
|
|
|
|
|
|
|
2019-08-13 15:23:19 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, chain_nuclides):
|
|
|
|
|
|
self._chain_nuclides = {}
|
2019-09-11 10:26:45 -05:00
|
|
|
|
self._constant_yields = defaultdict(dict)
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
|
|
|
|
|
# Get all nuclides with fission yield data
|
|
|
|
|
|
for nuc in chain_nuclides:
|
2019-08-21 11:34:54 -05:00
|
|
|
|
if nuc.yield_data is None:
|
|
|
|
|
|
continue
|
2019-08-13 15:23:19 -05:00
|
|
|
|
if len(nuc.yield_data) == 1:
|
|
|
|
|
|
self._constant_yields[nuc.name] = (
|
|
|
|
|
|
nuc.yield_data[nuc.yield_energies[0]])
|
|
|
|
|
|
elif len(nuc.yield_data) > 1:
|
|
|
|
|
|
self._chain_nuclides[nuc.name] = nuc
|
|
|
|
|
|
self._chain_set = set(self._chain_nuclides) | set(self._constant_yields)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def constant_yields(self):
|
2019-08-21 10:05:59 -05:00
|
|
|
|
return deepcopy(self._constant_yields)
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def weighted_yields(self, local_mat_index):
|
|
|
|
|
|
"""Return fission yields for a specific material
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
local_mat_index : int
|
2019-09-11 10:26:45 -05:00
|
|
|
|
Index for the material with requested fission yields.
|
|
|
|
|
|
Should correspond to the material represented in
|
|
|
|
|
|
``mat_indexes[local_mat_index]`` during
|
|
|
|
|
|
:meth:`generate_tallies`.
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2019-09-11 10:26:45 -05:00
|
|
|
|
library : collections.abc.Mapping
|
|
|
|
|
|
Dictionary-like object mapping ``{str: {str: float}``.
|
|
|
|
|
|
This reflects fission yields for ``{parent: {product: fyield}}``.
|
2019-08-13 15:23:19 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def unpack():
|
|
|
|
|
|
"""Unpack tally data prior to compute fission yields.
|
|
|
|
|
|
|
2022-08-01 12:44:28 -05:00
|
|
|
|
Called after a :meth:`openmc.deplete.abc.TransportOperator.__call__`
|
2019-08-13 15:23:19 -05:00
|
|
|
|
routine during the normalization of reaction rates.
|
|
|
|
|
|
|
|
|
|
|
|
Not necessary for all subclasses to implement, unless tallies
|
|
|
|
|
|
are used.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def generate_tallies(materials, mat_indexes):
|
|
|
|
|
|
"""Construct tallies necessary for computing fission yields
|
|
|
|
|
|
|
|
|
|
|
|
Called during the operator set up phase prior to depleting.
|
|
|
|
|
|
Not necessary for subclasses to implement
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
materials : iterable of C-API materials
|
2019-09-05 07:31:13 -05:00
|
|
|
|
Materials to be used in :class:`openmc.lib.MaterialFilter`
|
2019-08-13 15:23:19 -05:00
|
|
|
|
mat_indexes : iterable of int
|
2019-08-14 18:02:27 -05:00
|
|
|
|
Indices of tallied materials that will have their fission
|
|
|
|
|
|
yields computed by this helper. Necessary as the
|
2022-07-30 16:46:31 -05:00
|
|
|
|
:class:`openmc.deplete.CoupledOperator` that uses this helper
|
2019-08-14 18:02:27 -05:00
|
|
|
|
may only burn a subset of all materials when running
|
|
|
|
|
|
in parallel mode.
|
2019-08-13 15:23:19 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def update_tally_nuclides(self, nuclides: Sequence[str]) -> list:
|
2019-08-13 15:23:19 -05:00
|
|
|
|
"""Return nuclides with non-zero densities and yield data
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
nuclides : iterable of str
|
|
|
|
|
|
Nuclides with non-zero densities from the
|
2022-08-01 12:44:28 -05:00
|
|
|
|
:class:`openmc.deplete.abc.TransportOperator`
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2019-08-22 15:32:45 -05:00
|
|
|
|
nuclides : list of str
|
2022-07-30 16:46:31 -05:00
|
|
|
|
Union of nuclides that the
|
2022-08-01 12:44:28 -05:00
|
|
|
|
:class:`openmc.deplete.abc.TransportOperator` says have non-zero
|
2022-07-30 16:46:31 -05:00
|
|
|
|
densities at this stage and those that have yield data. Sorted by
|
|
|
|
|
|
nuclide name
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
|
|
|
|
|
"""
|
2019-08-22 15:32:45 -05:00
|
|
|
|
return sorted(self._chain_set & set(nuclides))
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
2019-08-13 17:47:34 -05:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def from_operator(cls, operator, **kwargs):
|
|
|
|
|
|
"""Create a new instance by pulling data from the operator
|
|
|
|
|
|
|
2019-08-21 09:34:44 -05:00
|
|
|
|
All keyword arguments should be identical to their counterpart
|
|
|
|
|
|
in the main ``__init__`` method
|
|
|
|
|
|
|
2019-08-13 17:47:34 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2022-08-01 12:44:28 -05:00
|
|
|
|
operator : openmc.deplete.abc.TransportOperator
|
2019-08-13 17:47:34 -05:00
|
|
|
|
Operator with a depletion chain
|
|
|
|
|
|
kwargs: optional
|
2019-08-21 09:34:44 -05:00
|
|
|
|
Additional keyword arguments to be used in constuction
|
2019-08-13 17:47:34 -05:00
|
|
|
|
"""
|
|
|
|
|
|
return cls(operator.chain.nuclides, **kwargs)
|
|
|
|
|
|
|
2019-08-13 15:23:19 -05:00
|
|
|
|
|
2020-07-15 21:28:35 -05:00
|
|
|
|
def add_params(cls):
|
|
|
|
|
|
cls.__doc__ += cls._params
|
|
|
|
|
|
return cls
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@add_params
|
2019-08-07 10:51:19 -05:00
|
|
|
|
class Integrator(ABC):
|
2020-05-09 14:54:03 -04:00
|
|
|
|
r"""Abstract class for solving the time-integration for depletion
|
2020-07-15 21:28:35 -05:00
|
|
|
|
"""
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2025-09-18 23:27:41 -05:00
|
|
|
|
_params = dedent(r"""
|
2019-08-07 10:51:19 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2022-08-01 12:44:28 -05:00
|
|
|
|
operator : openmc.deplete.abc.TransportOperator
|
|
|
|
|
|
Operator to perform transport simulations
|
2020-02-10 16:01:30 -06:00
|
|
|
|
timesteps : iterable of float or iterable of tuple
|
|
|
|
|
|
Array of timesteps. Note that values are not cumulative. The units are
|
|
|
|
|
|
specified by the `timestep_units` argument when `timesteps` is an
|
|
|
|
|
|
iterable of float. Alternatively, units can be specified for each step
|
|
|
|
|
|
by passing an iterable of (value, unit) tuples.
|
2019-08-07 10:51:19 -05:00
|
|
|
|
power : float or iterable of float, optional
|
2026-04-22 16:24:34 -07:00
|
|
|
|
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]. Either ``power``, ``power_density``, or
|
2020-07-15 08:09:37 -05:00
|
|
|
|
``source_rates`` must be specified.
|
2019-08-07 10:51:19 -05:00
|
|
|
|
power_density : float or iterable of float, optional
|
2026-04-22 16:24:34 -07:00
|
|
|
|
Power density of the reactor in [W/gHM]. It is multiplied by initial
|
|
|
|
|
|
heavy metal inventory to get total power if ``power`` is not specified.
|
2020-08-06 08:12:36 -05:00
|
|
|
|
source_rates : float or iterable of float, optional
|
2022-08-01 09:35:30 -05:00
|
|
|
|
Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for
|
|
|
|
|
|
each interval in :attr:`timesteps`
|
2020-07-15 08:09:37 -05:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.12.1
|
2022-08-26 16:30:24 +01:00
|
|
|
|
timestep_units : {'s', 'min', 'h', 'd', 'a', 'MWd/kg'}
|
2020-02-10 16:01:30 -06:00
|
|
|
|
Units for values specified in the `timesteps` argument. 's' means
|
2022-08-26 16:30:24 +01:00
|
|
|
|
seconds, 'min' means minutes, 'h' means hours, 'a' means Julian years
|
|
|
|
|
|
and 'MWd/kg' indicates that the values are given in burnup (MW-d of
|
|
|
|
|
|
energy deposited per kilogram of initial heavy metal).
|
2020-05-09 14:54:03 -04:00
|
|
|
|
solver : str or callable, optional
|
2026-04-22 16:24:34 -07:00
|
|
|
|
If a string, must be the name of the solver responsible for solving the
|
|
|
|
|
|
Bateman equations. Current options are:
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
|
|
|
|
|
* ``cram16`` - 16th order IPF CRAM
|
|
|
|
|
|
* ``cram48`` - 48th order IPF CRAM [default]
|
|
|
|
|
|
|
|
|
|
|
|
If a function or other callable, must adhere to the requirements in
|
|
|
|
|
|
:attr:`solver`.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.12
|
2026-04-22 16:24:34 -07:00
|
|
|
|
substeps : int, optional
|
|
|
|
|
|
Number of substeps per depletion interval. When greater than 1, each
|
|
|
|
|
|
interval is subdivided into `substeps` identical sub-intervals and LU
|
|
|
|
|
|
factorizations may be reused across them, improving accuracy for
|
|
|
|
|
|
nuclides with large decay-constant × timestep products.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.4
|
2025-03-05 19:07:50 -06:00
|
|
|
|
continue_timesteps : bool, optional
|
|
|
|
|
|
Whether or not to treat the current solve as a continuation of a
|
|
|
|
|
|
previous simulation. Defaults to `False`. When `False`, the depletion
|
|
|
|
|
|
steps provided are appended to any previous steps. If `True`, the
|
2026-04-22 16:24:34 -07:00
|
|
|
|
timesteps provided to the `Integrator` must exacly match any that exist
|
|
|
|
|
|
in the `prev_results` passed to the `Operator`. The `power`,
|
|
|
|
|
|
`power_density`, or `source_rates` must match as well. The method of
|
|
|
|
|
|
specifying `power`, `power_density`, or `source_rates` should be the
|
|
|
|
|
|
same as the initial run.
|
2025-03-05 19:07:50 -06:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
|
|
|
|
|
|
2019-08-07 10:51:19 -05:00
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
2022-08-01 12:44:28 -05:00
|
|
|
|
operator : openmc.deplete.abc.TransportOperator
|
|
|
|
|
|
Operator to perform transport simulations
|
2019-08-07 10:51:19 -05:00
|
|
|
|
chain : openmc.deplete.Chain
|
|
|
|
|
|
Depletion chain
|
|
|
|
|
|
timesteps : iterable of float
|
|
|
|
|
|
Size of each depletion interval in [s]
|
2020-07-15 08:09:37 -05:00
|
|
|
|
source_rates : iterable of float
|
2020-08-10 07:29:37 -05:00
|
|
|
|
Source rate in [W] or [neutron/sec] for each interval in
|
2020-07-15 08:09:37 -05:00
|
|
|
|
:attr:`timesteps`
|
2020-05-09 14:54:03 -04:00
|
|
|
|
solver : callable
|
|
|
|
|
|
Function that will solve the Bateman equations
|
2020-05-14 08:11:01 -04:00
|
|
|
|
:math:`\frac{\partial}{\partial t}\vec{n} = A_i\vec{n}_i` with a step
|
|
|
|
|
|
size :math:`t_i`. Can be configured using the ``solver`` argument.
|
|
|
|
|
|
User-supplied functions are expected to have the following signature:
|
2026-04-22 16:24:34 -07:00
|
|
|
|
``solver(A, n0, t, substeps=1) -> n1``, where
|
|
|
|
|
|
|
|
|
|
|
|
* ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion
|
|
|
|
|
|
matrix
|
|
|
|
|
|
* ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for
|
|
|
|
|
|
a given material in atoms/cm3
|
|
|
|
|
|
* ``t`` is a float of the time step size in seconds
|
|
|
|
|
|
* ``substeps`` is an optional integer number of substeps, and
|
|
|
|
|
|
* ``n1`` is a :class:`numpy.ndarray` of compositions at the next
|
|
|
|
|
|
time step. Expected to be of the same shape as ``n0``
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
2026-04-22 16:24:34 -07:00
|
|
|
|
Solvers that do not support multiple substeps should raise an exception
|
|
|
|
|
|
when ``substeps > 1``.
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
2023-04-28 05:11:17 +02:00
|
|
|
|
transfer_rates : openmc.deplete.TransferRates
|
2025-05-12 22:48:21 +02:00
|
|
|
|
Transfer rates for the depletion system used to model continuous
|
|
|
|
|
|
removal/feed between materials.
|
2023-04-28 05:11:17 +02:00
|
|
|
|
|
2023-10-31 16:55:19 -05:00
|
|
|
|
.. versionadded:: 0.14.0
|
2025-05-12 22:48:21 +02:00
|
|
|
|
external_source_rates : openmc.deplete.ExternalSourceRates
|
|
|
|
|
|
External source rates for the depletion system.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.3
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
2025-09-18 23:27:41 -05:00
|
|
|
|
""")
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
operator: TransportOperator,
|
2025-02-21 11:47:38 -06:00
|
|
|
|
timesteps: Sequence[float] | Sequence[tuple[float, str]],
|
2024-02-15 07:30:46 +00:00
|
|
|
|
power: Optional[Union[float, Sequence[float]]] = None,
|
|
|
|
|
|
power_density: Optional[Union[float, Sequence[float]]] = None,
|
2025-02-21 11:47:38 -06:00
|
|
|
|
source_rates: Optional[Union[float, Sequence[float]]] = None,
|
2024-02-15 07:30:46 +00:00
|
|
|
|
timestep_units: str = 's',
|
2025-03-05 19:07:50 -06:00
|
|
|
|
solver: str = "cram48",
|
2026-04-22 16:24:34 -07:00
|
|
|
|
substeps: int = 1,
|
2025-03-05 19:07:50 -06:00
|
|
|
|
continue_timesteps: bool = False,
|
2024-02-15 07:30:46 +00:00
|
|
|
|
):
|
2025-11-19 11:26:57 -06:00
|
|
|
|
if continue_timesteps and operator.prev_res is None:
|
2025-03-05 19:07:50 -06:00
|
|
|
|
raise ValueError("Continuation run requires passing prev_results.")
|
2019-08-07 10:51:19 -05:00
|
|
|
|
self.operator = operator
|
|
|
|
|
|
self.chain = operator.chain
|
2020-02-10 16:01:30 -06:00
|
|
|
|
|
2020-07-15 08:09:37 -05:00
|
|
|
|
# Determine source rate and normalize units to W in using power
|
|
|
|
|
|
if power is not None:
|
|
|
|
|
|
source_rates = power
|
|
|
|
|
|
elif power_density is not None:
|
2019-08-07 10:51:19 -05:00
|
|
|
|
if not isinstance(power_density, Iterable):
|
2020-07-15 08:09:37 -05:00
|
|
|
|
source_rates = power_density * operator.heavy_metal
|
2019-08-07 10:51:19 -05:00
|
|
|
|
else:
|
2020-07-15 08:09:37 -05:00
|
|
|
|
source_rates = [p*operator.heavy_metal for p in power_density]
|
|
|
|
|
|
elif source_rates is None:
|
2022-08-02 12:28:00 -05:00
|
|
|
|
raise ValueError("Either power, power_density, or source_rates must be set")
|
2020-07-15 08:09:37 -05:00
|
|
|
|
|
2025-02-21 11:47:38 -06:00
|
|
|
|
# Normalize timesteps and source rates
|
|
|
|
|
|
seconds, source_rates = _normalize_timesteps(
|
|
|
|
|
|
timesteps, source_rates, timestep_units, operator)
|
2026-04-22 16:24:34 -07:00
|
|
|
|
check_type("substeps", substeps, Integral)
|
|
|
|
|
|
check_greater_than("substeps", substeps, 0)
|
2025-03-05 19:07:50 -06:00
|
|
|
|
|
|
|
|
|
|
if continue_timesteps:
|
|
|
|
|
|
# Get timesteps and source rates from previous results
|
|
|
|
|
|
prev_times = operator.prev_res.get_times(timestep_units)
|
|
|
|
|
|
prev_source_rates = operator.prev_res.get_source_rates()
|
|
|
|
|
|
prev_timesteps = np.diff(prev_times)
|
|
|
|
|
|
|
|
|
|
|
|
# Make sure parameters from the previous results are consistent with
|
|
|
|
|
|
# those passed to operator
|
|
|
|
|
|
num_prev = len(prev_timesteps)
|
|
|
|
|
|
if not np.array_equal(prev_timesteps, timesteps[:num_prev]):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"You are attempting to continue a run in which the previous timesteps "
|
|
|
|
|
|
"do not have the same initial timesteps as those provided to the "
|
|
|
|
|
|
"Integrator. Please make sure you are using the correct timesteps."
|
|
|
|
|
|
)
|
|
|
|
|
|
if not np.array_equal(prev_source_rates, source_rates[:num_prev]):
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"You are attempting to continue a run in which the previous results "
|
|
|
|
|
|
"do not have the same initial source rates, powers, or power densities "
|
|
|
|
|
|
"as those provided to the Integrator. Please make sure you are using "
|
|
|
|
|
|
"the correct powers, power densities, or source rates and previous "
|
|
|
|
|
|
"results file."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Run with only the new time steps and source rates provided
|
|
|
|
|
|
seconds = seconds[num_prev:]
|
|
|
|
|
|
source_rates = source_rates[num_prev:]
|
|
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
self.timesteps = np.asarray(seconds)
|
|
|
|
|
|
self.source_rates = np.asarray(source_rates)
|
2026-04-22 16:24:34 -07:00
|
|
|
|
self.substeps = substeps
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2023-04-28 05:11:17 +02:00
|
|
|
|
self.transfer_rates = None
|
2025-05-12 22:48:21 +02:00
|
|
|
|
self.external_source_rates = None
|
2026-04-20 14:13:53 +02:00
|
|
|
|
self._keff_search_control = None
|
2023-04-28 05:11:17 +02:00
|
|
|
|
|
2020-05-09 14:54:03 -04:00
|
|
|
|
if isinstance(solver, str):
|
|
|
|
|
|
# Delay importing of cram module, which requires this file
|
|
|
|
|
|
if solver == "cram48":
|
|
|
|
|
|
from .cram import CRAM48
|
|
|
|
|
|
self._solver = CRAM48
|
|
|
|
|
|
elif solver == "cram16":
|
|
|
|
|
|
from .cram import CRAM16
|
|
|
|
|
|
self._solver = CRAM16
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(
|
2024-04-29 22:45:37 +01:00
|
|
|
|
f"Solver {solver} not understood. Expected 'cram48' or 'cram16'")
|
2020-05-09 14:54:03 -04:00
|
|
|
|
else:
|
|
|
|
|
|
self.solver = solver
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def solver(self):
|
|
|
|
|
|
return self._solver
|
|
|
|
|
|
|
|
|
|
|
|
@solver.setter
|
|
|
|
|
|
def solver(self, func):
|
|
|
|
|
|
if not isinstance(func, Callable):
|
|
|
|
|
|
raise TypeError(
|
2024-04-29 22:45:37 +01:00
|
|
|
|
f"Solver must be callable, not {type(func)}")
|
2020-05-09 14:54:03 -04:00
|
|
|
|
try:
|
|
|
|
|
|
sig = signature(func)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
# Guard against callables that aren't introspectable, e.g.
|
|
|
|
|
|
# fortran functions wrapped by F2PY
|
2024-04-29 22:45:37 +01:00
|
|
|
|
warn(f"Could not determine arguments to {func}. Proceeding anyways")
|
2020-05-09 14:54:03 -04:00
|
|
|
|
self._solver = func
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-04-22 16:24:34 -07:00
|
|
|
|
params = list(sig.parameters.values())
|
|
|
|
|
|
|
2020-05-09 14:54:03 -04:00
|
|
|
|
# Inspect arguments
|
2026-04-22 16:24:34 -07:00
|
|
|
|
if len(params) != 4:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"Function {} must support four arguments "
|
|
|
|
|
|
"(A, n0, t, substeps=1): {!s}"
|
|
|
|
|
|
.format(func, sig))
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
2026-04-22 16:24:34 -07:00
|
|
|
|
for ix, param in enumerate(params):
|
|
|
|
|
|
if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD,
|
|
|
|
|
|
param.VAR_POSITIONAL}:
|
2020-05-09 14:54:03 -04:00
|
|
|
|
raise ValueError(
|
2024-04-29 22:45:37 +01:00
|
|
|
|
f"Keyword arguments like {ix} at position {param} are not allowed")
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
2026-04-22 16:24:34 -07:00
|
|
|
|
if len(params) == 4 and params[3].default != 1:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Fourth solver argument must default to 1, not {params[3].default}")
|
|
|
|
|
|
|
2020-05-09 14:54:03 -04:00
|
|
|
|
self._solver = func
|
|
|
|
|
|
|
2025-05-12 22:48:21 +02:00
|
|
|
|
def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None):
|
2020-05-09 14:54:03 -04:00
|
|
|
|
start = time.time()
|
|
|
|
|
|
results = deplete(
|
2025-05-12 22:48:21 +02:00
|
|
|
|
self._solver, self.chain, n, rates, dt, i, matrix_func,
|
2026-04-22 16:24:34 -07:00
|
|
|
|
self.transfer_rates, self.external_source_rates, self.substeps)
|
2026-04-21 13:24:53 -07:00
|
|
|
|
|
|
|
|
|
|
# Clip unphysical negative number densities
|
|
|
|
|
|
for r in results:
|
|
|
|
|
|
r.clip(min=0.0, out=r)
|
|
|
|
|
|
|
2020-05-09 14:54:03 -04:00
|
|
|
|
return time.time() - start, results
|
|
|
|
|
|
|
2019-08-07 10:51:19 -05:00
|
|
|
|
@abstractmethod
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def __call__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
n: Sequence[np.ndarray],
|
|
|
|
|
|
rates: ReactionRates,
|
|
|
|
|
|
dt: float,
|
|
|
|
|
|
source_rate: float,
|
|
|
|
|
|
i: int
|
|
|
|
|
|
):
|
2019-08-07 10:51:19 -05:00
|
|
|
|
"""Perform the integration across one time step
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2023-07-06 14:48:36 -05:00
|
|
|
|
n : list of numpy.ndarray
|
|
|
|
|
|
List of atom number arrays for each material. Each array in the list
|
|
|
|
|
|
contains the number of [atom] of each nuclide.
|
2019-08-07 10:51:19 -05:00
|
|
|
|
rates : openmc.deplete.ReactionRates
|
|
|
|
|
|
Reaction rates from operator
|
|
|
|
|
|
dt : float
|
|
|
|
|
|
Time in [s] for the entire depletion interval
|
2020-07-15 21:28:35 -05:00
|
|
|
|
source_rate : float
|
2020-07-15 21:53:06 -05:00
|
|
|
|
Power in [W] or source rate in [neutron/sec]
|
2019-08-07 10:51:19 -05:00
|
|
|
|
i : int
|
|
|
|
|
|
Current depletion step index
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
proc_time : float
|
|
|
|
|
|
Time spent in CRAM routines for all materials in [s]
|
2025-11-19 11:26:57 -06:00
|
|
|
|
n_end : list of numpy.ndarray
|
|
|
|
|
|
Concentrations at end of timestep
|
2019-08-07 10:51:19 -05:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def _num_stages(self):
|
|
|
|
|
|
"""Number of intermediate transport solutions
|
|
|
|
|
|
|
|
|
|
|
|
Needed to ensure schemes are consistent with restarts
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __iter__(self):
|
2020-07-15 21:53:06 -05:00
|
|
|
|
"""Return pair of time step in [s] and source rate in [W] or [neutron/sec]"""
|
2020-07-15 08:09:37 -05:00
|
|
|
|
return zip(self.timesteps, self.source_rates)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
|
|
"""Return integer number of depletion intervals"""
|
|
|
|
|
|
return len(self.timesteps)
|
|
|
|
|
|
|
2020-07-15 08:09:37 -05:00
|
|
|
|
def _get_bos_data_from_operator(self, step_index, source_rate, bos_conc):
|
2019-08-07 10:51:19 -05:00
|
|
|
|
"""Get beginning of step concentrations, reaction rates from Operator
|
|
|
|
|
|
"""
|
|
|
|
|
|
x = deepcopy(bos_conc)
|
2020-07-15 08:09:37 -05:00
|
|
|
|
res = self.operator(x, source_rate)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
self.operator.write_bos_data(step_index + self._i_res)
|
|
|
|
|
|
return x, res
|
|
|
|
|
|
|
2024-02-28 22:13:45 +00:00
|
|
|
|
def _get_bos_data_from_restart(self, source_rate, bos_conc):
|
2019-08-07 10:51:19 -05:00
|
|
|
|
"""Get beginning of step concentrations, reaction rates from restart"""
|
|
|
|
|
|
res = self.operator.prev_res[-1]
|
|
|
|
|
|
# Depletion methods expect list of arrays
|
2025-11-19 11:26:57 -06:00
|
|
|
|
bos_conc = list(res.data)
|
|
|
|
|
|
rates = res.rates
|
|
|
|
|
|
k = ufloat(res.k[0], res.k[1])
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2024-10-14 21:47:22 +02:00
|
|
|
|
if res.source_rate != 0.0:
|
|
|
|
|
|
# Scale reaction rates by ratio of source rates
|
|
|
|
|
|
rates *= source_rate / res.source_rate
|
2019-08-07 10:51:19 -05:00
|
|
|
|
return bos_conc, OperatorResult(k, rates)
|
|
|
|
|
|
|
2025-06-04 18:40:55 -05:00
|
|
|
|
def _get_start_data(self) -> tuple[float, int]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
This function fetches the starting state of a depletion simulation in
|
|
|
|
|
|
terms of the simulation physical time at which to start and the index at
|
|
|
|
|
|
which the depletion simulation should start. When no previous results
|
|
|
|
|
|
exist, the time and index are both zero. When previous results do exist,
|
|
|
|
|
|
it returns the time corresponding to beginning the previous results last
|
|
|
|
|
|
timestep and the index as N-1 where N is the number of previous
|
|
|
|
|
|
StepResults found in the previous Results (as expected from 0-based
|
|
|
|
|
|
indexing).
|
|
|
|
|
|
|
|
|
|
|
|
Note that the openmc.deplete.Results.time object is a list of float with
|
|
|
|
|
|
[t,t+dt] where t is the beginning of timestep time and t+dt is the end
|
|
|
|
|
|
of timestep time. If the previous results correspond to a simulation
|
|
|
|
|
|
that finished to completeion, it will contain a results in the form of
|
|
|
|
|
|
[t,t], but if a simulation doesn't finish all the given timesteps, it is
|
|
|
|
|
|
the t that is the desired start time, not t+dt. Thus, it is always safe
|
|
|
|
|
|
to take time[0].
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
start_time : float
|
|
|
|
|
|
Time at which depletion simulation should start in [s]
|
|
|
|
|
|
index : int
|
|
|
|
|
|
Index at which depletion simulation should start
|
|
|
|
|
|
"""
|
2019-08-07 10:51:19 -05:00
|
|
|
|
if self.operator.prev_res is None:
|
|
|
|
|
|
return 0.0, 0
|
2025-06-04 18:40:55 -05:00
|
|
|
|
return (self.operator.prev_res[-1].time[0],
|
2019-08-07 10:51:19 -05:00
|
|
|
|
len(self.operator.prev_res) - 1)
|
|
|
|
|
|
|
2026-04-20 14:13:53 +02:00
|
|
|
|
def _restore_keff_search_control(self, res: StepResult):
|
|
|
|
|
|
"""Restore keff search control from restart results."""
|
|
|
|
|
|
keff_search_root = res.keff_search_root
|
|
|
|
|
|
if keff_search_root is None:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
"Cannot restore keff search control from restart "
|
|
|
|
|
|
"results because no stored keff_search_root is "
|
|
|
|
|
|
"available."
|
|
|
|
|
|
)
|
|
|
|
|
|
self._keff_search_control.function(keff_search_root)
|
|
|
|
|
|
return keff_search_root
|
|
|
|
|
|
|
|
|
|
|
|
def _get_bos_data(self, step_index, source_rate, bos_conc):
|
|
|
|
|
|
"""Get beginning-of-step concentrations, rates, and control state."""
|
|
|
|
|
|
if step_index > 0 or self.operator.prev_res is None:
|
|
|
|
|
|
if self._keff_search_control is not None and source_rate != 0.0:
|
|
|
|
|
|
keff_search_root = self._keff_search_control.run(bos_conc)
|
|
|
|
|
|
else:
|
|
|
|
|
|
keff_search_root = None
|
|
|
|
|
|
bos_conc, res = self._get_bos_data_from_operator(
|
|
|
|
|
|
step_index, source_rate, bos_conc)
|
|
|
|
|
|
else:
|
|
|
|
|
|
bos_conc, res = self._get_bos_data_from_restart(
|
|
|
|
|
|
source_rate, bos_conc)
|
|
|
|
|
|
if self._keff_search_control is not None and source_rate != 0.0:
|
|
|
|
|
|
keff_search_root = self._restore_keff_search_control(self.operator.prev_res[-1])
|
|
|
|
|
|
else:
|
|
|
|
|
|
keff_search_root = None
|
|
|
|
|
|
|
|
|
|
|
|
return bos_conc, res, keff_search_root
|
|
|
|
|
|
|
2023-12-12 19:32:09 +00:00
|
|
|
|
def integrate(
|
|
|
|
|
|
self,
|
|
|
|
|
|
final_step: bool = True,
|
|
|
|
|
|
output: bool = True,
|
2025-11-19 11:26:57 -06:00
|
|
|
|
path: PathLike = 'depletion_results.h5',
|
|
|
|
|
|
write_rates: bool = False
|
2023-12-12 19:32:09 +00:00
|
|
|
|
):
|
2020-08-06 09:46:51 -05:00
|
|
|
|
"""Perform the entire depletion process across all steps
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
final_step : bool, optional
|
|
|
|
|
|
Indicate whether or not a transport solve should be run at the end
|
|
|
|
|
|
of the last timestep.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.12.1
|
2022-04-21 14:31:40 -05:00
|
|
|
|
output : bool, optional
|
|
|
|
|
|
Indicate whether to display information about progress
|
2020-08-06 09:46:51 -05:00
|
|
|
|
|
2022-04-21 14:31:40 -05:00
|
|
|
|
.. versionadded:: 0.13.1
|
2023-12-12 19:32:09 +00:00
|
|
|
|
path : PathLike
|
|
|
|
|
|
Path to file to write. Defaults to 'depletion_results.h5'.
|
|
|
|
|
|
|
2024-06-21 20:28:56 -05:00
|
|
|
|
.. versionadded:: 0.15.0
|
2025-11-19 11:26:57 -06:00
|
|
|
|
write_rates : bool, optional
|
|
|
|
|
|
Whether reaction rates should be written to the results file for
|
|
|
|
|
|
each step. Defaults to ``False`` to reduce file size.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.3
|
2020-08-06 09:46:51 -05:00
|
|
|
|
"""
|
2022-07-12 16:07:59 -05:00
|
|
|
|
with change_directory(self.operator.output_dir):
|
2023-07-06 14:48:36 -05:00
|
|
|
|
n = self.operator.initial_condition()
|
2019-08-07 10:51:19 -05:00
|
|
|
|
t, self._i_res = self._get_start_data()
|
|
|
|
|
|
|
2020-07-15 08:09:37 -05:00
|
|
|
|
for i, (dt, source_rate) in enumerate(self):
|
2022-11-29 13:29:57 -06:00
|
|
|
|
if output and comm.rank == 0:
|
2022-04-21 14:31:40 -05:00
|
|
|
|
print(f"[openmc.deplete] t={t} s, dt={dt} s, source={source_rate}")
|
|
|
|
|
|
|
2026-04-20 14:13:53 +02:00
|
|
|
|
# Get beginning-of-step data from operator or restart results
|
|
|
|
|
|
n, res, keff_search_root = self._get_bos_data(i, source_rate, n)
|
2020-08-06 09:46:51 -05:00
|
|
|
|
|
|
|
|
|
|
# Solve Bateman equations over time interval
|
2025-11-19 11:26:57 -06:00
|
|
|
|
proc_time, n_end = self(n, res.rates, dt, source_rate, i)
|
|
|
|
|
|
|
|
|
|
|
|
StepResult.save(
|
|
|
|
|
|
self.operator,
|
|
|
|
|
|
n,
|
|
|
|
|
|
res,
|
|
|
|
|
|
[t, t + dt],
|
|
|
|
|
|
source_rate,
|
|
|
|
|
|
self._i_res + i,
|
|
|
|
|
|
proc_time,
|
|
|
|
|
|
write_rates=write_rates,
|
2026-04-20 14:13:53 +02:00
|
|
|
|
keff_search_root=keff_search_root,
|
2025-11-19 11:26:57 -06:00
|
|
|
|
path=path
|
|
|
|
|
|
)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2025-11-19 11:26:57 -06:00
|
|
|
|
# Update for next step
|
|
|
|
|
|
n = n_end
|
2019-08-07 10:51:19 -05:00
|
|
|
|
t += dt
|
|
|
|
|
|
|
2020-08-06 09:46:51 -05:00
|
|
|
|
# Final simulation -- in the case that final_step is False, a zero
|
|
|
|
|
|
# source rate is passed to the transport operator (which knows to
|
|
|
|
|
|
# just return zero reaction rates without actually doing a transport
|
|
|
|
|
|
# solve)
|
2022-11-29 13:29:57 -06:00
|
|
|
|
if output and final_step and comm.rank == 0:
|
2022-04-21 14:31:40 -05:00
|
|
|
|
print(f"[openmc.deplete] t={t} (final operator evaluation)")
|
2026-04-20 14:13:53 +02:00
|
|
|
|
if self._keff_search_control is not None and source_rate != 0.0:
|
|
|
|
|
|
keff_search_root = self._keff_search_control.run(n)
|
|
|
|
|
|
else:
|
|
|
|
|
|
keff_search_root = None
|
2025-11-19 11:26:57 -06:00
|
|
|
|
res_final = self.operator(n, source_rate if final_step else 0.0)
|
|
|
|
|
|
StepResult.save(
|
|
|
|
|
|
self.operator,
|
|
|
|
|
|
n,
|
|
|
|
|
|
res_final,
|
|
|
|
|
|
[t, t],
|
|
|
|
|
|
source_rate,
|
|
|
|
|
|
self._i_res + len(self),
|
|
|
|
|
|
proc_time,
|
|
|
|
|
|
write_rates=write_rates,
|
2026-04-20 14:13:53 +02:00
|
|
|
|
keff_search_root=keff_search_root,
|
2025-11-19 11:26:57 -06:00
|
|
|
|
path=path
|
|
|
|
|
|
)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
self.operator.write_bos_data(len(self) + self._i_res)
|
|
|
|
|
|
|
2022-07-12 16:07:59 -05:00
|
|
|
|
self.operator.finalize()
|
|
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def add_transfer_rate(
|
2025-05-12 22:48:21 +02:00
|
|
|
|
self,
|
|
|
|
|
|
material: str | int | Material,
|
|
|
|
|
|
components: Sequence[str],
|
|
|
|
|
|
transfer_rate: float,
|
|
|
|
|
|
transfer_rate_units: str = '1/s',
|
|
|
|
|
|
timesteps: Sequence[int] | None = None,
|
|
|
|
|
|
destination_material: str | int | Material | None = None
|
|
|
|
|
|
):
|
2023-04-28 05:11:17 +02:00
|
|
|
|
"""Add transfer rates to depletable material.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
material : openmc.Material or str or int
|
|
|
|
|
|
Depletable material
|
2023-06-26 14:23:32 +02:00
|
|
|
|
components : list of str
|
|
|
|
|
|
List of strings of elements and/or nuclides that share transfer rate.
|
|
|
|
|
|
A transfer rate for a nuclide cannot be added to a material
|
|
|
|
|
|
alongside a transfer rate for its element and vice versa.
|
2023-04-28 05:11:17 +02:00
|
|
|
|
transfer_rate : float
|
|
|
|
|
|
Rate at which elements are transferred. A positive or negative values
|
|
|
|
|
|
set removal of feed rates, respectively.
|
|
|
|
|
|
transfer_rate_units : {'1/s', '1/min', '1/h', '1/d', '1/a'}
|
|
|
|
|
|
Units for values specified in the transfer_rate argument. 's' means
|
|
|
|
|
|
seconds, 'min' means minutes, 'h' means hours, 'a' means Julian years.
|
2025-05-12 22:48:21 +02:00
|
|
|
|
timesteps : list of int, optional
|
|
|
|
|
|
List of timestep indices where to set external source rates.
|
|
|
|
|
|
Defaults to None, which means the external source rate is set for
|
|
|
|
|
|
all timesteps.
|
|
|
|
|
|
destination_material : openmc.Material or str or int, Optional
|
|
|
|
|
|
Destination material to where nuclides get fed.
|
2023-04-28 05:11:17 +02:00
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.transfer_rates is None:
|
2025-05-12 22:48:21 +02:00
|
|
|
|
if hasattr(self.operator, 'model'):
|
|
|
|
|
|
materials = self.operator.model.materials
|
|
|
|
|
|
elif hasattr(self.operator, 'materials'):
|
|
|
|
|
|
materials = self.operator.materials
|
|
|
|
|
|
self.transfer_rates = TransferRates(
|
|
|
|
|
|
self.operator, materials, len(self.timesteps))
|
|
|
|
|
|
|
|
|
|
|
|
self.transfer_rates.set_transfer_rate(
|
|
|
|
|
|
material, components, transfer_rate, transfer_rate_units,
|
|
|
|
|
|
timesteps, destination_material)
|
|
|
|
|
|
|
|
|
|
|
|
def add_external_source_rate(
|
|
|
|
|
|
self,
|
|
|
|
|
|
material: str | int | Material,
|
|
|
|
|
|
composition: dict[str, float],
|
|
|
|
|
|
rate: float,
|
|
|
|
|
|
rate_units: str = 'g/s',
|
|
|
|
|
|
timesteps: Sequence[int] | None = None
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Add external source rates to depletable material.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
material : openmc.Material or str or int
|
|
|
|
|
|
Depletable material
|
|
|
|
|
|
composition : dict of str to float
|
|
|
|
|
|
External source rate composition vector, where key can be an element
|
|
|
|
|
|
or a nuclide and value the corresponding weight percent.
|
|
|
|
|
|
rate : float
|
|
|
|
|
|
External source rate in units of mass per time. A positive or
|
|
|
|
|
|
negative value corresponds to a feed or removal rate, respectively.
|
|
|
|
|
|
units : {'g/s', 'g/min', 'g/h', 'g/d', 'g/a'}
|
|
|
|
|
|
Units for values specified in the `rate` argument. 's' for seconds,
|
|
|
|
|
|
'min' for minutes, 'h' for hours, 'a' for Julian years.
|
|
|
|
|
|
timesteps : list of int, optional
|
|
|
|
|
|
List of timestep indices where to set external source rates.
|
|
|
|
|
|
Defaults to None, which means the external source rate is set for
|
|
|
|
|
|
all timesteps.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.external_source_rates is None:
|
|
|
|
|
|
if hasattr(self.operator, 'model'):
|
|
|
|
|
|
materials = self.operator.model.materials
|
|
|
|
|
|
elif hasattr(self.operator, 'materials'):
|
|
|
|
|
|
materials = self.operator.materials
|
|
|
|
|
|
self.external_source_rates = ExternalSourceRates(
|
|
|
|
|
|
self.operator, materials, len(self.timesteps))
|
|
|
|
|
|
|
|
|
|
|
|
self.external_source_rates.set_external_source_rate(
|
|
|
|
|
|
material, composition, rate, rate_units, timesteps)
|
2023-04-28 05:11:17 +02:00
|
|
|
|
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2025-11-12 23:45:48 +01:00
|
|
|
|
def add_redox(self, material, buffer, oxidation_states, timesteps=None):
|
|
|
|
|
|
"""Add redox control to depletable material.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
material : openmc.Material or str or int
|
|
|
|
|
|
Depletable material
|
|
|
|
|
|
buffer : dict
|
|
|
|
|
|
Dictionary of buffer nuclides used to maintain redox balance. Keys
|
|
|
|
|
|
are nuclide names (strings) and values are their respective
|
|
|
|
|
|
fractions (float) that collectively sum to 1.
|
|
|
|
|
|
oxidation_states : dict
|
|
|
|
|
|
User-defined oxidation states for elements. Keys are element symbols
|
|
|
|
|
|
(e.g., 'H', 'He'), and values are their corresponding oxidation
|
|
|
|
|
|
states as integers (e.g., +1, 0).
|
|
|
|
|
|
timesteps : list of int, optional
|
|
|
|
|
|
List of timestep indices where to set external source rates.
|
|
|
|
|
|
Defaults to None, which means the external source rate is set for
|
|
|
|
|
|
all timesteps.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.transfer_rates is None:
|
|
|
|
|
|
if hasattr(self.operator, 'model'):
|
|
|
|
|
|
materials = self.operator.model.materials
|
|
|
|
|
|
elif hasattr(self.operator, 'materials'):
|
|
|
|
|
|
materials = self.operator.materials
|
|
|
|
|
|
self.transfer_rates = TransferRates(
|
|
|
|
|
|
self.operator, materials, len(self.timesteps))
|
|
|
|
|
|
|
|
|
|
|
|
self.transfer_rates.set_redox(material, buffer, oxidation_states, timesteps)
|
|
|
|
|
|
|
2026-04-20 14:13:53 +02:00
|
|
|
|
def add_keff_search_control(
|
|
|
|
|
|
self,
|
|
|
|
|
|
function: Callable,
|
|
|
|
|
|
x0: float,
|
|
|
|
|
|
x1: float,
|
|
|
|
|
|
bracket: Sequence[float],
|
|
|
|
|
|
**search_kwargs
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Add keff search to the integrator scheme.
|
|
|
|
|
|
|
|
|
|
|
|
This method causes OpenMC to perform a keff search during depletion to
|
|
|
|
|
|
maintain a target keff by adjusting a model parameter through the
|
|
|
|
|
|
provided function.
|
|
|
|
|
|
|
|
|
|
|
|
.. important::
|
|
|
|
|
|
The function **must** modify the model through ``openmc.lib`` (e.g.,
|
|
|
|
|
|
``openmc.lib.cells``, ``openmc.lib.materials``) and **NOT** through
|
|
|
|
|
|
``openmc.Model``. The function is called within a
|
|
|
|
|
|
:class:`openmc.lib.TemporarySession` context where only the C API
|
|
|
|
|
|
(``openmc.lib``) is available for modifications.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
function : Callable
|
|
|
|
|
|
Function that takes a single float argument and modifies the model
|
|
|
|
|
|
through :mod:`openmc.lib`.
|
|
|
|
|
|
x0 : float
|
|
|
|
|
|
Initial lower bound for the keff search.
|
|
|
|
|
|
x1 : float
|
|
|
|
|
|
Initial upper bound for the keff search.
|
|
|
|
|
|
bracket : sequence of float
|
|
|
|
|
|
Bracket interval [x_min, x_max] that constrains the allowed parameter
|
|
|
|
|
|
values during the keff search. This is a required parameter
|
|
|
|
|
|
that defines the absolute bounds for the search. The bracket must contain
|
|
|
|
|
|
exactly 2 elements with bracket[0] < bracket[1]. These values are passed
|
|
|
|
|
|
directly to the ``x_min`` and ``x_max`` optional arguments in
|
|
|
|
|
|
:meth:`openmc.Model.keff_search`, which enforce hard limits on the
|
|
|
|
|
|
parameter range. If the keff search converges to a value outside this
|
|
|
|
|
|
bracket, it will be clamped to the nearest bracket bound with a warning.
|
|
|
|
|
|
**search_kwargs
|
|
|
|
|
|
Additional keyword arguments passed to
|
|
|
|
|
|
:meth:`openmc.Model.keff_search`. Common options include:
|
|
|
|
|
|
|
|
|
|
|
|
* ``target`` : float, optional
|
|
|
|
|
|
Target keff value to search for. Defaults to 1.0.
|
|
|
|
|
|
* ``k_tol`` : float, optional
|
|
|
|
|
|
Stopping criterion on the function value. Defaults to 1e-4.
|
|
|
|
|
|
* ``sigma_final`` : float, optional
|
|
|
|
|
|
Maximum accepted k-effective uncertainty. Defaults to 3e-4.
|
|
|
|
|
|
* ``maxiter`` : int, optional
|
|
|
|
|
|
Maximum number of iterations. Defaults to 50.
|
|
|
|
|
|
|
|
|
|
|
|
See :meth:`openmc.Model.keff_search` for a complete list of
|
|
|
|
|
|
available options.
|
|
|
|
|
|
|
|
|
|
|
|
Examples
|
|
|
|
|
|
--------
|
|
|
|
|
|
Add keff search that adjusts a control rod position:
|
|
|
|
|
|
|
|
|
|
|
|
>>> def adjust_rod_position(position):
|
|
|
|
|
|
... openmc.lib.cells[rod_cell.id].translation = [0, 0, position]
|
|
|
|
|
|
>>> integrator.add_keff_search_control(
|
|
|
|
|
|
... adjust_rod_position,
|
|
|
|
|
|
... x0=0.0,
|
|
|
|
|
|
... x1=5.0,
|
|
|
|
|
|
... bracket=[-10,10],
|
|
|
|
|
|
... target=1.0,
|
|
|
|
|
|
... k_tol=1e-4
|
|
|
|
|
|
... )
|
|
|
|
|
|
|
|
|
|
|
|
Add keff search that adjusts the U235 density:
|
|
|
|
|
|
|
|
|
|
|
|
>>> def set_u235_density(u235_density):
|
|
|
|
|
|
... # Get the material from openmc.lib
|
|
|
|
|
|
... lib_mat = openmc.lib.materials[material_id]
|
|
|
|
|
|
... # Get current nuclides and densities
|
|
|
|
|
|
... nuclides = lib_mat.nuclides
|
|
|
|
|
|
... densities = lib_mat.densities
|
|
|
|
|
|
... u235_idx = nuclides.index('U235')
|
|
|
|
|
|
... densities[u235_idx] = u235_density
|
|
|
|
|
|
... lib_mat.set_densities(nuclides, densities)
|
|
|
|
|
|
>>> integrator.add_keff_search_control(
|
|
|
|
|
|
... set_u235_density,
|
|
|
|
|
|
... x0=5.0e-4,
|
|
|
|
|
|
... x1=1.0e-3,
|
|
|
|
|
|
... bracket=[1.0e-4, 2.0e-3],
|
|
|
|
|
|
... target=1.0
|
|
|
|
|
|
... )
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.4
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._keff_search_control = _KeffSearchControl(
|
|
|
|
|
|
self.operator, function, x0, x1, bracket, **search_kwargs)
|
|
|
|
|
|
|
2020-07-15 21:28:35 -05:00
|
|
|
|
@add_params
|
2019-08-07 10:51:19 -05:00
|
|
|
|
class SIIntegrator(Integrator):
|
2020-05-09 14:54:03 -04:00
|
|
|
|
r"""Abstract class for the Stochastic Implicit Euler integrators
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
|
|
|
|
|
Does not provide a ``__call__`` method, but scales and resets
|
|
|
|
|
|
the number of particles used in initial transport calculation
|
2020-07-15 21:28:35 -05:00
|
|
|
|
"""
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2025-09-18 23:27:41 -05:00
|
|
|
|
_params = dedent(r"""
|
2019-08-07 10:51:19 -05:00
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2022-08-01 12:44:28 -05:00
|
|
|
|
operator : openmc.deplete.abc.TransportOperator
|
|
|
|
|
|
Operator to perform transport simulations
|
2020-02-10 16:01:30 -06:00
|
|
|
|
timesteps : iterable of float or iterable of tuple
|
|
|
|
|
|
Array of timesteps. Note that values are not cumulative. The units are
|
|
|
|
|
|
specified by the `timestep_units` argument when `timesteps` is an
|
|
|
|
|
|
iterable of float. Alternatively, units can be specified for each step
|
|
|
|
|
|
by passing an iterable of (value, unit) tuples.
|
2019-08-07 10:51:19 -05:00
|
|
|
|
power : float or iterable of float, optional
|
2026-04-22 16:24:34 -07:00
|
|
|
|
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]. Either ``power``, ``power_density``, or
|
2020-08-06 08:12:36 -05:00
|
|
|
|
``source_rates`` must be specified.
|
2019-08-07 10:51:19 -05:00
|
|
|
|
power_density : float or iterable of float, optional
|
2026-04-22 16:24:34 -07:00
|
|
|
|
Power density of the reactor in [W/gHM]. It is multiplied by initial
|
|
|
|
|
|
heavy metal inventory to get total power if ``power`` is not specified.
|
2020-08-06 08:12:36 -05:00
|
|
|
|
source_rates : float or iterable of float, optional
|
2022-07-30 15:39:47 -05:00
|
|
|
|
Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for
|
|
|
|
|
|
each interval in :attr:`timesteps`
|
2020-08-06 08:12:36 -05:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.12.1
|
2020-02-11 07:08:53 -06:00
|
|
|
|
timestep_units : {'s', 'min', 'h', 'd', 'MWd/kg'}
|
2020-02-10 16:01:30 -06:00
|
|
|
|
Units for values specified in the `timesteps` argument. 's' means
|
2020-02-11 07:08:53 -06:00
|
|
|
|
seconds, 'min' means minutes, 'h' means hours, and 'MWd/kg' indicates
|
|
|
|
|
|
that the values are given in burnup (MW-d of energy deposited per
|
2020-02-12 07:19:31 -06:00
|
|
|
|
kilogram of initial heavy metal).
|
2019-08-07 10:51:19 -05:00
|
|
|
|
n_steps : int, optional
|
2026-04-22 16:24:34 -07:00
|
|
|
|
Number of stochastic iterations per depletion interval. Must be greater
|
|
|
|
|
|
than zero. Default : 10
|
2020-05-09 14:54:03 -04:00
|
|
|
|
solver : str or callable, optional
|
2026-04-22 16:24:34 -07:00
|
|
|
|
If a string, must be the name of the solver responsible for solving the
|
|
|
|
|
|
Bateman equations. Current options are:
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
|
|
|
|
|
* ``cram16`` - 16th order IPF CRAM
|
|
|
|
|
|
* ``cram48`` - 48th order IPF CRAM [default]
|
|
|
|
|
|
|
|
|
|
|
|
If a function or other callable, must adhere to the requirements in
|
|
|
|
|
|
:attr:`solver`.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.12
|
2026-04-22 16:24:34 -07:00
|
|
|
|
substeps : int, optional
|
|
|
|
|
|
Number of substeps per depletion interval. When greater than 1, each
|
|
|
|
|
|
interval is subdivided into `substeps` identical sub-intervals and LU
|
|
|
|
|
|
factorizations may be reused across them, improving accuracy for
|
|
|
|
|
|
nuclides with large decay-constant × timestep products.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.4
|
2025-03-05 19:07:50 -06:00
|
|
|
|
continue_timesteps : bool, optional
|
|
|
|
|
|
Whether or not to treat the current solve as a continuation of a
|
2026-04-22 16:24:34 -07:00
|
|
|
|
previous simulation. Defaults to `False`. If `False`, all time steps and
|
|
|
|
|
|
source rates will be run in an append fashion and will run after
|
|
|
|
|
|
whatever time steps exist, if any. If `True`, the timesteps provided to
|
|
|
|
|
|
the `Integrator` must match exactly those that exist in the
|
|
|
|
|
|
`prev_results` passed to the `Opereator`. The `power`, `power_density`,
|
|
|
|
|
|
or `source_rates` must match as well. The method of specifying `power`,
|
|
|
|
|
|
`power_density`, or `source_rates` should be the same as the initial
|
|
|
|
|
|
run.
|
2025-03-05 19:07:50 -06:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.1
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
|
|
----------
|
2022-08-01 12:44:28 -05:00
|
|
|
|
operator : openmc.deplete.abc.TransportOperator
|
|
|
|
|
|
Operator to perform transport simulations
|
2019-08-07 10:51:19 -05:00
|
|
|
|
chain : openmc.deplete.Chain
|
|
|
|
|
|
Depletion chain
|
|
|
|
|
|
timesteps : iterable of float
|
|
|
|
|
|
Size of each depletion interval in [s]
|
|
|
|
|
|
power : iterable of float
|
|
|
|
|
|
Power of the reactor in [W] for each interval in :attr:`timesteps`
|
|
|
|
|
|
n_steps : int
|
|
|
|
|
|
Number of stochastic iterations per depletion interval
|
2020-05-09 14:54:03 -04:00
|
|
|
|
solver : callable
|
|
|
|
|
|
Function that will solve the Bateman equations
|
2020-05-14 08:11:01 -04:00
|
|
|
|
:math:`\frac{\partial}{\partial t}\vec{n} = A_i\vec{n}_i` with a step
|
|
|
|
|
|
size :math:`t_i`. Can be configured using the ``solver`` argument.
|
|
|
|
|
|
User-supplied functions are expected to have the following signature:
|
2026-04-22 16:24:34 -07:00
|
|
|
|
``solver(A, n0, t, substeps=1) -> n1``, where
|
|
|
|
|
|
|
|
|
|
|
|
* ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion
|
|
|
|
|
|
matrix
|
|
|
|
|
|
* ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for
|
|
|
|
|
|
a given material in atoms/cm3
|
|
|
|
|
|
* ``t`` is a float of the time step size in seconds
|
|
|
|
|
|
* ``substeps`` is an optional integer number of substeps, and
|
|
|
|
|
|
* ``n1`` is a :class:`numpy.ndarray` of compositions at the next
|
|
|
|
|
|
time step. Expected to be of the same shape as ``n0``
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
2026-04-22 16:24:34 -07:00
|
|
|
|
Solvers that do not support multiple substeps should raise an exception
|
|
|
|
|
|
when ``substeps > 1``.
|
2020-05-09 14:54:03 -04:00
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.12
|
|
|
|
|
|
|
2025-09-18 23:27:41 -05:00
|
|
|
|
""")
|
2020-07-15 21:28:35 -05:00
|
|
|
|
|
2024-02-15 07:30:46 +00:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
operator: TransportOperator,
|
|
|
|
|
|
timesteps: Sequence[float],
|
|
|
|
|
|
power: Optional[Union[float, Sequence[float]]] = None,
|
|
|
|
|
|
power_density: Optional[Union[float, Sequence[float]]] = None,
|
|
|
|
|
|
source_rates: Optional[Sequence[float]] = None,
|
|
|
|
|
|
timestep_units: str = 's',
|
|
|
|
|
|
n_steps: int = 10,
|
2025-03-05 19:07:50 -06:00
|
|
|
|
solver: str = "cram48",
|
2026-04-22 16:24:34 -07:00
|
|
|
|
substeps: int = 1,
|
2025-03-05 19:07:50 -06:00
|
|
|
|
continue_timesteps: bool = False,
|
2024-02-15 07:30:46 +00:00
|
|
|
|
):
|
2019-08-07 10:51:19 -05:00
|
|
|
|
check_type("n_steps", n_steps, Integral)
|
|
|
|
|
|
check_greater_than("n_steps", n_steps, 0)
|
2020-05-09 14:54:03 -04:00
|
|
|
|
super().__init__(
|
2022-07-27 12:27:16 -05:00
|
|
|
|
operator, timesteps, power, power_density, source_rates,
|
2026-04-22 16:24:34 -07:00
|
|
|
|
timestep_units=timestep_units, solver=solver,
|
|
|
|
|
|
substeps=substeps,
|
|
|
|
|
|
continue_timesteps=continue_timesteps)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
self.n_steps = n_steps
|
|
|
|
|
|
|
2023-07-06 14:48:36 -05:00
|
|
|
|
def _get_bos_data_from_operator(self, step_index, step_power, n_bos):
|
2019-08-07 10:51:19 -05:00
|
|
|
|
reset_particles = False
|
|
|
|
|
|
if step_index == 0 and hasattr(self.operator, "settings"):
|
|
|
|
|
|
reset_particles = True
|
2019-11-18 00:21:19 -05:00
|
|
|
|
self.operator.settings.particles *= self.n_steps
|
2019-08-07 10:51:19 -05:00
|
|
|
|
inherited = super()._get_bos_data_from_operator(
|
2023-07-06 14:48:36 -05:00
|
|
|
|
step_index, step_power, n_bos)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
if reset_particles:
|
2019-11-18 00:21:19 -05:00
|
|
|
|
self.operator.settings.particles //= self.n_steps
|
2019-08-07 10:51:19 -05:00
|
|
|
|
return inherited
|
|
|
|
|
|
|
2025-11-19 11:26:57 -06:00
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def __call__(self, n, rates, dt, source_rate, i):
|
|
|
|
|
|
"""Perform the integration across one time step
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
n : list of numpy.ndarray
|
|
|
|
|
|
List of atom number arrays for each material. Each array has
|
|
|
|
|
|
shape ``(n_nucs,)`` where ``n_nucs`` is the number of nuclides
|
|
|
|
|
|
rates : openmc.deplete.ReactionRates
|
|
|
|
|
|
Reaction rates (from transport operator)
|
|
|
|
|
|
dt : float
|
|
|
|
|
|
Time step in [s]
|
|
|
|
|
|
source_rate : float
|
|
|
|
|
|
Power in [W] or source rate in [neutron/sec]
|
|
|
|
|
|
i : int
|
|
|
|
|
|
Current time step index
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
proc_time : float
|
|
|
|
|
|
Time spent in transport simulation
|
|
|
|
|
|
n_end : list of numpy.ndarray
|
|
|
|
|
|
Updated atom number densities for each material
|
|
|
|
|
|
op_result : OperatorResult
|
|
|
|
|
|
Eigenvalue and reaction rates resulting from transport simulation
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2023-12-12 19:32:09 +00:00
|
|
|
|
def integrate(
|
2025-05-12 22:48:21 +02:00
|
|
|
|
self,
|
|
|
|
|
|
output: bool = True,
|
2025-11-19 11:26:57 -06:00
|
|
|
|
path: PathLike = "depletion_results.h5",
|
|
|
|
|
|
write_rates: bool = False
|
2025-05-12 22:48:21 +02:00
|
|
|
|
):
|
2022-04-21 14:31:40 -05:00
|
|
|
|
"""Perform the entire depletion process across all steps
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
output : bool, optional
|
|
|
|
|
|
Indicate whether to display information about progress
|
2023-12-12 19:32:09 +00:00
|
|
|
|
path : PathLike
|
|
|
|
|
|
Path to file to write. Defaults to 'depletion_results.h5'.
|
2022-04-21 14:31:40 -05:00
|
|
|
|
|
2024-06-21 20:28:56 -05:00
|
|
|
|
.. versionadded:: 0.15.0
|
2025-11-19 11:26:57 -06:00
|
|
|
|
write_rates : bool, optional
|
|
|
|
|
|
Whether reaction rates should be written to the results file for
|
|
|
|
|
|
each step. Defaults to ``False`` to reduce file size.
|
|
|
|
|
|
|
|
|
|
|
|
.. versionadded:: 0.15.3
|
2022-04-21 14:31:40 -05:00
|
|
|
|
"""
|
2022-07-12 16:07:59 -05:00
|
|
|
|
with change_directory(self.operator.output_dir):
|
2023-07-06 14:48:36 -05:00
|
|
|
|
n = self.operator.initial_condition()
|
2019-08-07 10:51:19 -05:00
|
|
|
|
t, self._i_res = self._get_start_data()
|
|
|
|
|
|
|
2025-11-19 11:26:57 -06:00
|
|
|
|
res_end = None # Will be set in first iteration
|
2019-08-07 10:51:19 -05:00
|
|
|
|
for i, (dt, p) in enumerate(self):
|
2022-04-21 14:31:40 -05:00
|
|
|
|
if output:
|
|
|
|
|
|
print(f"[openmc.deplete] t={t} s, dt={dt} s, source={p}")
|
|
|
|
|
|
|
2019-08-07 10:51:19 -05:00
|
|
|
|
if i == 0:
|
|
|
|
|
|
if self.operator.prev_res is None:
|
2023-07-06 14:48:36 -05:00
|
|
|
|
n, res = self._get_bos_data_from_operator(i, p, n)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
else:
|
2024-02-28 22:13:45 +00:00
|
|
|
|
n, res = self._get_bos_data_from_restart(p, n)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2025-11-19 11:26:57 -06:00
|
|
|
|
proc_time, n_end, res_end = self(n, res.rates, dt, p, i)
|
|
|
|
|
|
|
|
|
|
|
|
StepResult.save(
|
|
|
|
|
|
self.operator,
|
|
|
|
|
|
n,
|
|
|
|
|
|
res,
|
|
|
|
|
|
[t, t + dt],
|
|
|
|
|
|
p,
|
|
|
|
|
|
self._i_res + i,
|
|
|
|
|
|
proc_time,
|
|
|
|
|
|
write_rates=write_rates,
|
|
|
|
|
|
path=path
|
|
|
|
|
|
)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
|
2025-11-19 11:26:57 -06:00
|
|
|
|
# Update for next step
|
|
|
|
|
|
n = n_end
|
|
|
|
|
|
res = res_end
|
2019-08-07 10:51:19 -05:00
|
|
|
|
t += dt
|
|
|
|
|
|
|
|
|
|
|
|
# No final simulation for SIE, use last iteration results
|
2025-11-19 11:26:57 -06:00
|
|
|
|
StepResult.save(
|
|
|
|
|
|
self.operator,
|
|
|
|
|
|
n,
|
|
|
|
|
|
res_end,
|
|
|
|
|
|
[t, t],
|
|
|
|
|
|
p,
|
|
|
|
|
|
self._i_res + len(self),
|
|
|
|
|
|
proc_time,
|
|
|
|
|
|
write_rates=write_rates,
|
|
|
|
|
|
path=path
|
|
|
|
|
|
)
|
2019-08-07 10:51:19 -05:00
|
|
|
|
self.operator.write_bos_data(self._i_res + len(self))
|
2019-09-19 08:50:42 -05:00
|
|
|
|
|
2022-07-12 16:07:59 -05:00
|
|
|
|
self.operator.finalize()
|
|
|
|
|
|
|
2019-09-19 08:50:42 -05:00
|
|
|
|
|
|
|
|
|
|
class DepSystemSolver(ABC):
|
|
|
|
|
|
r"""Abstract class for solving depletion equations
|
|
|
|
|
|
|
|
|
|
|
|
Responsible for solving
|
|
|
|
|
|
|
|
|
|
|
|
.. math::
|
|
|
|
|
|
|
|
|
|
|
|
\frac{\partial \vec{N}}{\partial t} = \bar{A}\vec{N}(t),
|
|
|
|
|
|
|
|
|
|
|
|
for :math:`0< t\leq t +\Delta t`, given :math:`\vec{N}(0) = \vec{N}_0`
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
2026-04-22 16:24:34 -07:00
|
|
|
|
def __call__(self, A, n0, dt, substeps=1):
|
2019-09-19 08:50:42 -05:00
|
|
|
|
"""Solve the linear system of equations for depletion
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2026-01-02 06:43:25 -05:00
|
|
|
|
A : scipy.sparse.csc_array
|
2022-07-27 12:27:16 -05:00
|
|
|
|
Sparse transmutation matrix ``A[j, i]`` describing rates at
|
2019-09-19 08:50:42 -05:00
|
|
|
|
which isotope ``i`` transmutes to isotope ``j``
|
|
|
|
|
|
n0 : numpy.ndarray
|
|
|
|
|
|
Initial compositions, typically given in number of atoms in some
|
|
|
|
|
|
material or an atom density
|
|
|
|
|
|
dt : float
|
|
|
|
|
|
Time [s] of the specific interval to be solved
|
2026-04-22 16:24:34 -07:00
|
|
|
|
substeps : int, optional
|
|
|
|
|
|
Number of substeps to use when the solver supports substepping.
|
2019-09-19 08:50:42 -05:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
numpy.ndarray
|
|
|
|
|
|
Final compositions after ``dt``. Should be of identical shape
|
|
|
|
|
|
to ``n0``.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|