mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-21 14:35:27 -04:00
Allow the use of substeps for CRAM (#3908)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
parent
1f7ac4215f
commit
2d5c50080c
5 changed files with 288 additions and 82 deletions
|
|
@ -541,17 +541,15 @@ class Integrator(ABC):
|
|||
iterable of float. Alternatively, units can be specified for each step
|
||||
by passing an iterable of (value, unit) tuples.
|
||||
power : float or iterable of float, optional
|
||||
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
|
||||
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
|
||||
``source_rates`` must be specified.
|
||||
power_density : float or iterable of float, optional
|
||||
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.
|
||||
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.
|
||||
source_rates : float or iterable of float, optional
|
||||
Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for
|
||||
each interval in :attr:`timesteps`
|
||||
|
|
@ -563,8 +561,8 @@ class Integrator(ABC):
|
|||
and 'MWd/kg' indicates that the values are given in burnup (MW-d of
|
||||
energy deposited per kilogram of initial heavy metal).
|
||||
solver : str or callable, optional
|
||||
If a string, must be the name of the solver responsible for
|
||||
solving the Bateman equations. Current options are:
|
||||
If a string, must be the name of the solver responsible for solving the
|
||||
Bateman equations. Current options are:
|
||||
|
||||
* ``cram16`` - 16th order IPF CRAM
|
||||
* ``cram48`` - 48th order IPF CRAM [default]
|
||||
|
|
@ -573,15 +571,22 @@ class Integrator(ABC):
|
|||
:attr:`solver`.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
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
|
||||
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
|
||||
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.
|
||||
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.
|
||||
|
||||
.. versionadded:: 0.15.1
|
||||
|
||||
|
|
@ -601,15 +606,19 @@ class Integrator(ABC):
|
|||
: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:
|
||||
``solver(A, n0, t) -> n1`` where
|
||||
``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, and
|
||||
* ``n1`` is a :class:`numpy.ndarray` of compositions at the
|
||||
next time step. Expected to be of the same shape as ``n0``
|
||||
* ``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``
|
||||
|
||||
Solvers that do not support multiple substeps should raise an exception
|
||||
when ``substeps > 1``.
|
||||
|
||||
transfer_rates : openmc.deplete.TransferRates
|
||||
Transfer rates for the depletion system used to model continuous
|
||||
|
|
@ -632,6 +641,7 @@ class Integrator(ABC):
|
|||
source_rates: Optional[Union[float, Sequence[float]]] = None,
|
||||
timestep_units: str = 's',
|
||||
solver: str = "cram48",
|
||||
substeps: int = 1,
|
||||
continue_timesteps: bool = False,
|
||||
):
|
||||
if continue_timesteps and operator.prev_res is None:
|
||||
|
|
@ -653,6 +663,8 @@ class Integrator(ABC):
|
|||
# Normalize timesteps and source rates
|
||||
seconds, source_rates = _normalize_timesteps(
|
||||
timesteps, source_rates, timestep_units, operator)
|
||||
check_type("substeps", substeps, Integral)
|
||||
check_greater_than("substeps", substeps, 0)
|
||||
|
||||
if continue_timesteps:
|
||||
# Get timesteps and source rates from previous results
|
||||
|
|
@ -684,6 +696,7 @@ class Integrator(ABC):
|
|||
|
||||
self.timesteps = np.asarray(seconds)
|
||||
self.source_rates = np.asarray(source_rates)
|
||||
self.substeps = substeps
|
||||
|
||||
self.transfer_rates = None
|
||||
self.external_source_rates = None
|
||||
|
|
@ -721,23 +734,32 @@ class Integrator(ABC):
|
|||
self._solver = func
|
||||
return
|
||||
|
||||
# Inspect arguments
|
||||
if len(sig.parameters) != 3:
|
||||
raise ValueError("Function {} does not support three arguments: "
|
||||
"{!s}".format(func, sig))
|
||||
params = list(sig.parameters.values())
|
||||
|
||||
for ix, param in enumerate(sig.parameters.values()):
|
||||
if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD}:
|
||||
# Inspect arguments
|
||||
if len(params) != 4:
|
||||
raise ValueError(
|
||||
"Function {} must support four arguments "
|
||||
"(A, n0, t, substeps=1): {!s}"
|
||||
.format(func, sig))
|
||||
|
||||
for ix, param in enumerate(params):
|
||||
if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD,
|
||||
param.VAR_POSITIONAL}:
|
||||
raise ValueError(
|
||||
f"Keyword arguments like {ix} at position {param} are not allowed")
|
||||
|
||||
if len(params) == 4 and params[3].default != 1:
|
||||
raise ValueError(
|
||||
f"Fourth solver argument must default to 1, not {params[3].default}")
|
||||
|
||||
self._solver = func
|
||||
|
||||
def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None):
|
||||
start = time.time()
|
||||
results = deplete(
|
||||
self._solver, self.chain, n, rates, dt, i, matrix_func,
|
||||
self.transfer_rates, self.external_source_rates)
|
||||
self.transfer_rates, self.external_source_rates, self.substeps)
|
||||
|
||||
# Clip unphysical negative number densities
|
||||
for r in results:
|
||||
|
|
@ -1205,17 +1227,15 @@ class SIIntegrator(Integrator):
|
|||
iterable of float. Alternatively, units can be specified for each step
|
||||
by passing an iterable of (value, unit) tuples.
|
||||
power : float or iterable of float, optional
|
||||
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
|
||||
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
|
||||
``source_rates`` must be specified.
|
||||
power_density : float or iterable of float, optional
|
||||
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.
|
||||
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.
|
||||
source_rates : float or iterable of float, optional
|
||||
Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for
|
||||
each interval in :attr:`timesteps`
|
||||
|
|
@ -1227,11 +1247,11 @@ class SIIntegrator(Integrator):
|
|||
that the values are given in burnup (MW-d of energy deposited per
|
||||
kilogram of initial heavy metal).
|
||||
n_steps : int, optional
|
||||
Number of stochastic iterations per depletion interval.
|
||||
Must be greater than zero. Default : 10
|
||||
Number of stochastic iterations per depletion interval. Must be greater
|
||||
than zero. Default : 10
|
||||
solver : str or callable, optional
|
||||
If a string, must be the name of the solver responsible for
|
||||
solving the Bateman equations. Current options are:
|
||||
If a string, must be the name of the solver responsible for solving the
|
||||
Bateman equations. Current options are:
|
||||
|
||||
* ``cram16`` - 16th order IPF CRAM
|
||||
* ``cram48`` - 48th order IPF CRAM [default]
|
||||
|
|
@ -1240,16 +1260,23 @@ class SIIntegrator(Integrator):
|
|||
:attr:`solver`.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
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
|
||||
continue_timesteps : bool, optional
|
||||
Whether or not to treat the current solve as a continuation of a
|
||||
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.
|
||||
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.
|
||||
|
||||
.. versionadded:: 0.15.1
|
||||
|
||||
|
|
@ -1270,15 +1297,19 @@ class SIIntegrator(Integrator):
|
|||
: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:
|
||||
``solver(A, n0, t) -> n1`` where
|
||||
``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, and
|
||||
* ``n1`` is a :class:`numpy.ndarray` of compositions at the
|
||||
next time step. Expected to be of the same shape as ``n0``
|
||||
* ``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``
|
||||
|
||||
Solvers that do not support multiple substeps should raise an exception
|
||||
when ``substeps > 1``.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
|
||||
|
|
@ -1294,13 +1325,16 @@ class SIIntegrator(Integrator):
|
|||
timestep_units: str = 's',
|
||||
n_steps: int = 10,
|
||||
solver: str = "cram48",
|
||||
substeps: int = 1,
|
||||
continue_timesteps: bool = False,
|
||||
):
|
||||
check_type("n_steps", n_steps, Integral)
|
||||
check_greater_than("n_steps", n_steps, 0)
|
||||
super().__init__(
|
||||
operator, timesteps, power, power_density, source_rates,
|
||||
timestep_units=timestep_units, solver=solver, continue_timesteps=continue_timesteps)
|
||||
timestep_units=timestep_units, solver=solver,
|
||||
substeps=substeps,
|
||||
continue_timesteps=continue_timesteps)
|
||||
self.n_steps = n_steps
|
||||
|
||||
def _get_bos_data_from_operator(self, step_index, step_power, n_bos):
|
||||
|
|
@ -1430,7 +1464,7 @@ class DepSystemSolver(ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __call__(self, A, n0, dt):
|
||||
def __call__(self, A, n0, dt, substeps=1):
|
||||
"""Solve the linear system of equations for depletion
|
||||
|
||||
Parameters
|
||||
|
|
@ -1443,6 +1477,8 @@ class DepSystemSolver(ABC):
|
|||
material or an atom density
|
||||
dt : float
|
||||
Time [s] of the specific interval to be solved
|
||||
substeps : int, optional
|
||||
Number of substeps to use when the solver supports substepping.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
Implements two different forms of CRAM for use in openmc.deplete.
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
import numbers
|
||||
|
||||
import numpy as np
|
||||
import scipy.sparse.linalg as sla
|
||||
from scipy.sparse.linalg import spsolve, splu
|
||||
|
||||
from openmc.checkvalue import check_type, check_length
|
||||
from openmc.checkvalue import check_type, check_length, check_greater_than
|
||||
from .abc import DepSystemSolver
|
||||
from .._sparse_compat import csc_array, eye_array
|
||||
|
||||
|
|
@ -24,6 +25,12 @@ class IPFCramSolver(DepSystemSolver):
|
|||
Chebyshev Rational Approximation Method and Application to Burnup Equations
|
||||
<https://doi.org/10.13182/NSE15-26>`_," Nucl. Sci. Eng., 182:3, 297-318.
|
||||
|
||||
When `substeps` > 1, the time interval is split into `substeps` identical
|
||||
sub-intervals and LU factorizations are reused across them, as described
|
||||
in: A. Isotalo and M. Pusa, "`Improving the Accuracy of the Chebyshev
|
||||
Rational Approximation Method Using Substeps
|
||||
<https://doi.org/10.13182/NSE15-67>`_," Nucl. Sci. Eng., 183:1, 65-77.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha : numpy.ndarray
|
||||
|
|
@ -55,7 +62,7 @@ class IPFCramSolver(DepSystemSolver):
|
|||
self.theta = theta
|
||||
self.alpha0 = alpha0
|
||||
|
||||
def __call__(self, A, n0, dt):
|
||||
def __call__(self, A, n0, dt, substeps=1):
|
||||
"""Solve depletion equations using IPF CRAM
|
||||
|
||||
Parameters
|
||||
|
|
@ -68,6 +75,8 @@ class IPFCramSolver(DepSystemSolver):
|
|||
material or an atom density
|
||||
dt : float
|
||||
Time [s] of the specific interval to be solved
|
||||
substeps : int, optional
|
||||
Number of substeps per depletion interval.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -75,12 +84,25 @@ class IPFCramSolver(DepSystemSolver):
|
|||
Final compositions after ``dt``
|
||||
|
||||
"""
|
||||
A = dt * csc_array(A, dtype=np.float64)
|
||||
y = n0.copy()
|
||||
check_type("substeps", substeps, numbers.Integral)
|
||||
check_greater_than("substeps", substeps, 0)
|
||||
|
||||
step_dt = dt if substeps == 1 else dt / substeps
|
||||
A = step_dt * csc_array(A, dtype=np.float64)
|
||||
ident = eye_array(A.shape[0], format='csc')
|
||||
for alpha, theta in zip(self.alpha, self.theta):
|
||||
y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y))
|
||||
return y * self.alpha0
|
||||
|
||||
if substeps == 1:
|
||||
solvers = [partial(spsolve, A - theta * ident) for theta in self.theta]
|
||||
else:
|
||||
# Pre-compute LU factorizations and reuse them across substeps.
|
||||
solvers = [splu(A - theta * ident).solve for theta in self.theta]
|
||||
|
||||
y = n0.copy()
|
||||
for _ in range(substeps):
|
||||
for alpha, solve in zip(self.alpha, solvers):
|
||||
y += 2 * np.real(alpha * solve(y))
|
||||
y *= self.alpha0
|
||||
return y
|
||||
|
||||
|
||||
# Coefficients for IPF Cram 16
|
||||
|
|
|
|||
|
|
@ -42,14 +42,15 @@ def _distribute(items):
|
|||
j += chunk_size
|
||||
|
||||
def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None,
|
||||
transfer_rates=None, external_source_rates=None, *matrix_args):
|
||||
transfer_rates=None, external_source_rates=None, substeps=1,
|
||||
*matrix_args):
|
||||
"""Deplete materials using given reaction rates for a specified time
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : callable
|
||||
Function to use to get new compositions. Expected to have the signature
|
||||
``func(A, n0, t) -> n1``
|
||||
``func(A, n0, t, substeps=1) -> n1``.
|
||||
chain : openmc.deplete.Chain
|
||||
Depletion chain
|
||||
n : list of numpy.ndarray
|
||||
|
|
@ -74,6 +75,8 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None,
|
|||
External source rates for continuous removal/feed.
|
||||
|
||||
.. versionadded:: 0.15.3
|
||||
substeps : int, optional
|
||||
Number of substeps to pass to solvers that support substepping.
|
||||
matrix_args: Any, optional
|
||||
Additional arguments passed to matrix_func
|
||||
|
||||
|
|
@ -164,7 +167,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None,
|
|||
|
||||
# Concatenate vectors of nuclides in one
|
||||
n_multi = np.concatenate(n)
|
||||
n_result = func(matrix, n_multi, dt)
|
||||
n_result = func(matrix, n_multi, dt, substeps)
|
||||
|
||||
# Split back the nuclide vector result into the original form
|
||||
n_result = np.split(n_result, np.cumsum([len(i) for i in n])[:-1])
|
||||
|
|
@ -198,7 +201,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None,
|
|||
matrix.resize(matrix.shape[1], matrix.shape[1])
|
||||
n[i] = np.append(n[i], 1.0)
|
||||
|
||||
inputs = zip(matrices, n, repeat(dt))
|
||||
inputs = zip(matrices, n, repeat(dt), repeat(substeps))
|
||||
|
||||
if USE_MULTIPROCESSING:
|
||||
with Pool(NUM_PROCESSES) as pool:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
""" Tests for cram.py
|
||||
"""Tests for cram.py.
|
||||
|
||||
Compares a few Mathematica matrix exponentials to CRAM16/CRAM48.
|
||||
Tests substep accuracy against self-converged reference solutions.
|
||||
"""
|
||||
|
||||
from pytest import approx
|
||||
import numpy as np
|
||||
import pytest
|
||||
import scipy.sparse as sp
|
||||
from openmc.deplete.cram import CRAM16, CRAM48
|
||||
from pytest import approx
|
||||
from openmc.deplete.cram import (CRAM16, CRAM48, Cram16Solver, Cram48Solver,
|
||||
IPFCramSolver)
|
||||
|
||||
|
||||
def test_CRAM16():
|
||||
|
|
@ -35,3 +38,63 @@ def test_CRAM48():
|
|||
z0 = np.array((0.904837418035960, 0.576799023327476))
|
||||
|
||||
assert z == approx(z0)
|
||||
|
||||
|
||||
def test_substeps1_matches_original():
|
||||
"""substeps=1 must be bitwise identical to original spsolve path."""
|
||||
x = np.array([1.0, 1.0])
|
||||
mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]])
|
||||
dt = 0.1
|
||||
|
||||
z_orig = CRAM48(mat, x, dt)
|
||||
z_sub1 = CRAM48(mat, x, dt, substeps=1)
|
||||
|
||||
np.testing.assert_array_equal(z_sub1, z_orig)
|
||||
|
||||
|
||||
def test_substeps2_matches_two_half_steps():
|
||||
"""substeps=2 must match two independent CRAM calls with dt/2."""
|
||||
x = np.array([1.0, 1.0])
|
||||
mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]])
|
||||
dt = 1.0
|
||||
|
||||
# Two manual half-steps using original spsolve path
|
||||
z_half = CRAM48(mat, x, dt / 2)
|
||||
z_two = CRAM48(mat, z_half, dt / 2)
|
||||
|
||||
# Single call with substeps=2
|
||||
z_sub2 = CRAM48(mat, x, dt, substeps=2)
|
||||
|
||||
assert z_sub2 == approx(z_two, rel=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("substeps", [0, -1])
|
||||
def test_invalid_substeps(substeps):
|
||||
"""substeps must be a positive integer at call time."""
|
||||
x = np.array([1.0, 1.0])
|
||||
mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]])
|
||||
|
||||
with pytest.raises(ValueError, match="substeps"):
|
||||
CRAM48(mat, x, 0.1, substeps=substeps)
|
||||
|
||||
|
||||
def test_substeps_self_convergence():
|
||||
"""Increasing substeps converges toward reference solution.
|
||||
|
||||
Uses CRAM16 (alpha0 ~ 2e-16) where substep convergence is visible.
|
||||
CRAM48 (alpha0 ~ 2e-47) is already near machine precision for small
|
||||
systems; its correctness is verified by the other substep tests.
|
||||
"""
|
||||
mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]])
|
||||
x = np.array([1.0, 1.0])
|
||||
dt = 50 # lambda*dt = 50 and 150, stresses CRAM16
|
||||
|
||||
n_ref = CRAM16(mat, x, dt, substeps=128)
|
||||
|
||||
prev_err = np.inf
|
||||
for s in [1, 2, 4, 8, 16]:
|
||||
n_s = CRAM16(mat, x, dt, substeps=s)
|
||||
err = np.linalg.norm(n_s - n_ref) / np.linalg.norm(n_ref)
|
||||
assert err < prev_err, \
|
||||
f"substeps={s} error {err:.2e} not less than previous {prev_err:.2e}"
|
||||
prev_err = err
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from openmc.mpi import comm
|
|||
from openmc.deplete import (
|
||||
ReactionRates, StepResult, Results, OperatorResult, PredictorIntegrator,
|
||||
CECMIntegrator, CF4Integrator, CELIIntegrator, EPCRK4Integrator,
|
||||
LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram)
|
||||
LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram, pool)
|
||||
|
||||
from tests import dummy_operator
|
||||
|
||||
|
|
@ -183,18 +183,42 @@ def test_bad_integrator_inputs():
|
|||
with pytest.raises(TypeError, match=".*callable.*NoneType"):
|
||||
PredictorIntegrator(op, timesteps, power=1, solver=None)
|
||||
|
||||
with pytest.raises(ValueError, match=".*arguments"):
|
||||
with pytest.raises(ValueError, match="four arguments"):
|
||||
PredictorIntegrator(op, timesteps, power=1, solver=mock_bad_solver_nargs)
|
||||
|
||||
with pytest.raises(ValueError, match="default to 1"):
|
||||
PredictorIntegrator(op, timesteps, power=1,
|
||||
solver=mock_bad_solver_fourth_required)
|
||||
|
||||
def mock_good_solver(A, n, t):
|
||||
pass
|
||||
with pytest.raises(ValueError, match="substeps"):
|
||||
PredictorIntegrator(op, timesteps, power=1, substeps=0)
|
||||
|
||||
with pytest.raises(ValueError, match="substeps"):
|
||||
PredictorIntegrator(op, timesteps, power=1, substeps=-1)
|
||||
|
||||
|
||||
def mock_good_solver(A, n, t, substeps=1):
|
||||
return n.copy()
|
||||
|
||||
|
||||
def mock_good_solver_substeps(A, n, t, substeps=1):
|
||||
return n + substeps
|
||||
|
||||
|
||||
def mock_unsupported_substeps_solver(A, n, t, substeps=1):
|
||||
if substeps > 1:
|
||||
raise NotImplementedError("substeps > 1 not supported")
|
||||
return n.copy()
|
||||
|
||||
|
||||
def mock_bad_solver_nargs(A, n):
|
||||
pass
|
||||
|
||||
|
||||
def mock_bad_solver_fourth_required(A, n, t, substeps):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scheme", dummy_operator.SCHEMES)
|
||||
def test_integrator(run_in_tmpdir, scheme):
|
||||
"""Test the integrators against their expected values"""
|
||||
|
|
@ -226,13 +250,71 @@ def test_integrator(run_in_tmpdir, scheme):
|
|||
integrator = bundle.solver(operator, [0.75], 1, solver="cram16")
|
||||
assert integrator.solver is cram.CRAM16
|
||||
|
||||
integrator = bundle.solver(operator, [0.75], 1, solver=cram.Cram48Solver,
|
||||
substeps=2)
|
||||
assert integrator.solver is cram.Cram48Solver
|
||||
assert integrator.substeps == 2
|
||||
|
||||
integrator.solver = mock_good_solver
|
||||
assert integrator.solver is mock_good_solver
|
||||
|
||||
lfunc = lambda A, n, t: mock_good_solver(A, n, t)
|
||||
lfunc = lambda A, n, t, substeps=1: mock_good_solver(A, n, t, substeps)
|
||||
integrator.solver = lfunc
|
||||
assert integrator.solver is lfunc
|
||||
|
||||
integrator.solver = mock_good_solver_substeps
|
||||
assert integrator.solver is mock_good_solver_substeps
|
||||
|
||||
|
||||
def test_custom_solver_with_default_substeps(monkeypatch):
|
||||
operator = dummy_operator.DummyOperator()
|
||||
n = operator.initial_condition()
|
||||
rates = operator(n, 1.0).rates
|
||||
integrator = PredictorIntegrator(
|
||||
operator, [0.75], power=1.0, solver=mock_good_solver)
|
||||
monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False)
|
||||
|
||||
_, result = integrator._timed_deplete(n, rates, 0.75)
|
||||
|
||||
np.testing.assert_array_equal(result[0], n[0])
|
||||
|
||||
|
||||
def test_substep_aware_custom_solver_receives_substeps(monkeypatch):
|
||||
operator = dummy_operator.DummyOperator()
|
||||
n = operator.initial_condition()
|
||||
rates = operator(n, 1.0).rates
|
||||
integrator = PredictorIntegrator(
|
||||
operator, [0.75], power=1.0, solver=mock_good_solver_substeps,
|
||||
substeps=3)
|
||||
monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False)
|
||||
|
||||
_, result = integrator._timed_deplete(n, rates, 0.75)
|
||||
|
||||
np.testing.assert_array_equal(result[0], n[0] + 3)
|
||||
|
||||
|
||||
def test_custom_solver_propagates_substeps_error(monkeypatch):
|
||||
operator = dummy_operator.DummyOperator()
|
||||
n = operator.initial_condition()
|
||||
rates = operator(n, 1.0).rates
|
||||
integrator = PredictorIntegrator(
|
||||
operator, [0.75], power=1.0,
|
||||
solver=mock_unsupported_substeps_solver, substeps=2)
|
||||
monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False)
|
||||
|
||||
with pytest.raises(NotImplementedError, match="not supported"):
|
||||
integrator._timed_deplete(n, rates, 0.75)
|
||||
|
||||
|
||||
def test_custom_solver_requires_four_args():
|
||||
op = MagicMock()
|
||||
op.prev_res = None
|
||||
op.chain = None
|
||||
op.heavy_metal = 1.0
|
||||
|
||||
with pytest.raises(ValueError, match="four arguments"):
|
||||
PredictorIntegrator(op, [1], power=1, solver=mock_bad_solver_nargs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("integrator", INTEGRATORS)
|
||||
def test_timesteps(integrator):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue