asd
This commit is contained in:
@ -0,0 +1,8 @@
|
||||
"""Suite of ODE solvers implemented in Python."""
|
||||
from .ivp import solve_ivp
|
||||
from .rk import RK23, RK45, DOP853
|
||||
from .radau import Radau
|
||||
from .bdf import BDF
|
||||
from .lsoda import LSODA
|
||||
from .common import OdeSolution
|
||||
from .base import DenseOutput, OdeSolver
|
||||
290
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/base.py
Normal file
290
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/base.py
Normal file
@ -0,0 +1,290 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def check_arguments(fun, y0, support_complex):
|
||||
"""Helper function for checking arguments common to all solvers."""
|
||||
y0 = np.asarray(y0)
|
||||
if np.issubdtype(y0.dtype, np.complexfloating):
|
||||
if not support_complex:
|
||||
raise ValueError("`y0` is complex, but the chosen solver does "
|
||||
"not support integration in a complex domain.")
|
||||
dtype = complex
|
||||
else:
|
||||
dtype = float
|
||||
y0 = y0.astype(dtype, copy=False)
|
||||
|
||||
if y0.ndim != 1:
|
||||
raise ValueError("`y0` must be 1-dimensional.")
|
||||
|
||||
if not np.isfinite(y0).all():
|
||||
raise ValueError("All components of the initial state `y0` must be finite.")
|
||||
|
||||
def fun_wrapped(t, y):
|
||||
return np.asarray(fun(t, y), dtype=dtype)
|
||||
|
||||
return fun_wrapped, y0
|
||||
|
||||
|
||||
class OdeSolver:
|
||||
"""Base class for ODE solvers.
|
||||
|
||||
In order to implement a new solver you need to follow the guidelines:
|
||||
|
||||
1. A constructor must accept parameters presented in the base class
|
||||
(listed below) along with any other parameters specific to a solver.
|
||||
2. A constructor must accept arbitrary extraneous arguments
|
||||
``**extraneous``, but warn that these arguments are irrelevant
|
||||
using `common.warn_extraneous` function. Do not pass these
|
||||
arguments to the base class.
|
||||
3. A solver must implement a private method `_step_impl(self)` which
|
||||
propagates a solver one step further. It must return tuple
|
||||
``(success, message)``, where ``success`` is a boolean indicating
|
||||
whether a step was successful, and ``message`` is a string
|
||||
containing description of a failure if a step failed or None
|
||||
otherwise.
|
||||
4. A solver must implement a private method `_dense_output_impl(self)`,
|
||||
which returns a `DenseOutput` object covering the last successful
|
||||
step.
|
||||
5. A solver must have attributes listed below in Attributes section.
|
||||
Note that ``t_old`` and ``step_size`` are updated automatically.
|
||||
6. Use `fun(self, t, y)` method for the system rhs evaluation, this
|
||||
way the number of function evaluations (`nfev`) will be tracked
|
||||
automatically.
|
||||
7. For convenience, a base class provides `fun_single(self, t, y)` and
|
||||
`fun_vectorized(self, t, y)` for evaluating the rhs in
|
||||
non-vectorized and vectorized fashions respectively (regardless of
|
||||
how `fun` from the constructor is implemented). These calls don't
|
||||
increment `nfev`.
|
||||
8. If a solver uses a Jacobian matrix and LU decompositions, it should
|
||||
track the number of Jacobian evaluations (`njev`) and the number of
|
||||
LU decompositions (`nlu`).
|
||||
9. By convention, the function evaluations used to compute a finite
|
||||
difference approximation of the Jacobian should not be counted in
|
||||
`nfev`, thus use `fun_single(self, t, y)` or
|
||||
`fun_vectorized(self, t, y)` when computing a finite difference
|
||||
approximation of the Jacobian.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system: the time derivative of the state ``y``
|
||||
at time ``t``. The calling signature is ``fun(t, y)``, where ``t`` is a
|
||||
scalar and ``y`` is an ndarray with ``len(y) = len(y0)``. ``fun`` must
|
||||
return an array of the same shape as ``y``. See `vectorized` for more
|
||||
information.
|
||||
t0 : float
|
||||
Initial time.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state.
|
||||
t_bound : float
|
||||
Boundary time --- the integration won't continue beyond it. It also
|
||||
determines the direction of the integration.
|
||||
vectorized : bool
|
||||
Whether `fun` can be called in a vectorized fashion. Default is False.
|
||||
|
||||
If ``vectorized`` is False, `fun` will always be called with ``y`` of
|
||||
shape ``(n,)``, where ``n = len(y0)``.
|
||||
|
||||
If ``vectorized`` is True, `fun` may be called with ``y`` of shape
|
||||
``(n, k)``, where ``k`` is an integer. In this case, `fun` must behave
|
||||
such that ``fun(t, y)[:, i] == fun(t, y[:, i])`` (i.e. each column of
|
||||
the returned array is the time derivative of the state corresponding
|
||||
with a column of ``y``).
|
||||
|
||||
Setting ``vectorized=True`` allows for faster finite difference
|
||||
approximation of the Jacobian by methods 'Radau' and 'BDF', but
|
||||
will result in slower execution for other methods. It can also
|
||||
result in slower overall execution for 'Radau' and 'BDF' in some
|
||||
circumstances (e.g. small ``len(y0)``).
|
||||
support_complex : bool, optional
|
||||
Whether integration in a complex domain should be supported.
|
||||
Generally determined by a derived solver class capabilities.
|
||||
Default is False.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
n : int
|
||||
Number of equations.
|
||||
status : string
|
||||
Current status of the solver: 'running', 'finished' or 'failed'.
|
||||
t_bound : float
|
||||
Boundary time.
|
||||
direction : float
|
||||
Integration direction: +1 or -1.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray
|
||||
Current state.
|
||||
t_old : float
|
||||
Previous time. None if no steps were made yet.
|
||||
step_size : float
|
||||
Size of the last successful step. None if no steps were made yet.
|
||||
nfev : int
|
||||
Number of the system's rhs evaluations.
|
||||
njev : int
|
||||
Number of the Jacobian evaluations.
|
||||
nlu : int
|
||||
Number of LU decompositions.
|
||||
"""
|
||||
TOO_SMALL_STEP = "Required step size is less than spacing between numbers."
|
||||
|
||||
def __init__(self, fun, t0, y0, t_bound, vectorized,
|
||||
support_complex=False):
|
||||
self.t_old = None
|
||||
self.t = t0
|
||||
self._fun, self.y = check_arguments(fun, y0, support_complex)
|
||||
self.t_bound = t_bound
|
||||
self.vectorized = vectorized
|
||||
|
||||
if vectorized:
|
||||
def fun_single(t, y):
|
||||
return self._fun(t, y[:, None]).ravel()
|
||||
fun_vectorized = self._fun
|
||||
else:
|
||||
fun_single = self._fun
|
||||
|
||||
def fun_vectorized(t, y):
|
||||
f = np.empty_like(y)
|
||||
for i, yi in enumerate(y.T):
|
||||
f[:, i] = self._fun(t, yi)
|
||||
return f
|
||||
|
||||
def fun(t, y):
|
||||
self.nfev += 1
|
||||
return self.fun_single(t, y)
|
||||
|
||||
self.fun = fun
|
||||
self.fun_single = fun_single
|
||||
self.fun_vectorized = fun_vectorized
|
||||
|
||||
self.direction = np.sign(t_bound - t0) if t_bound != t0 else 1
|
||||
self.n = self.y.size
|
||||
self.status = 'running'
|
||||
|
||||
self.nfev = 0
|
||||
self.njev = 0
|
||||
self.nlu = 0
|
||||
|
||||
@property
|
||||
def step_size(self):
|
||||
if self.t_old is None:
|
||||
return None
|
||||
else:
|
||||
return np.abs(self.t - self.t_old)
|
||||
|
||||
def step(self):
|
||||
"""Perform one integration step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
message : string or None
|
||||
Report from the solver. Typically a reason for a failure if
|
||||
`self.status` is 'failed' after the step was taken or None
|
||||
otherwise.
|
||||
"""
|
||||
if self.status != 'running':
|
||||
raise RuntimeError("Attempt to step on a failed or finished "
|
||||
"solver.")
|
||||
|
||||
if self.n == 0 or self.t == self.t_bound:
|
||||
# Handle corner cases of empty solver or no integration.
|
||||
self.t_old = self.t
|
||||
self.t = self.t_bound
|
||||
message = None
|
||||
self.status = 'finished'
|
||||
else:
|
||||
t = self.t
|
||||
success, message = self._step_impl()
|
||||
|
||||
if not success:
|
||||
self.status = 'failed'
|
||||
else:
|
||||
self.t_old = t
|
||||
if self.direction * (self.t - self.t_bound) >= 0:
|
||||
self.status = 'finished'
|
||||
|
||||
return message
|
||||
|
||||
def dense_output(self):
|
||||
"""Compute a local interpolant over the last successful step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sol : `DenseOutput`
|
||||
Local interpolant over the last successful step.
|
||||
"""
|
||||
if self.t_old is None:
|
||||
raise RuntimeError("Dense output is available after a successful "
|
||||
"step was made.")
|
||||
|
||||
if self.n == 0 or self.t == self.t_old:
|
||||
# Handle corner cases of empty solver and no integration.
|
||||
return ConstantDenseOutput(self.t_old, self.t, self.y)
|
||||
else:
|
||||
return self._dense_output_impl()
|
||||
|
||||
def _step_impl(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _dense_output_impl(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DenseOutput:
|
||||
"""Base class for local interpolant over step made by an ODE solver.
|
||||
|
||||
It interpolates between `t_min` and `t_max` (see Attributes below).
|
||||
Evaluation outside this interval is not forbidden, but the accuracy is not
|
||||
guaranteed.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
t_min, t_max : float
|
||||
Time range of the interpolation.
|
||||
"""
|
||||
def __init__(self, t_old, t):
|
||||
self.t_old = t_old
|
||||
self.t = t
|
||||
self.t_min = min(t, t_old)
|
||||
self.t_max = max(t, t_old)
|
||||
|
||||
def __call__(self, t):
|
||||
"""Evaluate the interpolant.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : float or array_like with shape (n_points,)
|
||||
Points to evaluate the solution at.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray, shape (n,) or (n, n_points)
|
||||
Computed values. Shape depends on whether `t` was a scalar or a
|
||||
1-D array.
|
||||
"""
|
||||
t = np.asarray(t)
|
||||
if t.ndim > 1:
|
||||
raise ValueError("`t` must be a float or a 1-D array.")
|
||||
return self._call_impl(t)
|
||||
|
||||
def _call_impl(self, t):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ConstantDenseOutput(DenseOutput):
|
||||
"""Constant value interpolator.
|
||||
|
||||
This class used for degenerate integration cases: equal integration limits
|
||||
or a system with 0 equations.
|
||||
"""
|
||||
def __init__(self, t_old, t, value):
|
||||
super().__init__(t_old, t)
|
||||
self.value = value
|
||||
|
||||
def _call_impl(self, t):
|
||||
if t.ndim == 0:
|
||||
return self.value
|
||||
else:
|
||||
ret = np.empty((self.value.shape[0], t.shape[0]))
|
||||
ret[:] = self.value[:, None]
|
||||
return ret
|
||||
480
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/bdf.py
Normal file
480
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/bdf.py
Normal file
@ -0,0 +1,480 @@
|
||||
import numpy as np
|
||||
from scipy.linalg import lu_factor, lu_solve
|
||||
from scipy.sparse import issparse, csc_matrix, eye
|
||||
from scipy.sparse.linalg import splu
|
||||
from scipy.optimize._numdiff import group_columns
|
||||
from .common import (validate_max_step, validate_tol, select_initial_step,
|
||||
norm, EPS, num_jac, validate_first_step,
|
||||
warn_extraneous)
|
||||
from .base import OdeSolver, DenseOutput
|
||||
|
||||
|
||||
MAX_ORDER = 5
|
||||
NEWTON_MAXITER = 4
|
||||
MIN_FACTOR = 0.2
|
||||
MAX_FACTOR = 10
|
||||
|
||||
|
||||
def compute_R(order, factor):
|
||||
"""Compute the matrix for changing the differences array."""
|
||||
I = np.arange(1, order + 1)[:, None]
|
||||
J = np.arange(1, order + 1)
|
||||
M = np.zeros((order + 1, order + 1))
|
||||
M[1:, 1:] = (I - 1 - factor * J) / I
|
||||
M[0] = 1
|
||||
return np.cumprod(M, axis=0)
|
||||
|
||||
|
||||
def change_D(D, order, factor):
|
||||
"""Change differences array in-place when step size is changed."""
|
||||
R = compute_R(order, factor)
|
||||
U = compute_R(order, 1)
|
||||
RU = R.dot(U)
|
||||
D[:order + 1] = np.dot(RU.T, D[:order + 1])
|
||||
|
||||
|
||||
def solve_bdf_system(fun, t_new, y_predict, c, psi, LU, solve_lu, scale, tol):
|
||||
"""Solve the algebraic system resulting from BDF method."""
|
||||
d = 0
|
||||
y = y_predict.copy()
|
||||
dy_norm_old = None
|
||||
converged = False
|
||||
for k in range(NEWTON_MAXITER):
|
||||
f = fun(t_new, y)
|
||||
if not np.all(np.isfinite(f)):
|
||||
break
|
||||
|
||||
dy = solve_lu(LU, c * f - psi - d)
|
||||
dy_norm = norm(dy / scale)
|
||||
|
||||
if dy_norm_old is None:
|
||||
rate = None
|
||||
else:
|
||||
rate = dy_norm / dy_norm_old
|
||||
|
||||
if (rate is not None and (rate >= 1 or
|
||||
rate ** (NEWTON_MAXITER - k) / (1 - rate) * dy_norm > tol)):
|
||||
break
|
||||
|
||||
y += dy
|
||||
d += dy
|
||||
|
||||
if (dy_norm == 0 or
|
||||
rate is not None and rate / (1 - rate) * dy_norm < tol):
|
||||
converged = True
|
||||
break
|
||||
|
||||
dy_norm_old = dy_norm
|
||||
|
||||
return converged, k + 1, y, d
|
||||
|
||||
|
||||
class BDF(OdeSolver):
|
||||
"""Implicit method based on backward-differentiation formulas.
|
||||
|
||||
This is a variable order method with the order varying automatically from
|
||||
1 to 5. The general framework of the BDF algorithm is described in [1]_.
|
||||
This class implements a quasi-constant step size as explained in [2]_.
|
||||
The error estimation strategy for the constant-step BDF is derived in [3]_.
|
||||
An accuracy enhancement using modified formulas (NDF) [2]_ is also implemented.
|
||||
|
||||
Can be applied in the complex domain.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system: the time derivative of the state ``y``
|
||||
at time ``t``. The calling signature is ``fun(t, y)``, where ``t`` is a
|
||||
scalar and ``y`` is an ndarray with ``len(y) = len(y0)``. ``fun`` must
|
||||
return an array of the same shape as ``y``. See `vectorized` for more
|
||||
information.
|
||||
t0 : float
|
||||
Initial time.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state.
|
||||
t_bound : float
|
||||
Boundary time - the integration won't continue beyond it. It also
|
||||
determines the direction of the integration.
|
||||
first_step : float or None, optional
|
||||
Initial step size. Default is ``None`` which means that the algorithm
|
||||
should choose.
|
||||
max_step : float, optional
|
||||
Maximum allowed step size. Default is np.inf, i.e., the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
rtol, atol : float and array_like, optional
|
||||
Relative and absolute tolerances. The solver keeps the local error
|
||||
estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a
|
||||
relative accuracy (number of correct digits), while `atol` controls
|
||||
absolute accuracy (number of correct decimal places). To achieve the
|
||||
desired `rtol`, set `atol` to be smaller than the smallest value that
|
||||
can be expected from ``rtol * abs(y)`` so that `rtol` dominates the
|
||||
allowable error. If `atol` is larger than ``rtol * abs(y)`` the
|
||||
number of correct digits is not guaranteed. Conversely, to achieve the
|
||||
desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller
|
||||
than `atol`. If components of y have different scales, it might be
|
||||
beneficial to set different `atol` values for different components by
|
||||
passing array_like with shape (n,) for `atol`. Default values are
|
||||
1e-3 for `rtol` and 1e-6 for `atol`.
|
||||
jac : {None, array_like, sparse_matrix, callable}, optional
|
||||
Jacobian matrix of the right-hand side of the system with respect to y,
|
||||
required by this method. The Jacobian matrix has shape (n, n) and its
|
||||
element (i, j) is equal to ``d f_i / d y_j``.
|
||||
There are three ways to define the Jacobian:
|
||||
|
||||
* If array_like or sparse_matrix, the Jacobian is assumed to
|
||||
be constant.
|
||||
* If callable, the Jacobian is assumed to depend on both
|
||||
t and y; it will be called as ``jac(t, y)`` as necessary.
|
||||
For the 'Radau' and 'BDF' methods, the return value might be a
|
||||
sparse matrix.
|
||||
* If None (default), the Jacobian will be approximated by
|
||||
finite differences.
|
||||
|
||||
It is generally recommended to provide the Jacobian rather than
|
||||
relying on a finite-difference approximation.
|
||||
jac_sparsity : {None, array_like, sparse matrix}, optional
|
||||
Defines a sparsity structure of the Jacobian matrix for a
|
||||
finite-difference approximation. Its shape must be (n, n). This argument
|
||||
is ignored if `jac` is not `None`. If the Jacobian has only few non-zero
|
||||
elements in *each* row, providing the sparsity structure will greatly
|
||||
speed up the computations [4]_. A zero entry means that a corresponding
|
||||
element in the Jacobian is always zero. If None (default), the Jacobian
|
||||
is assumed to be dense.
|
||||
vectorized : bool, optional
|
||||
Whether `fun` can be called in a vectorized fashion. Default is False.
|
||||
|
||||
If ``vectorized`` is False, `fun` will always be called with ``y`` of
|
||||
shape ``(n,)``, where ``n = len(y0)``.
|
||||
|
||||
If ``vectorized`` is True, `fun` may be called with ``y`` of shape
|
||||
``(n, k)``, where ``k`` is an integer. In this case, `fun` must behave
|
||||
such that ``fun(t, y)[:, i] == fun(t, y[:, i])`` (i.e. each column of
|
||||
the returned array is the time derivative of the state corresponding
|
||||
with a column of ``y``).
|
||||
|
||||
Setting ``vectorized=True`` allows for faster finite difference
|
||||
approximation of the Jacobian by this method, but may result in slower
|
||||
execution overall in some circumstances (e.g. small ``len(y0)``).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
n : int
|
||||
Number of equations.
|
||||
status : string
|
||||
Current status of the solver: 'running', 'finished' or 'failed'.
|
||||
t_bound : float
|
||||
Boundary time.
|
||||
direction : float
|
||||
Integration direction: +1 or -1.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray
|
||||
Current state.
|
||||
t_old : float
|
||||
Previous time. None if no steps were made yet.
|
||||
step_size : float
|
||||
Size of the last successful step. None if no steps were made yet.
|
||||
nfev : int
|
||||
Number of evaluations of the right-hand side.
|
||||
njev : int
|
||||
Number of evaluations of the Jacobian.
|
||||
nlu : int
|
||||
Number of LU decompositions.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] G. D. Byrne, A. C. Hindmarsh, "A Polyalgorithm for the Numerical
|
||||
Solution of Ordinary Differential Equations", ACM Transactions on
|
||||
Mathematical Software, Vol. 1, No. 1, pp. 71-96, March 1975.
|
||||
.. [2] L. F. Shampine, M. W. Reichelt, "THE MATLAB ODE SUITE", SIAM J. SCI.
|
||||
COMPUTE., Vol. 18, No. 1, pp. 1-22, January 1997.
|
||||
.. [3] E. Hairer, G. Wanner, "Solving Ordinary Differential Equations I:
|
||||
Nonstiff Problems", Sec. III.2.
|
||||
.. [4] A. Curtis, M. J. D. Powell, and J. Reid, "On the estimation of
|
||||
sparse Jacobian matrices", Journal of the Institute of Mathematics
|
||||
and its Applications, 13, pp. 117-120, 1974.
|
||||
"""
|
||||
def __init__(self, fun, t0, y0, t_bound, max_step=np.inf,
|
||||
rtol=1e-3, atol=1e-6, jac=None, jac_sparsity=None,
|
||||
vectorized=False, first_step=None, **extraneous):
|
||||
warn_extraneous(extraneous)
|
||||
super().__init__(fun, t0, y0, t_bound, vectorized,
|
||||
support_complex=True)
|
||||
self.max_step = validate_max_step(max_step)
|
||||
self.rtol, self.atol = validate_tol(rtol, atol, self.n)
|
||||
f = self.fun(self.t, self.y)
|
||||
if first_step is None:
|
||||
self.h_abs = select_initial_step(self.fun, self.t, self.y,
|
||||
t_bound, max_step, f,
|
||||
self.direction, 1,
|
||||
self.rtol, self.atol)
|
||||
else:
|
||||
self.h_abs = validate_first_step(first_step, t0, t_bound)
|
||||
self.h_abs_old = None
|
||||
self.error_norm_old = None
|
||||
|
||||
self.newton_tol = max(10 * EPS / rtol, min(0.03, rtol ** 0.5))
|
||||
|
||||
self.jac_factor = None
|
||||
self.jac, self.J = self._validate_jac(jac, jac_sparsity)
|
||||
if issparse(self.J):
|
||||
def lu(A):
|
||||
self.nlu += 1
|
||||
return splu(A)
|
||||
|
||||
def solve_lu(LU, b):
|
||||
return LU.solve(b)
|
||||
|
||||
I = eye(self.n, format='csc', dtype=self.y.dtype)
|
||||
else:
|
||||
def lu(A):
|
||||
self.nlu += 1
|
||||
return lu_factor(A, overwrite_a=True)
|
||||
|
||||
def solve_lu(LU, b):
|
||||
return lu_solve(LU, b, overwrite_b=True)
|
||||
|
||||
I = np.identity(self.n, dtype=self.y.dtype)
|
||||
|
||||
self.lu = lu
|
||||
self.solve_lu = solve_lu
|
||||
self.I = I
|
||||
|
||||
kappa = np.array([0, -0.1850, -1/9, -0.0823, -0.0415, 0])
|
||||
self.gamma = np.hstack((0, np.cumsum(1 / np.arange(1, MAX_ORDER + 1))))
|
||||
self.alpha = (1 - kappa) * self.gamma
|
||||
self.error_const = kappa * self.gamma + 1 / np.arange(1, MAX_ORDER + 2)
|
||||
|
||||
D = np.empty((MAX_ORDER + 3, self.n), dtype=self.y.dtype)
|
||||
D[0] = self.y
|
||||
D[1] = f * self.h_abs * self.direction
|
||||
self.D = D
|
||||
|
||||
self.order = 1
|
||||
self.n_equal_steps = 0
|
||||
self.LU = None
|
||||
|
||||
def _validate_jac(self, jac, sparsity):
|
||||
t0 = self.t
|
||||
y0 = self.y
|
||||
|
||||
if jac is None:
|
||||
if sparsity is not None:
|
||||
if issparse(sparsity):
|
||||
sparsity = csc_matrix(sparsity)
|
||||
groups = group_columns(sparsity)
|
||||
sparsity = (sparsity, groups)
|
||||
|
||||
def jac_wrapped(t, y):
|
||||
self.njev += 1
|
||||
f = self.fun_single(t, y)
|
||||
J, self.jac_factor = num_jac(self.fun_vectorized, t, y, f,
|
||||
self.atol, self.jac_factor,
|
||||
sparsity)
|
||||
return J
|
||||
J = jac_wrapped(t0, y0)
|
||||
elif callable(jac):
|
||||
J = jac(t0, y0)
|
||||
self.njev += 1
|
||||
if issparse(J):
|
||||
J = csc_matrix(J, dtype=y0.dtype)
|
||||
|
||||
def jac_wrapped(t, y):
|
||||
self.njev += 1
|
||||
return csc_matrix(jac(t, y), dtype=y0.dtype)
|
||||
else:
|
||||
J = np.asarray(J, dtype=y0.dtype)
|
||||
|
||||
def jac_wrapped(t, y):
|
||||
self.njev += 1
|
||||
return np.asarray(jac(t, y), dtype=y0.dtype)
|
||||
|
||||
if J.shape != (self.n, self.n):
|
||||
raise ValueError("`jac` is expected to have shape {}, but "
|
||||
"actually has {}."
|
||||
.format((self.n, self.n), J.shape))
|
||||
else:
|
||||
if issparse(jac):
|
||||
J = csc_matrix(jac, dtype=y0.dtype)
|
||||
else:
|
||||
J = np.asarray(jac, dtype=y0.dtype)
|
||||
|
||||
if J.shape != (self.n, self.n):
|
||||
raise ValueError("`jac` is expected to have shape {}, but "
|
||||
"actually has {}."
|
||||
.format((self.n, self.n), J.shape))
|
||||
jac_wrapped = None
|
||||
|
||||
return jac_wrapped, J
|
||||
|
||||
def _step_impl(self):
|
||||
t = self.t
|
||||
D = self.D
|
||||
|
||||
max_step = self.max_step
|
||||
min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t)
|
||||
if self.h_abs > max_step:
|
||||
h_abs = max_step
|
||||
change_D(D, self.order, max_step / self.h_abs)
|
||||
self.n_equal_steps = 0
|
||||
elif self.h_abs < min_step:
|
||||
h_abs = min_step
|
||||
change_D(D, self.order, min_step / self.h_abs)
|
||||
self.n_equal_steps = 0
|
||||
else:
|
||||
h_abs = self.h_abs
|
||||
|
||||
atol = self.atol
|
||||
rtol = self.rtol
|
||||
order = self.order
|
||||
|
||||
alpha = self.alpha
|
||||
gamma = self.gamma
|
||||
error_const = self.error_const
|
||||
|
||||
J = self.J
|
||||
LU = self.LU
|
||||
current_jac = self.jac is None
|
||||
|
||||
step_accepted = False
|
||||
while not step_accepted:
|
||||
if h_abs < min_step:
|
||||
return False, self.TOO_SMALL_STEP
|
||||
|
||||
h = h_abs * self.direction
|
||||
t_new = t + h
|
||||
|
||||
if self.direction * (t_new - self.t_bound) > 0:
|
||||
t_new = self.t_bound
|
||||
change_D(D, order, np.abs(t_new - t) / h_abs)
|
||||
self.n_equal_steps = 0
|
||||
LU = None
|
||||
|
||||
h = t_new - t
|
||||
h_abs = np.abs(h)
|
||||
|
||||
y_predict = np.sum(D[:order + 1], axis=0)
|
||||
|
||||
scale = atol + rtol * np.abs(y_predict)
|
||||
psi = np.dot(D[1: order + 1].T, gamma[1: order + 1]) / alpha[order]
|
||||
|
||||
converged = False
|
||||
c = h / alpha[order]
|
||||
while not converged:
|
||||
if LU is None:
|
||||
LU = self.lu(self.I - c * J)
|
||||
|
||||
converged, n_iter, y_new, d = solve_bdf_system(
|
||||
self.fun, t_new, y_predict, c, psi, LU, self.solve_lu,
|
||||
scale, self.newton_tol)
|
||||
|
||||
if not converged:
|
||||
if current_jac:
|
||||
break
|
||||
J = self.jac(t_new, y_predict)
|
||||
LU = None
|
||||
current_jac = True
|
||||
|
||||
if not converged:
|
||||
factor = 0.5
|
||||
h_abs *= factor
|
||||
change_D(D, order, factor)
|
||||
self.n_equal_steps = 0
|
||||
LU = None
|
||||
continue
|
||||
|
||||
safety = 0.9 * (2 * NEWTON_MAXITER + 1) / (2 * NEWTON_MAXITER
|
||||
+ n_iter)
|
||||
|
||||
scale = atol + rtol * np.abs(y_new)
|
||||
error = error_const[order] * d
|
||||
error_norm = norm(error / scale)
|
||||
|
||||
if error_norm > 1:
|
||||
factor = max(MIN_FACTOR,
|
||||
safety * error_norm ** (-1 / (order + 1)))
|
||||
h_abs *= factor
|
||||
change_D(D, order, factor)
|
||||
self.n_equal_steps = 0
|
||||
# As we didn't have problems with convergence, we don't
|
||||
# reset LU here.
|
||||
else:
|
||||
step_accepted = True
|
||||
|
||||
self.n_equal_steps += 1
|
||||
|
||||
self.t = t_new
|
||||
self.y = y_new
|
||||
|
||||
self.h_abs = h_abs
|
||||
self.J = J
|
||||
self.LU = LU
|
||||
|
||||
# Update differences. The principal relation here is
|
||||
# D^{j + 1} y_n = D^{j} y_n - D^{j} y_{n - 1}. Keep in mind that D
|
||||
# contained difference for previous interpolating polynomial and
|
||||
# d = D^{k + 1} y_n. Thus this elegant code follows.
|
||||
D[order + 2] = d - D[order + 1]
|
||||
D[order + 1] = d
|
||||
for i in reversed(range(order + 1)):
|
||||
D[i] += D[i + 1]
|
||||
|
||||
if self.n_equal_steps < order + 1:
|
||||
return True, None
|
||||
|
||||
if order > 1:
|
||||
error_m = error_const[order - 1] * D[order]
|
||||
error_m_norm = norm(error_m / scale)
|
||||
else:
|
||||
error_m_norm = np.inf
|
||||
|
||||
if order < MAX_ORDER:
|
||||
error_p = error_const[order + 1] * D[order + 2]
|
||||
error_p_norm = norm(error_p / scale)
|
||||
else:
|
||||
error_p_norm = np.inf
|
||||
|
||||
error_norms = np.array([error_m_norm, error_norm, error_p_norm])
|
||||
with np.errstate(divide='ignore'):
|
||||
factors = error_norms ** (-1 / np.arange(order, order + 3))
|
||||
|
||||
delta_order = np.argmax(factors) - 1
|
||||
order += delta_order
|
||||
self.order = order
|
||||
|
||||
factor = min(MAX_FACTOR, safety * np.max(factors))
|
||||
self.h_abs *= factor
|
||||
change_D(D, order, factor)
|
||||
self.n_equal_steps = 0
|
||||
self.LU = None
|
||||
|
||||
return True, None
|
||||
|
||||
def _dense_output_impl(self):
|
||||
return BdfDenseOutput(self.t_old, self.t, self.h_abs * self.direction,
|
||||
self.order, self.D[:self.order + 1].copy())
|
||||
|
||||
|
||||
class BdfDenseOutput(DenseOutput):
|
||||
def __init__(self, t_old, t, h, order, D):
|
||||
super().__init__(t_old, t)
|
||||
self.order = order
|
||||
self.t_shift = self.t - h * np.arange(self.order)
|
||||
self.denom = h * (1 + np.arange(self.order))
|
||||
self.D = D
|
||||
|
||||
def _call_impl(self, t):
|
||||
if t.ndim == 0:
|
||||
x = (t - self.t_shift) / self.denom
|
||||
p = np.cumprod(x)
|
||||
else:
|
||||
x = (t - self.t_shift[:, None]) / self.denom[:, None]
|
||||
p = np.cumprod(x, axis=0)
|
||||
|
||||
y = np.dot(self.D[1:].T, p)
|
||||
if y.ndim == 1:
|
||||
y += self.D[0]
|
||||
else:
|
||||
y += self.D[0, :, None]
|
||||
|
||||
return y
|
||||
451
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/common.py
Normal file
451
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/common.py
Normal file
@ -0,0 +1,451 @@
|
||||
from itertools import groupby
|
||||
from warnings import warn
|
||||
import numpy as np
|
||||
from scipy.sparse import find, coo_matrix
|
||||
|
||||
|
||||
EPS = np.finfo(float).eps
|
||||
|
||||
|
||||
def validate_first_step(first_step, t0, t_bound):
|
||||
"""Assert that first_step is valid and return it."""
|
||||
if first_step <= 0:
|
||||
raise ValueError("`first_step` must be positive.")
|
||||
if first_step > np.abs(t_bound - t0):
|
||||
raise ValueError("`first_step` exceeds bounds.")
|
||||
return first_step
|
||||
|
||||
|
||||
def validate_max_step(max_step):
|
||||
"""Assert that max_Step is valid and return it."""
|
||||
if max_step <= 0:
|
||||
raise ValueError("`max_step` must be positive.")
|
||||
return max_step
|
||||
|
||||
|
||||
def warn_extraneous(extraneous):
|
||||
"""Display a warning for extraneous keyword arguments.
|
||||
|
||||
The initializer of each solver class is expected to collect keyword
|
||||
arguments that it doesn't understand and warn about them. This function
|
||||
prints a warning for each key in the supplied dictionary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
extraneous : dict
|
||||
Extraneous keyword arguments
|
||||
"""
|
||||
if extraneous:
|
||||
warn("The following arguments have no effect for a chosen solver: {}."
|
||||
.format(", ".join(f"`{x}`" for x in extraneous)),
|
||||
stacklevel=3)
|
||||
|
||||
|
||||
def validate_tol(rtol, atol, n):
|
||||
"""Validate tolerance values."""
|
||||
|
||||
if np.any(rtol < 100 * EPS):
|
||||
warn("At least one element of `rtol` is too small. "
|
||||
f"Setting `rtol = np.maximum(rtol, {100 * EPS})`.",
|
||||
stacklevel=3)
|
||||
rtol = np.maximum(rtol, 100 * EPS)
|
||||
|
||||
atol = np.asarray(atol)
|
||||
if atol.ndim > 0 and atol.shape != (n,):
|
||||
raise ValueError("`atol` has wrong shape.")
|
||||
|
||||
if np.any(atol < 0):
|
||||
raise ValueError("`atol` must be positive.")
|
||||
|
||||
return rtol, atol
|
||||
|
||||
|
||||
def norm(x):
|
||||
"""Compute RMS norm."""
|
||||
return np.linalg.norm(x) / x.size ** 0.5
|
||||
|
||||
|
||||
def select_initial_step(fun, t0, y0, t_bound,
|
||||
max_step, f0, direction, order, rtol, atol):
|
||||
"""Empirically select a good initial step.
|
||||
|
||||
The algorithm is described in [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system.
|
||||
t0 : float
|
||||
Initial value of the independent variable.
|
||||
y0 : ndarray, shape (n,)
|
||||
Initial value of the dependent variable.
|
||||
t_bound : float
|
||||
End-point of integration interval; used to ensure that t0+step<=tbound
|
||||
and that fun is only evaluated in the interval [t0,tbound]
|
||||
max_step : float
|
||||
Maximum allowable step size.
|
||||
f0 : ndarray, shape (n,)
|
||||
Initial value of the derivative, i.e., ``fun(t0, y0)``.
|
||||
direction : float
|
||||
Integration direction.
|
||||
order : float
|
||||
Error estimator order. It means that the error controlled by the
|
||||
algorithm is proportional to ``step_size ** (order + 1)`.
|
||||
rtol : float
|
||||
Desired relative tolerance.
|
||||
atol : float
|
||||
Desired absolute tolerance.
|
||||
|
||||
Returns
|
||||
-------
|
||||
h_abs : float
|
||||
Absolute value of the suggested initial step.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. Hairer, S. P. Norsett G. Wanner, "Solving Ordinary Differential
|
||||
Equations I: Nonstiff Problems", Sec. II.4.
|
||||
"""
|
||||
if y0.size == 0:
|
||||
return np.inf
|
||||
|
||||
interval_length = abs(t_bound - t0)
|
||||
if interval_length == 0.0:
|
||||
return 0.0
|
||||
|
||||
scale = atol + np.abs(y0) * rtol
|
||||
d0 = norm(y0 / scale)
|
||||
d1 = norm(f0 / scale)
|
||||
if d0 < 1e-5 or d1 < 1e-5:
|
||||
h0 = 1e-6
|
||||
else:
|
||||
h0 = 0.01 * d0 / d1
|
||||
# Check t0+h0*direction doesn't take us beyond t_bound
|
||||
h0 = min(h0, interval_length)
|
||||
y1 = y0 + h0 * direction * f0
|
||||
f1 = fun(t0 + h0 * direction, y1)
|
||||
d2 = norm((f1 - f0) / scale) / h0
|
||||
|
||||
if d1 <= 1e-15 and d2 <= 1e-15:
|
||||
h1 = max(1e-6, h0 * 1e-3)
|
||||
else:
|
||||
h1 = (0.01 / max(d1, d2)) ** (1 / (order + 1))
|
||||
|
||||
return min(100 * h0, h1, interval_length, max_step)
|
||||
|
||||
|
||||
class OdeSolution:
|
||||
"""Continuous ODE solution.
|
||||
|
||||
It is organized as a collection of `DenseOutput` objects which represent
|
||||
local interpolants. It provides an algorithm to select a right interpolant
|
||||
for each given point.
|
||||
|
||||
The interpolants cover the range between `t_min` and `t_max` (see
|
||||
Attributes below). Evaluation outside this interval is not forbidden, but
|
||||
the accuracy is not guaranteed.
|
||||
|
||||
When evaluating at a breakpoint (one of the values in `ts`) a segment with
|
||||
the lower index is selected.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ts : array_like, shape (n_segments + 1,)
|
||||
Time instants between which local interpolants are defined. Must
|
||||
be strictly increasing or decreasing (zero segment with two points is
|
||||
also allowed).
|
||||
interpolants : list of DenseOutput with n_segments elements
|
||||
Local interpolants. An i-th interpolant is assumed to be defined
|
||||
between ``ts[i]`` and ``ts[i + 1]``.
|
||||
alt_segment : boolean
|
||||
Requests the alternative interpolant segment selection scheme. At each
|
||||
solver integration point, two interpolant segments are available. The
|
||||
default (False) and alternative (True) behaviours select the segment
|
||||
for which the requested time corresponded to ``t`` and ``t_old``,
|
||||
respectively. This functionality is only relevant for testing the
|
||||
interpolants' accuracy: different integrators use different
|
||||
construction strategies.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
t_min, t_max : float
|
||||
Time range of the interpolation.
|
||||
"""
|
||||
def __init__(self, ts, interpolants, alt_segment=False):
|
||||
ts = np.asarray(ts)
|
||||
d = np.diff(ts)
|
||||
# The first case covers integration on zero segment.
|
||||
if not ((ts.size == 2 and ts[0] == ts[-1])
|
||||
or np.all(d > 0) or np.all(d < 0)):
|
||||
raise ValueError("`ts` must be strictly increasing or decreasing.")
|
||||
|
||||
self.n_segments = len(interpolants)
|
||||
if ts.shape != (self.n_segments + 1,):
|
||||
raise ValueError("Numbers of time stamps and interpolants "
|
||||
"don't match.")
|
||||
|
||||
self.ts = ts
|
||||
self.interpolants = interpolants
|
||||
if ts[-1] >= ts[0]:
|
||||
self.t_min = ts[0]
|
||||
self.t_max = ts[-1]
|
||||
self.ascending = True
|
||||
self.side = "right" if alt_segment else "left"
|
||||
self.ts_sorted = ts
|
||||
else:
|
||||
self.t_min = ts[-1]
|
||||
self.t_max = ts[0]
|
||||
self.ascending = False
|
||||
self.side = "left" if alt_segment else "right"
|
||||
self.ts_sorted = ts[::-1]
|
||||
|
||||
def _call_single(self, t):
|
||||
# Here we preserve a certain symmetry that when t is in self.ts,
|
||||
# if alt_segment=False, then we prioritize a segment with a lower
|
||||
# index.
|
||||
ind = np.searchsorted(self.ts_sorted, t, side=self.side)
|
||||
|
||||
segment = min(max(ind - 1, 0), self.n_segments - 1)
|
||||
if not self.ascending:
|
||||
segment = self.n_segments - 1 - segment
|
||||
|
||||
return self.interpolants[segment](t)
|
||||
|
||||
def __call__(self, t):
|
||||
"""Evaluate the solution.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : float or array_like with shape (n_points,)
|
||||
Points to evaluate at.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : ndarray, shape (n_states,) or (n_states, n_points)
|
||||
Computed values. Shape depends on whether `t` is a scalar or a
|
||||
1-D array.
|
||||
"""
|
||||
t = np.asarray(t)
|
||||
|
||||
if t.ndim == 0:
|
||||
return self._call_single(t)
|
||||
|
||||
order = np.argsort(t)
|
||||
reverse = np.empty_like(order)
|
||||
reverse[order] = np.arange(order.shape[0])
|
||||
t_sorted = t[order]
|
||||
|
||||
# See comment in self._call_single.
|
||||
segments = np.searchsorted(self.ts_sorted, t_sorted, side=self.side)
|
||||
segments -= 1
|
||||
segments[segments < 0] = 0
|
||||
segments[segments > self.n_segments - 1] = self.n_segments - 1
|
||||
if not self.ascending:
|
||||
segments = self.n_segments - 1 - segments
|
||||
|
||||
ys = []
|
||||
group_start = 0
|
||||
for segment, group in groupby(segments):
|
||||
group_end = group_start + len(list(group))
|
||||
y = self.interpolants[segment](t_sorted[group_start:group_end])
|
||||
ys.append(y)
|
||||
group_start = group_end
|
||||
|
||||
ys = np.hstack(ys)
|
||||
ys = ys[:, reverse]
|
||||
|
||||
return ys
|
||||
|
||||
|
||||
NUM_JAC_DIFF_REJECT = EPS ** 0.875
|
||||
NUM_JAC_DIFF_SMALL = EPS ** 0.75
|
||||
NUM_JAC_DIFF_BIG = EPS ** 0.25
|
||||
NUM_JAC_MIN_FACTOR = 1e3 * EPS
|
||||
NUM_JAC_FACTOR_INCREASE = 10
|
||||
NUM_JAC_FACTOR_DECREASE = 0.1
|
||||
|
||||
|
||||
def num_jac(fun, t, y, f, threshold, factor, sparsity=None):
|
||||
"""Finite differences Jacobian approximation tailored for ODE solvers.
|
||||
|
||||
This function computes finite difference approximation to the Jacobian
|
||||
matrix of `fun` with respect to `y` using forward differences.
|
||||
The Jacobian matrix has shape (n, n) and its element (i, j) is equal to
|
||||
``d f_i / d y_j``.
|
||||
|
||||
A special feature of this function is the ability to correct the step
|
||||
size from iteration to iteration. The main idea is to keep the finite
|
||||
difference significantly separated from its round-off error which
|
||||
approximately equals ``EPS * np.abs(f)``. It reduces a possibility of a
|
||||
huge error and assures that the estimated derivative are reasonably close
|
||||
to the true values (i.e., the finite difference approximation is at least
|
||||
qualitatively reflects the structure of the true Jacobian).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system implemented in a vectorized fashion.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray, shape (n,)
|
||||
Current state.
|
||||
f : ndarray, shape (n,)
|
||||
Value of the right hand side at (t, y).
|
||||
threshold : float
|
||||
Threshold for `y` value used for computing the step size as
|
||||
``factor * np.maximum(np.abs(y), threshold)``. Typically, the value of
|
||||
absolute tolerance (atol) for a solver should be passed as `threshold`.
|
||||
factor : ndarray with shape (n,) or None
|
||||
Factor to use for computing the step size. Pass None for the very
|
||||
evaluation, then use the value returned from this function.
|
||||
sparsity : tuple (structure, groups) or None
|
||||
Sparsity structure of the Jacobian, `structure` must be csc_matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
J : ndarray or csc_matrix, shape (n, n)
|
||||
Jacobian matrix.
|
||||
factor : ndarray, shape (n,)
|
||||
Suggested `factor` for the next evaluation.
|
||||
"""
|
||||
y = np.asarray(y)
|
||||
n = y.shape[0]
|
||||
if n == 0:
|
||||
return np.empty((0, 0)), factor
|
||||
|
||||
if factor is None:
|
||||
factor = np.full(n, EPS ** 0.5)
|
||||
else:
|
||||
factor = factor.copy()
|
||||
|
||||
# Direct the step as ODE dictates, hoping that such a step won't lead to
|
||||
# a problematic region. For complex ODEs it makes sense to use the real
|
||||
# part of f as we use steps along real axis.
|
||||
f_sign = 2 * (np.real(f) >= 0).astype(float) - 1
|
||||
y_scale = f_sign * np.maximum(threshold, np.abs(y))
|
||||
h = (y + factor * y_scale) - y
|
||||
|
||||
# Make sure that the step is not 0 to start with. Not likely it will be
|
||||
# executed often.
|
||||
for i in np.nonzero(h == 0)[0]:
|
||||
while h[i] == 0:
|
||||
factor[i] *= 10
|
||||
h[i] = (y[i] + factor[i] * y_scale[i]) - y[i]
|
||||
|
||||
if sparsity is None:
|
||||
return _dense_num_jac(fun, t, y, f, h, factor, y_scale)
|
||||
else:
|
||||
structure, groups = sparsity
|
||||
return _sparse_num_jac(fun, t, y, f, h, factor, y_scale,
|
||||
structure, groups)
|
||||
|
||||
|
||||
def _dense_num_jac(fun, t, y, f, h, factor, y_scale):
|
||||
n = y.shape[0]
|
||||
h_vecs = np.diag(h)
|
||||
f_new = fun(t, y[:, None] + h_vecs)
|
||||
diff = f_new - f[:, None]
|
||||
max_ind = np.argmax(np.abs(diff), axis=0)
|
||||
r = np.arange(n)
|
||||
max_diff = np.abs(diff[max_ind, r])
|
||||
scale = np.maximum(np.abs(f[max_ind]), np.abs(f_new[max_ind, r]))
|
||||
|
||||
diff_too_small = max_diff < NUM_JAC_DIFF_REJECT * scale
|
||||
if np.any(diff_too_small):
|
||||
ind, = np.nonzero(diff_too_small)
|
||||
new_factor = NUM_JAC_FACTOR_INCREASE * factor[ind]
|
||||
h_new = (y[ind] + new_factor * y_scale[ind]) - y[ind]
|
||||
h_vecs[ind, ind] = h_new
|
||||
f_new = fun(t, y[:, None] + h_vecs[:, ind])
|
||||
diff_new = f_new - f[:, None]
|
||||
max_ind = np.argmax(np.abs(diff_new), axis=0)
|
||||
r = np.arange(ind.shape[0])
|
||||
max_diff_new = np.abs(diff_new[max_ind, r])
|
||||
scale_new = np.maximum(np.abs(f[max_ind]), np.abs(f_new[max_ind, r]))
|
||||
|
||||
update = max_diff[ind] * scale_new < max_diff_new * scale[ind]
|
||||
if np.any(update):
|
||||
update, = np.nonzero(update)
|
||||
update_ind = ind[update]
|
||||
factor[update_ind] = new_factor[update]
|
||||
h[update_ind] = h_new[update]
|
||||
diff[:, update_ind] = diff_new[:, update]
|
||||
scale[update_ind] = scale_new[update]
|
||||
max_diff[update_ind] = max_diff_new[update]
|
||||
|
||||
diff /= h
|
||||
|
||||
factor[max_diff < NUM_JAC_DIFF_SMALL * scale] *= NUM_JAC_FACTOR_INCREASE
|
||||
factor[max_diff > NUM_JAC_DIFF_BIG * scale] *= NUM_JAC_FACTOR_DECREASE
|
||||
factor = np.maximum(factor, NUM_JAC_MIN_FACTOR)
|
||||
|
||||
return diff, factor
|
||||
|
||||
|
||||
def _sparse_num_jac(fun, t, y, f, h, factor, y_scale, structure, groups):
|
||||
n = y.shape[0]
|
||||
n_groups = np.max(groups) + 1
|
||||
h_vecs = np.empty((n_groups, n))
|
||||
for group in range(n_groups):
|
||||
e = np.equal(group, groups)
|
||||
h_vecs[group] = h * e
|
||||
h_vecs = h_vecs.T
|
||||
|
||||
f_new = fun(t, y[:, None] + h_vecs)
|
||||
df = f_new - f[:, None]
|
||||
|
||||
i, j, _ = find(structure)
|
||||
diff = coo_matrix((df[i, groups[j]], (i, j)), shape=(n, n)).tocsc()
|
||||
max_ind = np.array(abs(diff).argmax(axis=0)).ravel()
|
||||
r = np.arange(n)
|
||||
max_diff = np.asarray(np.abs(diff[max_ind, r])).ravel()
|
||||
scale = np.maximum(np.abs(f[max_ind]),
|
||||
np.abs(f_new[max_ind, groups[r]]))
|
||||
|
||||
diff_too_small = max_diff < NUM_JAC_DIFF_REJECT * scale
|
||||
if np.any(diff_too_small):
|
||||
ind, = np.nonzero(diff_too_small)
|
||||
new_factor = NUM_JAC_FACTOR_INCREASE * factor[ind]
|
||||
h_new = (y[ind] + new_factor * y_scale[ind]) - y[ind]
|
||||
h_new_all = np.zeros(n)
|
||||
h_new_all[ind] = h_new
|
||||
|
||||
groups_unique = np.unique(groups[ind])
|
||||
groups_map = np.empty(n_groups, dtype=int)
|
||||
h_vecs = np.empty((groups_unique.shape[0], n))
|
||||
for k, group in enumerate(groups_unique):
|
||||
e = np.equal(group, groups)
|
||||
h_vecs[k] = h_new_all * e
|
||||
groups_map[group] = k
|
||||
h_vecs = h_vecs.T
|
||||
|
||||
f_new = fun(t, y[:, None] + h_vecs)
|
||||
df = f_new - f[:, None]
|
||||
i, j, _ = find(structure[:, ind])
|
||||
diff_new = coo_matrix((df[i, groups_map[groups[ind[j]]]],
|
||||
(i, j)), shape=(n, ind.shape[0])).tocsc()
|
||||
|
||||
max_ind_new = np.array(abs(diff_new).argmax(axis=0)).ravel()
|
||||
r = np.arange(ind.shape[0])
|
||||
max_diff_new = np.asarray(np.abs(diff_new[max_ind_new, r])).ravel()
|
||||
scale_new = np.maximum(
|
||||
np.abs(f[max_ind_new]),
|
||||
np.abs(f_new[max_ind_new, groups_map[groups[ind]]]))
|
||||
|
||||
update = max_diff[ind] * scale_new < max_diff_new * scale[ind]
|
||||
if np.any(update):
|
||||
update, = np.nonzero(update)
|
||||
update_ind = ind[update]
|
||||
factor[update_ind] = new_factor[update]
|
||||
h[update_ind] = h_new[update]
|
||||
diff[:, update_ind] = diff_new[:, update]
|
||||
scale[update_ind] = scale_new[update]
|
||||
max_diff[update_ind] = max_diff_new[update]
|
||||
|
||||
diff.data /= np.repeat(h, np.diff(diff.indptr))
|
||||
|
||||
factor[max_diff < NUM_JAC_DIFF_SMALL * scale] *= NUM_JAC_FACTOR_INCREASE
|
||||
factor[max_diff > NUM_JAC_DIFF_BIG * scale] *= NUM_JAC_FACTOR_DECREASE
|
||||
factor = np.maximum(factor, NUM_JAC_MIN_FACTOR)
|
||||
|
||||
return diff, factor
|
||||
@ -0,0 +1,193 @@
|
||||
import numpy as np
|
||||
|
||||
N_STAGES = 12
|
||||
N_STAGES_EXTENDED = 16
|
||||
INTERPOLATOR_POWER = 7
|
||||
|
||||
C = np.array([0.0,
|
||||
0.526001519587677318785587544488e-01,
|
||||
0.789002279381515978178381316732e-01,
|
||||
0.118350341907227396726757197510,
|
||||
0.281649658092772603273242802490,
|
||||
0.333333333333333333333333333333,
|
||||
0.25,
|
||||
0.307692307692307692307692307692,
|
||||
0.651282051282051282051282051282,
|
||||
0.6,
|
||||
0.857142857142857142857142857142,
|
||||
1.0,
|
||||
1.0,
|
||||
0.1,
|
||||
0.2,
|
||||
0.777777777777777777777777777778])
|
||||
|
||||
A = np.zeros((N_STAGES_EXTENDED, N_STAGES_EXTENDED))
|
||||
A[1, 0] = 5.26001519587677318785587544488e-2
|
||||
|
||||
A[2, 0] = 1.97250569845378994544595329183e-2
|
||||
A[2, 1] = 5.91751709536136983633785987549e-2
|
||||
|
||||
A[3, 0] = 2.95875854768068491816892993775e-2
|
||||
A[3, 2] = 8.87627564304205475450678981324e-2
|
||||
|
||||
A[4, 0] = 2.41365134159266685502369798665e-1
|
||||
A[4, 2] = -8.84549479328286085344864962717e-1
|
||||
A[4, 3] = 9.24834003261792003115737966543e-1
|
||||
|
||||
A[5, 0] = 3.7037037037037037037037037037e-2
|
||||
A[5, 3] = 1.70828608729473871279604482173e-1
|
||||
A[5, 4] = 1.25467687566822425016691814123e-1
|
||||
|
||||
A[6, 0] = 3.7109375e-2
|
||||
A[6, 3] = 1.70252211019544039314978060272e-1
|
||||
A[6, 4] = 6.02165389804559606850219397283e-2
|
||||
A[6, 5] = -1.7578125e-2
|
||||
|
||||
A[7, 0] = 3.70920001185047927108779319836e-2
|
||||
A[7, 3] = 1.70383925712239993810214054705e-1
|
||||
A[7, 4] = 1.07262030446373284651809199168e-1
|
||||
A[7, 5] = -1.53194377486244017527936158236e-2
|
||||
A[7, 6] = 8.27378916381402288758473766002e-3
|
||||
|
||||
A[8, 0] = 6.24110958716075717114429577812e-1
|
||||
A[8, 3] = -3.36089262944694129406857109825
|
||||
A[8, 4] = -8.68219346841726006818189891453e-1
|
||||
A[8, 5] = 2.75920996994467083049415600797e1
|
||||
A[8, 6] = 2.01540675504778934086186788979e1
|
||||
A[8, 7] = -4.34898841810699588477366255144e1
|
||||
|
||||
A[9, 0] = 4.77662536438264365890433908527e-1
|
||||
A[9, 3] = -2.48811461997166764192642586468
|
||||
A[9, 4] = -5.90290826836842996371446475743e-1
|
||||
A[9, 5] = 2.12300514481811942347288949897e1
|
||||
A[9, 6] = 1.52792336328824235832596922938e1
|
||||
A[9, 7] = -3.32882109689848629194453265587e1
|
||||
A[9, 8] = -2.03312017085086261358222928593e-2
|
||||
|
||||
A[10, 0] = -9.3714243008598732571704021658e-1
|
||||
A[10, 3] = 5.18637242884406370830023853209
|
||||
A[10, 4] = 1.09143734899672957818500254654
|
||||
A[10, 5] = -8.14978701074692612513997267357
|
||||
A[10, 6] = -1.85200656599969598641566180701e1
|
||||
A[10, 7] = 2.27394870993505042818970056734e1
|
||||
A[10, 8] = 2.49360555267965238987089396762
|
||||
A[10, 9] = -3.0467644718982195003823669022
|
||||
|
||||
A[11, 0] = 2.27331014751653820792359768449
|
||||
A[11, 3] = -1.05344954667372501984066689879e1
|
||||
A[11, 4] = -2.00087205822486249909675718444
|
||||
A[11, 5] = -1.79589318631187989172765950534e1
|
||||
A[11, 6] = 2.79488845294199600508499808837e1
|
||||
A[11, 7] = -2.85899827713502369474065508674
|
||||
A[11, 8] = -8.87285693353062954433549289258
|
||||
A[11, 9] = 1.23605671757943030647266201528e1
|
||||
A[11, 10] = 6.43392746015763530355970484046e-1
|
||||
|
||||
A[12, 0] = 5.42937341165687622380535766363e-2
|
||||
A[12, 5] = 4.45031289275240888144113950566
|
||||
A[12, 6] = 1.89151789931450038304281599044
|
||||
A[12, 7] = -5.8012039600105847814672114227
|
||||
A[12, 8] = 3.1116436695781989440891606237e-1
|
||||
A[12, 9] = -1.52160949662516078556178806805e-1
|
||||
A[12, 10] = 2.01365400804030348374776537501e-1
|
||||
A[12, 11] = 4.47106157277725905176885569043e-2
|
||||
|
||||
A[13, 0] = 5.61675022830479523392909219681e-2
|
||||
A[13, 6] = 2.53500210216624811088794765333e-1
|
||||
A[13, 7] = -2.46239037470802489917441475441e-1
|
||||
A[13, 8] = -1.24191423263816360469010140626e-1
|
||||
A[13, 9] = 1.5329179827876569731206322685e-1
|
||||
A[13, 10] = 8.20105229563468988491666602057e-3
|
||||
A[13, 11] = 7.56789766054569976138603589584e-3
|
||||
A[13, 12] = -8.298e-3
|
||||
|
||||
A[14, 0] = 3.18346481635021405060768473261e-2
|
||||
A[14, 5] = 2.83009096723667755288322961402e-2
|
||||
A[14, 6] = 5.35419883074385676223797384372e-2
|
||||
A[14, 7] = -5.49237485713909884646569340306e-2
|
||||
A[14, 10] = -1.08347328697249322858509316994e-4
|
||||
A[14, 11] = 3.82571090835658412954920192323e-4
|
||||
A[14, 12] = -3.40465008687404560802977114492e-4
|
||||
A[14, 13] = 1.41312443674632500278074618366e-1
|
||||
|
||||
A[15, 0] = -4.28896301583791923408573538692e-1
|
||||
A[15, 5] = -4.69762141536116384314449447206
|
||||
A[15, 6] = 7.68342119606259904184240953878
|
||||
A[15, 7] = 4.06898981839711007970213554331
|
||||
A[15, 8] = 3.56727187455281109270669543021e-1
|
||||
A[15, 12] = -1.39902416515901462129418009734e-3
|
||||
A[15, 13] = 2.9475147891527723389556272149
|
||||
A[15, 14] = -9.15095847217987001081870187138
|
||||
|
||||
|
||||
B = A[N_STAGES, :N_STAGES]
|
||||
|
||||
E3 = np.zeros(N_STAGES + 1)
|
||||
E3[:-1] = B.copy()
|
||||
E3[0] -= 0.244094488188976377952755905512
|
||||
E3[8] -= 0.733846688281611857341361741547
|
||||
E3[11] -= 0.220588235294117647058823529412e-1
|
||||
|
||||
E5 = np.zeros(N_STAGES + 1)
|
||||
E5[0] = 0.1312004499419488073250102996e-1
|
||||
E5[5] = -0.1225156446376204440720569753e+1
|
||||
E5[6] = -0.4957589496572501915214079952
|
||||
E5[7] = 0.1664377182454986536961530415e+1
|
||||
E5[8] = -0.3503288487499736816886487290
|
||||
E5[9] = 0.3341791187130174790297318841
|
||||
E5[10] = 0.8192320648511571246570742613e-1
|
||||
E5[11] = -0.2235530786388629525884427845e-1
|
||||
|
||||
# First 3 coefficients are computed separately.
|
||||
D = np.zeros((INTERPOLATOR_POWER - 3, N_STAGES_EXTENDED))
|
||||
D[0, 0] = -0.84289382761090128651353491142e+1
|
||||
D[0, 5] = 0.56671495351937776962531783590
|
||||
D[0, 6] = -0.30689499459498916912797304727e+1
|
||||
D[0, 7] = 0.23846676565120698287728149680e+1
|
||||
D[0, 8] = 0.21170345824450282767155149946e+1
|
||||
D[0, 9] = -0.87139158377797299206789907490
|
||||
D[0, 10] = 0.22404374302607882758541771650e+1
|
||||
D[0, 11] = 0.63157877876946881815570249290
|
||||
D[0, 12] = -0.88990336451333310820698117400e-1
|
||||
D[0, 13] = 0.18148505520854727256656404962e+2
|
||||
D[0, 14] = -0.91946323924783554000451984436e+1
|
||||
D[0, 15] = -0.44360363875948939664310572000e+1
|
||||
|
||||
D[1, 0] = 0.10427508642579134603413151009e+2
|
||||
D[1, 5] = 0.24228349177525818288430175319e+3
|
||||
D[1, 6] = 0.16520045171727028198505394887e+3
|
||||
D[1, 7] = -0.37454675472269020279518312152e+3
|
||||
D[1, 8] = -0.22113666853125306036270938578e+2
|
||||
D[1, 9] = 0.77334326684722638389603898808e+1
|
||||
D[1, 10] = -0.30674084731089398182061213626e+2
|
||||
D[1, 11] = -0.93321305264302278729567221706e+1
|
||||
D[1, 12] = 0.15697238121770843886131091075e+2
|
||||
D[1, 13] = -0.31139403219565177677282850411e+2
|
||||
D[1, 14] = -0.93529243588444783865713862664e+1
|
||||
D[1, 15] = 0.35816841486394083752465898540e+2
|
||||
|
||||
D[2, 0] = 0.19985053242002433820987653617e+2
|
||||
D[2, 5] = -0.38703730874935176555105901742e+3
|
||||
D[2, 6] = -0.18917813819516756882830838328e+3
|
||||
D[2, 7] = 0.52780815920542364900561016686e+3
|
||||
D[2, 8] = -0.11573902539959630126141871134e+2
|
||||
D[2, 9] = 0.68812326946963000169666922661e+1
|
||||
D[2, 10] = -0.10006050966910838403183860980e+1
|
||||
D[2, 11] = 0.77771377980534432092869265740
|
||||
D[2, 12] = -0.27782057523535084065932004339e+1
|
||||
D[2, 13] = -0.60196695231264120758267380846e+2
|
||||
D[2, 14] = 0.84320405506677161018159903784e+2
|
||||
D[2, 15] = 0.11992291136182789328035130030e+2
|
||||
|
||||
D[3, 0] = -0.25693933462703749003312586129e+2
|
||||
D[3, 5] = -0.15418974869023643374053993627e+3
|
||||
D[3, 6] = -0.23152937917604549567536039109e+3
|
||||
D[3, 7] = 0.35763911791061412378285349910e+3
|
||||
D[3, 8] = 0.93405324183624310003907691704e+2
|
||||
D[3, 9] = -0.37458323136451633156875139351e+2
|
||||
D[3, 10] = 0.10409964950896230045147246184e+3
|
||||
D[3, 11] = 0.29840293426660503123344363579e+2
|
||||
D[3, 12] = -0.43533456590011143754432175058e+2
|
||||
D[3, 13] = 0.96324553959188282948394950600e+2
|
||||
D[3, 14] = -0.39177261675615439165231486172e+2
|
||||
D[3, 15] = -0.14972683625798562581422125276e+3
|
||||
748
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/ivp.py
Normal file
748
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/ivp.py
Normal file
@ -0,0 +1,748 @@
|
||||
import inspect
|
||||
import numpy as np
|
||||
from .bdf import BDF
|
||||
from .radau import Radau
|
||||
from .rk import RK23, RK45, DOP853
|
||||
from .lsoda import LSODA
|
||||
from scipy.optimize import OptimizeResult
|
||||
from .common import EPS, OdeSolution
|
||||
from .base import OdeSolver
|
||||
|
||||
|
||||
METHODS = {'RK23': RK23,
|
||||
'RK45': RK45,
|
||||
'DOP853': DOP853,
|
||||
'Radau': Radau,
|
||||
'BDF': BDF,
|
||||
'LSODA': LSODA}
|
||||
|
||||
|
||||
MESSAGES = {0: "The solver successfully reached the end of the integration interval.",
|
||||
1: "A termination event occurred."}
|
||||
|
||||
|
||||
class OdeResult(OptimizeResult):
|
||||
pass
|
||||
|
||||
|
||||
def prepare_events(events):
|
||||
"""Standardize event functions and extract attributes."""
|
||||
if callable(events):
|
||||
events = (events,)
|
||||
|
||||
max_events = np.empty(len(events))
|
||||
direction = np.empty(len(events))
|
||||
for i, event in enumerate(events):
|
||||
terminal = getattr(event, 'terminal', None)
|
||||
direction[i] = getattr(event, 'direction', 0)
|
||||
|
||||
message = ('The `terminal` attribute of each event '
|
||||
'must be a boolean or positive integer.')
|
||||
if terminal is None or terminal == 0:
|
||||
max_events[i] = np.inf
|
||||
elif int(terminal) == terminal and terminal > 0:
|
||||
max_events[i] = terminal
|
||||
else:
|
||||
raise ValueError(message)
|
||||
|
||||
return events, max_events, direction
|
||||
|
||||
|
||||
def solve_event_equation(event, sol, t_old, t):
|
||||
"""Solve an equation corresponding to an ODE event.
|
||||
|
||||
The equation is ``event(t, y(t)) = 0``, here ``y(t)`` is known from an
|
||||
ODE solver using some sort of interpolation. It is solved by
|
||||
`scipy.optimize.brentq` with xtol=atol=4*EPS.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : callable
|
||||
Function ``event(t, y)``.
|
||||
sol : callable
|
||||
Function ``sol(t)`` which evaluates an ODE solution between `t_old`
|
||||
and `t`.
|
||||
t_old, t : float
|
||||
Previous and new values of time. They will be used as a bracketing
|
||||
interval.
|
||||
|
||||
Returns
|
||||
-------
|
||||
root : float
|
||||
Found solution.
|
||||
"""
|
||||
from scipy.optimize import brentq
|
||||
return brentq(lambda t: event(t, sol(t)), t_old, t,
|
||||
xtol=4 * EPS, rtol=4 * EPS)
|
||||
|
||||
|
||||
def handle_events(sol, events, active_events, event_count, max_events,
|
||||
t_old, t):
|
||||
"""Helper function to handle events.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sol : DenseOutput
|
||||
Function ``sol(t)`` which evaluates an ODE solution between `t_old`
|
||||
and `t`.
|
||||
events : list of callables, length n_events
|
||||
Event functions with signatures ``event(t, y)``.
|
||||
active_events : ndarray
|
||||
Indices of events which occurred.
|
||||
event_count : ndarray
|
||||
Current number of occurrences for each event.
|
||||
max_events : ndarray, shape (n_events,)
|
||||
Number of occurrences allowed for each event before integration
|
||||
termination is issued.
|
||||
t_old, t : float
|
||||
Previous and new values of time.
|
||||
|
||||
Returns
|
||||
-------
|
||||
root_indices : ndarray
|
||||
Indices of events which take zero between `t_old` and `t` and before
|
||||
a possible termination.
|
||||
roots : ndarray
|
||||
Values of t at which events occurred.
|
||||
terminate : bool
|
||||
Whether a terminal event occurred.
|
||||
"""
|
||||
roots = [solve_event_equation(events[event_index], sol, t_old, t)
|
||||
for event_index in active_events]
|
||||
|
||||
roots = np.asarray(roots)
|
||||
|
||||
if np.any(event_count[active_events] >= max_events[active_events]):
|
||||
if t > t_old:
|
||||
order = np.argsort(roots)
|
||||
else:
|
||||
order = np.argsort(-roots)
|
||||
active_events = active_events[order]
|
||||
roots = roots[order]
|
||||
t = np.nonzero(event_count[active_events]
|
||||
>= max_events[active_events])[0][0]
|
||||
active_events = active_events[:t + 1]
|
||||
roots = roots[:t + 1]
|
||||
terminate = True
|
||||
else:
|
||||
terminate = False
|
||||
|
||||
return active_events, roots, terminate
|
||||
|
||||
|
||||
def find_active_events(g, g_new, direction):
|
||||
"""Find which event occurred during an integration step.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
g, g_new : array_like, shape (n_events,)
|
||||
Values of event functions at a current and next points.
|
||||
direction : ndarray, shape (n_events,)
|
||||
Event "direction" according to the definition in `solve_ivp`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
active_events : ndarray
|
||||
Indices of events which occurred during the step.
|
||||
"""
|
||||
g, g_new = np.asarray(g), np.asarray(g_new)
|
||||
up = (g <= 0) & (g_new >= 0)
|
||||
down = (g >= 0) & (g_new <= 0)
|
||||
either = up | down
|
||||
mask = (up & (direction > 0) |
|
||||
down & (direction < 0) |
|
||||
either & (direction == 0))
|
||||
|
||||
return np.nonzero(mask)[0]
|
||||
|
||||
|
||||
def solve_ivp(fun, t_span, y0, method='RK45', t_eval=None, dense_output=False,
|
||||
events=None, vectorized=False, args=None, **options):
|
||||
"""Solve an initial value problem for a system of ODEs.
|
||||
|
||||
This function numerically integrates a system of ordinary differential
|
||||
equations given an initial value::
|
||||
|
||||
dy / dt = f(t, y)
|
||||
y(t0) = y0
|
||||
|
||||
Here t is a 1-D independent variable (time), y(t) is an
|
||||
N-D vector-valued function (state), and an N-D
|
||||
vector-valued function f(t, y) determines the differential equations.
|
||||
The goal is to find y(t) approximately satisfying the differential
|
||||
equations, given an initial value y(t0)=y0.
|
||||
|
||||
Some of the solvers support integration in the complex domain, but note
|
||||
that for stiff ODE solvers, the right-hand side must be
|
||||
complex-differentiable (satisfy Cauchy-Riemann equations [11]_).
|
||||
To solve a problem in the complex domain, pass y0 with a complex data type.
|
||||
Another option always available is to rewrite your problem for real and
|
||||
imaginary parts separately.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system: the time derivative of the state ``y``
|
||||
at time ``t``. The calling signature is ``fun(t, y)``, where ``t`` is a
|
||||
scalar and ``y`` is an ndarray with ``len(y) = len(y0)``. Additional
|
||||
arguments need to be passed if ``args`` is used (see documentation of
|
||||
``args`` argument). ``fun`` must return an array of the same shape as
|
||||
``y``. See `vectorized` for more information.
|
||||
t_span : 2-member sequence
|
||||
Interval of integration (t0, tf). The solver starts with t=t0 and
|
||||
integrates until it reaches t=tf. Both t0 and tf must be floats
|
||||
or values interpretable by the float conversion function.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state. For problems in the complex domain, pass `y0` with a
|
||||
complex data type (even if the initial value is purely real).
|
||||
method : string or `OdeSolver`, optional
|
||||
Integration method to use:
|
||||
|
||||
* 'RK45' (default): Explicit Runge-Kutta method of order 5(4) [1]_.
|
||||
The error is controlled assuming accuracy of the fourth-order
|
||||
method, but steps are taken using the fifth-order accurate
|
||||
formula (local extrapolation is done). A quartic interpolation
|
||||
polynomial is used for the dense output [2]_. Can be applied in
|
||||
the complex domain.
|
||||
* 'RK23': Explicit Runge-Kutta method of order 3(2) [3]_. The error
|
||||
is controlled assuming accuracy of the second-order method, but
|
||||
steps are taken using the third-order accurate formula (local
|
||||
extrapolation is done). A cubic Hermite polynomial is used for the
|
||||
dense output. Can be applied in the complex domain.
|
||||
* 'DOP853': Explicit Runge-Kutta method of order 8 [13]_.
|
||||
Python implementation of the "DOP853" algorithm originally
|
||||
written in Fortran [14]_. A 7-th order interpolation polynomial
|
||||
accurate to 7-th order is used for the dense output.
|
||||
Can be applied in the complex domain.
|
||||
* 'Radau': Implicit Runge-Kutta method of the Radau IIA family of
|
||||
order 5 [4]_. The error is controlled with a third-order accurate
|
||||
embedded formula. A cubic polynomial which satisfies the
|
||||
collocation conditions is used for the dense output.
|
||||
* 'BDF': Implicit multi-step variable-order (1 to 5) method based
|
||||
on a backward differentiation formula for the derivative
|
||||
approximation [5]_. The implementation follows the one described
|
||||
in [6]_. A quasi-constant step scheme is used and accuracy is
|
||||
enhanced using the NDF modification. Can be applied in the
|
||||
complex domain.
|
||||
* 'LSODA': Adams/BDF method with automatic stiffness detection and
|
||||
switching [7]_, [8]_. This is a wrapper of the Fortran solver
|
||||
from ODEPACK.
|
||||
|
||||
Explicit Runge-Kutta methods ('RK23', 'RK45', 'DOP853') should be used
|
||||
for non-stiff problems and implicit methods ('Radau', 'BDF') for
|
||||
stiff problems [9]_. Among Runge-Kutta methods, 'DOP853' is recommended
|
||||
for solving with high precision (low values of `rtol` and `atol`).
|
||||
|
||||
If not sure, first try to run 'RK45'. If it makes unusually many
|
||||
iterations, diverges, or fails, your problem is likely to be stiff and
|
||||
you should use 'Radau' or 'BDF'. 'LSODA' can also be a good universal
|
||||
choice, but it might be somewhat less convenient to work with as it
|
||||
wraps old Fortran code.
|
||||
|
||||
You can also pass an arbitrary class derived from `OdeSolver` which
|
||||
implements the solver.
|
||||
t_eval : array_like or None, optional
|
||||
Times at which to store the computed solution, must be sorted and lie
|
||||
within `t_span`. If None (default), use points selected by the solver.
|
||||
dense_output : bool, optional
|
||||
Whether to compute a continuous solution. Default is False.
|
||||
events : callable, or list of callables, optional
|
||||
Events to track. If None (default), no events will be tracked.
|
||||
Each event occurs at the zeros of a continuous function of time and
|
||||
state. Each function must have the signature ``event(t, y)`` where
|
||||
additional argument have to be passed if ``args`` is used (see
|
||||
documentation of ``args`` argument). Each function must return a
|
||||
float. The solver will find an accurate value of `t` at which
|
||||
``event(t, y(t)) = 0`` using a root-finding algorithm. By default,
|
||||
all zeros will be found. The solver looks for a sign change over
|
||||
each step, so if multiple zero crossings occur within one step,
|
||||
events may be missed. Additionally each `event` function might
|
||||
have the following attributes:
|
||||
|
||||
terminal: bool or int, optional
|
||||
When boolean, whether to terminate integration if this event occurs.
|
||||
When integral, termination occurs after the specified the number of
|
||||
occurences of this event.
|
||||
Implicitly False if not assigned.
|
||||
direction: float, optional
|
||||
Direction of a zero crossing. If `direction` is positive,
|
||||
`event` will only trigger when going from negative to positive,
|
||||
and vice versa if `direction` is negative. If 0, then either
|
||||
direction will trigger event. Implicitly 0 if not assigned.
|
||||
|
||||
You can assign attributes like ``event.terminal = True`` to any
|
||||
function in Python.
|
||||
vectorized : bool, optional
|
||||
Whether `fun` can be called in a vectorized fashion. Default is False.
|
||||
|
||||
If ``vectorized`` is False, `fun` will always be called with ``y`` of
|
||||
shape ``(n,)``, where ``n = len(y0)``.
|
||||
|
||||
If ``vectorized`` is True, `fun` may be called with ``y`` of shape
|
||||
``(n, k)``, where ``k`` is an integer. In this case, `fun` must behave
|
||||
such that ``fun(t, y)[:, i] == fun(t, y[:, i])`` (i.e. each column of
|
||||
the returned array is the time derivative of the state corresponding
|
||||
with a column of ``y``).
|
||||
|
||||
Setting ``vectorized=True`` allows for faster finite difference
|
||||
approximation of the Jacobian by methods 'Radau' and 'BDF', but
|
||||
will result in slower execution for other methods and for 'Radau' and
|
||||
'BDF' in some circumstances (e.g. small ``len(y0)``).
|
||||
args : tuple, optional
|
||||
Additional arguments to pass to the user-defined functions. If given,
|
||||
the additional arguments are passed to all user-defined functions.
|
||||
So if, for example, `fun` has the signature ``fun(t, y, a, b, c)``,
|
||||
then `jac` (if given) and any event functions must have the same
|
||||
signature, and `args` must be a tuple of length 3.
|
||||
**options
|
||||
Options passed to a chosen solver. All options available for already
|
||||
implemented solvers are listed below.
|
||||
first_step : float or None, optional
|
||||
Initial step size. Default is `None` which means that the algorithm
|
||||
should choose.
|
||||
max_step : float, optional
|
||||
Maximum allowed step size. Default is np.inf, i.e., the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
rtol, atol : float or array_like, optional
|
||||
Relative and absolute tolerances. The solver keeps the local error
|
||||
estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a
|
||||
relative accuracy (number of correct digits), while `atol` controls
|
||||
absolute accuracy (number of correct decimal places). To achieve the
|
||||
desired `rtol`, set `atol` to be smaller than the smallest value that
|
||||
can be expected from ``rtol * abs(y)`` so that `rtol` dominates the
|
||||
allowable error. If `atol` is larger than ``rtol * abs(y)`` the
|
||||
number of correct digits is not guaranteed. Conversely, to achieve the
|
||||
desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller
|
||||
than `atol`. If components of y have different scales, it might be
|
||||
beneficial to set different `atol` values for different components by
|
||||
passing array_like with shape (n,) for `atol`. Default values are
|
||||
1e-3 for `rtol` and 1e-6 for `atol`.
|
||||
jac : array_like, sparse_matrix, callable or None, optional
|
||||
Jacobian matrix of the right-hand side of the system with respect
|
||||
to y, required by the 'Radau', 'BDF' and 'LSODA' method. The
|
||||
Jacobian matrix has shape (n, n) and its element (i, j) is equal to
|
||||
``d f_i / d y_j``. There are three ways to define the Jacobian:
|
||||
|
||||
* If array_like or sparse_matrix, the Jacobian is assumed to
|
||||
be constant. Not supported by 'LSODA'.
|
||||
* If callable, the Jacobian is assumed to depend on both
|
||||
t and y; it will be called as ``jac(t, y)``, as necessary.
|
||||
Additional arguments have to be passed if ``args`` is
|
||||
used (see documentation of ``args`` argument).
|
||||
For 'Radau' and 'BDF' methods, the return value might be a
|
||||
sparse matrix.
|
||||
* If None (default), the Jacobian will be approximated by
|
||||
finite differences.
|
||||
|
||||
It is generally recommended to provide the Jacobian rather than
|
||||
relying on a finite-difference approximation.
|
||||
jac_sparsity : array_like, sparse matrix or None, optional
|
||||
Defines a sparsity structure of the Jacobian matrix for a finite-
|
||||
difference approximation. Its shape must be (n, n). This argument
|
||||
is ignored if `jac` is not `None`. If the Jacobian has only few
|
||||
non-zero elements in *each* row, providing the sparsity structure
|
||||
will greatly speed up the computations [10]_. A zero entry means that
|
||||
a corresponding element in the Jacobian is always zero. If None
|
||||
(default), the Jacobian is assumed to be dense.
|
||||
Not supported by 'LSODA', see `lband` and `uband` instead.
|
||||
lband, uband : int or None, optional
|
||||
Parameters defining the bandwidth of the Jacobian for the 'LSODA'
|
||||
method, i.e., ``jac[i, j] != 0 only for i - lband <= j <= i + uband``.
|
||||
Default is None. Setting these requires your jac routine to return the
|
||||
Jacobian in the packed format: the returned array must have ``n``
|
||||
columns and ``uband + lband + 1`` rows in which Jacobian diagonals are
|
||||
written. Specifically ``jac_packed[uband + i - j , j] = jac[i, j]``.
|
||||
The same format is used in `scipy.linalg.solve_banded` (check for an
|
||||
illustration). These parameters can be also used with ``jac=None`` to
|
||||
reduce the number of Jacobian elements estimated by finite differences.
|
||||
min_step : float, optional
|
||||
The minimum allowed step size for 'LSODA' method.
|
||||
By default `min_step` is zero.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Bunch object with the following fields defined:
|
||||
t : ndarray, shape (n_points,)
|
||||
Time points.
|
||||
y : ndarray, shape (n, n_points)
|
||||
Values of the solution at `t`.
|
||||
sol : `OdeSolution` or None
|
||||
Found solution as `OdeSolution` instance; None if `dense_output` was
|
||||
set to False.
|
||||
t_events : list of ndarray or None
|
||||
Contains for each event type a list of arrays at which an event of
|
||||
that type event was detected. None if `events` was None.
|
||||
y_events : list of ndarray or None
|
||||
For each value of `t_events`, the corresponding value of the solution.
|
||||
None if `events` was None.
|
||||
nfev : int
|
||||
Number of evaluations of the right-hand side.
|
||||
njev : int
|
||||
Number of evaluations of the Jacobian.
|
||||
nlu : int
|
||||
Number of LU decompositions.
|
||||
status : int
|
||||
Reason for algorithm termination:
|
||||
|
||||
* -1: Integration step failed.
|
||||
* 0: The solver successfully reached the end of `tspan`.
|
||||
* 1: A termination event occurred.
|
||||
|
||||
message : string
|
||||
Human-readable description of the termination reason.
|
||||
success : bool
|
||||
True if the solver reached the interval end or a termination event
|
||||
occurred (``status >= 0``).
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] J. R. Dormand, P. J. Prince, "A family of embedded Runge-Kutta
|
||||
formulae", Journal of Computational and Applied Mathematics, Vol. 6,
|
||||
No. 1, pp. 19-26, 1980.
|
||||
.. [2] L. W. Shampine, "Some Practical Runge-Kutta Formulas", Mathematics
|
||||
of Computation,, Vol. 46, No. 173, pp. 135-150, 1986.
|
||||
.. [3] P. Bogacki, L.F. Shampine, "A 3(2) Pair of Runge-Kutta Formulas",
|
||||
Appl. Math. Lett. Vol. 2, No. 4. pp. 321-325, 1989.
|
||||
.. [4] E. Hairer, G. Wanner, "Solving Ordinary Differential Equations II:
|
||||
Stiff and Differential-Algebraic Problems", Sec. IV.8.
|
||||
.. [5] `Backward Differentiation Formula
|
||||
<https://en.wikipedia.org/wiki/Backward_differentiation_formula>`_
|
||||
on Wikipedia.
|
||||
.. [6] L. F. Shampine, M. W. Reichelt, "THE MATLAB ODE SUITE", SIAM J. SCI.
|
||||
COMPUTE., Vol. 18, No. 1, pp. 1-22, January 1997.
|
||||
.. [7] A. C. Hindmarsh, "ODEPACK, A Systematized Collection of ODE
|
||||
Solvers," IMACS Transactions on Scientific Computation, Vol 1.,
|
||||
pp. 55-64, 1983.
|
||||
.. [8] L. Petzold, "Automatic selection of methods for solving stiff and
|
||||
nonstiff systems of ordinary differential equations", SIAM Journal
|
||||
on Scientific and Statistical Computing, Vol. 4, No. 1, pp. 136-148,
|
||||
1983.
|
||||
.. [9] `Stiff equation <https://en.wikipedia.org/wiki/Stiff_equation>`_ on
|
||||
Wikipedia.
|
||||
.. [10] A. Curtis, M. J. D. Powell, and J. Reid, "On the estimation of
|
||||
sparse Jacobian matrices", Journal of the Institute of Mathematics
|
||||
and its Applications, 13, pp. 117-120, 1974.
|
||||
.. [11] `Cauchy-Riemann equations
|
||||
<https://en.wikipedia.org/wiki/Cauchy-Riemann_equations>`_ on
|
||||
Wikipedia.
|
||||
.. [12] `Lotka-Volterra equations
|
||||
<https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations>`_
|
||||
on Wikipedia.
|
||||
.. [13] E. Hairer, S. P. Norsett G. Wanner, "Solving Ordinary Differential
|
||||
Equations I: Nonstiff Problems", Sec. II.
|
||||
.. [14] `Page with original Fortran code of DOP853
|
||||
<http://www.unige.ch/~hairer/software.html>`_.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Basic exponential decay showing automatically chosen time points.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate import solve_ivp
|
||||
>>> def exponential_decay(t, y): return -0.5 * y
|
||||
>>> sol = solve_ivp(exponential_decay, [0, 10], [2, 4, 8])
|
||||
>>> print(sol.t)
|
||||
[ 0. 0.11487653 1.26364188 3.06061781 4.81611105 6.57445806
|
||||
8.33328988 10. ]
|
||||
>>> print(sol.y)
|
||||
[[2. 1.88836035 1.06327177 0.43319312 0.18017253 0.07483045
|
||||
0.03107158 0.01350781]
|
||||
[4. 3.7767207 2.12654355 0.86638624 0.36034507 0.14966091
|
||||
0.06214316 0.02701561]
|
||||
[8. 7.5534414 4.25308709 1.73277247 0.72069014 0.29932181
|
||||
0.12428631 0.05403123]]
|
||||
|
||||
Specifying points where the solution is desired.
|
||||
|
||||
>>> sol = solve_ivp(exponential_decay, [0, 10], [2, 4, 8],
|
||||
... t_eval=[0, 1, 2, 4, 10])
|
||||
>>> print(sol.t)
|
||||
[ 0 1 2 4 10]
|
||||
>>> print(sol.y)
|
||||
[[2. 1.21305369 0.73534021 0.27066736 0.01350938]
|
||||
[4. 2.42610739 1.47068043 0.54133472 0.02701876]
|
||||
[8. 4.85221478 2.94136085 1.08266944 0.05403753]]
|
||||
|
||||
Cannon fired upward with terminal event upon impact. The ``terminal`` and
|
||||
``direction`` fields of an event are applied by monkey patching a function.
|
||||
Here ``y[0]`` is position and ``y[1]`` is velocity. The projectile starts
|
||||
at position 0 with velocity +10. Note that the integration never reaches
|
||||
t=100 because the event is terminal.
|
||||
|
||||
>>> def upward_cannon(t, y): return [y[1], -0.5]
|
||||
>>> def hit_ground(t, y): return y[0]
|
||||
>>> hit_ground.terminal = True
|
||||
>>> hit_ground.direction = -1
|
||||
>>> sol = solve_ivp(upward_cannon, [0, 100], [0, 10], events=hit_ground)
|
||||
>>> print(sol.t_events)
|
||||
[array([40.])]
|
||||
>>> print(sol.t)
|
||||
[0.00000000e+00 9.99900010e-05 1.09989001e-03 1.10988901e-02
|
||||
1.11088891e-01 1.11098890e+00 1.11099890e+01 4.00000000e+01]
|
||||
|
||||
Use `dense_output` and `events` to find position, which is 100, at the apex
|
||||
of the cannonball's trajectory. Apex is not defined as terminal, so both
|
||||
apex and hit_ground are found. There is no information at t=20, so the sol
|
||||
attribute is used to evaluate the solution. The sol attribute is returned
|
||||
by setting ``dense_output=True``. Alternatively, the `y_events` attribute
|
||||
can be used to access the solution at the time of the event.
|
||||
|
||||
>>> def apex(t, y): return y[1]
|
||||
>>> sol = solve_ivp(upward_cannon, [0, 100], [0, 10],
|
||||
... events=(hit_ground, apex), dense_output=True)
|
||||
>>> print(sol.t_events)
|
||||
[array([40.]), array([20.])]
|
||||
>>> print(sol.t)
|
||||
[0.00000000e+00 9.99900010e-05 1.09989001e-03 1.10988901e-02
|
||||
1.11088891e-01 1.11098890e+00 1.11099890e+01 4.00000000e+01]
|
||||
>>> print(sol.sol(sol.t_events[1][0]))
|
||||
[100. 0.]
|
||||
>>> print(sol.y_events)
|
||||
[array([[-5.68434189e-14, -1.00000000e+01]]),
|
||||
array([[1.00000000e+02, 1.77635684e-15]])]
|
||||
|
||||
As an example of a system with additional parameters, we'll implement
|
||||
the Lotka-Volterra equations [12]_.
|
||||
|
||||
>>> def lotkavolterra(t, z, a, b, c, d):
|
||||
... x, y = z
|
||||
... return [a*x - b*x*y, -c*y + d*x*y]
|
||||
...
|
||||
|
||||
We pass in the parameter values a=1.5, b=1, c=3 and d=1 with the `args`
|
||||
argument.
|
||||
|
||||
>>> sol = solve_ivp(lotkavolterra, [0, 15], [10, 5], args=(1.5, 1, 3, 1),
|
||||
... dense_output=True)
|
||||
|
||||
Compute a dense solution and plot it.
|
||||
|
||||
>>> t = np.linspace(0, 15, 300)
|
||||
>>> z = sol.sol(t)
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> plt.plot(t, z.T)
|
||||
>>> plt.xlabel('t')
|
||||
>>> plt.legend(['x', 'y'], shadow=True)
|
||||
>>> plt.title('Lotka-Volterra System')
|
||||
>>> plt.show()
|
||||
|
||||
A couple examples of using solve_ivp to solve the differential
|
||||
equation ``y' = Ay`` with complex matrix ``A``.
|
||||
|
||||
>>> A = np.array([[-0.25 + 0.14j, 0, 0.33 + 0.44j],
|
||||
... [0.25 + 0.58j, -0.2 + 0.14j, 0],
|
||||
... [0, 0.2 + 0.4j, -0.1 + 0.97j]])
|
||||
|
||||
Solving an IVP with ``A`` from above and ``y`` as 3x1 vector:
|
||||
|
||||
>>> def deriv_vec(t, y):
|
||||
... return A @ y
|
||||
>>> result = solve_ivp(deriv_vec, [0, 25],
|
||||
... np.array([10 + 0j, 20 + 0j, 30 + 0j]),
|
||||
... t_eval=np.linspace(0, 25, 101))
|
||||
>>> print(result.y[:, 0])
|
||||
[10.+0.j 20.+0.j 30.+0.j]
|
||||
>>> print(result.y[:, -1])
|
||||
[18.46291039+45.25653651j 10.01569306+36.23293216j
|
||||
-4.98662741+80.07360388j]
|
||||
|
||||
Solving an IVP with ``A`` from above with ``y`` as 3x3 matrix :
|
||||
|
||||
>>> def deriv_mat(t, y):
|
||||
... return (A @ y.reshape(3, 3)).flatten()
|
||||
>>> y0 = np.array([[2 + 0j, 3 + 0j, 4 + 0j],
|
||||
... [5 + 0j, 6 + 0j, 7 + 0j],
|
||||
... [9 + 0j, 34 + 0j, 78 + 0j]])
|
||||
|
||||
>>> result = solve_ivp(deriv_mat, [0, 25], y0.flatten(),
|
||||
... t_eval=np.linspace(0, 25, 101))
|
||||
>>> print(result.y[:, 0].reshape(3, 3))
|
||||
[[ 2.+0.j 3.+0.j 4.+0.j]
|
||||
[ 5.+0.j 6.+0.j 7.+0.j]
|
||||
[ 9.+0.j 34.+0.j 78.+0.j]]
|
||||
>>> print(result.y[:, -1].reshape(3, 3))
|
||||
[[ 5.67451179 +12.07938445j 17.2888073 +31.03278837j
|
||||
37.83405768 +63.25138759j]
|
||||
[ 3.39949503 +11.82123994j 21.32530996 +44.88668871j
|
||||
53.17531184+103.80400411j]
|
||||
[ -2.26105874 +22.19277664j -15.1255713 +70.19616341j
|
||||
-38.34616845+153.29039931j]]
|
||||
|
||||
|
||||
"""
|
||||
if method not in METHODS and not (
|
||||
inspect.isclass(method) and issubclass(method, OdeSolver)):
|
||||
raise ValueError(f"`method` must be one of {METHODS} or OdeSolver class.")
|
||||
|
||||
t0, tf = map(float, t_span)
|
||||
|
||||
if args is not None:
|
||||
# Wrap the user's fun (and jac, if given) in lambdas to hide the
|
||||
# additional parameters. Pass in the original fun as a keyword
|
||||
# argument to keep it in the scope of the lambda.
|
||||
try:
|
||||
_ = [*(args)]
|
||||
except TypeError as exp:
|
||||
suggestion_tuple = (
|
||||
"Supplied 'args' cannot be unpacked. Please supply `args`"
|
||||
f" as a tuple (e.g. `args=({args},)`)"
|
||||
)
|
||||
raise TypeError(suggestion_tuple) from exp
|
||||
|
||||
def fun(t, x, fun=fun):
|
||||
return fun(t, x, *args)
|
||||
jac = options.get('jac')
|
||||
if callable(jac):
|
||||
options['jac'] = lambda t, x: jac(t, x, *args)
|
||||
|
||||
if t_eval is not None:
|
||||
t_eval = np.asarray(t_eval)
|
||||
if t_eval.ndim != 1:
|
||||
raise ValueError("`t_eval` must be 1-dimensional.")
|
||||
|
||||
if np.any(t_eval < min(t0, tf)) or np.any(t_eval > max(t0, tf)):
|
||||
raise ValueError("Values in `t_eval` are not within `t_span`.")
|
||||
|
||||
d = np.diff(t_eval)
|
||||
if tf > t0 and np.any(d <= 0) or tf < t0 and np.any(d >= 0):
|
||||
raise ValueError("Values in `t_eval` are not properly sorted.")
|
||||
|
||||
if tf > t0:
|
||||
t_eval_i = 0
|
||||
else:
|
||||
# Make order of t_eval decreasing to use np.searchsorted.
|
||||
t_eval = t_eval[::-1]
|
||||
# This will be an upper bound for slices.
|
||||
t_eval_i = t_eval.shape[0]
|
||||
|
||||
if method in METHODS:
|
||||
method = METHODS[method]
|
||||
|
||||
solver = method(fun, t0, y0, tf, vectorized=vectorized, **options)
|
||||
|
||||
if t_eval is None:
|
||||
ts = [t0]
|
||||
ys = [y0]
|
||||
elif t_eval is not None and dense_output:
|
||||
ts = []
|
||||
ti = [t0]
|
||||
ys = []
|
||||
else:
|
||||
ts = []
|
||||
ys = []
|
||||
|
||||
interpolants = []
|
||||
|
||||
if events is not None:
|
||||
events, max_events, event_dir = prepare_events(events)
|
||||
event_count = np.zeros(len(events))
|
||||
if args is not None:
|
||||
# Wrap user functions in lambdas to hide the additional parameters.
|
||||
# The original event function is passed as a keyword argument to the
|
||||
# lambda to keep the original function in scope (i.e., avoid the
|
||||
# late binding closure "gotcha").
|
||||
events = [lambda t, x, event=event: event(t, x, *args)
|
||||
for event in events]
|
||||
g = [event(t0, y0) for event in events]
|
||||
t_events = [[] for _ in range(len(events))]
|
||||
y_events = [[] for _ in range(len(events))]
|
||||
else:
|
||||
t_events = None
|
||||
y_events = None
|
||||
|
||||
status = None
|
||||
while status is None:
|
||||
message = solver.step()
|
||||
|
||||
if solver.status == 'finished':
|
||||
status = 0
|
||||
elif solver.status == 'failed':
|
||||
status = -1
|
||||
break
|
||||
|
||||
t_old = solver.t_old
|
||||
t = solver.t
|
||||
y = solver.y
|
||||
|
||||
if dense_output:
|
||||
sol = solver.dense_output()
|
||||
interpolants.append(sol)
|
||||
else:
|
||||
sol = None
|
||||
|
||||
if events is not None:
|
||||
g_new = [event(t, y) for event in events]
|
||||
active_events = find_active_events(g, g_new, event_dir)
|
||||
if active_events.size > 0:
|
||||
if sol is None:
|
||||
sol = solver.dense_output()
|
||||
|
||||
event_count[active_events] += 1
|
||||
root_indices, roots, terminate = handle_events(
|
||||
sol, events, active_events, event_count, max_events,
|
||||
t_old, t)
|
||||
|
||||
for e, te in zip(root_indices, roots):
|
||||
t_events[e].append(te)
|
||||
y_events[e].append(sol(te))
|
||||
|
||||
if terminate:
|
||||
status = 1
|
||||
t = roots[-1]
|
||||
y = sol(t)
|
||||
|
||||
g = g_new
|
||||
|
||||
if t_eval is None:
|
||||
ts.append(t)
|
||||
ys.append(y)
|
||||
else:
|
||||
# The value in t_eval equal to t will be included.
|
||||
if solver.direction > 0:
|
||||
t_eval_i_new = np.searchsorted(t_eval, t, side='right')
|
||||
t_eval_step = t_eval[t_eval_i:t_eval_i_new]
|
||||
else:
|
||||
t_eval_i_new = np.searchsorted(t_eval, t, side='left')
|
||||
# It has to be done with two slice operations, because
|
||||
# you can't slice to 0th element inclusive using backward
|
||||
# slicing.
|
||||
t_eval_step = t_eval[t_eval_i_new:t_eval_i][::-1]
|
||||
|
||||
if t_eval_step.size > 0:
|
||||
if sol is None:
|
||||
sol = solver.dense_output()
|
||||
ts.append(t_eval_step)
|
||||
ys.append(sol(t_eval_step))
|
||||
t_eval_i = t_eval_i_new
|
||||
|
||||
if t_eval is not None and dense_output:
|
||||
ti.append(t)
|
||||
|
||||
message = MESSAGES.get(status, message)
|
||||
|
||||
if t_events is not None:
|
||||
t_events = [np.asarray(te) for te in t_events]
|
||||
y_events = [np.asarray(ye) for ye in y_events]
|
||||
|
||||
if t_eval is None:
|
||||
ts = np.array(ts)
|
||||
ys = np.vstack(ys).T
|
||||
elif ts:
|
||||
ts = np.hstack(ts)
|
||||
ys = np.hstack(ys)
|
||||
|
||||
if dense_output:
|
||||
if t_eval is None:
|
||||
sol = OdeSolution(
|
||||
ts, interpolants, alt_segment=True if method in [BDF, LSODA] else False
|
||||
)
|
||||
else:
|
||||
sol = OdeSolution(
|
||||
ti, interpolants, alt_segment=True if method in [BDF, LSODA] else False
|
||||
)
|
||||
else:
|
||||
sol = None
|
||||
|
||||
return OdeResult(t=ts, y=ys, sol=sol, t_events=t_events, y_events=y_events,
|
||||
nfev=solver.nfev, njev=solver.njev, nlu=solver.nlu,
|
||||
status=status, message=message, success=status >= 0)
|
||||
224
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/lsoda.py
Normal file
224
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/lsoda.py
Normal file
@ -0,0 +1,224 @@
|
||||
import numpy as np
|
||||
from scipy.integrate import ode
|
||||
from .common import validate_tol, validate_first_step, warn_extraneous
|
||||
from .base import OdeSolver, DenseOutput
|
||||
|
||||
|
||||
class LSODA(OdeSolver):
|
||||
"""Adams/BDF method with automatic stiffness detection and switching.
|
||||
|
||||
This is a wrapper to the Fortran solver from ODEPACK [1]_. It switches
|
||||
automatically between the nonstiff Adams method and the stiff BDF method.
|
||||
The method was originally detailed in [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system: the time derivative of the state ``y``
|
||||
at time ``t``. The calling signature is ``fun(t, y)``, where ``t`` is a
|
||||
scalar and ``y`` is an ndarray with ``len(y) = len(y0)``. ``fun`` must
|
||||
return an array of the same shape as ``y``. See `vectorized` for more
|
||||
information.
|
||||
t0 : float
|
||||
Initial time.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state.
|
||||
t_bound : float
|
||||
Boundary time - the integration won't continue beyond it. It also
|
||||
determines the direction of the integration.
|
||||
first_step : float or None, optional
|
||||
Initial step size. Default is ``None`` which means that the algorithm
|
||||
should choose.
|
||||
min_step : float, optional
|
||||
Minimum allowed step size. Default is 0.0, i.e., the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
max_step : float, optional
|
||||
Maximum allowed step size. Default is np.inf, i.e., the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
rtol, atol : float and array_like, optional
|
||||
Relative and absolute tolerances. The solver keeps the local error
|
||||
estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a
|
||||
relative accuracy (number of correct digits), while `atol` controls
|
||||
absolute accuracy (number of correct decimal places). To achieve the
|
||||
desired `rtol`, set `atol` to be smaller than the smallest value that
|
||||
can be expected from ``rtol * abs(y)`` so that `rtol` dominates the
|
||||
allowable error. If `atol` is larger than ``rtol * abs(y)`` the
|
||||
number of correct digits is not guaranteed. Conversely, to achieve the
|
||||
desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller
|
||||
than `atol`. If components of y have different scales, it might be
|
||||
beneficial to set different `atol` values for different components by
|
||||
passing array_like with shape (n,) for `atol`. Default values are
|
||||
1e-3 for `rtol` and 1e-6 for `atol`.
|
||||
jac : None or callable, optional
|
||||
Jacobian matrix of the right-hand side of the system with respect to
|
||||
``y``. The Jacobian matrix has shape (n, n) and its element (i, j) is
|
||||
equal to ``d f_i / d y_j``. The function will be called as
|
||||
``jac(t, y)``. If None (default), the Jacobian will be
|
||||
approximated by finite differences. It is generally recommended to
|
||||
provide the Jacobian rather than relying on a finite-difference
|
||||
approximation.
|
||||
lband, uband : int or None
|
||||
Parameters defining the bandwidth of the Jacobian,
|
||||
i.e., ``jac[i, j] != 0 only for i - lband <= j <= i + uband``. Setting
|
||||
these requires your jac routine to return the Jacobian in the packed format:
|
||||
the returned array must have ``n`` columns and ``uband + lband + 1``
|
||||
rows in which Jacobian diagonals are written. Specifically
|
||||
``jac_packed[uband + i - j , j] = jac[i, j]``. The same format is used
|
||||
in `scipy.linalg.solve_banded` (check for an illustration).
|
||||
These parameters can be also used with ``jac=None`` to reduce the
|
||||
number of Jacobian elements estimated by finite differences.
|
||||
vectorized : bool, optional
|
||||
Whether `fun` may be called in a vectorized fashion. False (default)
|
||||
is recommended for this solver.
|
||||
|
||||
If ``vectorized`` is False, `fun` will always be called with ``y`` of
|
||||
shape ``(n,)``, where ``n = len(y0)``.
|
||||
|
||||
If ``vectorized`` is True, `fun` may be called with ``y`` of shape
|
||||
``(n, k)``, where ``k`` is an integer. In this case, `fun` must behave
|
||||
such that ``fun(t, y)[:, i] == fun(t, y[:, i])`` (i.e. each column of
|
||||
the returned array is the time derivative of the state corresponding
|
||||
with a column of ``y``).
|
||||
|
||||
Setting ``vectorized=True`` allows for faster finite difference
|
||||
approximation of the Jacobian by methods 'Radau' and 'BDF', but
|
||||
will result in slower execution for this solver.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
n : int
|
||||
Number of equations.
|
||||
status : string
|
||||
Current status of the solver: 'running', 'finished' or 'failed'.
|
||||
t_bound : float
|
||||
Boundary time.
|
||||
direction : float
|
||||
Integration direction: +1 or -1.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray
|
||||
Current state.
|
||||
t_old : float
|
||||
Previous time. None if no steps were made yet.
|
||||
nfev : int
|
||||
Number of evaluations of the right-hand side.
|
||||
njev : int
|
||||
Number of evaluations of the Jacobian.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] A. C. Hindmarsh, "ODEPACK, A Systematized Collection of ODE
|
||||
Solvers," IMACS Transactions on Scientific Computation, Vol 1.,
|
||||
pp. 55-64, 1983.
|
||||
.. [2] L. Petzold, "Automatic selection of methods for solving stiff and
|
||||
nonstiff systems of ordinary differential equations", SIAM Journal
|
||||
on Scientific and Statistical Computing, Vol. 4, No. 1, pp. 136-148,
|
||||
1983.
|
||||
"""
|
||||
def __init__(self, fun, t0, y0, t_bound, first_step=None, min_step=0.0,
|
||||
max_step=np.inf, rtol=1e-3, atol=1e-6, jac=None, lband=None,
|
||||
uband=None, vectorized=False, **extraneous):
|
||||
warn_extraneous(extraneous)
|
||||
super().__init__(fun, t0, y0, t_bound, vectorized)
|
||||
|
||||
if first_step is None:
|
||||
first_step = 0 # LSODA value for automatic selection.
|
||||
else:
|
||||
first_step = validate_first_step(first_step, t0, t_bound)
|
||||
|
||||
first_step *= self.direction
|
||||
|
||||
if max_step == np.inf:
|
||||
max_step = 0 # LSODA value for infinity.
|
||||
elif max_step <= 0:
|
||||
raise ValueError("`max_step` must be positive.")
|
||||
|
||||
if min_step < 0:
|
||||
raise ValueError("`min_step` must be nonnegative.")
|
||||
|
||||
rtol, atol = validate_tol(rtol, atol, self.n)
|
||||
|
||||
solver = ode(self.fun, jac)
|
||||
solver.set_integrator('lsoda', rtol=rtol, atol=atol, max_step=max_step,
|
||||
min_step=min_step, first_step=first_step,
|
||||
lband=lband, uband=uband)
|
||||
solver.set_initial_value(y0, t0)
|
||||
|
||||
# Inject t_bound into rwork array as needed for itask=5.
|
||||
solver._integrator.rwork[0] = self.t_bound
|
||||
solver._integrator.call_args[4] = solver._integrator.rwork
|
||||
|
||||
self._lsoda_solver = solver
|
||||
|
||||
def _step_impl(self):
|
||||
solver = self._lsoda_solver
|
||||
integrator = solver._integrator
|
||||
|
||||
# From lsoda.step and lsoda.integrate itask=5 means take a single
|
||||
# step and do not go past t_bound.
|
||||
itask = integrator.call_args[2]
|
||||
integrator.call_args[2] = 5
|
||||
solver._y, solver.t = integrator.run(
|
||||
solver.f, solver.jac or (lambda: None), solver._y, solver.t,
|
||||
self.t_bound, solver.f_params, solver.jac_params)
|
||||
integrator.call_args[2] = itask
|
||||
|
||||
if solver.successful():
|
||||
self.t = solver.t
|
||||
self.y = solver._y
|
||||
# From LSODA Fortran source njev is equal to nlu.
|
||||
self.njev = integrator.iwork[12]
|
||||
self.nlu = integrator.iwork[12]
|
||||
return True, None
|
||||
else:
|
||||
return False, 'Unexpected istate in LSODA.'
|
||||
|
||||
def _dense_output_impl(self):
|
||||
iwork = self._lsoda_solver._integrator.iwork
|
||||
rwork = self._lsoda_solver._integrator.rwork
|
||||
|
||||
# We want to produce the Nordsieck history array, yh, up to the order
|
||||
# used in the last successful iteration. The step size is unimportant
|
||||
# because it will be scaled out in LsodaDenseOutput. Some additional
|
||||
# work may be required because ODEPACK's LSODA implementation produces
|
||||
# the Nordsieck history in the state needed for the next iteration.
|
||||
|
||||
# iwork[13] contains order from last successful iteration, while
|
||||
# iwork[14] contains order to be attempted next.
|
||||
order = iwork[13]
|
||||
|
||||
# rwork[11] contains the step size to be attempted next, while
|
||||
# rwork[10] contains step size from last successful iteration.
|
||||
h = rwork[11]
|
||||
|
||||
# rwork[20:20 + (iwork[14] + 1) * self.n] contains entries of the
|
||||
# Nordsieck array in state needed for next iteration. We want
|
||||
# the entries up to order for the last successful step so use the
|
||||
# following.
|
||||
yh = np.reshape(rwork[20:20 + (order + 1) * self.n],
|
||||
(self.n, order + 1), order='F').copy()
|
||||
if iwork[14] < order:
|
||||
# If the order is set to decrease then the final column of yh
|
||||
# has not been updated within ODEPACK's LSODA
|
||||
# implementation because this column will not be used in the
|
||||
# next iteration. We must rescale this column to make the
|
||||
# associated step size consistent with the other columns.
|
||||
yh[:, -1] *= (h / rwork[10]) ** order
|
||||
|
||||
return LsodaDenseOutput(self.t_old, self.t, h, order, yh)
|
||||
|
||||
|
||||
class LsodaDenseOutput(DenseOutput):
|
||||
def __init__(self, t_old, t, h, order, yh):
|
||||
super().__init__(t_old, t)
|
||||
self.h = h
|
||||
self.yh = yh
|
||||
self.p = np.arange(order + 1)
|
||||
|
||||
def _call_impl(self, t):
|
||||
if t.ndim == 0:
|
||||
x = ((t - self.t) / self.h) ** self.p
|
||||
else:
|
||||
x = ((t - self.t) / self.h) ** self.p[:, None]
|
||||
|
||||
return np.dot(self.yh, x)
|
||||
574
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/radau.py
Normal file
574
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/radau.py
Normal file
@ -0,0 +1,574 @@
|
||||
import numpy as np
|
||||
from scipy.linalg import lu_factor, lu_solve
|
||||
from scipy.sparse import csc_matrix, issparse, eye
|
||||
from scipy.sparse.linalg import splu
|
||||
from scipy.optimize._numdiff import group_columns
|
||||
from .common import (validate_max_step, validate_tol, select_initial_step,
|
||||
norm, num_jac, EPS, warn_extraneous,
|
||||
validate_first_step)
|
||||
from .base import OdeSolver, DenseOutput
|
||||
|
||||
S6 = 6 ** 0.5
|
||||
|
||||
# Butcher tableau. A is not used directly, see below.
|
||||
C = np.array([(4 - S6) / 10, (4 + S6) / 10, 1])
|
||||
E = np.array([-13 - 7 * S6, -13 + 7 * S6, -1]) / 3
|
||||
|
||||
# Eigendecomposition of A is done: A = T L T**-1. There is 1 real eigenvalue
|
||||
# and a complex conjugate pair. They are written below.
|
||||
MU_REAL = 3 + 3 ** (2 / 3) - 3 ** (1 / 3)
|
||||
MU_COMPLEX = (3 + 0.5 * (3 ** (1 / 3) - 3 ** (2 / 3))
|
||||
- 0.5j * (3 ** (5 / 6) + 3 ** (7 / 6)))
|
||||
|
||||
# These are transformation matrices.
|
||||
T = np.array([
|
||||
[0.09443876248897524, -0.14125529502095421, 0.03002919410514742],
|
||||
[0.25021312296533332, 0.20412935229379994, -0.38294211275726192],
|
||||
[1, 1, 0]])
|
||||
TI = np.array([
|
||||
[4.17871859155190428, 0.32768282076106237, 0.52337644549944951],
|
||||
[-4.17871859155190428, -0.32768282076106237, 0.47662355450055044],
|
||||
[0.50287263494578682, -2.57192694985560522, 0.59603920482822492]])
|
||||
# These linear combinations are used in the algorithm.
|
||||
TI_REAL = TI[0]
|
||||
TI_COMPLEX = TI[1] + 1j * TI[2]
|
||||
|
||||
# Interpolator coefficients.
|
||||
P = np.array([
|
||||
[13/3 + 7*S6/3, -23/3 - 22*S6/3, 10/3 + 5 * S6],
|
||||
[13/3 - 7*S6/3, -23/3 + 22*S6/3, 10/3 - 5 * S6],
|
||||
[1/3, -8/3, 10/3]])
|
||||
|
||||
|
||||
NEWTON_MAXITER = 6 # Maximum number of Newton iterations.
|
||||
MIN_FACTOR = 0.2 # Minimum allowed decrease in a step size.
|
||||
MAX_FACTOR = 10 # Maximum allowed increase in a step size.
|
||||
|
||||
|
||||
def solve_collocation_system(fun, t, y, h, Z0, scale, tol,
|
||||
LU_real, LU_complex, solve_lu):
|
||||
"""Solve the collocation system.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray, shape (n,)
|
||||
Current state.
|
||||
h : float
|
||||
Step to try.
|
||||
Z0 : ndarray, shape (3, n)
|
||||
Initial guess for the solution. It determines new values of `y` at
|
||||
``t + h * C`` as ``y + Z0``, where ``C`` is the Radau method constants.
|
||||
scale : ndarray, shape (n)
|
||||
Problem tolerance scale, i.e. ``rtol * abs(y) + atol``.
|
||||
tol : float
|
||||
Tolerance to which solve the system. This value is compared with
|
||||
the normalized by `scale` error.
|
||||
LU_real, LU_complex
|
||||
LU decompositions of the system Jacobians.
|
||||
solve_lu : callable
|
||||
Callable which solves a linear system given a LU decomposition. The
|
||||
signature is ``solve_lu(LU, b)``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
converged : bool
|
||||
Whether iterations converged.
|
||||
n_iter : int
|
||||
Number of completed iterations.
|
||||
Z : ndarray, shape (3, n)
|
||||
Found solution.
|
||||
rate : float
|
||||
The rate of convergence.
|
||||
"""
|
||||
n = y.shape[0]
|
||||
M_real = MU_REAL / h
|
||||
M_complex = MU_COMPLEX / h
|
||||
|
||||
W = TI.dot(Z0)
|
||||
Z = Z0
|
||||
|
||||
F = np.empty((3, n))
|
||||
ch = h * C
|
||||
|
||||
dW_norm_old = None
|
||||
dW = np.empty_like(W)
|
||||
converged = False
|
||||
rate = None
|
||||
for k in range(NEWTON_MAXITER):
|
||||
for i in range(3):
|
||||
F[i] = fun(t + ch[i], y + Z[i])
|
||||
|
||||
if not np.all(np.isfinite(F)):
|
||||
break
|
||||
|
||||
f_real = F.T.dot(TI_REAL) - M_real * W[0]
|
||||
f_complex = F.T.dot(TI_COMPLEX) - M_complex * (W[1] + 1j * W[2])
|
||||
|
||||
dW_real = solve_lu(LU_real, f_real)
|
||||
dW_complex = solve_lu(LU_complex, f_complex)
|
||||
|
||||
dW[0] = dW_real
|
||||
dW[1] = dW_complex.real
|
||||
dW[2] = dW_complex.imag
|
||||
|
||||
dW_norm = norm(dW / scale)
|
||||
if dW_norm_old is not None:
|
||||
rate = dW_norm / dW_norm_old
|
||||
|
||||
if (rate is not None and (rate >= 1 or
|
||||
rate ** (NEWTON_MAXITER - k) / (1 - rate) * dW_norm > tol)):
|
||||
break
|
||||
|
||||
W += dW
|
||||
Z = T.dot(W)
|
||||
|
||||
if (dW_norm == 0 or
|
||||
rate is not None and rate / (1 - rate) * dW_norm < tol):
|
||||
converged = True
|
||||
break
|
||||
|
||||
dW_norm_old = dW_norm
|
||||
|
||||
return converged, k + 1, Z, rate
|
||||
|
||||
|
||||
def predict_factor(h_abs, h_abs_old, error_norm, error_norm_old):
|
||||
"""Predict by which factor to increase/decrease the step size.
|
||||
|
||||
The algorithm is described in [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
h_abs, h_abs_old : float
|
||||
Current and previous values of the step size, `h_abs_old` can be None
|
||||
(see Notes).
|
||||
error_norm, error_norm_old : float
|
||||
Current and previous values of the error norm, `error_norm_old` can
|
||||
be None (see Notes).
|
||||
|
||||
Returns
|
||||
-------
|
||||
factor : float
|
||||
Predicted factor.
|
||||
|
||||
Notes
|
||||
-----
|
||||
If `h_abs_old` and `error_norm_old` are both not None then a two-step
|
||||
algorithm is used, otherwise a one-step algorithm is used.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. Hairer, S. P. Norsett G. Wanner, "Solving Ordinary Differential
|
||||
Equations II: Stiff and Differential-Algebraic Problems", Sec. IV.8.
|
||||
"""
|
||||
if error_norm_old is None or h_abs_old is None or error_norm == 0:
|
||||
multiplier = 1
|
||||
else:
|
||||
multiplier = h_abs / h_abs_old * (error_norm_old / error_norm) ** 0.25
|
||||
|
||||
with np.errstate(divide='ignore'):
|
||||
factor = min(1, multiplier) * error_norm ** -0.25
|
||||
|
||||
return factor
|
||||
|
||||
|
||||
class Radau(OdeSolver):
|
||||
"""Implicit Runge-Kutta method of Radau IIA family of order 5.
|
||||
|
||||
The implementation follows [1]_. The error is controlled with a
|
||||
third-order accurate embedded formula. A cubic polynomial which satisfies
|
||||
the collocation conditions is used for the dense output.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system: the time derivative of the state ``y``
|
||||
at time ``t``. The calling signature is ``fun(t, y)``, where ``t`` is a
|
||||
scalar and ``y`` is an ndarray with ``len(y) = len(y0)``. ``fun`` must
|
||||
return an array of the same shape as ``y``. See `vectorized` for more
|
||||
information.
|
||||
t0 : float
|
||||
Initial time.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state.
|
||||
t_bound : float
|
||||
Boundary time - the integration won't continue beyond it. It also
|
||||
determines the direction of the integration.
|
||||
first_step : float or None, optional
|
||||
Initial step size. Default is ``None`` which means that the algorithm
|
||||
should choose.
|
||||
max_step : float, optional
|
||||
Maximum allowed step size. Default is np.inf, i.e., the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
rtol, atol : float and array_like, optional
|
||||
Relative and absolute tolerances. The solver keeps the local error
|
||||
estimates less than ``atol + rtol * abs(y)``. HHere `rtol` controls a
|
||||
relative accuracy (number of correct digits), while `atol` controls
|
||||
absolute accuracy (number of correct decimal places). To achieve the
|
||||
desired `rtol`, set `atol` to be smaller than the smallest value that
|
||||
can be expected from ``rtol * abs(y)`` so that `rtol` dominates the
|
||||
allowable error. If `atol` is larger than ``rtol * abs(y)`` the
|
||||
number of correct digits is not guaranteed. Conversely, to achieve the
|
||||
desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller
|
||||
than `atol`. If components of y have different scales, it might be
|
||||
beneficial to set different `atol` values for different components by
|
||||
passing array_like with shape (n,) for `atol`. Default values are
|
||||
1e-3 for `rtol` and 1e-6 for `atol`.
|
||||
jac : {None, array_like, sparse_matrix, callable}, optional
|
||||
Jacobian matrix of the right-hand side of the system with respect to
|
||||
y, required by this method. The Jacobian matrix has shape (n, n) and
|
||||
its element (i, j) is equal to ``d f_i / d y_j``.
|
||||
There are three ways to define the Jacobian:
|
||||
|
||||
* If array_like or sparse_matrix, the Jacobian is assumed to
|
||||
be constant.
|
||||
* If callable, the Jacobian is assumed to depend on both
|
||||
t and y; it will be called as ``jac(t, y)`` as necessary.
|
||||
For the 'Radau' and 'BDF' methods, the return value might be a
|
||||
sparse matrix.
|
||||
* If None (default), the Jacobian will be approximated by
|
||||
finite differences.
|
||||
|
||||
It is generally recommended to provide the Jacobian rather than
|
||||
relying on a finite-difference approximation.
|
||||
jac_sparsity : {None, array_like, sparse matrix}, optional
|
||||
Defines a sparsity structure of the Jacobian matrix for a
|
||||
finite-difference approximation. Its shape must be (n, n). This argument
|
||||
is ignored if `jac` is not `None`. If the Jacobian has only few non-zero
|
||||
elements in *each* row, providing the sparsity structure will greatly
|
||||
speed up the computations [2]_. A zero entry means that a corresponding
|
||||
element in the Jacobian is always zero. If None (default), the Jacobian
|
||||
is assumed to be dense.
|
||||
vectorized : bool, optional
|
||||
Whether `fun` can be called in a vectorized fashion. Default is False.
|
||||
|
||||
If ``vectorized`` is False, `fun` will always be called with ``y`` of
|
||||
shape ``(n,)``, where ``n = len(y0)``.
|
||||
|
||||
If ``vectorized`` is True, `fun` may be called with ``y`` of shape
|
||||
``(n, k)``, where ``k`` is an integer. In this case, `fun` must behave
|
||||
such that ``fun(t, y)[:, i] == fun(t, y[:, i])`` (i.e. each column of
|
||||
the returned array is the time derivative of the state corresponding
|
||||
with a column of ``y``).
|
||||
|
||||
Setting ``vectorized=True`` allows for faster finite difference
|
||||
approximation of the Jacobian by this method, but may result in slower
|
||||
execution overall in some circumstances (e.g. small ``len(y0)``).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
n : int
|
||||
Number of equations.
|
||||
status : string
|
||||
Current status of the solver: 'running', 'finished' or 'failed'.
|
||||
t_bound : float
|
||||
Boundary time.
|
||||
direction : float
|
||||
Integration direction: +1 or -1.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray
|
||||
Current state.
|
||||
t_old : float
|
||||
Previous time. None if no steps were made yet.
|
||||
step_size : float
|
||||
Size of the last successful step. None if no steps were made yet.
|
||||
nfev : int
|
||||
Number of evaluations of the right-hand side.
|
||||
njev : int
|
||||
Number of evaluations of the Jacobian.
|
||||
nlu : int
|
||||
Number of LU decompositions.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. Hairer, G. Wanner, "Solving Ordinary Differential Equations II:
|
||||
Stiff and Differential-Algebraic Problems", Sec. IV.8.
|
||||
.. [2] A. Curtis, M. J. D. Powell, and J. Reid, "On the estimation of
|
||||
sparse Jacobian matrices", Journal of the Institute of Mathematics
|
||||
and its Applications, 13, pp. 117-120, 1974.
|
||||
"""
|
||||
def __init__(self, fun, t0, y0, t_bound, max_step=np.inf,
|
||||
rtol=1e-3, atol=1e-6, jac=None, jac_sparsity=None,
|
||||
vectorized=False, first_step=None, **extraneous):
|
||||
warn_extraneous(extraneous)
|
||||
super().__init__(fun, t0, y0, t_bound, vectorized)
|
||||
self.y_old = None
|
||||
self.max_step = validate_max_step(max_step)
|
||||
self.rtol, self.atol = validate_tol(rtol, atol, self.n)
|
||||
self.f = self.fun(self.t, self.y)
|
||||
# Select initial step assuming the same order which is used to control
|
||||
# the error.
|
||||
if first_step is None:
|
||||
self.h_abs = select_initial_step(
|
||||
self.fun, self.t, self.y, t_bound, max_step, self.f, self.direction,
|
||||
3, self.rtol, self.atol)
|
||||
else:
|
||||
self.h_abs = validate_first_step(first_step, t0, t_bound)
|
||||
self.h_abs_old = None
|
||||
self.error_norm_old = None
|
||||
|
||||
self.newton_tol = max(10 * EPS / rtol, min(0.03, rtol ** 0.5))
|
||||
self.sol = None
|
||||
|
||||
self.jac_factor = None
|
||||
self.jac, self.J = self._validate_jac(jac, jac_sparsity)
|
||||
if issparse(self.J):
|
||||
def lu(A):
|
||||
self.nlu += 1
|
||||
return splu(A)
|
||||
|
||||
def solve_lu(LU, b):
|
||||
return LU.solve(b)
|
||||
|
||||
I = eye(self.n, format='csc')
|
||||
else:
|
||||
def lu(A):
|
||||
self.nlu += 1
|
||||
return lu_factor(A, overwrite_a=True)
|
||||
|
||||
def solve_lu(LU, b):
|
||||
return lu_solve(LU, b, overwrite_b=True)
|
||||
|
||||
I = np.identity(self.n)
|
||||
|
||||
self.lu = lu
|
||||
self.solve_lu = solve_lu
|
||||
self.I = I
|
||||
|
||||
self.current_jac = True
|
||||
self.LU_real = None
|
||||
self.LU_complex = None
|
||||
self.Z = None
|
||||
|
||||
def _validate_jac(self, jac, sparsity):
|
||||
t0 = self.t
|
||||
y0 = self.y
|
||||
|
||||
if jac is None:
|
||||
if sparsity is not None:
|
||||
if issparse(sparsity):
|
||||
sparsity = csc_matrix(sparsity)
|
||||
groups = group_columns(sparsity)
|
||||
sparsity = (sparsity, groups)
|
||||
|
||||
def jac_wrapped(t, y, f):
|
||||
self.njev += 1
|
||||
J, self.jac_factor = num_jac(self.fun_vectorized, t, y, f,
|
||||
self.atol, self.jac_factor,
|
||||
sparsity)
|
||||
return J
|
||||
J = jac_wrapped(t0, y0, self.f)
|
||||
elif callable(jac):
|
||||
J = jac(t0, y0)
|
||||
self.njev = 1
|
||||
if issparse(J):
|
||||
J = csc_matrix(J)
|
||||
|
||||
def jac_wrapped(t, y, _=None):
|
||||
self.njev += 1
|
||||
return csc_matrix(jac(t, y), dtype=float)
|
||||
|
||||
else:
|
||||
J = np.asarray(J, dtype=float)
|
||||
|
||||
def jac_wrapped(t, y, _=None):
|
||||
self.njev += 1
|
||||
return np.asarray(jac(t, y), dtype=float)
|
||||
|
||||
if J.shape != (self.n, self.n):
|
||||
raise ValueError("`jac` is expected to have shape {}, but "
|
||||
"actually has {}."
|
||||
.format((self.n, self.n), J.shape))
|
||||
else:
|
||||
if issparse(jac):
|
||||
J = csc_matrix(jac)
|
||||
else:
|
||||
J = np.asarray(jac, dtype=float)
|
||||
|
||||
if J.shape != (self.n, self.n):
|
||||
raise ValueError("`jac` is expected to have shape {}, but "
|
||||
"actually has {}."
|
||||
.format((self.n, self.n), J.shape))
|
||||
jac_wrapped = None
|
||||
|
||||
return jac_wrapped, J
|
||||
|
||||
def _step_impl(self):
|
||||
t = self.t
|
||||
y = self.y
|
||||
f = self.f
|
||||
|
||||
max_step = self.max_step
|
||||
atol = self.atol
|
||||
rtol = self.rtol
|
||||
|
||||
min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t)
|
||||
if self.h_abs > max_step:
|
||||
h_abs = max_step
|
||||
h_abs_old = None
|
||||
error_norm_old = None
|
||||
elif self.h_abs < min_step:
|
||||
h_abs = min_step
|
||||
h_abs_old = None
|
||||
error_norm_old = None
|
||||
else:
|
||||
h_abs = self.h_abs
|
||||
h_abs_old = self.h_abs_old
|
||||
error_norm_old = self.error_norm_old
|
||||
|
||||
J = self.J
|
||||
LU_real = self.LU_real
|
||||
LU_complex = self.LU_complex
|
||||
|
||||
current_jac = self.current_jac
|
||||
jac = self.jac
|
||||
|
||||
rejected = False
|
||||
step_accepted = False
|
||||
message = None
|
||||
while not step_accepted:
|
||||
if h_abs < min_step:
|
||||
return False, self.TOO_SMALL_STEP
|
||||
|
||||
h = h_abs * self.direction
|
||||
t_new = t + h
|
||||
|
||||
if self.direction * (t_new - self.t_bound) > 0:
|
||||
t_new = self.t_bound
|
||||
|
||||
h = t_new - t
|
||||
h_abs = np.abs(h)
|
||||
|
||||
if self.sol is None:
|
||||
Z0 = np.zeros((3, y.shape[0]))
|
||||
else:
|
||||
Z0 = self.sol(t + h * C).T - y
|
||||
|
||||
scale = atol + np.abs(y) * rtol
|
||||
|
||||
converged = False
|
||||
while not converged:
|
||||
if LU_real is None or LU_complex is None:
|
||||
LU_real = self.lu(MU_REAL / h * self.I - J)
|
||||
LU_complex = self.lu(MU_COMPLEX / h * self.I - J)
|
||||
|
||||
converged, n_iter, Z, rate = solve_collocation_system(
|
||||
self.fun, t, y, h, Z0, scale, self.newton_tol,
|
||||
LU_real, LU_complex, self.solve_lu)
|
||||
|
||||
if not converged:
|
||||
if current_jac:
|
||||
break
|
||||
|
||||
J = self.jac(t, y, f)
|
||||
current_jac = True
|
||||
LU_real = None
|
||||
LU_complex = None
|
||||
|
||||
if not converged:
|
||||
h_abs *= 0.5
|
||||
LU_real = None
|
||||
LU_complex = None
|
||||
continue
|
||||
|
||||
y_new = y + Z[-1]
|
||||
ZE = Z.T.dot(E) / h
|
||||
error = self.solve_lu(LU_real, f + ZE)
|
||||
scale = atol + np.maximum(np.abs(y), np.abs(y_new)) * rtol
|
||||
error_norm = norm(error / scale)
|
||||
safety = 0.9 * (2 * NEWTON_MAXITER + 1) / (2 * NEWTON_MAXITER
|
||||
+ n_iter)
|
||||
|
||||
if rejected and error_norm > 1:
|
||||
error = self.solve_lu(LU_real, self.fun(t, y + error) + ZE)
|
||||
error_norm = norm(error / scale)
|
||||
|
||||
if error_norm > 1:
|
||||
factor = predict_factor(h_abs, h_abs_old,
|
||||
error_norm, error_norm_old)
|
||||
h_abs *= max(MIN_FACTOR, safety * factor)
|
||||
|
||||
LU_real = None
|
||||
LU_complex = None
|
||||
rejected = True
|
||||
else:
|
||||
step_accepted = True
|
||||
|
||||
recompute_jac = jac is not None and n_iter > 2 and rate > 1e-3
|
||||
|
||||
factor = predict_factor(h_abs, h_abs_old, error_norm, error_norm_old)
|
||||
factor = min(MAX_FACTOR, safety * factor)
|
||||
|
||||
if not recompute_jac and factor < 1.2:
|
||||
factor = 1
|
||||
else:
|
||||
LU_real = None
|
||||
LU_complex = None
|
||||
|
||||
f_new = self.fun(t_new, y_new)
|
||||
if recompute_jac:
|
||||
J = jac(t_new, y_new, f_new)
|
||||
current_jac = True
|
||||
elif jac is not None:
|
||||
current_jac = False
|
||||
|
||||
self.h_abs_old = self.h_abs
|
||||
self.error_norm_old = error_norm
|
||||
|
||||
self.h_abs = h_abs * factor
|
||||
|
||||
self.y_old = y
|
||||
|
||||
self.t = t_new
|
||||
self.y = y_new
|
||||
self.f = f_new
|
||||
|
||||
self.Z = Z
|
||||
|
||||
self.LU_real = LU_real
|
||||
self.LU_complex = LU_complex
|
||||
self.current_jac = current_jac
|
||||
self.J = J
|
||||
|
||||
self.t_old = t
|
||||
self.sol = self._compute_dense_output()
|
||||
|
||||
return step_accepted, message
|
||||
|
||||
def _compute_dense_output(self):
|
||||
Q = np.dot(self.Z.T, P)
|
||||
return RadauDenseOutput(self.t_old, self.t, self.y_old, Q)
|
||||
|
||||
def _dense_output_impl(self):
|
||||
return self.sol
|
||||
|
||||
|
||||
class RadauDenseOutput(DenseOutput):
|
||||
def __init__(self, t_old, t, y_old, Q):
|
||||
super().__init__(t_old, t)
|
||||
self.h = t - t_old
|
||||
self.Q = Q
|
||||
self.order = Q.shape[1] - 1
|
||||
self.y_old = y_old
|
||||
|
||||
def _call_impl(self, t):
|
||||
x = (t - self.t_old) / self.h
|
||||
if t.ndim == 0:
|
||||
p = np.tile(x, self.order + 1)
|
||||
p = np.cumprod(p)
|
||||
else:
|
||||
p = np.tile(x, (self.order + 1, 1))
|
||||
p = np.cumprod(p, axis=0)
|
||||
# Here we don't multiply by h, not a mistake.
|
||||
y = np.dot(self.Q, p)
|
||||
if y.ndim == 2:
|
||||
y += self.y_old[:, None]
|
||||
else:
|
||||
y += self.y_old
|
||||
|
||||
return y
|
||||
601
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/rk.py
Normal file
601
venv/lib/python3.12/site-packages/scipy/integrate/_ivp/rk.py
Normal file
@ -0,0 +1,601 @@
|
||||
import numpy as np
|
||||
from .base import OdeSolver, DenseOutput
|
||||
from .common import (validate_max_step, validate_tol, select_initial_step,
|
||||
norm, warn_extraneous, validate_first_step)
|
||||
from . import dop853_coefficients
|
||||
|
||||
# Multiply steps computed from asymptotic behaviour of errors by this.
|
||||
SAFETY = 0.9
|
||||
|
||||
MIN_FACTOR = 0.2 # Minimum allowed decrease in a step size.
|
||||
MAX_FACTOR = 10 # Maximum allowed increase in a step size.
|
||||
|
||||
|
||||
def rk_step(fun, t, y, f, h, A, B, C, K):
|
||||
"""Perform a single Runge-Kutta step.
|
||||
|
||||
This function computes a prediction of an explicit Runge-Kutta method and
|
||||
also estimates the error of a less accurate method.
|
||||
|
||||
Notation for Butcher tableau is as in [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray, shape (n,)
|
||||
Current state.
|
||||
f : ndarray, shape (n,)
|
||||
Current value of the derivative, i.e., ``fun(x, y)``.
|
||||
h : float
|
||||
Step to use.
|
||||
A : ndarray, shape (n_stages, n_stages)
|
||||
Coefficients for combining previous RK stages to compute the next
|
||||
stage. For explicit methods the coefficients at and above the main
|
||||
diagonal are zeros.
|
||||
B : ndarray, shape (n_stages,)
|
||||
Coefficients for combining RK stages for computing the final
|
||||
prediction.
|
||||
C : ndarray, shape (n_stages,)
|
||||
Coefficients for incrementing time for consecutive RK stages.
|
||||
The value for the first stage is always zero.
|
||||
K : ndarray, shape (n_stages + 1, n)
|
||||
Storage array for putting RK stages here. Stages are stored in rows.
|
||||
The last row is a linear combination of the previous rows with
|
||||
coefficients
|
||||
|
||||
Returns
|
||||
-------
|
||||
y_new : ndarray, shape (n,)
|
||||
Solution at t + h computed with a higher accuracy.
|
||||
f_new : ndarray, shape (n,)
|
||||
Derivative ``fun(t + h, y_new)``.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. Hairer, S. P. Norsett G. Wanner, "Solving Ordinary Differential
|
||||
Equations I: Nonstiff Problems", Sec. II.4.
|
||||
"""
|
||||
K[0] = f
|
||||
for s, (a, c) in enumerate(zip(A[1:], C[1:]), start=1):
|
||||
dy = np.dot(K[:s].T, a[:s]) * h
|
||||
K[s] = fun(t + c * h, y + dy)
|
||||
|
||||
y_new = y + h * np.dot(K[:-1].T, B)
|
||||
f_new = fun(t + h, y_new)
|
||||
|
||||
K[-1] = f_new
|
||||
|
||||
return y_new, f_new
|
||||
|
||||
|
||||
class RungeKutta(OdeSolver):
|
||||
"""Base class for explicit Runge-Kutta methods."""
|
||||
C: np.ndarray = NotImplemented
|
||||
A: np.ndarray = NotImplemented
|
||||
B: np.ndarray = NotImplemented
|
||||
E: np.ndarray = NotImplemented
|
||||
P: np.ndarray = NotImplemented
|
||||
order: int = NotImplemented
|
||||
error_estimator_order: int = NotImplemented
|
||||
n_stages: int = NotImplemented
|
||||
|
||||
def __init__(self, fun, t0, y0, t_bound, max_step=np.inf,
|
||||
rtol=1e-3, atol=1e-6, vectorized=False,
|
||||
first_step=None, **extraneous):
|
||||
warn_extraneous(extraneous)
|
||||
super().__init__(fun, t0, y0, t_bound, vectorized,
|
||||
support_complex=True)
|
||||
self.y_old = None
|
||||
self.max_step = validate_max_step(max_step)
|
||||
self.rtol, self.atol = validate_tol(rtol, atol, self.n)
|
||||
self.f = self.fun(self.t, self.y)
|
||||
if first_step is None:
|
||||
self.h_abs = select_initial_step(
|
||||
self.fun, self.t, self.y, t_bound, max_step, self.f, self.direction,
|
||||
self.error_estimator_order, self.rtol, self.atol)
|
||||
else:
|
||||
self.h_abs = validate_first_step(first_step, t0, t_bound)
|
||||
self.K = np.empty((self.n_stages + 1, self.n), dtype=self.y.dtype)
|
||||
self.error_exponent = -1 / (self.error_estimator_order + 1)
|
||||
self.h_previous = None
|
||||
|
||||
def _estimate_error(self, K, h):
|
||||
return np.dot(K.T, self.E) * h
|
||||
|
||||
def _estimate_error_norm(self, K, h, scale):
|
||||
return norm(self._estimate_error(K, h) / scale)
|
||||
|
||||
def _step_impl(self):
|
||||
t = self.t
|
||||
y = self.y
|
||||
|
||||
max_step = self.max_step
|
||||
rtol = self.rtol
|
||||
atol = self.atol
|
||||
|
||||
min_step = 10 * np.abs(np.nextafter(t, self.direction * np.inf) - t)
|
||||
|
||||
if self.h_abs > max_step:
|
||||
h_abs = max_step
|
||||
elif self.h_abs < min_step:
|
||||
h_abs = min_step
|
||||
else:
|
||||
h_abs = self.h_abs
|
||||
|
||||
step_accepted = False
|
||||
step_rejected = False
|
||||
|
||||
while not step_accepted:
|
||||
if h_abs < min_step:
|
||||
return False, self.TOO_SMALL_STEP
|
||||
|
||||
h = h_abs * self.direction
|
||||
t_new = t + h
|
||||
|
||||
if self.direction * (t_new - self.t_bound) > 0:
|
||||
t_new = self.t_bound
|
||||
|
||||
h = t_new - t
|
||||
h_abs = np.abs(h)
|
||||
|
||||
y_new, f_new = rk_step(self.fun, t, y, self.f, h, self.A,
|
||||
self.B, self.C, self.K)
|
||||
scale = atol + np.maximum(np.abs(y), np.abs(y_new)) * rtol
|
||||
error_norm = self._estimate_error_norm(self.K, h, scale)
|
||||
|
||||
if error_norm < 1:
|
||||
if error_norm == 0:
|
||||
factor = MAX_FACTOR
|
||||
else:
|
||||
factor = min(MAX_FACTOR,
|
||||
SAFETY * error_norm ** self.error_exponent)
|
||||
|
||||
if step_rejected:
|
||||
factor = min(1, factor)
|
||||
|
||||
h_abs *= factor
|
||||
|
||||
step_accepted = True
|
||||
else:
|
||||
h_abs *= max(MIN_FACTOR,
|
||||
SAFETY * error_norm ** self.error_exponent)
|
||||
step_rejected = True
|
||||
|
||||
self.h_previous = h
|
||||
self.y_old = y
|
||||
|
||||
self.t = t_new
|
||||
self.y = y_new
|
||||
|
||||
self.h_abs = h_abs
|
||||
self.f = f_new
|
||||
|
||||
return True, None
|
||||
|
||||
def _dense_output_impl(self):
|
||||
Q = self.K.T.dot(self.P)
|
||||
return RkDenseOutput(self.t_old, self.t, self.y_old, Q)
|
||||
|
||||
|
||||
class RK23(RungeKutta):
|
||||
"""Explicit Runge-Kutta method of order 3(2).
|
||||
|
||||
This uses the Bogacki-Shampine pair of formulas [1]_. The error is controlled
|
||||
assuming accuracy of the second-order method, but steps are taken using the
|
||||
third-order accurate formula (local extrapolation is done). A cubic Hermite
|
||||
polynomial is used for the dense output.
|
||||
|
||||
Can be applied in the complex domain.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system: the time derivative of the state ``y``
|
||||
at time ``t``. The calling signature is ``fun(t, y)``, where ``t`` is a
|
||||
scalar and ``y`` is an ndarray with ``len(y) = len(y0)``. ``fun`` must
|
||||
return an array of the same shape as ``y``. See `vectorized` for more
|
||||
information.
|
||||
t0 : float
|
||||
Initial time.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state.
|
||||
t_bound : float
|
||||
Boundary time - the integration won't continue beyond it. It also
|
||||
determines the direction of the integration.
|
||||
first_step : float or None, optional
|
||||
Initial step size. Default is ``None`` which means that the algorithm
|
||||
should choose.
|
||||
max_step : float, optional
|
||||
Maximum allowed step size. Default is np.inf, i.e., the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
rtol, atol : float and array_like, optional
|
||||
Relative and absolute tolerances. The solver keeps the local error
|
||||
estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a
|
||||
relative accuracy (number of correct digits), while `atol` controls
|
||||
absolute accuracy (number of correct decimal places). To achieve the
|
||||
desired `rtol`, set `atol` to be smaller than the smallest value that
|
||||
can be expected from ``rtol * abs(y)`` so that `rtol` dominates the
|
||||
allowable error. If `atol` is larger than ``rtol * abs(y)`` the
|
||||
number of correct digits is not guaranteed. Conversely, to achieve the
|
||||
desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller
|
||||
than `atol`. If components of y have different scales, it might be
|
||||
beneficial to set different `atol` values for different components by
|
||||
passing array_like with shape (n,) for `atol`. Default values are
|
||||
1e-3 for `rtol` and 1e-6 for `atol`.
|
||||
vectorized : bool, optional
|
||||
Whether `fun` may be called in a vectorized fashion. False (default)
|
||||
is recommended for this solver.
|
||||
|
||||
If ``vectorized`` is False, `fun` will always be called with ``y`` of
|
||||
shape ``(n,)``, where ``n = len(y0)``.
|
||||
|
||||
If ``vectorized`` is True, `fun` may be called with ``y`` of shape
|
||||
``(n, k)``, where ``k`` is an integer. In this case, `fun` must behave
|
||||
such that ``fun(t, y)[:, i] == fun(t, y[:, i])`` (i.e. each column of
|
||||
the returned array is the time derivative of the state corresponding
|
||||
with a column of ``y``).
|
||||
|
||||
Setting ``vectorized=True`` allows for faster finite difference
|
||||
approximation of the Jacobian by methods 'Radau' and 'BDF', but
|
||||
will result in slower execution for this solver.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
n : int
|
||||
Number of equations.
|
||||
status : string
|
||||
Current status of the solver: 'running', 'finished' or 'failed'.
|
||||
t_bound : float
|
||||
Boundary time.
|
||||
direction : float
|
||||
Integration direction: +1 or -1.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray
|
||||
Current state.
|
||||
t_old : float
|
||||
Previous time. None if no steps were made yet.
|
||||
step_size : float
|
||||
Size of the last successful step. None if no steps were made yet.
|
||||
nfev : int
|
||||
Number evaluations of the system's right-hand side.
|
||||
njev : int
|
||||
Number of evaluations of the Jacobian.
|
||||
Is always 0 for this solver as it does not use the Jacobian.
|
||||
nlu : int
|
||||
Number of LU decompositions. Is always 0 for this solver.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] P. Bogacki, L.F. Shampine, "A 3(2) Pair of Runge-Kutta Formulas",
|
||||
Appl. Math. Lett. Vol. 2, No. 4. pp. 321-325, 1989.
|
||||
"""
|
||||
order = 3
|
||||
error_estimator_order = 2
|
||||
n_stages = 3
|
||||
C = np.array([0, 1/2, 3/4])
|
||||
A = np.array([
|
||||
[0, 0, 0],
|
||||
[1/2, 0, 0],
|
||||
[0, 3/4, 0]
|
||||
])
|
||||
B = np.array([2/9, 1/3, 4/9])
|
||||
E = np.array([5/72, -1/12, -1/9, 1/8])
|
||||
P = np.array([[1, -4 / 3, 5 / 9],
|
||||
[0, 1, -2/3],
|
||||
[0, 4/3, -8/9],
|
||||
[0, -1, 1]])
|
||||
|
||||
|
||||
class RK45(RungeKutta):
|
||||
"""Explicit Runge-Kutta method of order 5(4).
|
||||
|
||||
This uses the Dormand-Prince pair of formulas [1]_. The error is controlled
|
||||
assuming accuracy of the fourth-order method accuracy, but steps are taken
|
||||
using the fifth-order accurate formula (local extrapolation is done).
|
||||
A quartic interpolation polynomial is used for the dense output [2]_.
|
||||
|
||||
Can be applied in the complex domain.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system. The calling signature is ``fun(t, y)``.
|
||||
Here ``t`` is a scalar, and there are two options for the ndarray ``y``:
|
||||
It can either have shape (n,); then ``fun`` must return array_like with
|
||||
shape (n,). Alternatively it can have shape (n, k); then ``fun``
|
||||
must return an array_like with shape (n, k), i.e., each column
|
||||
corresponds to a single column in ``y``. The choice between the two
|
||||
options is determined by `vectorized` argument (see below).
|
||||
t0 : float
|
||||
Initial time.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state.
|
||||
t_bound : float
|
||||
Boundary time - the integration won't continue beyond it. It also
|
||||
determines the direction of the integration.
|
||||
first_step : float or None, optional
|
||||
Initial step size. Default is ``None`` which means that the algorithm
|
||||
should choose.
|
||||
max_step : float, optional
|
||||
Maximum allowed step size. Default is np.inf, i.e., the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
rtol, atol : float and array_like, optional
|
||||
Relative and absolute tolerances. The solver keeps the local error
|
||||
estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a
|
||||
relative accuracy (number of correct digits), while `atol` controls
|
||||
absolute accuracy (number of correct decimal places). To achieve the
|
||||
desired `rtol`, set `atol` to be smaller than the smallest value that
|
||||
can be expected from ``rtol * abs(y)`` so that `rtol` dominates the
|
||||
allowable error. If `atol` is larger than ``rtol * abs(y)`` the
|
||||
number of correct digits is not guaranteed. Conversely, to achieve the
|
||||
desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller
|
||||
than `atol`. If components of y have different scales, it might be
|
||||
beneficial to set different `atol` values for different components by
|
||||
passing array_like with shape (n,) for `atol`. Default values are
|
||||
1e-3 for `rtol` and 1e-6 for `atol`.
|
||||
vectorized : bool, optional
|
||||
Whether `fun` is implemented in a vectorized fashion. Default is False.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
n : int
|
||||
Number of equations.
|
||||
status : string
|
||||
Current status of the solver: 'running', 'finished' or 'failed'.
|
||||
t_bound : float
|
||||
Boundary time.
|
||||
direction : float
|
||||
Integration direction: +1 or -1.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray
|
||||
Current state.
|
||||
t_old : float
|
||||
Previous time. None if no steps were made yet.
|
||||
step_size : float
|
||||
Size of the last successful step. None if no steps were made yet.
|
||||
nfev : int
|
||||
Number evaluations of the system's right-hand side.
|
||||
njev : int
|
||||
Number of evaluations of the Jacobian.
|
||||
Is always 0 for this solver as it does not use the Jacobian.
|
||||
nlu : int
|
||||
Number of LU decompositions. Is always 0 for this solver.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] J. R. Dormand, P. J. Prince, "A family of embedded Runge-Kutta
|
||||
formulae", Journal of Computational and Applied Mathematics, Vol. 6,
|
||||
No. 1, pp. 19-26, 1980.
|
||||
.. [2] L. W. Shampine, "Some Practical Runge-Kutta Formulas", Mathematics
|
||||
of Computation,, Vol. 46, No. 173, pp. 135-150, 1986.
|
||||
"""
|
||||
order = 5
|
||||
error_estimator_order = 4
|
||||
n_stages = 6
|
||||
C = np.array([0, 1/5, 3/10, 4/5, 8/9, 1])
|
||||
A = np.array([
|
||||
[0, 0, 0, 0, 0],
|
||||
[1/5, 0, 0, 0, 0],
|
||||
[3/40, 9/40, 0, 0, 0],
|
||||
[44/45, -56/15, 32/9, 0, 0],
|
||||
[19372/6561, -25360/2187, 64448/6561, -212/729, 0],
|
||||
[9017/3168, -355/33, 46732/5247, 49/176, -5103/18656]
|
||||
])
|
||||
B = np.array([35/384, 0, 500/1113, 125/192, -2187/6784, 11/84])
|
||||
E = np.array([-71/57600, 0, 71/16695, -71/1920, 17253/339200, -22/525,
|
||||
1/40])
|
||||
# Corresponds to the optimum value of c_6 from [2]_.
|
||||
P = np.array([
|
||||
[1, -8048581381/2820520608, 8663915743/2820520608,
|
||||
-12715105075/11282082432],
|
||||
[0, 0, 0, 0],
|
||||
[0, 131558114200/32700410799, -68118460800/10900136933,
|
||||
87487479700/32700410799],
|
||||
[0, -1754552775/470086768, 14199869525/1410260304,
|
||||
-10690763975/1880347072],
|
||||
[0, 127303824393/49829197408, -318862633887/49829197408,
|
||||
701980252875 / 199316789632],
|
||||
[0, -282668133/205662961, 2019193451/616988883, -1453857185/822651844],
|
||||
[0, 40617522/29380423, -110615467/29380423, 69997945/29380423]])
|
||||
|
||||
|
||||
class DOP853(RungeKutta):
|
||||
"""Explicit Runge-Kutta method of order 8.
|
||||
|
||||
This is a Python implementation of "DOP853" algorithm originally written
|
||||
in Fortran [1]_, [2]_. Note that this is not a literal translation, but
|
||||
the algorithmic core and coefficients are the same.
|
||||
|
||||
Can be applied in the complex domain.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fun : callable
|
||||
Right-hand side of the system. The calling signature is ``fun(t, y)``.
|
||||
Here, ``t`` is a scalar, and there are two options for the ndarray ``y``:
|
||||
It can either have shape (n,); then ``fun`` must return array_like with
|
||||
shape (n,). Alternatively it can have shape (n, k); then ``fun``
|
||||
must return an array_like with shape (n, k), i.e. each column
|
||||
corresponds to a single column in ``y``. The choice between the two
|
||||
options is determined by `vectorized` argument (see below).
|
||||
t0 : float
|
||||
Initial time.
|
||||
y0 : array_like, shape (n,)
|
||||
Initial state.
|
||||
t_bound : float
|
||||
Boundary time - the integration won't continue beyond it. It also
|
||||
determines the direction of the integration.
|
||||
first_step : float or None, optional
|
||||
Initial step size. Default is ``None`` which means that the algorithm
|
||||
should choose.
|
||||
max_step : float, optional
|
||||
Maximum allowed step size. Default is np.inf, i.e. the step size is not
|
||||
bounded and determined solely by the solver.
|
||||
rtol, atol : float and array_like, optional
|
||||
Relative and absolute tolerances. The solver keeps the local error
|
||||
estimates less than ``atol + rtol * abs(y)``. Here `rtol` controls a
|
||||
relative accuracy (number of correct digits), while `atol` controls
|
||||
absolute accuracy (number of correct decimal places). To achieve the
|
||||
desired `rtol`, set `atol` to be smaller than the smallest value that
|
||||
can be expected from ``rtol * abs(y)`` so that `rtol` dominates the
|
||||
allowable error. If `atol` is larger than ``rtol * abs(y)`` the
|
||||
number of correct digits is not guaranteed. Conversely, to achieve the
|
||||
desired `atol` set `rtol` such that ``rtol * abs(y)`` is always smaller
|
||||
than `atol`. If components of y have different scales, it might be
|
||||
beneficial to set different `atol` values for different components by
|
||||
passing array_like with shape (n,) for `atol`. Default values are
|
||||
1e-3 for `rtol` and 1e-6 for `atol`.
|
||||
vectorized : bool, optional
|
||||
Whether `fun` is implemented in a vectorized fashion. Default is False.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
n : int
|
||||
Number of equations.
|
||||
status : string
|
||||
Current status of the solver: 'running', 'finished' or 'failed'.
|
||||
t_bound : float
|
||||
Boundary time.
|
||||
direction : float
|
||||
Integration direction: +1 or -1.
|
||||
t : float
|
||||
Current time.
|
||||
y : ndarray
|
||||
Current state.
|
||||
t_old : float
|
||||
Previous time. None if no steps were made yet.
|
||||
step_size : float
|
||||
Size of the last successful step. None if no steps were made yet.
|
||||
nfev : int
|
||||
Number evaluations of the system's right-hand side.
|
||||
njev : int
|
||||
Number of evaluations of the Jacobian. Is always 0 for this solver
|
||||
as it does not use the Jacobian.
|
||||
nlu : int
|
||||
Number of LU decompositions. Is always 0 for this solver.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. Hairer, S. P. Norsett G. Wanner, "Solving Ordinary Differential
|
||||
Equations I: Nonstiff Problems", Sec. II.
|
||||
.. [2] `Page with original Fortran code of DOP853
|
||||
<http://www.unige.ch/~hairer/software.html>`_.
|
||||
"""
|
||||
n_stages = dop853_coefficients.N_STAGES
|
||||
order = 8
|
||||
error_estimator_order = 7
|
||||
A = dop853_coefficients.A[:n_stages, :n_stages]
|
||||
B = dop853_coefficients.B
|
||||
C = dop853_coefficients.C[:n_stages]
|
||||
E3 = dop853_coefficients.E3
|
||||
E5 = dop853_coefficients.E5
|
||||
D = dop853_coefficients.D
|
||||
|
||||
A_EXTRA = dop853_coefficients.A[n_stages + 1:]
|
||||
C_EXTRA = dop853_coefficients.C[n_stages + 1:]
|
||||
|
||||
def __init__(self, fun, t0, y0, t_bound, max_step=np.inf,
|
||||
rtol=1e-3, atol=1e-6, vectorized=False,
|
||||
first_step=None, **extraneous):
|
||||
super().__init__(fun, t0, y0, t_bound, max_step, rtol, atol,
|
||||
vectorized, first_step, **extraneous)
|
||||
self.K_extended = np.empty((dop853_coefficients.N_STAGES_EXTENDED,
|
||||
self.n), dtype=self.y.dtype)
|
||||
self.K = self.K_extended[:self.n_stages + 1]
|
||||
|
||||
def _estimate_error(self, K, h): # Left for testing purposes.
|
||||
err5 = np.dot(K.T, self.E5)
|
||||
err3 = np.dot(K.T, self.E3)
|
||||
denom = np.hypot(np.abs(err5), 0.1 * np.abs(err3))
|
||||
correction_factor = np.ones_like(err5)
|
||||
mask = denom > 0
|
||||
correction_factor[mask] = np.abs(err5[mask]) / denom[mask]
|
||||
return h * err5 * correction_factor
|
||||
|
||||
def _estimate_error_norm(self, K, h, scale):
|
||||
err5 = np.dot(K.T, self.E5) / scale
|
||||
err3 = np.dot(K.T, self.E3) / scale
|
||||
err5_norm_2 = np.linalg.norm(err5)**2
|
||||
err3_norm_2 = np.linalg.norm(err3)**2
|
||||
if err5_norm_2 == 0 and err3_norm_2 == 0:
|
||||
return 0.0
|
||||
denom = err5_norm_2 + 0.01 * err3_norm_2
|
||||
return np.abs(h) * err5_norm_2 / np.sqrt(denom * len(scale))
|
||||
|
||||
def _dense_output_impl(self):
|
||||
K = self.K_extended
|
||||
h = self.h_previous
|
||||
for s, (a, c) in enumerate(zip(self.A_EXTRA, self.C_EXTRA),
|
||||
start=self.n_stages + 1):
|
||||
dy = np.dot(K[:s].T, a[:s]) * h
|
||||
K[s] = self.fun(self.t_old + c * h, self.y_old + dy)
|
||||
|
||||
F = np.empty((dop853_coefficients.INTERPOLATOR_POWER, self.n),
|
||||
dtype=self.y_old.dtype)
|
||||
|
||||
f_old = K[0]
|
||||
delta_y = self.y - self.y_old
|
||||
|
||||
F[0] = delta_y
|
||||
F[1] = h * f_old - delta_y
|
||||
F[2] = 2 * delta_y - h * (self.f + f_old)
|
||||
F[3:] = h * np.dot(self.D, K)
|
||||
|
||||
return Dop853DenseOutput(self.t_old, self.t, self.y_old, F)
|
||||
|
||||
|
||||
class RkDenseOutput(DenseOutput):
|
||||
def __init__(self, t_old, t, y_old, Q):
|
||||
super().__init__(t_old, t)
|
||||
self.h = t - t_old
|
||||
self.Q = Q
|
||||
self.order = Q.shape[1] - 1
|
||||
self.y_old = y_old
|
||||
|
||||
def _call_impl(self, t):
|
||||
x = (t - self.t_old) / self.h
|
||||
if t.ndim == 0:
|
||||
p = np.tile(x, self.order + 1)
|
||||
p = np.cumprod(p)
|
||||
else:
|
||||
p = np.tile(x, (self.order + 1, 1))
|
||||
p = np.cumprod(p, axis=0)
|
||||
y = self.h * np.dot(self.Q, p)
|
||||
if y.ndim == 2:
|
||||
y += self.y_old[:, None]
|
||||
else:
|
||||
y += self.y_old
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Dop853DenseOutput(DenseOutput):
|
||||
def __init__(self, t_old, t, y_old, F):
|
||||
super().__init__(t_old, t)
|
||||
self.h = t - t_old
|
||||
self.F = F
|
||||
self.y_old = y_old
|
||||
|
||||
def _call_impl(self, t):
|
||||
x = (t - self.t_old) / self.h
|
||||
|
||||
if t.ndim == 0:
|
||||
y = np.zeros_like(self.y_old)
|
||||
else:
|
||||
x = x[:, None]
|
||||
y = np.zeros((len(x), len(self.y_old)), dtype=self.y_old.dtype)
|
||||
|
||||
for i, f in enumerate(reversed(self.F)):
|
||||
y += f
|
||||
if i % 2 == 0:
|
||||
y *= x
|
||||
else:
|
||||
y *= 1 - x
|
||||
y += self.y_old
|
||||
|
||||
return y.T
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose, assert_
|
||||
import numpy as np
|
||||
from scipy.integrate import RK23, RK45, DOP853
|
||||
from scipy.integrate._ivp import dop853_coefficients
|
||||
|
||||
|
||||
@pytest.mark.parametrize("solver", [RK23, RK45, DOP853])
|
||||
def test_coefficient_properties(solver):
|
||||
assert_allclose(np.sum(solver.B), 1, rtol=1e-15)
|
||||
assert_allclose(np.sum(solver.A, axis=1), solver.C, rtol=1e-14)
|
||||
|
||||
|
||||
def test_coefficient_properties_dop853():
|
||||
assert_allclose(np.sum(dop853_coefficients.B), 1, rtol=1e-15)
|
||||
assert_allclose(np.sum(dop853_coefficients.A, axis=1),
|
||||
dop853_coefficients.C,
|
||||
rtol=1e-14)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("solver_class", [RK23, RK45, DOP853])
|
||||
def test_error_estimation(solver_class):
|
||||
step = 0.2
|
||||
solver = solver_class(lambda t, y: y, 0, [1], 1, first_step=step)
|
||||
solver.step()
|
||||
error_estimate = solver._estimate_error(solver.K, step)
|
||||
error = solver.y - np.exp([step])
|
||||
assert_(np.abs(error) < np.abs(error_estimate))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("solver_class", [RK23, RK45, DOP853])
|
||||
def test_error_estimation_complex(solver_class):
|
||||
h = 0.2
|
||||
solver = solver_class(lambda t, y: 1j * y, 0, [1j], 1, first_step=h)
|
||||
solver.step()
|
||||
err_norm = solver._estimate_error_norm(solver.K, h, scale=[1])
|
||||
assert np.isrealobj(err_norm)
|
||||
Reference in New Issue
Block a user