asd
This commit is contained in:
110
venv/lib/python3.12/site-packages/scipy/integrate/__init__.py
Normal file
110
venv/lib/python3.12/site-packages/scipy/integrate/__init__.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""
|
||||
=============================================
|
||||
Integration and ODEs (:mod:`scipy.integrate`)
|
||||
=============================================
|
||||
|
||||
.. currentmodule:: scipy.integrate
|
||||
|
||||
Integrating functions, given function object
|
||||
============================================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
quad -- General purpose integration
|
||||
quad_vec -- General purpose integration of vector-valued functions
|
||||
dblquad -- General purpose double integration
|
||||
tplquad -- General purpose triple integration
|
||||
nquad -- General purpose N-D integration
|
||||
fixed_quad -- Integrate func(x) using Gaussian quadrature of order n
|
||||
quadrature -- Integrate with given tolerance using Gaussian quadrature
|
||||
romberg -- Integrate func using Romberg integration
|
||||
newton_cotes -- Weights and error coefficient for Newton-Cotes integration
|
||||
qmc_quad -- N-D integration using Quasi-Monte Carlo quadrature
|
||||
IntegrationWarning -- Warning on issues during integration
|
||||
AccuracyWarning -- Warning on issues during quadrature integration
|
||||
|
||||
Integrating functions, given fixed samples
|
||||
==========================================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
trapezoid -- Use trapezoidal rule to compute integral.
|
||||
cumulative_trapezoid -- Use trapezoidal rule to cumulatively compute integral.
|
||||
simpson -- Use Simpson's rule to compute integral from samples.
|
||||
cumulative_simpson -- Use Simpson's rule to cumulatively compute integral from samples.
|
||||
romb -- Use Romberg Integration to compute integral from
|
||||
-- (2**k + 1) evenly-spaced samples.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:mod:`scipy.special` for orthogonal polynomials (special) for Gaussian
|
||||
quadrature roots and weights for other weighting factors and regions.
|
||||
|
||||
Solving initial value problems for ODE systems
|
||||
==============================================
|
||||
|
||||
The solvers are implemented as individual classes, which can be used directly
|
||||
(low-level usage) or through a convenience function.
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
solve_ivp -- Convenient function for ODE integration.
|
||||
RK23 -- Explicit Runge-Kutta solver of order 3(2).
|
||||
RK45 -- Explicit Runge-Kutta solver of order 5(4).
|
||||
DOP853 -- Explicit Runge-Kutta solver of order 8.
|
||||
Radau -- Implicit Runge-Kutta solver of order 5.
|
||||
BDF -- Implicit multi-step variable order (1 to 5) solver.
|
||||
LSODA -- LSODA solver from ODEPACK Fortran package.
|
||||
OdeSolver -- Base class for ODE solvers.
|
||||
DenseOutput -- Local interpolant for computing a dense output.
|
||||
OdeSolution -- Class which represents a continuous ODE solution.
|
||||
|
||||
|
||||
Old API
|
||||
-------
|
||||
|
||||
These are the routines developed earlier for SciPy. They wrap older solvers
|
||||
implemented in Fortran (mostly ODEPACK). While the interface to them is not
|
||||
particularly convenient and certain features are missing compared to the new
|
||||
API, the solvers themselves are of good quality and work fast as compiled
|
||||
Fortran code. In some cases, it might be worth using this old API.
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
odeint -- General integration of ordinary differential equations.
|
||||
ode -- Integrate ODE using VODE and ZVODE routines.
|
||||
complex_ode -- Convert a complex-valued ODE to real-valued and integrate.
|
||||
ODEintWarning -- Warning raised during the execution of `odeint`.
|
||||
|
||||
|
||||
Solving boundary value problems for ODE systems
|
||||
===============================================
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
solve_bvp -- Solve a boundary value problem for a system of ODEs.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from ._quadrature import *
|
||||
from ._odepack_py import *
|
||||
from ._quadpack_py import *
|
||||
from ._ode import *
|
||||
from ._bvp import solve_bvp
|
||||
from ._ivp import (solve_ivp, OdeSolution, DenseOutput,
|
||||
OdeSolver, RK23, RK45, DOP853, Radau, BDF, LSODA)
|
||||
from ._quad_vec import quad_vec
|
||||
|
||||
# Deprecated namespaces, to be removed in v2.0.0
|
||||
from . import dop, lsoda, vode, odepack, quadpack
|
||||
|
||||
__all__ = [s for s in dir() if not s.startswith('_')]
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
1155
venv/lib/python3.12/site-packages/scipy/integrate/_bvp.py
Normal file
1155
venv/lib/python3.12/site-packages/scipy/integrate/_bvp.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -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)
|
||||
Binary file not shown.
1376
venv/lib/python3.12/site-packages/scipy/integrate/_ode.py
Normal file
1376
venv/lib/python3.12/site-packages/scipy/integrate/_ode.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
266
venv/lib/python3.12/site-packages/scipy/integrate/_odepack_py.py
Normal file
266
venv/lib/python3.12/site-packages/scipy/integrate/_odepack_py.py
Normal file
@ -0,0 +1,266 @@
|
||||
# Author: Travis Oliphant
|
||||
|
||||
__all__ = ['odeint', 'ODEintWarning']
|
||||
|
||||
import numpy as np
|
||||
from . import _odepack
|
||||
from copy import copy
|
||||
import warnings
|
||||
|
||||
|
||||
class ODEintWarning(Warning):
|
||||
"""Warning raised during the execution of `odeint`."""
|
||||
pass
|
||||
|
||||
|
||||
_msgs = {2: "Integration successful.",
|
||||
1: "Nothing was done; the integration time was 0.",
|
||||
-1: "Excess work done on this call (perhaps wrong Dfun type).",
|
||||
-2: "Excess accuracy requested (tolerances too small).",
|
||||
-3: "Illegal input detected (internal error).",
|
||||
-4: "Repeated error test failures (internal error).",
|
||||
-5: "Repeated convergence failures (perhaps bad Jacobian or tolerances).",
|
||||
-6: "Error weight became zero during problem.",
|
||||
-7: "Internal workspace insufficient to finish (internal error).",
|
||||
-8: "Run terminated (internal error)."
|
||||
}
|
||||
|
||||
|
||||
def odeint(func, y0, t, args=(), Dfun=None, col_deriv=0, full_output=0,
|
||||
ml=None, mu=None, rtol=None, atol=None, tcrit=None, h0=0.0,
|
||||
hmax=0.0, hmin=0.0, ixpr=0, mxstep=0, mxhnil=0, mxordn=12,
|
||||
mxords=5, printmessg=0, tfirst=False):
|
||||
"""
|
||||
Integrate a system of ordinary differential equations.
|
||||
|
||||
.. note:: For new code, use `scipy.integrate.solve_ivp` to solve a
|
||||
differential equation.
|
||||
|
||||
Solve a system of ordinary differential equations using lsoda from the
|
||||
FORTRAN library odepack.
|
||||
|
||||
Solves the initial value problem for stiff or non-stiff systems
|
||||
of first order ode-s::
|
||||
|
||||
dy/dt = func(y, t, ...) [or func(t, y, ...)]
|
||||
|
||||
where y can be a vector.
|
||||
|
||||
.. note:: By default, the required order of the first two arguments of
|
||||
`func` are in the opposite order of the arguments in the system
|
||||
definition function used by the `scipy.integrate.ode` class and
|
||||
the function `scipy.integrate.solve_ivp`. To use a function with
|
||||
the signature ``func(t, y, ...)``, the argument `tfirst` must be
|
||||
set to ``True``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : callable(y, t, ...) or callable(t, y, ...)
|
||||
Computes the derivative of y at t.
|
||||
If the signature is ``callable(t, y, ...)``, then the argument
|
||||
`tfirst` must be set ``True``.
|
||||
`func` must not modify the data in `y`, as it is a
|
||||
view of the data used internally by the ODE solver.
|
||||
y0 : array
|
||||
Initial condition on y (can be a vector).
|
||||
t : array
|
||||
A sequence of time points for which to solve for y. The initial
|
||||
value point should be the first element of this sequence.
|
||||
This sequence must be monotonically increasing or monotonically
|
||||
decreasing; repeated values are allowed.
|
||||
args : tuple, optional
|
||||
Extra arguments to pass to function.
|
||||
Dfun : callable(y, t, ...) or callable(t, y, ...)
|
||||
Gradient (Jacobian) of `func`.
|
||||
If the signature is ``callable(t, y, ...)``, then the argument
|
||||
`tfirst` must be set ``True``.
|
||||
`Dfun` must not modify the data in `y`, as it is a
|
||||
view of the data used internally by the ODE solver.
|
||||
col_deriv : bool, optional
|
||||
True if `Dfun` defines derivatives down columns (faster),
|
||||
otherwise `Dfun` should define derivatives across rows.
|
||||
full_output : bool, optional
|
||||
True if to return a dictionary of optional outputs as the second output
|
||||
printmessg : bool, optional
|
||||
Whether to print the convergence message
|
||||
tfirst : bool, optional
|
||||
If True, the first two arguments of `func` (and `Dfun`, if given)
|
||||
must ``t, y`` instead of the default ``y, t``.
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : array, shape (len(t), len(y0))
|
||||
Array containing the value of y for each desired time in t,
|
||||
with the initial value `y0` in the first row.
|
||||
infodict : dict, only returned if full_output == True
|
||||
Dictionary containing additional output information
|
||||
|
||||
======= ============================================================
|
||||
key meaning
|
||||
======= ============================================================
|
||||
'hu' vector of step sizes successfully used for each time step
|
||||
'tcur' vector with the value of t reached for each time step
|
||||
(will always be at least as large as the input times)
|
||||
'tolsf' vector of tolerance scale factors, greater than 1.0,
|
||||
computed when a request for too much accuracy was detected
|
||||
'tsw' value of t at the time of the last method switch
|
||||
(given for each time step)
|
||||
'nst' cumulative number of time steps
|
||||
'nfe' cumulative number of function evaluations for each time step
|
||||
'nje' cumulative number of jacobian evaluations for each time step
|
||||
'nqu' a vector of method orders for each successful step
|
||||
'imxer' index of the component of largest magnitude in the
|
||||
weighted local error vector (e / ewt) on an error return, -1
|
||||
otherwise
|
||||
'lenrw' the length of the double work array required
|
||||
'leniw' the length of integer work array required
|
||||
'mused' a vector of method indicators for each successful time step:
|
||||
1: adams (nonstiff), 2: bdf (stiff)
|
||||
======= ============================================================
|
||||
|
||||
Other Parameters
|
||||
----------------
|
||||
ml, mu : int, optional
|
||||
If either of these are not None or non-negative, then the
|
||||
Jacobian is assumed to be banded. These give the number of
|
||||
lower and upper non-zero diagonals in this banded matrix.
|
||||
For the banded case, `Dfun` should return a matrix whose
|
||||
rows contain the non-zero bands (starting with the lowest diagonal).
|
||||
Thus, the return matrix `jac` from `Dfun` should have shape
|
||||
``(ml + mu + 1, len(y0))`` when ``ml >=0`` or ``mu >=0``.
|
||||
The data in `jac` must be stored such that ``jac[i - j + mu, j]``
|
||||
holds the derivative of the ``i``\\ th equation with respect to the
|
||||
``j``\\ th state variable. If `col_deriv` is True, the transpose of
|
||||
this `jac` must be returned.
|
||||
rtol, atol : float, optional
|
||||
The input parameters `rtol` and `atol` determine the error
|
||||
control performed by the solver. The solver will control the
|
||||
vector, e, of estimated local errors in y, according to an
|
||||
inequality of the form ``max-norm of (e / ewt) <= 1``,
|
||||
where ewt is a vector of positive error weights computed as
|
||||
``ewt = rtol * abs(y) + atol``.
|
||||
rtol and atol can be either vectors the same length as y or scalars.
|
||||
Defaults to 1.49012e-8.
|
||||
tcrit : ndarray, optional
|
||||
Vector of critical points (e.g., singularities) where integration
|
||||
care should be taken.
|
||||
h0 : float, (0: solver-determined), optional
|
||||
The step size to be attempted on the first step.
|
||||
hmax : float, (0: solver-determined), optional
|
||||
The maximum absolute step size allowed.
|
||||
hmin : float, (0: solver-determined), optional
|
||||
The minimum absolute step size allowed.
|
||||
ixpr : bool, optional
|
||||
Whether to generate extra printing at method switches.
|
||||
mxstep : int, (0: solver-determined), optional
|
||||
Maximum number of (internally defined) steps allowed for each
|
||||
integration point in t.
|
||||
mxhnil : int, (0: solver-determined), optional
|
||||
Maximum number of messages printed.
|
||||
mxordn : int, (0: solver-determined), optional
|
||||
Maximum order to be allowed for the non-stiff (Adams) method.
|
||||
mxords : int, (0: solver-determined), optional
|
||||
Maximum order to be allowed for the stiff (BDF) method.
|
||||
|
||||
See Also
|
||||
--------
|
||||
solve_ivp : solve an initial value problem for a system of ODEs
|
||||
ode : a more object-oriented integrator based on VODE
|
||||
quad : for finding the area under a curve
|
||||
|
||||
Examples
|
||||
--------
|
||||
The second order differential equation for the angle `theta` of a
|
||||
pendulum acted on by gravity with friction can be written::
|
||||
|
||||
theta''(t) + b*theta'(t) + c*sin(theta(t)) = 0
|
||||
|
||||
where `b` and `c` are positive constants, and a prime (') denotes a
|
||||
derivative. To solve this equation with `odeint`, we must first convert
|
||||
it to a system of first order equations. By defining the angular
|
||||
velocity ``omega(t) = theta'(t)``, we obtain the system::
|
||||
|
||||
theta'(t) = omega(t)
|
||||
omega'(t) = -b*omega(t) - c*sin(theta(t))
|
||||
|
||||
Let `y` be the vector [`theta`, `omega`]. We implement this system
|
||||
in Python as:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> def pend(y, t, b, c):
|
||||
... theta, omega = y
|
||||
... dydt = [omega, -b*omega - c*np.sin(theta)]
|
||||
... return dydt
|
||||
...
|
||||
|
||||
We assume the constants are `b` = 0.25 and `c` = 5.0:
|
||||
|
||||
>>> b = 0.25
|
||||
>>> c = 5.0
|
||||
|
||||
For initial conditions, we assume the pendulum is nearly vertical
|
||||
with `theta(0)` = `pi` - 0.1, and is initially at rest, so
|
||||
`omega(0)` = 0. Then the vector of initial conditions is
|
||||
|
||||
>>> y0 = [np.pi - 0.1, 0.0]
|
||||
|
||||
We will generate a solution at 101 evenly spaced samples in the interval
|
||||
0 <= `t` <= 10. So our array of times is:
|
||||
|
||||
>>> t = np.linspace(0, 10, 101)
|
||||
|
||||
Call `odeint` to generate the solution. To pass the parameters
|
||||
`b` and `c` to `pend`, we give them to `odeint` using the `args`
|
||||
argument.
|
||||
|
||||
>>> from scipy.integrate import odeint
|
||||
>>> sol = odeint(pend, y0, t, args=(b, c))
|
||||
|
||||
The solution is an array with shape (101, 2). The first column
|
||||
is `theta(t)`, and the second is `omega(t)`. The following code
|
||||
plots both components.
|
||||
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> plt.plot(t, sol[:, 0], 'b', label='theta(t)')
|
||||
>>> plt.plot(t, sol[:, 1], 'g', label='omega(t)')
|
||||
>>> plt.legend(loc='best')
|
||||
>>> plt.xlabel('t')
|
||||
>>> plt.grid()
|
||||
>>> plt.show()
|
||||
"""
|
||||
|
||||
if ml is None:
|
||||
ml = -1 # changed to zero inside function call
|
||||
if mu is None:
|
||||
mu = -1 # changed to zero inside function call
|
||||
|
||||
dt = np.diff(t)
|
||||
if not ((dt >= 0).all() or (dt <= 0).all()):
|
||||
raise ValueError("The values in t must be monotonically increasing "
|
||||
"or monotonically decreasing; repeated values are "
|
||||
"allowed.")
|
||||
|
||||
t = copy(t)
|
||||
y0 = copy(y0)
|
||||
output = _odepack.odeint(func, y0, t, args, Dfun, col_deriv, ml, mu,
|
||||
full_output, rtol, atol, tcrit, h0, hmax, hmin,
|
||||
ixpr, mxstep, mxhnil, mxordn, mxords,
|
||||
int(bool(tfirst)))
|
||||
if output[-1] < 0:
|
||||
warning_msg = (f"{_msgs[output[-1]]} Run with full_output = 1 to "
|
||||
f"get quantitative information.")
|
||||
warnings.warn(warning_msg, ODEintWarning, stacklevel=2)
|
||||
elif printmessg:
|
||||
warning_msg = _msgs[output[-1]]
|
||||
warnings.warn(warning_msg, ODEintWarning, stacklevel=2)
|
||||
|
||||
if full_output:
|
||||
output[1]['message'] = _msgs[output[-1]]
|
||||
|
||||
output = output[:-1]
|
||||
if len(output) == 1:
|
||||
return output[0]
|
||||
else:
|
||||
return output
|
||||
663
venv/lib/python3.12/site-packages/scipy/integrate/_quad_vec.py
Normal file
663
venv/lib/python3.12/site-packages/scipy/integrate/_quad_vec.py
Normal file
@ -0,0 +1,663 @@
|
||||
import sys
|
||||
import copy
|
||||
import heapq
|
||||
import collections
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
from scipy._lib._util import MapWrapper, _FunctionWrapper
|
||||
|
||||
|
||||
class LRUDict(collections.OrderedDict):
|
||||
def __init__(self, max_size):
|
||||
self.__max_size = max_size
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
existing_key = (key in self)
|
||||
super().__setitem__(key, value)
|
||||
if existing_key:
|
||||
self.move_to_end(key)
|
||||
elif len(self) > self.__max_size:
|
||||
self.popitem(last=False)
|
||||
|
||||
def update(self, other):
|
||||
# Not needed below
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SemiInfiniteFunc:
|
||||
"""
|
||||
Argument transform from (start, +-oo) to (0, 1)
|
||||
"""
|
||||
def __init__(self, func, start, infty):
|
||||
self._func = func
|
||||
self._start = start
|
||||
self._sgn = -1 if infty < 0 else 1
|
||||
|
||||
# Overflow threshold for the 1/t**2 factor
|
||||
self._tmin = sys.float_info.min**0.5
|
||||
|
||||
def get_t(self, x):
|
||||
z = self._sgn * (x - self._start) + 1
|
||||
if z == 0:
|
||||
# Can happen only if point not in range
|
||||
return np.inf
|
||||
return 1 / z
|
||||
|
||||
def __call__(self, t):
|
||||
if t < self._tmin:
|
||||
return 0.0
|
||||
else:
|
||||
x = self._start + self._sgn * (1 - t) / t
|
||||
f = self._func(x)
|
||||
return self._sgn * (f / t) / t
|
||||
|
||||
|
||||
class DoubleInfiniteFunc:
|
||||
"""
|
||||
Argument transform from (-oo, oo) to (-1, 1)
|
||||
"""
|
||||
def __init__(self, func):
|
||||
self._func = func
|
||||
|
||||
# Overflow threshold for the 1/t**2 factor
|
||||
self._tmin = sys.float_info.min**0.5
|
||||
|
||||
def get_t(self, x):
|
||||
s = -1 if x < 0 else 1
|
||||
return s / (abs(x) + 1)
|
||||
|
||||
def __call__(self, t):
|
||||
if abs(t) < self._tmin:
|
||||
return 0.0
|
||||
else:
|
||||
x = (1 - abs(t)) / t
|
||||
f = self._func(x)
|
||||
return (f / t) / t
|
||||
|
||||
|
||||
def _max_norm(x):
|
||||
return np.amax(abs(x))
|
||||
|
||||
|
||||
def _get_sizeof(obj):
|
||||
try:
|
||||
return sys.getsizeof(obj)
|
||||
except TypeError:
|
||||
# occurs on pypy
|
||||
if hasattr(obj, '__sizeof__'):
|
||||
return int(obj.__sizeof__())
|
||||
return 64
|
||||
|
||||
|
||||
class _Bunch:
|
||||
def __init__(self, **kwargs):
|
||||
self.__keys = kwargs.keys()
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return "_Bunch({})".format(", ".join(f"{k}={repr(self.__dict__[k])}"
|
||||
for k in self.__keys))
|
||||
|
||||
|
||||
def quad_vec(f, a, b, epsabs=1e-200, epsrel=1e-8, norm='2', cache_size=100e6,
|
||||
limit=10000, workers=1, points=None, quadrature=None, full_output=False,
|
||||
*, args=()):
|
||||
r"""Adaptive integration of a vector-valued function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : callable
|
||||
Vector-valued function f(x) to integrate.
|
||||
a : float
|
||||
Initial point.
|
||||
b : float
|
||||
Final point.
|
||||
epsabs : float, optional
|
||||
Absolute tolerance.
|
||||
epsrel : float, optional
|
||||
Relative tolerance.
|
||||
norm : {'max', '2'}, optional
|
||||
Vector norm to use for error estimation.
|
||||
cache_size : int, optional
|
||||
Number of bytes to use for memoization.
|
||||
limit : float or int, optional
|
||||
An upper bound on the number of subintervals used in the adaptive
|
||||
algorithm.
|
||||
workers : int or map-like callable, optional
|
||||
If `workers` is an integer, part of the computation is done in
|
||||
parallel subdivided to this many tasks (using
|
||||
:class:`python:multiprocessing.pool.Pool`).
|
||||
Supply `-1` to use all cores available to the Process.
|
||||
Alternatively, supply a map-like callable, such as
|
||||
:meth:`python:multiprocessing.pool.Pool.map` for evaluating the
|
||||
population in parallel.
|
||||
This evaluation is carried out as ``workers(func, iterable)``.
|
||||
points : list, optional
|
||||
List of additional breakpoints.
|
||||
quadrature : {'gk21', 'gk15', 'trapezoid'}, optional
|
||||
Quadrature rule to use on subintervals.
|
||||
Options: 'gk21' (Gauss-Kronrod 21-point rule),
|
||||
'gk15' (Gauss-Kronrod 15-point rule),
|
||||
'trapezoid' (composite trapezoid rule).
|
||||
Default: 'gk21' for finite intervals and 'gk15' for (semi-)infinite
|
||||
full_output : bool, optional
|
||||
Return an additional ``info`` dictionary.
|
||||
args : tuple, optional
|
||||
Extra arguments to pass to function, if any.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
res : {float, array-like}
|
||||
Estimate for the result
|
||||
err : float
|
||||
Error estimate for the result in the given norm
|
||||
info : dict
|
||||
Returned only when ``full_output=True``.
|
||||
Info dictionary. Is an object with the attributes:
|
||||
|
||||
success : bool
|
||||
Whether integration reached target precision.
|
||||
status : int
|
||||
Indicator for convergence, success (0),
|
||||
failure (1), and failure due to rounding error (2).
|
||||
neval : int
|
||||
Number of function evaluations.
|
||||
intervals : ndarray, shape (num_intervals, 2)
|
||||
Start and end points of subdivision intervals.
|
||||
integrals : ndarray, shape (num_intervals, ...)
|
||||
Integral for each interval.
|
||||
Note that at most ``cache_size`` values are recorded,
|
||||
and the array may contains *nan* for missing items.
|
||||
errors : ndarray, shape (num_intervals,)
|
||||
Estimated integration error for each interval.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm mainly follows the implementation of QUADPACK's
|
||||
DQAG* algorithms, implementing global error control and adaptive
|
||||
subdivision.
|
||||
|
||||
The algorithm here has some differences to the QUADPACK approach:
|
||||
|
||||
Instead of subdividing one interval at a time, the algorithm
|
||||
subdivides N intervals with largest errors at once. This enables
|
||||
(partial) parallelization of the integration.
|
||||
|
||||
The logic of subdividing "next largest" intervals first is then
|
||||
not implemented, and we rely on the above extension to avoid
|
||||
concentrating on "small" intervals only.
|
||||
|
||||
The Wynn epsilon table extrapolation is not used (QUADPACK uses it
|
||||
for infinite intervals). This is because the algorithm here is
|
||||
supposed to work on vector-valued functions, in an user-specified
|
||||
norm, and the extension of the epsilon algorithm to this case does
|
||||
not appear to be widely agreed. For max-norm, using elementwise
|
||||
Wynn epsilon could be possible, but we do not do this here with
|
||||
the hope that the epsilon extrapolation is mainly useful in
|
||||
special cases.
|
||||
|
||||
References
|
||||
----------
|
||||
[1] R. Piessens, E. de Doncker, QUADPACK (1983).
|
||||
|
||||
Examples
|
||||
--------
|
||||
We can compute integrations of a vector-valued function:
|
||||
|
||||
>>> from scipy.integrate import quad_vec
|
||||
>>> import numpy as np
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> alpha = np.linspace(0.0, 2.0, num=30)
|
||||
>>> f = lambda x: x**alpha
|
||||
>>> x0, x1 = 0, 2
|
||||
>>> y, err = quad_vec(f, x0, x1)
|
||||
>>> plt.plot(alpha, y)
|
||||
>>> plt.xlabel(r"$\alpha$")
|
||||
>>> plt.ylabel(r"$\int_{0}^{2} x^\alpha dx$")
|
||||
>>> plt.show()
|
||||
|
||||
"""
|
||||
a = float(a)
|
||||
b = float(b)
|
||||
|
||||
if args:
|
||||
if not isinstance(args, tuple):
|
||||
args = (args,)
|
||||
|
||||
# create a wrapped function to allow the use of map and Pool.map
|
||||
f = _FunctionWrapper(f, args)
|
||||
|
||||
# Use simple transformations to deal with integrals over infinite
|
||||
# intervals.
|
||||
kwargs = dict(epsabs=epsabs,
|
||||
epsrel=epsrel,
|
||||
norm=norm,
|
||||
cache_size=cache_size,
|
||||
limit=limit,
|
||||
workers=workers,
|
||||
points=points,
|
||||
quadrature='gk15' if quadrature is None else quadrature,
|
||||
full_output=full_output)
|
||||
if np.isfinite(a) and np.isinf(b):
|
||||
f2 = SemiInfiniteFunc(f, start=a, infty=b)
|
||||
if points is not None:
|
||||
kwargs['points'] = tuple(f2.get_t(xp) for xp in points)
|
||||
return quad_vec(f2, 0, 1, **kwargs)
|
||||
elif np.isfinite(b) and np.isinf(a):
|
||||
f2 = SemiInfiniteFunc(f, start=b, infty=a)
|
||||
if points is not None:
|
||||
kwargs['points'] = tuple(f2.get_t(xp) for xp in points)
|
||||
res = quad_vec(f2, 0, 1, **kwargs)
|
||||
return (-res[0],) + res[1:]
|
||||
elif np.isinf(a) and np.isinf(b):
|
||||
sgn = -1 if b < a else 1
|
||||
|
||||
# NB. explicitly split integral at t=0, which separates
|
||||
# the positive and negative sides
|
||||
f2 = DoubleInfiniteFunc(f)
|
||||
if points is not None:
|
||||
kwargs['points'] = (0,) + tuple(f2.get_t(xp) for xp in points)
|
||||
else:
|
||||
kwargs['points'] = (0,)
|
||||
|
||||
if a != b:
|
||||
res = quad_vec(f2, -1, 1, **kwargs)
|
||||
else:
|
||||
res = quad_vec(f2, 1, 1, **kwargs)
|
||||
|
||||
return (res[0]*sgn,) + res[1:]
|
||||
elif not (np.isfinite(a) and np.isfinite(b)):
|
||||
raise ValueError(f"invalid integration bounds a={a}, b={b}")
|
||||
|
||||
norm_funcs = {
|
||||
None: _max_norm,
|
||||
'max': _max_norm,
|
||||
'2': np.linalg.norm
|
||||
}
|
||||
if callable(norm):
|
||||
norm_func = norm
|
||||
else:
|
||||
norm_func = norm_funcs[norm]
|
||||
|
||||
parallel_count = 128
|
||||
min_intervals = 2
|
||||
|
||||
try:
|
||||
_quadrature = {None: _quadrature_gk21,
|
||||
'gk21': _quadrature_gk21,
|
||||
'gk15': _quadrature_gk15,
|
||||
'trapz': _quadrature_trapezoid, # alias for backcompat
|
||||
'trapezoid': _quadrature_trapezoid}[quadrature]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"unknown quadrature {quadrature!r}") from e
|
||||
|
||||
if quadrature == "trapz":
|
||||
msg = ("`quadrature='trapz'` is deprecated in favour of "
|
||||
"`quadrature='trapezoid' and will raise an error from SciPy 1.16.0 "
|
||||
"onwards.")
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
|
||||
# Initial interval set
|
||||
if points is None:
|
||||
initial_intervals = [(a, b)]
|
||||
else:
|
||||
prev = a
|
||||
initial_intervals = []
|
||||
for p in sorted(points):
|
||||
p = float(p)
|
||||
if not (a < p < b) or p == prev:
|
||||
continue
|
||||
initial_intervals.append((prev, p))
|
||||
prev = p
|
||||
initial_intervals.append((prev, b))
|
||||
|
||||
global_integral = None
|
||||
global_error = None
|
||||
rounding_error = None
|
||||
interval_cache = None
|
||||
intervals = []
|
||||
neval = 0
|
||||
|
||||
for x1, x2 in initial_intervals:
|
||||
ig, err, rnd = _quadrature(x1, x2, f, norm_func)
|
||||
neval += _quadrature.num_eval
|
||||
|
||||
if global_integral is None:
|
||||
if isinstance(ig, (float, complex)):
|
||||
# Specialize for scalars
|
||||
if norm_func in (_max_norm, np.linalg.norm):
|
||||
norm_func = abs
|
||||
|
||||
global_integral = ig
|
||||
global_error = float(err)
|
||||
rounding_error = float(rnd)
|
||||
|
||||
cache_count = cache_size // _get_sizeof(ig)
|
||||
interval_cache = LRUDict(cache_count)
|
||||
else:
|
||||
global_integral += ig
|
||||
global_error += err
|
||||
rounding_error += rnd
|
||||
|
||||
interval_cache[(x1, x2)] = copy.copy(ig)
|
||||
intervals.append((-err, x1, x2))
|
||||
|
||||
heapq.heapify(intervals)
|
||||
|
||||
CONVERGED = 0
|
||||
NOT_CONVERGED = 1
|
||||
ROUNDING_ERROR = 2
|
||||
NOT_A_NUMBER = 3
|
||||
|
||||
status_msg = {
|
||||
CONVERGED: "Target precision reached.",
|
||||
NOT_CONVERGED: "Target precision not reached.",
|
||||
ROUNDING_ERROR: "Target precision could not be reached due to rounding error.",
|
||||
NOT_A_NUMBER: "Non-finite values encountered."
|
||||
}
|
||||
|
||||
# Process intervals
|
||||
with MapWrapper(workers) as mapwrapper:
|
||||
ier = NOT_CONVERGED
|
||||
|
||||
while intervals and len(intervals) < limit:
|
||||
# Select intervals with largest errors for subdivision
|
||||
tol = max(epsabs, epsrel*norm_func(global_integral))
|
||||
|
||||
to_process = []
|
||||
err_sum = 0
|
||||
|
||||
for j in range(parallel_count):
|
||||
if not intervals:
|
||||
break
|
||||
|
||||
if j > 0 and err_sum > global_error - tol/8:
|
||||
# avoid unnecessary parallel splitting
|
||||
break
|
||||
|
||||
interval = heapq.heappop(intervals)
|
||||
|
||||
neg_old_err, a, b = interval
|
||||
old_int = interval_cache.pop((a, b), None)
|
||||
to_process.append(
|
||||
((-neg_old_err, a, b, old_int), f, norm_func, _quadrature)
|
||||
)
|
||||
err_sum += -neg_old_err
|
||||
|
||||
# Subdivide intervals
|
||||
for parts in mapwrapper(_subdivide_interval, to_process):
|
||||
dint, derr, dround_err, subint, dneval = parts
|
||||
neval += dneval
|
||||
global_integral += dint
|
||||
global_error += derr
|
||||
rounding_error += dround_err
|
||||
for x in subint:
|
||||
x1, x2, ig, err = x
|
||||
interval_cache[(x1, x2)] = ig
|
||||
heapq.heappush(intervals, (-err, x1, x2))
|
||||
|
||||
# Termination check
|
||||
if len(intervals) >= min_intervals:
|
||||
tol = max(epsabs, epsrel*norm_func(global_integral))
|
||||
if global_error < tol/8:
|
||||
ier = CONVERGED
|
||||
break
|
||||
if global_error < rounding_error:
|
||||
ier = ROUNDING_ERROR
|
||||
break
|
||||
|
||||
if not (np.isfinite(global_error) and np.isfinite(rounding_error)):
|
||||
ier = NOT_A_NUMBER
|
||||
break
|
||||
|
||||
res = global_integral
|
||||
err = global_error + rounding_error
|
||||
|
||||
if full_output:
|
||||
res_arr = np.asarray(res)
|
||||
dummy = np.full(res_arr.shape, np.nan, dtype=res_arr.dtype)
|
||||
integrals = np.array([interval_cache.get((z[1], z[2]), dummy)
|
||||
for z in intervals], dtype=res_arr.dtype)
|
||||
errors = np.array([-z[0] for z in intervals])
|
||||
intervals = np.array([[z[1], z[2]] for z in intervals])
|
||||
|
||||
info = _Bunch(neval=neval,
|
||||
success=(ier == CONVERGED),
|
||||
status=ier,
|
||||
message=status_msg[ier],
|
||||
intervals=intervals,
|
||||
integrals=integrals,
|
||||
errors=errors)
|
||||
return (res, err, info)
|
||||
else:
|
||||
return (res, err)
|
||||
|
||||
|
||||
def _subdivide_interval(args):
|
||||
interval, f, norm_func, _quadrature = args
|
||||
old_err, a, b, old_int = interval
|
||||
|
||||
c = 0.5 * (a + b)
|
||||
|
||||
# Left-hand side
|
||||
if getattr(_quadrature, 'cache_size', 0) > 0:
|
||||
f = functools.lru_cache(_quadrature.cache_size)(f)
|
||||
|
||||
s1, err1, round1 = _quadrature(a, c, f, norm_func)
|
||||
dneval = _quadrature.num_eval
|
||||
s2, err2, round2 = _quadrature(c, b, f, norm_func)
|
||||
dneval += _quadrature.num_eval
|
||||
if old_int is None:
|
||||
old_int, _, _ = _quadrature(a, b, f, norm_func)
|
||||
dneval += _quadrature.num_eval
|
||||
|
||||
if getattr(_quadrature, 'cache_size', 0) > 0:
|
||||
dneval = f.cache_info().misses
|
||||
|
||||
dint = s1 + s2 - old_int
|
||||
derr = err1 + err2 - old_err
|
||||
dround_err = round1 + round2
|
||||
|
||||
subintervals = ((a, c, s1, err1), (c, b, s2, err2))
|
||||
return dint, derr, dround_err, subintervals, dneval
|
||||
|
||||
|
||||
def _quadrature_trapezoid(x1, x2, f, norm_func):
|
||||
"""
|
||||
Composite trapezoid quadrature
|
||||
"""
|
||||
x3 = 0.5*(x1 + x2)
|
||||
f1 = f(x1)
|
||||
f2 = f(x2)
|
||||
f3 = f(x3)
|
||||
|
||||
s2 = 0.25 * (x2 - x1) * (f1 + 2*f3 + f2)
|
||||
|
||||
round_err = 0.25 * abs(x2 - x1) * (float(norm_func(f1))
|
||||
+ 2*float(norm_func(f3))
|
||||
+ float(norm_func(f2))) * 2e-16
|
||||
|
||||
s1 = 0.5 * (x2 - x1) * (f1 + f2)
|
||||
err = 1/3 * float(norm_func(s1 - s2))
|
||||
return s2, err, round_err
|
||||
|
||||
|
||||
_quadrature_trapezoid.cache_size = 3 * 3
|
||||
_quadrature_trapezoid.num_eval = 3
|
||||
|
||||
|
||||
def _quadrature_gk(a, b, f, norm_func, x, w, v):
|
||||
"""
|
||||
Generic Gauss-Kronrod quadrature
|
||||
"""
|
||||
|
||||
fv = [0.0]*len(x)
|
||||
|
||||
c = 0.5 * (a + b)
|
||||
h = 0.5 * (b - a)
|
||||
|
||||
# Gauss-Kronrod
|
||||
s_k = 0.0
|
||||
s_k_abs = 0.0
|
||||
for i in range(len(x)):
|
||||
ff = f(c + h*x[i])
|
||||
fv[i] = ff
|
||||
|
||||
vv = v[i]
|
||||
|
||||
# \int f(x)
|
||||
s_k += vv * ff
|
||||
# \int |f(x)|
|
||||
s_k_abs += vv * abs(ff)
|
||||
|
||||
# Gauss
|
||||
s_g = 0.0
|
||||
for i in range(len(w)):
|
||||
s_g += w[i] * fv[2*i + 1]
|
||||
|
||||
# Quadrature of abs-deviation from average
|
||||
s_k_dabs = 0.0
|
||||
y0 = s_k / 2.0
|
||||
for i in range(len(x)):
|
||||
# \int |f(x) - y0|
|
||||
s_k_dabs += v[i] * abs(fv[i] - y0)
|
||||
|
||||
# Use similar error estimation as quadpack
|
||||
err = float(norm_func((s_k - s_g) * h))
|
||||
dabs = float(norm_func(s_k_dabs * h))
|
||||
if dabs != 0 and err != 0:
|
||||
err = dabs * min(1.0, (200 * err / dabs)**1.5)
|
||||
|
||||
eps = sys.float_info.epsilon
|
||||
round_err = float(norm_func(50 * eps * h * s_k_abs))
|
||||
|
||||
if round_err > sys.float_info.min:
|
||||
err = max(err, round_err)
|
||||
|
||||
return h * s_k, err, round_err
|
||||
|
||||
|
||||
def _quadrature_gk21(a, b, f, norm_func):
|
||||
"""
|
||||
Gauss-Kronrod 21 quadrature with error estimate
|
||||
"""
|
||||
# Gauss-Kronrod points
|
||||
x = (0.995657163025808080735527280689003,
|
||||
0.973906528517171720077964012084452,
|
||||
0.930157491355708226001207180059508,
|
||||
0.865063366688984510732096688423493,
|
||||
0.780817726586416897063717578345042,
|
||||
0.679409568299024406234327365114874,
|
||||
0.562757134668604683339000099272694,
|
||||
0.433395394129247190799265943165784,
|
||||
0.294392862701460198131126603103866,
|
||||
0.148874338981631210884826001129720,
|
||||
0,
|
||||
-0.148874338981631210884826001129720,
|
||||
-0.294392862701460198131126603103866,
|
||||
-0.433395394129247190799265943165784,
|
||||
-0.562757134668604683339000099272694,
|
||||
-0.679409568299024406234327365114874,
|
||||
-0.780817726586416897063717578345042,
|
||||
-0.865063366688984510732096688423493,
|
||||
-0.930157491355708226001207180059508,
|
||||
-0.973906528517171720077964012084452,
|
||||
-0.995657163025808080735527280689003)
|
||||
|
||||
# 10-point weights
|
||||
w = (0.066671344308688137593568809893332,
|
||||
0.149451349150580593145776339657697,
|
||||
0.219086362515982043995534934228163,
|
||||
0.269266719309996355091226921569469,
|
||||
0.295524224714752870173892994651338,
|
||||
0.295524224714752870173892994651338,
|
||||
0.269266719309996355091226921569469,
|
||||
0.219086362515982043995534934228163,
|
||||
0.149451349150580593145776339657697,
|
||||
0.066671344308688137593568809893332)
|
||||
|
||||
# 21-point weights
|
||||
v = (0.011694638867371874278064396062192,
|
||||
0.032558162307964727478818972459390,
|
||||
0.054755896574351996031381300244580,
|
||||
0.075039674810919952767043140916190,
|
||||
0.093125454583697605535065465083366,
|
||||
0.109387158802297641899210590325805,
|
||||
0.123491976262065851077958109831074,
|
||||
0.134709217311473325928054001771707,
|
||||
0.142775938577060080797094273138717,
|
||||
0.147739104901338491374841515972068,
|
||||
0.149445554002916905664936468389821,
|
||||
0.147739104901338491374841515972068,
|
||||
0.142775938577060080797094273138717,
|
||||
0.134709217311473325928054001771707,
|
||||
0.123491976262065851077958109831074,
|
||||
0.109387158802297641899210590325805,
|
||||
0.093125454583697605535065465083366,
|
||||
0.075039674810919952767043140916190,
|
||||
0.054755896574351996031381300244580,
|
||||
0.032558162307964727478818972459390,
|
||||
0.011694638867371874278064396062192)
|
||||
|
||||
return _quadrature_gk(a, b, f, norm_func, x, w, v)
|
||||
|
||||
|
||||
_quadrature_gk21.num_eval = 21
|
||||
|
||||
|
||||
def _quadrature_gk15(a, b, f, norm_func):
|
||||
"""
|
||||
Gauss-Kronrod 15 quadrature with error estimate
|
||||
"""
|
||||
# Gauss-Kronrod points
|
||||
x = (0.991455371120812639206854697526329,
|
||||
0.949107912342758524526189684047851,
|
||||
0.864864423359769072789712788640926,
|
||||
0.741531185599394439863864773280788,
|
||||
0.586087235467691130294144838258730,
|
||||
0.405845151377397166906606412076961,
|
||||
0.207784955007898467600689403773245,
|
||||
0.000000000000000000000000000000000,
|
||||
-0.207784955007898467600689403773245,
|
||||
-0.405845151377397166906606412076961,
|
||||
-0.586087235467691130294144838258730,
|
||||
-0.741531185599394439863864773280788,
|
||||
-0.864864423359769072789712788640926,
|
||||
-0.949107912342758524526189684047851,
|
||||
-0.991455371120812639206854697526329)
|
||||
|
||||
# 7-point weights
|
||||
w = (0.129484966168869693270611432679082,
|
||||
0.279705391489276667901467771423780,
|
||||
0.381830050505118944950369775488975,
|
||||
0.417959183673469387755102040816327,
|
||||
0.381830050505118944950369775488975,
|
||||
0.279705391489276667901467771423780,
|
||||
0.129484966168869693270611432679082)
|
||||
|
||||
# 15-point weights
|
||||
v = (0.022935322010529224963732008058970,
|
||||
0.063092092629978553290700663189204,
|
||||
0.104790010322250183839876322541518,
|
||||
0.140653259715525918745189590510238,
|
||||
0.169004726639267902826583426598550,
|
||||
0.190350578064785409913256402421014,
|
||||
0.204432940075298892414161999234649,
|
||||
0.209482141084727828012999174891714,
|
||||
0.204432940075298892414161999234649,
|
||||
0.190350578064785409913256402421014,
|
||||
0.169004726639267902826583426598550,
|
||||
0.140653259715525918745189590510238,
|
||||
0.104790010322250183839876322541518,
|
||||
0.063092092629978553290700663189204,
|
||||
0.022935322010529224963732008058970)
|
||||
|
||||
return _quadrature_gk(a, b, f, norm_func, x, w, v)
|
||||
|
||||
|
||||
_quadrature_gk15.num_eval = 15
|
||||
Binary file not shown.
1279
venv/lib/python3.12/site-packages/scipy/integrate/_quadpack_py.py
Normal file
1279
venv/lib/python3.12/site-packages/scipy/integrate/_quadpack_py.py
Normal file
File diff suppressed because it is too large
Load Diff
1684
venv/lib/python3.12/site-packages/scipy/integrate/_quadrature.py
Normal file
1684
venv/lib/python3.12/site-packages/scipy/integrate/_quadrature.py
Normal file
File diff suppressed because it is too large
Load Diff
1231
venv/lib/python3.12/site-packages/scipy/integrate/_tanhsinh.py
Normal file
1231
venv/lib/python3.12/site-packages/scipy/integrate/_tanhsinh.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
15
venv/lib/python3.12/site-packages/scipy/integrate/dop.py
Normal file
15
venv/lib/python3.12/site-packages/scipy/integrate/dop.py
Normal file
@ -0,0 +1,15 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__: list[str] = []
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="integrate", module="dop",
|
||||
private_modules=["_dop"], all=__all__,
|
||||
attribute=name)
|
||||
15
venv/lib/python3.12/site-packages/scipy/integrate/lsoda.py
Normal file
15
venv/lib/python3.12/site-packages/scipy/integrate/lsoda.py
Normal file
@ -0,0 +1,15 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = ['lsoda'] # noqa: F822
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="integrate", module="lsoda",
|
||||
private_modules=["_lsoda"], all=__all__,
|
||||
attribute=name)
|
||||
17
venv/lib/python3.12/site-packages/scipy/integrate/odepack.py
Normal file
17
venv/lib/python3.12/site-packages/scipy/integrate/odepack.py
Normal file
@ -0,0 +1,17 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.integrate` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = ['odeint', 'ODEintWarning'] # noqa: F822
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="integrate", module="odepack",
|
||||
private_modules=["_odepack_py"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,23 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.integrate` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
"quad",
|
||||
"dblquad",
|
||||
"tplquad",
|
||||
"nquad",
|
||||
"IntegrationWarning",
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="integrate", module="quadpack",
|
||||
private_modules=["_quadpack_py"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,215 @@
|
||||
import pytest
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
from scipy.integrate import quad_vec
|
||||
|
||||
from multiprocessing.dummy import Pool
|
||||
|
||||
|
||||
quadrature_params = pytest.mark.parametrize(
|
||||
'quadrature', [None, "gk15", "gk21", "trapezoid"])
|
||||
|
||||
|
||||
@quadrature_params
|
||||
def test_quad_vec_simple(quadrature):
|
||||
n = np.arange(10)
|
||||
def f(x):
|
||||
return x ** n
|
||||
for epsabs in [0.1, 1e-3, 1e-6]:
|
||||
if quadrature == 'trapezoid' and epsabs < 1e-4:
|
||||
# slow: skip
|
||||
continue
|
||||
|
||||
kwargs = dict(epsabs=epsabs, quadrature=quadrature)
|
||||
|
||||
exact = 2**(n+1)/(n + 1)
|
||||
|
||||
res, err = quad_vec(f, 0, 2, norm='max', **kwargs)
|
||||
assert_allclose(res, exact, rtol=0, atol=epsabs)
|
||||
|
||||
res, err = quad_vec(f, 0, 2, norm='2', **kwargs)
|
||||
assert np.linalg.norm(res - exact) < epsabs
|
||||
|
||||
res, err = quad_vec(f, 0, 2, norm='max', points=(0.5, 1.0), **kwargs)
|
||||
assert_allclose(res, exact, rtol=0, atol=epsabs)
|
||||
|
||||
res, err, *rest = quad_vec(f, 0, 2, norm='max',
|
||||
epsrel=1e-8,
|
||||
full_output=True,
|
||||
limit=10000,
|
||||
**kwargs)
|
||||
assert_allclose(res, exact, rtol=0, atol=epsabs)
|
||||
|
||||
|
||||
@quadrature_params
|
||||
def test_quad_vec_simple_inf(quadrature):
|
||||
def f(x):
|
||||
return 1 / (1 + np.float64(x) ** 2)
|
||||
|
||||
for epsabs in [0.1, 1e-3, 1e-6]:
|
||||
if quadrature == 'trapezoid' and epsabs < 1e-4:
|
||||
# slow: skip
|
||||
continue
|
||||
|
||||
kwargs = dict(norm='max', epsabs=epsabs, quadrature=quadrature)
|
||||
|
||||
res, err = quad_vec(f, 0, np.inf, **kwargs)
|
||||
assert_allclose(res, np.pi/2, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, 0, -np.inf, **kwargs)
|
||||
assert_allclose(res, -np.pi/2, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, -np.inf, 0, **kwargs)
|
||||
assert_allclose(res, np.pi/2, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, np.inf, 0, **kwargs)
|
||||
assert_allclose(res, -np.pi/2, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, -np.inf, np.inf, **kwargs)
|
||||
assert_allclose(res, np.pi, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, np.inf, -np.inf, **kwargs)
|
||||
assert_allclose(res, -np.pi, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, np.inf, np.inf, **kwargs)
|
||||
assert_allclose(res, 0, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, -np.inf, -np.inf, **kwargs)
|
||||
assert_allclose(res, 0, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
res, err = quad_vec(f, 0, np.inf, points=(1.0, 2.0), **kwargs)
|
||||
assert_allclose(res, np.pi/2, rtol=0, atol=max(epsabs, err))
|
||||
|
||||
def f(x):
|
||||
return np.sin(x + 2) / (1 + x ** 2)
|
||||
exact = np.pi / np.e * np.sin(2)
|
||||
epsabs = 1e-5
|
||||
|
||||
res, err, info = quad_vec(f, -np.inf, np.inf, limit=1000, norm='max', epsabs=epsabs,
|
||||
quadrature=quadrature, full_output=True)
|
||||
assert info.status == 1
|
||||
assert_allclose(res, exact, rtol=0, atol=max(epsabs, 1.5 * err))
|
||||
|
||||
|
||||
def test_quad_vec_args():
|
||||
def f(x, a):
|
||||
return x * (x + a) * np.arange(3)
|
||||
a = 2
|
||||
exact = np.array([0, 4/3, 8/3])
|
||||
|
||||
res, err = quad_vec(f, 0, 1, args=(a,))
|
||||
assert_allclose(res, exact, rtol=0, atol=1e-4)
|
||||
|
||||
|
||||
def _lorenzian(x):
|
||||
return 1 / (1 + x**2)
|
||||
|
||||
|
||||
@pytest.mark.fail_slow(5)
|
||||
def test_quad_vec_pool():
|
||||
f = _lorenzian
|
||||
res, err = quad_vec(f, -np.inf, np.inf, norm='max', epsabs=1e-4, workers=4)
|
||||
assert_allclose(res, np.pi, rtol=0, atol=1e-4)
|
||||
|
||||
with Pool(10) as pool:
|
||||
def f(x):
|
||||
return 1 / (1 + x ** 2)
|
||||
res, _ = quad_vec(f, -np.inf, np.inf, norm='max', epsabs=1e-4, workers=pool.map)
|
||||
assert_allclose(res, np.pi, rtol=0, atol=1e-4)
|
||||
|
||||
|
||||
def _func_with_args(x, a):
|
||||
return x * (x + a) * np.arange(3)
|
||||
|
||||
|
||||
@pytest.mark.fail_slow(5)
|
||||
@pytest.mark.parametrize('extra_args', [2, (2,)])
|
||||
@pytest.mark.parametrize('workers', [1, 10])
|
||||
def test_quad_vec_pool_args(extra_args, workers):
|
||||
f = _func_with_args
|
||||
exact = np.array([0, 4/3, 8/3])
|
||||
|
||||
res, err = quad_vec(f, 0, 1, args=extra_args, workers=workers)
|
||||
assert_allclose(res, exact, rtol=0, atol=1e-4)
|
||||
|
||||
with Pool(workers) as pool:
|
||||
res, err = quad_vec(f, 0, 1, args=extra_args, workers=pool.map)
|
||||
assert_allclose(res, exact, rtol=0, atol=1e-4)
|
||||
|
||||
|
||||
@quadrature_params
|
||||
def test_num_eval(quadrature):
|
||||
def f(x):
|
||||
count[0] += 1
|
||||
return x**5
|
||||
|
||||
count = [0]
|
||||
res = quad_vec(f, 0, 1, norm='max', full_output=True, quadrature=quadrature)
|
||||
assert res[2].neval == count[0]
|
||||
|
||||
|
||||
def test_info():
|
||||
def f(x):
|
||||
return np.ones((3, 2, 1))
|
||||
|
||||
res, err, info = quad_vec(f, 0, 1, norm='max', full_output=True)
|
||||
|
||||
assert info.success is True
|
||||
assert info.status == 0
|
||||
assert info.message == 'Target precision reached.'
|
||||
assert info.neval > 0
|
||||
assert info.intervals.shape[1] == 2
|
||||
assert info.integrals.shape == (info.intervals.shape[0], 3, 2, 1)
|
||||
assert info.errors.shape == (info.intervals.shape[0],)
|
||||
|
||||
|
||||
def test_nan_inf():
|
||||
def f_nan(x):
|
||||
return np.nan
|
||||
|
||||
def f_inf(x):
|
||||
return np.inf if x < 0.1 else 1/x
|
||||
|
||||
res, err, info = quad_vec(f_nan, 0, 1, full_output=True)
|
||||
assert info.status == 3
|
||||
|
||||
res, err, info = quad_vec(f_inf, 0, 1, full_output=True)
|
||||
assert info.status == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize('a,b', [(0, 1), (0, np.inf), (np.inf, 0),
|
||||
(-np.inf, np.inf), (np.inf, -np.inf)])
|
||||
def test_points(a, b):
|
||||
# Check that initial interval splitting is done according to
|
||||
# `points`, by checking that consecutive sets of 15 point (for
|
||||
# gk15) function evaluations lie between `points`
|
||||
|
||||
points = (0, 0.25, 0.5, 0.75, 1.0)
|
||||
points += tuple(-x for x in points)
|
||||
|
||||
quadrature_points = 15
|
||||
interval_sets = []
|
||||
count = 0
|
||||
|
||||
def f(x):
|
||||
nonlocal count
|
||||
|
||||
if count % quadrature_points == 0:
|
||||
interval_sets.append(set())
|
||||
|
||||
count += 1
|
||||
interval_sets[-1].add(float(x))
|
||||
return 0.0
|
||||
|
||||
quad_vec(f, a, b, points=points, quadrature='gk15', limit=0)
|
||||
|
||||
# Check that all point sets lie in a single `points` interval
|
||||
for p in interval_sets:
|
||||
j = np.searchsorted(sorted(points), tuple(p))
|
||||
assert np.all(j == j[0])
|
||||
|
||||
def test_trapz_deprecation():
|
||||
with pytest.deprecated_call(match="`quadrature='trapz'`"):
|
||||
quad_vec(lambda x: x, 0, 1, quadrature="trapz")
|
||||
@ -0,0 +1,218 @@
|
||||
import itertools
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose
|
||||
from scipy.integrate import ode
|
||||
|
||||
|
||||
def _band_count(a):
|
||||
"""Returns ml and mu, the lower and upper band sizes of a."""
|
||||
nrows, ncols = a.shape
|
||||
ml = 0
|
||||
for k in range(-nrows+1, 0):
|
||||
if np.diag(a, k).any():
|
||||
ml = -k
|
||||
break
|
||||
mu = 0
|
||||
for k in range(nrows-1, 0, -1):
|
||||
if np.diag(a, k).any():
|
||||
mu = k
|
||||
break
|
||||
return ml, mu
|
||||
|
||||
|
||||
def _linear_func(t, y, a):
|
||||
"""Linear system dy/dt = a * y"""
|
||||
return a.dot(y)
|
||||
|
||||
|
||||
def _linear_jac(t, y, a):
|
||||
"""Jacobian of a * y is a."""
|
||||
return a
|
||||
|
||||
|
||||
def _linear_banded_jac(t, y, a):
|
||||
"""Banded Jacobian."""
|
||||
ml, mu = _band_count(a)
|
||||
bjac = [np.r_[[0] * k, np.diag(a, k)] for k in range(mu, 0, -1)]
|
||||
bjac.append(np.diag(a))
|
||||
for k in range(-1, -ml-1, -1):
|
||||
bjac.append(np.r_[np.diag(a, k), [0] * (-k)])
|
||||
return bjac
|
||||
|
||||
|
||||
def _solve_linear_sys(a, y0, tend=1, dt=0.1,
|
||||
solver=None, method='bdf', use_jac=True,
|
||||
with_jacobian=False, banded=False):
|
||||
"""Use scipy.integrate.ode to solve a linear system of ODEs.
|
||||
|
||||
a : square ndarray
|
||||
Matrix of the linear system to be solved.
|
||||
y0 : ndarray
|
||||
Initial condition
|
||||
tend : float
|
||||
Stop time.
|
||||
dt : float
|
||||
Step size of the output.
|
||||
solver : str
|
||||
If not None, this must be "vode", "lsoda" or "zvode".
|
||||
method : str
|
||||
Either "bdf" or "adams".
|
||||
use_jac : bool
|
||||
Determines if the jacobian function is passed to ode().
|
||||
with_jacobian : bool
|
||||
Passed to ode.set_integrator().
|
||||
banded : bool
|
||||
Determines whether a banded or full jacobian is used.
|
||||
If `banded` is True, `lband` and `uband` are determined by the
|
||||
values in `a`.
|
||||
"""
|
||||
if banded:
|
||||
lband, uband = _band_count(a)
|
||||
else:
|
||||
lband = None
|
||||
uband = None
|
||||
|
||||
if use_jac:
|
||||
if banded:
|
||||
r = ode(_linear_func, _linear_banded_jac)
|
||||
else:
|
||||
r = ode(_linear_func, _linear_jac)
|
||||
else:
|
||||
r = ode(_linear_func)
|
||||
|
||||
if solver is None:
|
||||
if np.iscomplexobj(a):
|
||||
solver = "zvode"
|
||||
else:
|
||||
solver = "vode"
|
||||
|
||||
r.set_integrator(solver,
|
||||
with_jacobian=with_jacobian,
|
||||
method=method,
|
||||
lband=lband, uband=uband,
|
||||
rtol=1e-9, atol=1e-10,
|
||||
)
|
||||
t0 = 0
|
||||
r.set_initial_value(y0, t0)
|
||||
r.set_f_params(a)
|
||||
r.set_jac_params(a)
|
||||
|
||||
t = [t0]
|
||||
y = [y0]
|
||||
while r.successful() and r.t < tend:
|
||||
r.integrate(r.t + dt)
|
||||
t.append(r.t)
|
||||
y.append(r.y)
|
||||
|
||||
t = np.array(t)
|
||||
y = np.array(y)
|
||||
return t, y
|
||||
|
||||
|
||||
def _analytical_solution(a, y0, t):
|
||||
"""
|
||||
Analytical solution to the linear differential equations dy/dt = a*y.
|
||||
|
||||
The solution is only valid if `a` is diagonalizable.
|
||||
|
||||
Returns a 2-D array with shape (len(t), len(y0)).
|
||||
"""
|
||||
lam, v = np.linalg.eig(a)
|
||||
c = np.linalg.solve(v, y0)
|
||||
e = c * np.exp(lam * t.reshape(-1, 1))
|
||||
sol = e.dot(v.T)
|
||||
return sol
|
||||
|
||||
|
||||
def test_banded_ode_solvers():
|
||||
# Test the "lsoda", "vode" and "zvode" solvers of the `ode` class
|
||||
# with a system that has a banded Jacobian matrix.
|
||||
|
||||
t_exact = np.linspace(0, 1.0, 5)
|
||||
|
||||
# --- Real arrays for testing the "lsoda" and "vode" solvers ---
|
||||
|
||||
# lband = 2, uband = 1:
|
||||
a_real = np.array([[-0.6, 0.1, 0.0, 0.0, 0.0],
|
||||
[0.2, -0.5, 0.9, 0.0, 0.0],
|
||||
[0.1, 0.1, -0.4, 0.1, 0.0],
|
||||
[0.0, 0.3, -0.1, -0.9, -0.3],
|
||||
[0.0, 0.0, 0.1, 0.1, -0.7]])
|
||||
|
||||
# lband = 0, uband = 1:
|
||||
a_real_upper = np.triu(a_real)
|
||||
|
||||
# lband = 2, uband = 0:
|
||||
a_real_lower = np.tril(a_real)
|
||||
|
||||
# lband = 0, uband = 0:
|
||||
a_real_diag = np.triu(a_real_lower)
|
||||
|
||||
real_matrices = [a_real, a_real_upper, a_real_lower, a_real_diag]
|
||||
real_solutions = []
|
||||
|
||||
for a in real_matrices:
|
||||
y0 = np.arange(1, a.shape[0] + 1)
|
||||
y_exact = _analytical_solution(a, y0, t_exact)
|
||||
real_solutions.append((y0, t_exact, y_exact))
|
||||
|
||||
def check_real(idx, solver, meth, use_jac, with_jac, banded):
|
||||
a = real_matrices[idx]
|
||||
y0, t_exact, y_exact = real_solutions[idx]
|
||||
t, y = _solve_linear_sys(a, y0,
|
||||
tend=t_exact[-1],
|
||||
dt=t_exact[1] - t_exact[0],
|
||||
solver=solver,
|
||||
method=meth,
|
||||
use_jac=use_jac,
|
||||
with_jacobian=with_jac,
|
||||
banded=banded)
|
||||
assert_allclose(t, t_exact)
|
||||
assert_allclose(y, y_exact)
|
||||
|
||||
for idx in range(len(real_matrices)):
|
||||
p = [['vode', 'lsoda'], # solver
|
||||
['bdf', 'adams'], # method
|
||||
[False, True], # use_jac
|
||||
[False, True], # with_jacobian
|
||||
[False, True]] # banded
|
||||
for solver, meth, use_jac, with_jac, banded in itertools.product(*p):
|
||||
check_real(idx, solver, meth, use_jac, with_jac, banded)
|
||||
|
||||
# --- Complex arrays for testing the "zvode" solver ---
|
||||
|
||||
# complex, lband = 2, uband = 1:
|
||||
a_complex = a_real - 0.5j * a_real
|
||||
|
||||
# complex, lband = 0, uband = 0:
|
||||
a_complex_diag = np.diag(np.diag(a_complex))
|
||||
|
||||
complex_matrices = [a_complex, a_complex_diag]
|
||||
complex_solutions = []
|
||||
|
||||
for a in complex_matrices:
|
||||
y0 = np.arange(1, a.shape[0] + 1) + 1j
|
||||
y_exact = _analytical_solution(a, y0, t_exact)
|
||||
complex_solutions.append((y0, t_exact, y_exact))
|
||||
|
||||
def check_complex(idx, solver, meth, use_jac, with_jac, banded):
|
||||
a = complex_matrices[idx]
|
||||
y0, t_exact, y_exact = complex_solutions[idx]
|
||||
t, y = _solve_linear_sys(a, y0,
|
||||
tend=t_exact[-1],
|
||||
dt=t_exact[1] - t_exact[0],
|
||||
solver=solver,
|
||||
method=meth,
|
||||
use_jac=use_jac,
|
||||
with_jacobian=with_jac,
|
||||
banded=banded)
|
||||
assert_allclose(t, t_exact)
|
||||
assert_allclose(y, y_exact)
|
||||
|
||||
for idx in range(len(complex_matrices)):
|
||||
p = [['bdf', 'adams'], # method
|
||||
[False, True], # use_jac
|
||||
[False, True], # with_jacobian
|
||||
[False, True]] # banded
|
||||
for meth, use_jac, with_jac, banded in itertools.product(*p):
|
||||
check_complex(idx, "zvode", meth, use_jac, with_jac, banded)
|
||||
@ -0,0 +1,711 @@
|
||||
import sys
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import (assert_, assert_array_equal, assert_allclose,
|
||||
assert_equal)
|
||||
from pytest import raises as assert_raises
|
||||
|
||||
from scipy.sparse import coo_matrix
|
||||
from scipy.special import erf
|
||||
from scipy.integrate._bvp import (modify_mesh, estimate_fun_jac,
|
||||
estimate_bc_jac, compute_jac_indices,
|
||||
construct_global_jac, solve_bvp)
|
||||
|
||||
|
||||
def exp_fun(x, y):
|
||||
return np.vstack((y[1], y[0]))
|
||||
|
||||
|
||||
def exp_fun_jac(x, y):
|
||||
df_dy = np.empty((2, 2, x.shape[0]))
|
||||
df_dy[0, 0] = 0
|
||||
df_dy[0, 1] = 1
|
||||
df_dy[1, 0] = 1
|
||||
df_dy[1, 1] = 0
|
||||
return df_dy
|
||||
|
||||
|
||||
def exp_bc(ya, yb):
|
||||
return np.hstack((ya[0] - 1, yb[0]))
|
||||
|
||||
|
||||
def exp_bc_complex(ya, yb):
|
||||
return np.hstack((ya[0] - 1 - 1j, yb[0]))
|
||||
|
||||
|
||||
def exp_bc_jac(ya, yb):
|
||||
dbc_dya = np.array([
|
||||
[1, 0],
|
||||
[0, 0]
|
||||
])
|
||||
dbc_dyb = np.array([
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
])
|
||||
return dbc_dya, dbc_dyb
|
||||
|
||||
|
||||
def exp_sol(x):
|
||||
return (np.exp(-x) - np.exp(x - 2)) / (1 - np.exp(-2))
|
||||
|
||||
|
||||
def sl_fun(x, y, p):
|
||||
return np.vstack((y[1], -p[0]**2 * y[0]))
|
||||
|
||||
|
||||
def sl_fun_jac(x, y, p):
|
||||
n, m = y.shape
|
||||
df_dy = np.empty((n, 2, m))
|
||||
df_dy[0, 0] = 0
|
||||
df_dy[0, 1] = 1
|
||||
df_dy[1, 0] = -p[0]**2
|
||||
df_dy[1, 1] = 0
|
||||
|
||||
df_dp = np.empty((n, 1, m))
|
||||
df_dp[0, 0] = 0
|
||||
df_dp[1, 0] = -2 * p[0] * y[0]
|
||||
|
||||
return df_dy, df_dp
|
||||
|
||||
|
||||
def sl_bc(ya, yb, p):
|
||||
return np.hstack((ya[0], yb[0], ya[1] - p[0]))
|
||||
|
||||
|
||||
def sl_bc_jac(ya, yb, p):
|
||||
dbc_dya = np.zeros((3, 2))
|
||||
dbc_dya[0, 0] = 1
|
||||
dbc_dya[2, 1] = 1
|
||||
|
||||
dbc_dyb = np.zeros((3, 2))
|
||||
dbc_dyb[1, 0] = 1
|
||||
|
||||
dbc_dp = np.zeros((3, 1))
|
||||
dbc_dp[2, 0] = -1
|
||||
|
||||
return dbc_dya, dbc_dyb, dbc_dp
|
||||
|
||||
|
||||
def sl_sol(x, p):
|
||||
return np.sin(p[0] * x)
|
||||
|
||||
|
||||
def emden_fun(x, y):
|
||||
return np.vstack((y[1], -y[0]**5))
|
||||
|
||||
|
||||
def emden_fun_jac(x, y):
|
||||
df_dy = np.empty((2, 2, x.shape[0]))
|
||||
df_dy[0, 0] = 0
|
||||
df_dy[0, 1] = 1
|
||||
df_dy[1, 0] = -5 * y[0]**4
|
||||
df_dy[1, 1] = 0
|
||||
return df_dy
|
||||
|
||||
|
||||
def emden_bc(ya, yb):
|
||||
return np.array([ya[1], yb[0] - (3/4)**0.5])
|
||||
|
||||
|
||||
def emden_bc_jac(ya, yb):
|
||||
dbc_dya = np.array([
|
||||
[0, 1],
|
||||
[0, 0]
|
||||
])
|
||||
dbc_dyb = np.array([
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
])
|
||||
return dbc_dya, dbc_dyb
|
||||
|
||||
|
||||
def emden_sol(x):
|
||||
return (1 + x**2/3)**-0.5
|
||||
|
||||
|
||||
def undefined_fun(x, y):
|
||||
return np.zeros_like(y)
|
||||
|
||||
|
||||
def undefined_bc(ya, yb):
|
||||
return np.array([ya[0], yb[0] - 1])
|
||||
|
||||
|
||||
def big_fun(x, y):
|
||||
f = np.zeros_like(y)
|
||||
f[::2] = y[1::2]
|
||||
return f
|
||||
|
||||
|
||||
def big_bc(ya, yb):
|
||||
return np.hstack((ya[::2], yb[::2] - 1))
|
||||
|
||||
|
||||
def big_sol(x, n):
|
||||
y = np.ones((2 * n, x.size))
|
||||
y[::2] = x
|
||||
return x
|
||||
|
||||
|
||||
def big_fun_with_parameters(x, y, p):
|
||||
""" Big version of sl_fun, with two parameters.
|
||||
|
||||
The two differential equations represented by sl_fun are broadcast to the
|
||||
number of rows of y, rotating between the parameters p[0] and p[1].
|
||||
Here are the differential equations:
|
||||
|
||||
dy[0]/dt = y[1]
|
||||
dy[1]/dt = -p[0]**2 * y[0]
|
||||
dy[2]/dt = y[3]
|
||||
dy[3]/dt = -p[1]**2 * y[2]
|
||||
dy[4]/dt = y[5]
|
||||
dy[5]/dt = -p[0]**2 * y[4]
|
||||
dy[6]/dt = y[7]
|
||||
dy[7]/dt = -p[1]**2 * y[6]
|
||||
.
|
||||
.
|
||||
.
|
||||
|
||||
"""
|
||||
f = np.zeros_like(y)
|
||||
f[::2] = y[1::2]
|
||||
f[1::4] = -p[0]**2 * y[::4]
|
||||
f[3::4] = -p[1]**2 * y[2::4]
|
||||
return f
|
||||
|
||||
|
||||
def big_fun_with_parameters_jac(x, y, p):
|
||||
# big version of sl_fun_jac, with two parameters
|
||||
n, m = y.shape
|
||||
df_dy = np.zeros((n, n, m))
|
||||
df_dy[range(0, n, 2), range(1, n, 2)] = 1
|
||||
df_dy[range(1, n, 4), range(0, n, 4)] = -p[0]**2
|
||||
df_dy[range(3, n, 4), range(2, n, 4)] = -p[1]**2
|
||||
|
||||
df_dp = np.zeros((n, 2, m))
|
||||
df_dp[range(1, n, 4), 0] = -2 * p[0] * y[range(0, n, 4)]
|
||||
df_dp[range(3, n, 4), 1] = -2 * p[1] * y[range(2, n, 4)]
|
||||
|
||||
return df_dy, df_dp
|
||||
|
||||
|
||||
def big_bc_with_parameters(ya, yb, p):
|
||||
# big version of sl_bc, with two parameters
|
||||
return np.hstack((ya[::2], yb[::2], ya[1] - p[0], ya[3] - p[1]))
|
||||
|
||||
|
||||
def big_bc_with_parameters_jac(ya, yb, p):
|
||||
# big version of sl_bc_jac, with two parameters
|
||||
n = ya.shape[0]
|
||||
dbc_dya = np.zeros((n + 2, n))
|
||||
dbc_dyb = np.zeros((n + 2, n))
|
||||
|
||||
dbc_dya[range(n // 2), range(0, n, 2)] = 1
|
||||
dbc_dyb[range(n // 2, n), range(0, n, 2)] = 1
|
||||
|
||||
dbc_dp = np.zeros((n + 2, 2))
|
||||
dbc_dp[n, 0] = -1
|
||||
dbc_dya[n, 1] = 1
|
||||
dbc_dp[n + 1, 1] = -1
|
||||
dbc_dya[n + 1, 3] = 1
|
||||
|
||||
return dbc_dya, dbc_dyb, dbc_dp
|
||||
|
||||
|
||||
def big_sol_with_parameters(x, p):
|
||||
# big version of sl_sol, with two parameters
|
||||
return np.vstack((np.sin(p[0] * x), np.sin(p[1] * x)))
|
||||
|
||||
|
||||
def shock_fun(x, y):
|
||||
eps = 1e-3
|
||||
return np.vstack((
|
||||
y[1],
|
||||
-(x * y[1] + eps * np.pi**2 * np.cos(np.pi * x) +
|
||||
np.pi * x * np.sin(np.pi * x)) / eps
|
||||
))
|
||||
|
||||
|
||||
def shock_bc(ya, yb):
|
||||
return np.array([ya[0] + 2, yb[0]])
|
||||
|
||||
|
||||
def shock_sol(x):
|
||||
eps = 1e-3
|
||||
k = np.sqrt(2 * eps)
|
||||
return np.cos(np.pi * x) + erf(x / k) / erf(1 / k)
|
||||
|
||||
|
||||
def nonlin_bc_fun(x, y):
|
||||
# laplace eq.
|
||||
return np.stack([y[1], np.zeros_like(x)])
|
||||
|
||||
|
||||
def nonlin_bc_bc(ya, yb):
|
||||
phiA, phipA = ya
|
||||
phiC, phipC = yb
|
||||
|
||||
kappa, ioA, ioC, V, f = 1.64, 0.01, 1.0e-4, 0.5, 38.9
|
||||
|
||||
# Butler-Volmer Kinetics at Anode
|
||||
hA = 0.0-phiA-0.0
|
||||
iA = ioA * (np.exp(f*hA) - np.exp(-f*hA))
|
||||
res0 = iA + kappa * phipA
|
||||
|
||||
# Butler-Volmer Kinetics at Cathode
|
||||
hC = V - phiC - 1.0
|
||||
iC = ioC * (np.exp(f*hC) - np.exp(-f*hC))
|
||||
res1 = iC - kappa*phipC
|
||||
|
||||
return np.array([res0, res1])
|
||||
|
||||
|
||||
def nonlin_bc_sol(x):
|
||||
return -0.13426436116763119 - 1.1308709 * x
|
||||
|
||||
|
||||
def test_modify_mesh():
|
||||
x = np.array([0, 1, 3, 9], dtype=float)
|
||||
x_new = modify_mesh(x, np.array([0]), np.array([2]))
|
||||
assert_array_equal(x_new, np.array([0, 0.5, 1, 3, 5, 7, 9]))
|
||||
|
||||
x = np.array([-6, -3, 0, 3, 6], dtype=float)
|
||||
x_new = modify_mesh(x, np.array([1], dtype=int), np.array([0, 2, 3]))
|
||||
assert_array_equal(x_new, [-6, -5, -4, -3, -1.5, 0, 1, 2, 3, 4, 5, 6])
|
||||
|
||||
|
||||
def test_compute_fun_jac():
|
||||
x = np.linspace(0, 1, 5)
|
||||
y = np.empty((2, x.shape[0]))
|
||||
y[0] = 0.01
|
||||
y[1] = 0.02
|
||||
p = np.array([])
|
||||
df_dy, df_dp = estimate_fun_jac(lambda x, y, p: exp_fun(x, y), x, y, p)
|
||||
df_dy_an = exp_fun_jac(x, y)
|
||||
assert_allclose(df_dy, df_dy_an)
|
||||
assert_(df_dp is None)
|
||||
|
||||
x = np.linspace(0, np.pi, 5)
|
||||
y = np.empty((2, x.shape[0]))
|
||||
y[0] = np.sin(x)
|
||||
y[1] = np.cos(x)
|
||||
p = np.array([1.0])
|
||||
df_dy, df_dp = estimate_fun_jac(sl_fun, x, y, p)
|
||||
df_dy_an, df_dp_an = sl_fun_jac(x, y, p)
|
||||
assert_allclose(df_dy, df_dy_an)
|
||||
assert_allclose(df_dp, df_dp_an)
|
||||
|
||||
x = np.linspace(0, 1, 10)
|
||||
y = np.empty((2, x.shape[0]))
|
||||
y[0] = (3/4)**0.5
|
||||
y[1] = 1e-4
|
||||
p = np.array([])
|
||||
df_dy, df_dp = estimate_fun_jac(lambda x, y, p: emden_fun(x, y), x, y, p)
|
||||
df_dy_an = emden_fun_jac(x, y)
|
||||
assert_allclose(df_dy, df_dy_an)
|
||||
assert_(df_dp is None)
|
||||
|
||||
|
||||
def test_compute_bc_jac():
|
||||
ya = np.array([-1.0, 2])
|
||||
yb = np.array([0.5, 3])
|
||||
p = np.array([])
|
||||
dbc_dya, dbc_dyb, dbc_dp = estimate_bc_jac(
|
||||
lambda ya, yb, p: exp_bc(ya, yb), ya, yb, p)
|
||||
dbc_dya_an, dbc_dyb_an = exp_bc_jac(ya, yb)
|
||||
assert_allclose(dbc_dya, dbc_dya_an)
|
||||
assert_allclose(dbc_dyb, dbc_dyb_an)
|
||||
assert_(dbc_dp is None)
|
||||
|
||||
ya = np.array([0.0, 1])
|
||||
yb = np.array([0.0, -1])
|
||||
p = np.array([0.5])
|
||||
dbc_dya, dbc_dyb, dbc_dp = estimate_bc_jac(sl_bc, ya, yb, p)
|
||||
dbc_dya_an, dbc_dyb_an, dbc_dp_an = sl_bc_jac(ya, yb, p)
|
||||
assert_allclose(dbc_dya, dbc_dya_an)
|
||||
assert_allclose(dbc_dyb, dbc_dyb_an)
|
||||
assert_allclose(dbc_dp, dbc_dp_an)
|
||||
|
||||
ya = np.array([0.5, 100])
|
||||
yb = np.array([-1000, 10.5])
|
||||
p = np.array([])
|
||||
dbc_dya, dbc_dyb, dbc_dp = estimate_bc_jac(
|
||||
lambda ya, yb, p: emden_bc(ya, yb), ya, yb, p)
|
||||
dbc_dya_an, dbc_dyb_an = emden_bc_jac(ya, yb)
|
||||
assert_allclose(dbc_dya, dbc_dya_an)
|
||||
assert_allclose(dbc_dyb, dbc_dyb_an)
|
||||
assert_(dbc_dp is None)
|
||||
|
||||
|
||||
def test_compute_jac_indices():
|
||||
n = 2
|
||||
m = 4
|
||||
k = 2
|
||||
i, j = compute_jac_indices(n, m, k)
|
||||
s = coo_matrix((np.ones_like(i), (i, j))).toarray()
|
||||
s_true = np.array([
|
||||
[1, 1, 1, 1, 0, 0, 0, 0, 1, 1],
|
||||
[1, 1, 1, 1, 0, 0, 0, 0, 1, 1],
|
||||
[0, 0, 1, 1, 1, 1, 0, 0, 1, 1],
|
||||
[0, 0, 1, 1, 1, 1, 0, 0, 1, 1],
|
||||
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1],
|
||||
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 0, 0, 0, 0, 1, 1, 1, 1],
|
||||
[1, 1, 0, 0, 0, 0, 1, 1, 1, 1],
|
||||
[1, 1, 0, 0, 0, 0, 1, 1, 1, 1],
|
||||
[1, 1, 0, 0, 0, 0, 1, 1, 1, 1],
|
||||
])
|
||||
assert_array_equal(s, s_true)
|
||||
|
||||
|
||||
def test_compute_global_jac():
|
||||
n = 2
|
||||
m = 5
|
||||
k = 1
|
||||
i_jac, j_jac = compute_jac_indices(2, 5, 1)
|
||||
x = np.linspace(0, 1, 5)
|
||||
h = np.diff(x)
|
||||
y = np.vstack((np.sin(np.pi * x), np.pi * np.cos(np.pi * x)))
|
||||
p = np.array([3.0])
|
||||
|
||||
f = sl_fun(x, y, p)
|
||||
|
||||
x_middle = x[:-1] + 0.5 * h
|
||||
y_middle = 0.5 * (y[:, :-1] + y[:, 1:]) - h/8 * (f[:, 1:] - f[:, :-1])
|
||||
|
||||
df_dy, df_dp = sl_fun_jac(x, y, p)
|
||||
df_dy_middle, df_dp_middle = sl_fun_jac(x_middle, y_middle, p)
|
||||
dbc_dya, dbc_dyb, dbc_dp = sl_bc_jac(y[:, 0], y[:, -1], p)
|
||||
|
||||
J = construct_global_jac(n, m, k, i_jac, j_jac, h, df_dy, df_dy_middle,
|
||||
df_dp, df_dp_middle, dbc_dya, dbc_dyb, dbc_dp)
|
||||
J = J.toarray()
|
||||
|
||||
def J_block(h, p):
|
||||
return np.array([
|
||||
[h**2*p**2/12 - 1, -0.5*h, -h**2*p**2/12 + 1, -0.5*h],
|
||||
[0.5*h*p**2, h**2*p**2/12 - 1, 0.5*h*p**2, 1 - h**2*p**2/12]
|
||||
])
|
||||
|
||||
J_true = np.zeros((m * n + k, m * n + k))
|
||||
for i in range(m - 1):
|
||||
J_true[i * n: (i + 1) * n, i * n: (i + 2) * n] = J_block(h[i], p[0])
|
||||
|
||||
J_true[:(m - 1) * n:2, -1] = p * h**2/6 * (y[0, :-1] - y[0, 1:])
|
||||
J_true[1:(m - 1) * n:2, -1] = p * (h * (y[0, :-1] + y[0, 1:]) +
|
||||
h**2/6 * (y[1, :-1] - y[1, 1:]))
|
||||
|
||||
J_true[8, 0] = 1
|
||||
J_true[9, 8] = 1
|
||||
J_true[10, 1] = 1
|
||||
J_true[10, 10] = -1
|
||||
|
||||
assert_allclose(J, J_true, rtol=1e-10)
|
||||
|
||||
df_dy, df_dp = estimate_fun_jac(sl_fun, x, y, p)
|
||||
df_dy_middle, df_dp_middle = estimate_fun_jac(sl_fun, x_middle, y_middle, p)
|
||||
dbc_dya, dbc_dyb, dbc_dp = estimate_bc_jac(sl_bc, y[:, 0], y[:, -1], p)
|
||||
J = construct_global_jac(n, m, k, i_jac, j_jac, h, df_dy, df_dy_middle,
|
||||
df_dp, df_dp_middle, dbc_dya, dbc_dyb, dbc_dp)
|
||||
J = J.toarray()
|
||||
assert_allclose(J, J_true, rtol=2e-8, atol=2e-8)
|
||||
|
||||
|
||||
def test_parameter_validation():
|
||||
x = [0, 1, 0.5]
|
||||
y = np.zeros((2, 3))
|
||||
assert_raises(ValueError, solve_bvp, exp_fun, exp_bc, x, y)
|
||||
|
||||
x = np.linspace(0, 1, 5)
|
||||
y = np.zeros((2, 4))
|
||||
assert_raises(ValueError, solve_bvp, exp_fun, exp_bc, x, y)
|
||||
|
||||
def fun(x, y, p):
|
||||
return exp_fun(x, y)
|
||||
def bc(ya, yb, p):
|
||||
return exp_bc(ya, yb)
|
||||
|
||||
y = np.zeros((2, x.shape[0]))
|
||||
assert_raises(ValueError, solve_bvp, fun, bc, x, y, p=[1])
|
||||
|
||||
def wrong_shape_fun(x, y):
|
||||
return np.zeros(3)
|
||||
|
||||
assert_raises(ValueError, solve_bvp, wrong_shape_fun, bc, x, y)
|
||||
|
||||
S = np.array([[0, 0]])
|
||||
assert_raises(ValueError, solve_bvp, exp_fun, exp_bc, x, y, S=S)
|
||||
|
||||
|
||||
def test_no_params():
|
||||
x = np.linspace(0, 1, 5)
|
||||
x_test = np.linspace(0, 1, 100)
|
||||
y = np.zeros((2, x.shape[0]))
|
||||
for fun_jac in [None, exp_fun_jac]:
|
||||
for bc_jac in [None, exp_bc_jac]:
|
||||
sol = solve_bvp(exp_fun, exp_bc, x, y, fun_jac=fun_jac,
|
||||
bc_jac=bc_jac)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
assert_equal(sol.x.size, 5)
|
||||
|
||||
sol_test = sol.sol(x_test)
|
||||
|
||||
assert_allclose(sol_test[0], exp_sol(x_test), atol=1e-5)
|
||||
|
||||
f_test = exp_fun(x_test, sol_test)
|
||||
r = sol.sol(x_test, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(rel_res**2, axis=0)**0.5
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
|
||||
assert_(np.all(sol.rms_residuals < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_with_params():
|
||||
x = np.linspace(0, np.pi, 5)
|
||||
x_test = np.linspace(0, np.pi, 100)
|
||||
y = np.ones((2, x.shape[0]))
|
||||
|
||||
for fun_jac in [None, sl_fun_jac]:
|
||||
for bc_jac in [None, sl_bc_jac]:
|
||||
sol = solve_bvp(sl_fun, sl_bc, x, y, p=[0.5], fun_jac=fun_jac,
|
||||
bc_jac=bc_jac)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
assert_(sol.x.size < 10)
|
||||
|
||||
assert_allclose(sol.p, [1], rtol=1e-4)
|
||||
|
||||
sol_test = sol.sol(x_test)
|
||||
|
||||
assert_allclose(sol_test[0], sl_sol(x_test, [1]),
|
||||
rtol=1e-4, atol=1e-4)
|
||||
|
||||
f_test = sl_fun(x_test, sol_test, [1])
|
||||
r = sol.sol(x_test, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(rel_res ** 2, axis=0) ** 0.5
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
|
||||
assert_(np.all(sol.rms_residuals < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_singular_term():
|
||||
x = np.linspace(0, 1, 10)
|
||||
x_test = np.linspace(0.05, 1, 100)
|
||||
y = np.empty((2, 10))
|
||||
y[0] = (3/4)**0.5
|
||||
y[1] = 1e-4
|
||||
S = np.array([[0, 0], [0, -2]])
|
||||
|
||||
for fun_jac in [None, emden_fun_jac]:
|
||||
for bc_jac in [None, emden_bc_jac]:
|
||||
sol = solve_bvp(emden_fun, emden_bc, x, y, S=S, fun_jac=fun_jac,
|
||||
bc_jac=bc_jac)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
assert_equal(sol.x.size, 10)
|
||||
|
||||
sol_test = sol.sol(x_test)
|
||||
assert_allclose(sol_test[0], emden_sol(x_test), atol=1e-5)
|
||||
|
||||
f_test = emden_fun(x_test, sol_test) + S.dot(sol_test) / x_test
|
||||
r = sol.sol(x_test, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(rel_res ** 2, axis=0) ** 0.5
|
||||
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_complex():
|
||||
# The test is essentially the same as test_no_params, but boundary
|
||||
# conditions are turned into complex.
|
||||
x = np.linspace(0, 1, 5)
|
||||
x_test = np.linspace(0, 1, 100)
|
||||
y = np.zeros((2, x.shape[0]), dtype=complex)
|
||||
for fun_jac in [None, exp_fun_jac]:
|
||||
for bc_jac in [None, exp_bc_jac]:
|
||||
sol = solve_bvp(exp_fun, exp_bc_complex, x, y, fun_jac=fun_jac,
|
||||
bc_jac=bc_jac)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
sol_test = sol.sol(x_test)
|
||||
|
||||
assert_allclose(sol_test[0].real, exp_sol(x_test), atol=1e-5)
|
||||
assert_allclose(sol_test[0].imag, exp_sol(x_test), atol=1e-5)
|
||||
|
||||
f_test = exp_fun(x_test, sol_test)
|
||||
r = sol.sol(x_test, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(np.real(rel_res * np.conj(rel_res)),
|
||||
axis=0) ** 0.5
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
|
||||
assert_(np.all(sol.rms_residuals < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_failures():
|
||||
x = np.linspace(0, 1, 2)
|
||||
y = np.zeros((2, x.size))
|
||||
res = solve_bvp(exp_fun, exp_bc, x, y, tol=1e-5, max_nodes=5)
|
||||
assert_equal(res.status, 1)
|
||||
assert_(not res.success)
|
||||
|
||||
x = np.linspace(0, 1, 5)
|
||||
y = np.zeros((2, x.size))
|
||||
res = solve_bvp(undefined_fun, undefined_bc, x, y)
|
||||
assert_equal(res.status, 2)
|
||||
assert_(not res.success)
|
||||
|
||||
|
||||
def test_big_problem():
|
||||
n = 30
|
||||
x = np.linspace(0, 1, 5)
|
||||
y = np.zeros((2 * n, x.size))
|
||||
sol = solve_bvp(big_fun, big_bc, x, y)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
sol_test = sol.sol(x)
|
||||
|
||||
assert_allclose(sol_test[0], big_sol(x, n))
|
||||
|
||||
f_test = big_fun(x, sol_test)
|
||||
r = sol.sol(x, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(np.real(rel_res * np.conj(rel_res)), axis=0) ** 0.5
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
|
||||
assert_(np.all(sol.rms_residuals < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_big_problem_with_parameters():
|
||||
n = 30
|
||||
x = np.linspace(0, np.pi, 5)
|
||||
x_test = np.linspace(0, np.pi, 100)
|
||||
y = np.ones((2 * n, x.size))
|
||||
|
||||
for fun_jac in [None, big_fun_with_parameters_jac]:
|
||||
for bc_jac in [None, big_bc_with_parameters_jac]:
|
||||
sol = solve_bvp(big_fun_with_parameters, big_bc_with_parameters, x,
|
||||
y, p=[0.5, 0.5], fun_jac=fun_jac, bc_jac=bc_jac)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
assert_allclose(sol.p, [1, 1], rtol=1e-4)
|
||||
|
||||
sol_test = sol.sol(x_test)
|
||||
|
||||
for isol in range(0, n, 4):
|
||||
assert_allclose(sol_test[isol],
|
||||
big_sol_with_parameters(x_test, [1, 1])[0],
|
||||
rtol=1e-4, atol=1e-4)
|
||||
assert_allclose(sol_test[isol + 2],
|
||||
big_sol_with_parameters(x_test, [1, 1])[1],
|
||||
rtol=1e-4, atol=1e-4)
|
||||
|
||||
f_test = big_fun_with_parameters(x_test, sol_test, [1, 1])
|
||||
r = sol.sol(x_test, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(rel_res ** 2, axis=0) ** 0.5
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
|
||||
assert_(np.all(sol.rms_residuals < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_shock_layer():
|
||||
x = np.linspace(-1, 1, 5)
|
||||
x_test = np.linspace(-1, 1, 100)
|
||||
y = np.zeros((2, x.size))
|
||||
sol = solve_bvp(shock_fun, shock_bc, x, y)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
assert_(sol.x.size < 110)
|
||||
|
||||
sol_test = sol.sol(x_test)
|
||||
assert_allclose(sol_test[0], shock_sol(x_test), rtol=1e-5, atol=1e-5)
|
||||
|
||||
f_test = shock_fun(x_test, sol_test)
|
||||
r = sol.sol(x_test, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(rel_res ** 2, axis=0) ** 0.5
|
||||
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_nonlin_bc():
|
||||
x = np.linspace(0, 0.1, 5)
|
||||
x_test = x
|
||||
y = np.zeros([2, x.size])
|
||||
sol = solve_bvp(nonlin_bc_fun, nonlin_bc_bc, x, y)
|
||||
|
||||
assert_equal(sol.status, 0)
|
||||
assert_(sol.success)
|
||||
|
||||
assert_(sol.x.size < 8)
|
||||
|
||||
sol_test = sol.sol(x_test)
|
||||
assert_allclose(sol_test[0], nonlin_bc_sol(x_test), rtol=1e-5, atol=1e-5)
|
||||
|
||||
f_test = nonlin_bc_fun(x_test, sol_test)
|
||||
r = sol.sol(x_test, 1) - f_test
|
||||
rel_res = r / (1 + np.abs(f_test))
|
||||
norm_res = np.sum(rel_res ** 2, axis=0) ** 0.5
|
||||
|
||||
assert_(np.all(norm_res < 1e-3))
|
||||
assert_allclose(sol.sol(sol.x), sol.y, rtol=1e-10, atol=1e-10)
|
||||
assert_allclose(sol.sol(sol.x, 1), sol.yp, rtol=1e-10, atol=1e-10)
|
||||
|
||||
|
||||
def test_verbose():
|
||||
# Smoke test that checks the printing does something and does not crash
|
||||
x = np.linspace(0, 1, 5)
|
||||
y = np.zeros((2, x.shape[0]))
|
||||
for verbose in [0, 1, 2]:
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = StringIO()
|
||||
try:
|
||||
sol = solve_bvp(exp_fun, exp_bc, x, y, verbose=verbose)
|
||||
text = sys.stdout.getvalue()
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
assert_(sol.success)
|
||||
if verbose == 0:
|
||||
assert_(not text, text)
|
||||
if verbose >= 1:
|
||||
assert_("Solved in" in text, text)
|
||||
if verbose >= 2:
|
||||
assert_("Max residual" in text, text)
|
||||
@ -0,0 +1,834 @@
|
||||
# Authors: Nils Wagner, Ed Schofield, Pauli Virtanen, John Travers
|
||||
"""
|
||||
Tests for numerical integration.
|
||||
"""
|
||||
import numpy as np
|
||||
from numpy import (arange, zeros, array, dot, sqrt, cos, sin, eye, pi, exp,
|
||||
allclose)
|
||||
|
||||
from numpy.testing import (
|
||||
assert_, assert_array_almost_equal,
|
||||
assert_allclose, assert_array_equal, assert_equal, assert_warns)
|
||||
from pytest import raises as assert_raises
|
||||
from scipy.integrate import odeint, ode, complex_ode
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Test ODE integrators
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOdeint:
|
||||
# Check integrate.odeint
|
||||
|
||||
def _do_problem(self, problem):
|
||||
t = arange(0.0, problem.stop_t, 0.05)
|
||||
|
||||
# Basic case
|
||||
z, infodict = odeint(problem.f, problem.z0, t, full_output=True)
|
||||
assert_(problem.verify(z, t))
|
||||
|
||||
# Use tfirst=True
|
||||
z, infodict = odeint(lambda t, y: problem.f(y, t), problem.z0, t,
|
||||
full_output=True, tfirst=True)
|
||||
assert_(problem.verify(z, t))
|
||||
|
||||
if hasattr(problem, 'jac'):
|
||||
# Use Dfun
|
||||
z, infodict = odeint(problem.f, problem.z0, t, Dfun=problem.jac,
|
||||
full_output=True)
|
||||
assert_(problem.verify(z, t))
|
||||
|
||||
# Use Dfun and tfirst=True
|
||||
z, infodict = odeint(lambda t, y: problem.f(y, t), problem.z0, t,
|
||||
Dfun=lambda t, y: problem.jac(y, t),
|
||||
full_output=True, tfirst=True)
|
||||
assert_(problem.verify(z, t))
|
||||
|
||||
def test_odeint(self):
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if problem.cmplx:
|
||||
continue
|
||||
self._do_problem(problem)
|
||||
|
||||
|
||||
class TestODEClass:
|
||||
|
||||
ode_class = None # Set in subclass.
|
||||
|
||||
def _do_problem(self, problem, integrator, method='adams'):
|
||||
|
||||
# ode has callback arguments in different order than odeint
|
||||
def f(t, z):
|
||||
return problem.f(z, t)
|
||||
jac = None
|
||||
if hasattr(problem, 'jac'):
|
||||
def jac(t, z):
|
||||
return problem.jac(z, t)
|
||||
|
||||
integrator_params = {}
|
||||
if problem.lband is not None or problem.uband is not None:
|
||||
integrator_params['uband'] = problem.uband
|
||||
integrator_params['lband'] = problem.lband
|
||||
|
||||
ig = self.ode_class(f, jac)
|
||||
ig.set_integrator(integrator,
|
||||
atol=problem.atol/10,
|
||||
rtol=problem.rtol/10,
|
||||
method=method,
|
||||
**integrator_params)
|
||||
|
||||
ig.set_initial_value(problem.z0, t=0.0)
|
||||
z = ig.integrate(problem.stop_t)
|
||||
|
||||
assert_array_equal(z, ig.y)
|
||||
assert_(ig.successful(), (problem, method))
|
||||
assert_(ig.get_return_code() > 0, (problem, method))
|
||||
assert_(problem.verify(array([z]), problem.stop_t), (problem, method))
|
||||
|
||||
|
||||
class TestOde(TestODEClass):
|
||||
|
||||
ode_class = ode
|
||||
|
||||
def test_vode(self):
|
||||
# Check the vode solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if problem.cmplx:
|
||||
continue
|
||||
if not problem.stiff:
|
||||
self._do_problem(problem, 'vode', 'adams')
|
||||
self._do_problem(problem, 'vode', 'bdf')
|
||||
|
||||
def test_zvode(self):
|
||||
# Check the zvode solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if not problem.stiff:
|
||||
self._do_problem(problem, 'zvode', 'adams')
|
||||
self._do_problem(problem, 'zvode', 'bdf')
|
||||
|
||||
def test_lsoda(self):
|
||||
# Check the lsoda solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if problem.cmplx:
|
||||
continue
|
||||
self._do_problem(problem, 'lsoda')
|
||||
|
||||
def test_dopri5(self):
|
||||
# Check the dopri5 solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if problem.cmplx:
|
||||
continue
|
||||
if problem.stiff:
|
||||
continue
|
||||
if hasattr(problem, 'jac'):
|
||||
continue
|
||||
self._do_problem(problem, 'dopri5')
|
||||
|
||||
def test_dop853(self):
|
||||
# Check the dop853 solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if problem.cmplx:
|
||||
continue
|
||||
if problem.stiff:
|
||||
continue
|
||||
if hasattr(problem, 'jac'):
|
||||
continue
|
||||
self._do_problem(problem, 'dop853')
|
||||
|
||||
def test_concurrent_fail(self):
|
||||
for sol in ('vode', 'zvode', 'lsoda'):
|
||||
def f(t, y):
|
||||
return 1.0
|
||||
|
||||
r = ode(f).set_integrator(sol)
|
||||
r.set_initial_value(0, 0)
|
||||
|
||||
r2 = ode(f).set_integrator(sol)
|
||||
r2.set_initial_value(0, 0)
|
||||
|
||||
r.integrate(r.t + 0.1)
|
||||
r2.integrate(r2.t + 0.1)
|
||||
|
||||
assert_raises(RuntimeError, r.integrate, r.t + 0.1)
|
||||
|
||||
def test_concurrent_ok(self):
|
||||
def f(t, y):
|
||||
return 1.0
|
||||
|
||||
for k in range(3):
|
||||
for sol in ('vode', 'zvode', 'lsoda', 'dopri5', 'dop853'):
|
||||
r = ode(f).set_integrator(sol)
|
||||
r.set_initial_value(0, 0)
|
||||
|
||||
r2 = ode(f).set_integrator(sol)
|
||||
r2.set_initial_value(0, 0)
|
||||
|
||||
r.integrate(r.t + 0.1)
|
||||
r2.integrate(r2.t + 0.1)
|
||||
r2.integrate(r2.t + 0.1)
|
||||
|
||||
assert_allclose(r.y, 0.1)
|
||||
assert_allclose(r2.y, 0.2)
|
||||
|
||||
for sol in ('dopri5', 'dop853'):
|
||||
r = ode(f).set_integrator(sol)
|
||||
r.set_initial_value(0, 0)
|
||||
|
||||
r2 = ode(f).set_integrator(sol)
|
||||
r2.set_initial_value(0, 0)
|
||||
|
||||
r.integrate(r.t + 0.1)
|
||||
r.integrate(r.t + 0.1)
|
||||
r2.integrate(r2.t + 0.1)
|
||||
r.integrate(r.t + 0.1)
|
||||
r2.integrate(r2.t + 0.1)
|
||||
|
||||
assert_allclose(r.y, 0.3)
|
||||
assert_allclose(r2.y, 0.2)
|
||||
|
||||
|
||||
class TestComplexOde(TestODEClass):
|
||||
|
||||
ode_class = complex_ode
|
||||
|
||||
def test_vode(self):
|
||||
# Check the vode solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if not problem.stiff:
|
||||
self._do_problem(problem, 'vode', 'adams')
|
||||
else:
|
||||
self._do_problem(problem, 'vode', 'bdf')
|
||||
|
||||
def test_lsoda(self):
|
||||
# Check the lsoda solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
self._do_problem(problem, 'lsoda')
|
||||
|
||||
def test_dopri5(self):
|
||||
# Check the dopri5 solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if problem.stiff:
|
||||
continue
|
||||
if hasattr(problem, 'jac'):
|
||||
continue
|
||||
self._do_problem(problem, 'dopri5')
|
||||
|
||||
def test_dop853(self):
|
||||
# Check the dop853 solver
|
||||
for problem_cls in PROBLEMS:
|
||||
problem = problem_cls()
|
||||
if problem.stiff:
|
||||
continue
|
||||
if hasattr(problem, 'jac'):
|
||||
continue
|
||||
self._do_problem(problem, 'dop853')
|
||||
|
||||
|
||||
class TestSolout:
|
||||
# Check integrate.ode correctly handles solout for dopri5 and dop853
|
||||
def _run_solout_test(self, integrator):
|
||||
# Check correct usage of solout
|
||||
ts = []
|
||||
ys = []
|
||||
t0 = 0.0
|
||||
tend = 10.0
|
||||
y0 = [1.0, 2.0]
|
||||
|
||||
def solout(t, y):
|
||||
ts.append(t)
|
||||
ys.append(y.copy())
|
||||
|
||||
def rhs(t, y):
|
||||
return [y[0] + y[1], -y[1]**2]
|
||||
|
||||
ig = ode(rhs).set_integrator(integrator)
|
||||
ig.set_solout(solout)
|
||||
ig.set_initial_value(y0, t0)
|
||||
ret = ig.integrate(tend)
|
||||
assert_array_equal(ys[0], y0)
|
||||
assert_array_equal(ys[-1], ret)
|
||||
assert_equal(ts[0], t0)
|
||||
assert_equal(ts[-1], tend)
|
||||
|
||||
def test_solout(self):
|
||||
for integrator in ('dopri5', 'dop853'):
|
||||
self._run_solout_test(integrator)
|
||||
|
||||
def _run_solout_after_initial_test(self, integrator):
|
||||
# Check if solout works even if it is set after the initial value.
|
||||
ts = []
|
||||
ys = []
|
||||
t0 = 0.0
|
||||
tend = 10.0
|
||||
y0 = [1.0, 2.0]
|
||||
|
||||
def solout(t, y):
|
||||
ts.append(t)
|
||||
ys.append(y.copy())
|
||||
|
||||
def rhs(t, y):
|
||||
return [y[0] + y[1], -y[1]**2]
|
||||
|
||||
ig = ode(rhs).set_integrator(integrator)
|
||||
ig.set_initial_value(y0, t0)
|
||||
ig.set_solout(solout)
|
||||
ret = ig.integrate(tend)
|
||||
assert_array_equal(ys[0], y0)
|
||||
assert_array_equal(ys[-1], ret)
|
||||
assert_equal(ts[0], t0)
|
||||
assert_equal(ts[-1], tend)
|
||||
|
||||
def test_solout_after_initial(self):
|
||||
for integrator in ('dopri5', 'dop853'):
|
||||
self._run_solout_after_initial_test(integrator)
|
||||
|
||||
def _run_solout_break_test(self, integrator):
|
||||
# Check correct usage of stopping via solout
|
||||
ts = []
|
||||
ys = []
|
||||
t0 = 0.0
|
||||
tend = 10.0
|
||||
y0 = [1.0, 2.0]
|
||||
|
||||
def solout(t, y):
|
||||
ts.append(t)
|
||||
ys.append(y.copy())
|
||||
if t > tend/2.0:
|
||||
return -1
|
||||
|
||||
def rhs(t, y):
|
||||
return [y[0] + y[1], -y[1]**2]
|
||||
|
||||
ig = ode(rhs).set_integrator(integrator)
|
||||
ig.set_solout(solout)
|
||||
ig.set_initial_value(y0, t0)
|
||||
ret = ig.integrate(tend)
|
||||
assert_array_equal(ys[0], y0)
|
||||
assert_array_equal(ys[-1], ret)
|
||||
assert_equal(ts[0], t0)
|
||||
assert_(ts[-1] > tend/2.0)
|
||||
assert_(ts[-1] < tend)
|
||||
|
||||
def test_solout_break(self):
|
||||
for integrator in ('dopri5', 'dop853'):
|
||||
self._run_solout_break_test(integrator)
|
||||
|
||||
|
||||
class TestComplexSolout:
|
||||
# Check integrate.ode correctly handles solout for dopri5 and dop853
|
||||
def _run_solout_test(self, integrator):
|
||||
# Check correct usage of solout
|
||||
ts = []
|
||||
ys = []
|
||||
t0 = 0.0
|
||||
tend = 20.0
|
||||
y0 = [0.0]
|
||||
|
||||
def solout(t, y):
|
||||
ts.append(t)
|
||||
ys.append(y.copy())
|
||||
|
||||
def rhs(t, y):
|
||||
return [1.0/(t - 10.0 - 1j)]
|
||||
|
||||
ig = complex_ode(rhs).set_integrator(integrator)
|
||||
ig.set_solout(solout)
|
||||
ig.set_initial_value(y0, t0)
|
||||
ret = ig.integrate(tend)
|
||||
assert_array_equal(ys[0], y0)
|
||||
assert_array_equal(ys[-1], ret)
|
||||
assert_equal(ts[0], t0)
|
||||
assert_equal(ts[-1], tend)
|
||||
|
||||
def test_solout(self):
|
||||
for integrator in ('dopri5', 'dop853'):
|
||||
self._run_solout_test(integrator)
|
||||
|
||||
def _run_solout_break_test(self, integrator):
|
||||
# Check correct usage of stopping via solout
|
||||
ts = []
|
||||
ys = []
|
||||
t0 = 0.0
|
||||
tend = 20.0
|
||||
y0 = [0.0]
|
||||
|
||||
def solout(t, y):
|
||||
ts.append(t)
|
||||
ys.append(y.copy())
|
||||
if t > tend/2.0:
|
||||
return -1
|
||||
|
||||
def rhs(t, y):
|
||||
return [1.0/(t - 10.0 - 1j)]
|
||||
|
||||
ig = complex_ode(rhs).set_integrator(integrator)
|
||||
ig.set_solout(solout)
|
||||
ig.set_initial_value(y0, t0)
|
||||
ret = ig.integrate(tend)
|
||||
assert_array_equal(ys[0], y0)
|
||||
assert_array_equal(ys[-1], ret)
|
||||
assert_equal(ts[0], t0)
|
||||
assert_(ts[-1] > tend/2.0)
|
||||
assert_(ts[-1] < tend)
|
||||
|
||||
def test_solout_break(self):
|
||||
for integrator in ('dopri5', 'dop853'):
|
||||
self._run_solout_break_test(integrator)
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Test problems
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ODE:
|
||||
"""
|
||||
ODE problem
|
||||
"""
|
||||
stiff = False
|
||||
cmplx = False
|
||||
stop_t = 1
|
||||
z0 = []
|
||||
|
||||
lband = None
|
||||
uband = None
|
||||
|
||||
atol = 1e-6
|
||||
rtol = 1e-5
|
||||
|
||||
|
||||
class SimpleOscillator(ODE):
|
||||
r"""
|
||||
Free vibration of a simple oscillator::
|
||||
m \ddot{u} + k u = 0, u(0) = u_0 \dot{u}(0) \dot{u}_0
|
||||
Solution::
|
||||
u(t) = u_0*cos(sqrt(k/m)*t)+\dot{u}_0*sin(sqrt(k/m)*t)/sqrt(k/m)
|
||||
"""
|
||||
stop_t = 1 + 0.09
|
||||
z0 = array([1.0, 0.1], float)
|
||||
|
||||
k = 4.0
|
||||
m = 1.0
|
||||
|
||||
def f(self, z, t):
|
||||
tmp = zeros((2, 2), float)
|
||||
tmp[0, 1] = 1.0
|
||||
tmp[1, 0] = -self.k / self.m
|
||||
return dot(tmp, z)
|
||||
|
||||
def verify(self, zs, t):
|
||||
omega = sqrt(self.k / self.m)
|
||||
u = self.z0[0]*cos(omega*t) + self.z0[1]*sin(omega*t)/omega
|
||||
return allclose(u, zs[:, 0], atol=self.atol, rtol=self.rtol)
|
||||
|
||||
|
||||
class ComplexExp(ODE):
|
||||
r"""The equation :lm:`\dot u = i u`"""
|
||||
stop_t = 1.23*pi
|
||||
z0 = exp([1j, 2j, 3j, 4j, 5j])
|
||||
cmplx = True
|
||||
|
||||
def f(self, z, t):
|
||||
return 1j*z
|
||||
|
||||
def jac(self, z, t):
|
||||
return 1j*eye(5)
|
||||
|
||||
def verify(self, zs, t):
|
||||
u = self.z0 * exp(1j*t)
|
||||
return allclose(u, zs, atol=self.atol, rtol=self.rtol)
|
||||
|
||||
|
||||
class Pi(ODE):
|
||||
r"""Integrate 1/(t + 1j) from t=-10 to t=10"""
|
||||
stop_t = 20
|
||||
z0 = [0]
|
||||
cmplx = True
|
||||
|
||||
def f(self, z, t):
|
||||
return array([1./(t - 10 + 1j)])
|
||||
|
||||
def verify(self, zs, t):
|
||||
u = -2j * np.arctan(10)
|
||||
return allclose(u, zs[-1, :], atol=self.atol, rtol=self.rtol)
|
||||
|
||||
|
||||
class CoupledDecay(ODE):
|
||||
r"""
|
||||
3 coupled decays suited for banded treatment
|
||||
(banded mode makes it necessary when N>>3)
|
||||
"""
|
||||
|
||||
stiff = True
|
||||
stop_t = 0.5
|
||||
z0 = [5.0, 7.0, 13.0]
|
||||
lband = 1
|
||||
uband = 0
|
||||
|
||||
lmbd = [0.17, 0.23, 0.29] # fictitious decay constants
|
||||
|
||||
def f(self, z, t):
|
||||
lmbd = self.lmbd
|
||||
return np.array([-lmbd[0]*z[0],
|
||||
-lmbd[1]*z[1] + lmbd[0]*z[0],
|
||||
-lmbd[2]*z[2] + lmbd[1]*z[1]])
|
||||
|
||||
def jac(self, z, t):
|
||||
# The full Jacobian is
|
||||
#
|
||||
# [-lmbd[0] 0 0 ]
|
||||
# [ lmbd[0] -lmbd[1] 0 ]
|
||||
# [ 0 lmbd[1] -lmbd[2]]
|
||||
#
|
||||
# The lower and upper bandwidths are lband=1 and uband=0, resp.
|
||||
# The representation of this array in packed format is
|
||||
#
|
||||
# [-lmbd[0] -lmbd[1] -lmbd[2]]
|
||||
# [ lmbd[0] lmbd[1] 0 ]
|
||||
|
||||
lmbd = self.lmbd
|
||||
j = np.zeros((self.lband + self.uband + 1, 3), order='F')
|
||||
|
||||
def set_j(ri, ci, val):
|
||||
j[self.uband + ri - ci, ci] = val
|
||||
set_j(0, 0, -lmbd[0])
|
||||
set_j(1, 0, lmbd[0])
|
||||
set_j(1, 1, -lmbd[1])
|
||||
set_j(2, 1, lmbd[1])
|
||||
set_j(2, 2, -lmbd[2])
|
||||
return j
|
||||
|
||||
def verify(self, zs, t):
|
||||
# Formulae derived by hand
|
||||
lmbd = np.array(self.lmbd)
|
||||
d10 = lmbd[1] - lmbd[0]
|
||||
d21 = lmbd[2] - lmbd[1]
|
||||
d20 = lmbd[2] - lmbd[0]
|
||||
e0 = np.exp(-lmbd[0] * t)
|
||||
e1 = np.exp(-lmbd[1] * t)
|
||||
e2 = np.exp(-lmbd[2] * t)
|
||||
u = np.vstack((
|
||||
self.z0[0] * e0,
|
||||
self.z0[1] * e1 + self.z0[0] * lmbd[0] / d10 * (e0 - e1),
|
||||
self.z0[2] * e2 + self.z0[1] * lmbd[1] / d21 * (e1 - e2) +
|
||||
lmbd[1] * lmbd[0] * self.z0[0] / d10 *
|
||||
(1 / d20 * (e0 - e2) - 1 / d21 * (e1 - e2)))).transpose()
|
||||
return allclose(u, zs, atol=self.atol, rtol=self.rtol)
|
||||
|
||||
|
||||
PROBLEMS = [SimpleOscillator, ComplexExp, Pi, CoupledDecay]
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def f(t, x):
|
||||
dxdt = [x[1], -x[0]]
|
||||
return dxdt
|
||||
|
||||
|
||||
def jac(t, x):
|
||||
j = array([[0.0, 1.0],
|
||||
[-1.0, 0.0]])
|
||||
return j
|
||||
|
||||
|
||||
def f1(t, x, omega):
|
||||
dxdt = [omega*x[1], -omega*x[0]]
|
||||
return dxdt
|
||||
|
||||
|
||||
def jac1(t, x, omega):
|
||||
j = array([[0.0, omega],
|
||||
[-omega, 0.0]])
|
||||
return j
|
||||
|
||||
|
||||
def f2(t, x, omega1, omega2):
|
||||
dxdt = [omega1*x[1], -omega2*x[0]]
|
||||
return dxdt
|
||||
|
||||
|
||||
def jac2(t, x, omega1, omega2):
|
||||
j = array([[0.0, omega1],
|
||||
[-omega2, 0.0]])
|
||||
return j
|
||||
|
||||
|
||||
def fv(t, x, omega):
|
||||
dxdt = [omega[0]*x[1], -omega[1]*x[0]]
|
||||
return dxdt
|
||||
|
||||
|
||||
def jacv(t, x, omega):
|
||||
j = array([[0.0, omega[0]],
|
||||
[-omega[1], 0.0]])
|
||||
return j
|
||||
|
||||
|
||||
class ODECheckParameterUse:
|
||||
"""Call an ode-class solver with several cases of parameter use."""
|
||||
|
||||
# solver_name must be set before tests can be run with this class.
|
||||
|
||||
# Set these in subclasses.
|
||||
solver_name = ''
|
||||
solver_uses_jac = False
|
||||
|
||||
def _get_solver(self, f, jac):
|
||||
solver = ode(f, jac)
|
||||
if self.solver_uses_jac:
|
||||
solver.set_integrator(self.solver_name, atol=1e-9, rtol=1e-7,
|
||||
with_jacobian=self.solver_uses_jac)
|
||||
else:
|
||||
# XXX Shouldn't set_integrator *always* accept the keyword arg
|
||||
# 'with_jacobian', and perhaps raise an exception if it is set
|
||||
# to True if the solver can't actually use it?
|
||||
solver.set_integrator(self.solver_name, atol=1e-9, rtol=1e-7)
|
||||
return solver
|
||||
|
||||
def _check_solver(self, solver):
|
||||
ic = [1.0, 0.0]
|
||||
solver.set_initial_value(ic, 0.0)
|
||||
solver.integrate(pi)
|
||||
assert_array_almost_equal(solver.y, [-1.0, 0.0])
|
||||
|
||||
def test_no_params(self):
|
||||
solver = self._get_solver(f, jac)
|
||||
self._check_solver(solver)
|
||||
|
||||
def test_one_scalar_param(self):
|
||||
solver = self._get_solver(f1, jac1)
|
||||
omega = 1.0
|
||||
solver.set_f_params(omega)
|
||||
if self.solver_uses_jac:
|
||||
solver.set_jac_params(omega)
|
||||
self._check_solver(solver)
|
||||
|
||||
def test_two_scalar_params(self):
|
||||
solver = self._get_solver(f2, jac2)
|
||||
omega1 = 1.0
|
||||
omega2 = 1.0
|
||||
solver.set_f_params(omega1, omega2)
|
||||
if self.solver_uses_jac:
|
||||
solver.set_jac_params(omega1, omega2)
|
||||
self._check_solver(solver)
|
||||
|
||||
def test_vector_param(self):
|
||||
solver = self._get_solver(fv, jacv)
|
||||
omega = [1.0, 1.0]
|
||||
solver.set_f_params(omega)
|
||||
if self.solver_uses_jac:
|
||||
solver.set_jac_params(omega)
|
||||
self._check_solver(solver)
|
||||
|
||||
def test_warns_on_failure(self):
|
||||
# Set nsteps small to ensure failure
|
||||
solver = self._get_solver(f, jac)
|
||||
solver.set_integrator(self.solver_name, nsteps=1)
|
||||
ic = [1.0, 0.0]
|
||||
solver.set_initial_value(ic, 0.0)
|
||||
assert_warns(UserWarning, solver.integrate, pi)
|
||||
|
||||
|
||||
class TestDOPRI5CheckParameterUse(ODECheckParameterUse):
|
||||
solver_name = 'dopri5'
|
||||
solver_uses_jac = False
|
||||
|
||||
|
||||
class TestDOP853CheckParameterUse(ODECheckParameterUse):
|
||||
solver_name = 'dop853'
|
||||
solver_uses_jac = False
|
||||
|
||||
|
||||
class TestVODECheckParameterUse(ODECheckParameterUse):
|
||||
solver_name = 'vode'
|
||||
solver_uses_jac = True
|
||||
|
||||
|
||||
class TestZVODECheckParameterUse(ODECheckParameterUse):
|
||||
solver_name = 'zvode'
|
||||
solver_uses_jac = True
|
||||
|
||||
|
||||
class TestLSODACheckParameterUse(ODECheckParameterUse):
|
||||
solver_name = 'lsoda'
|
||||
solver_uses_jac = True
|
||||
|
||||
|
||||
def test_odeint_trivial_time():
|
||||
# Test that odeint succeeds when given a single time point
|
||||
# and full_output=True. This is a regression test for gh-4282.
|
||||
y0 = 1
|
||||
t = [0]
|
||||
y, info = odeint(lambda y, t: -y, y0, t, full_output=True)
|
||||
assert_array_equal(y, np.array([[y0]]))
|
||||
|
||||
|
||||
def test_odeint_banded_jacobian():
|
||||
# Test the use of the `Dfun`, `ml` and `mu` options of odeint.
|
||||
|
||||
def func(y, t, c):
|
||||
return c.dot(y)
|
||||
|
||||
def jac(y, t, c):
|
||||
return c
|
||||
|
||||
def jac_transpose(y, t, c):
|
||||
return c.T.copy(order='C')
|
||||
|
||||
def bjac_rows(y, t, c):
|
||||
jac = np.vstack((np.r_[0, np.diag(c, 1)],
|
||||
np.diag(c),
|
||||
np.r_[np.diag(c, -1), 0],
|
||||
np.r_[np.diag(c, -2), 0, 0]))
|
||||
return jac
|
||||
|
||||
def bjac_cols(y, t, c):
|
||||
return bjac_rows(y, t, c).T.copy(order='C')
|
||||
|
||||
c = array([[-205, 0.01, 0.00, 0.0],
|
||||
[0.1, -2.50, 0.02, 0.0],
|
||||
[1e-3, 0.01, -2.0, 0.01],
|
||||
[0.00, 0.00, 0.1, -1.0]])
|
||||
|
||||
y0 = np.ones(4)
|
||||
t = np.array([0, 5, 10, 100])
|
||||
|
||||
# Use the full Jacobian.
|
||||
sol1, info1 = odeint(func, y0, t, args=(c,), full_output=True,
|
||||
atol=1e-13, rtol=1e-11, mxstep=10000,
|
||||
Dfun=jac)
|
||||
|
||||
# Use the transposed full Jacobian, with col_deriv=True.
|
||||
sol2, info2 = odeint(func, y0, t, args=(c,), full_output=True,
|
||||
atol=1e-13, rtol=1e-11, mxstep=10000,
|
||||
Dfun=jac_transpose, col_deriv=True)
|
||||
|
||||
# Use the banded Jacobian.
|
||||
sol3, info3 = odeint(func, y0, t, args=(c,), full_output=True,
|
||||
atol=1e-13, rtol=1e-11, mxstep=10000,
|
||||
Dfun=bjac_rows, ml=2, mu=1)
|
||||
|
||||
# Use the transposed banded Jacobian, with col_deriv=True.
|
||||
sol4, info4 = odeint(func, y0, t, args=(c,), full_output=True,
|
||||
atol=1e-13, rtol=1e-11, mxstep=10000,
|
||||
Dfun=bjac_cols, ml=2, mu=1, col_deriv=True)
|
||||
|
||||
assert_allclose(sol1, sol2, err_msg="sol1 != sol2")
|
||||
assert_allclose(sol1, sol3, atol=1e-12, err_msg="sol1 != sol3")
|
||||
assert_allclose(sol3, sol4, err_msg="sol3 != sol4")
|
||||
|
||||
# Verify that the number of jacobian evaluations was the same for the
|
||||
# calls of odeint with a full jacobian and with a banded jacobian. This is
|
||||
# a regression test--there was a bug in the handling of banded jacobians
|
||||
# that resulted in an incorrect jacobian matrix being passed to the LSODA
|
||||
# code. That would cause errors or excessive jacobian evaluations.
|
||||
assert_array_equal(info1['nje'], info2['nje'])
|
||||
assert_array_equal(info3['nje'], info4['nje'])
|
||||
|
||||
# Test the use of tfirst
|
||||
sol1ty, info1ty = odeint(lambda t, y, c: func(y, t, c), y0, t, args=(c,),
|
||||
full_output=True, atol=1e-13, rtol=1e-11,
|
||||
mxstep=10000,
|
||||
Dfun=lambda t, y, c: jac(y, t, c), tfirst=True)
|
||||
# The code should execute the exact same sequence of floating point
|
||||
# calculations, so these should be exactly equal. We'll be safe and use
|
||||
# a small tolerance.
|
||||
assert_allclose(sol1, sol1ty, rtol=1e-12, err_msg="sol1 != sol1ty")
|
||||
|
||||
|
||||
def test_odeint_errors():
|
||||
def sys1d(x, t):
|
||||
return -100*x
|
||||
|
||||
def bad1(x, t):
|
||||
return 1.0/0
|
||||
|
||||
def bad2(x, t):
|
||||
return "foo"
|
||||
|
||||
def bad_jac1(x, t):
|
||||
return 1.0/0
|
||||
|
||||
def bad_jac2(x, t):
|
||||
return [["foo"]]
|
||||
|
||||
def sys2d(x, t):
|
||||
return [-100*x[0], -0.1*x[1]]
|
||||
|
||||
def sys2d_bad_jac(x, t):
|
||||
return [[1.0/0, 0], [0, -0.1]]
|
||||
|
||||
assert_raises(ZeroDivisionError, odeint, bad1, 1.0, [0, 1])
|
||||
assert_raises(ValueError, odeint, bad2, 1.0, [0, 1])
|
||||
|
||||
assert_raises(ZeroDivisionError, odeint, sys1d, 1.0, [0, 1], Dfun=bad_jac1)
|
||||
assert_raises(ValueError, odeint, sys1d, 1.0, [0, 1], Dfun=bad_jac2)
|
||||
|
||||
assert_raises(ZeroDivisionError, odeint, sys2d, [1.0, 1.0], [0, 1],
|
||||
Dfun=sys2d_bad_jac)
|
||||
|
||||
|
||||
def test_odeint_bad_shapes():
|
||||
# Tests of some errors that can occur with odeint.
|
||||
|
||||
def badrhs(x, t):
|
||||
return [1, -1]
|
||||
|
||||
def sys1(x, t):
|
||||
return -100*x
|
||||
|
||||
def badjac(x, t):
|
||||
return [[0, 0, 0]]
|
||||
|
||||
# y0 must be at most 1-d.
|
||||
bad_y0 = [[0, 0], [0, 0]]
|
||||
assert_raises(ValueError, odeint, sys1, bad_y0, [0, 1])
|
||||
|
||||
# t must be at most 1-d.
|
||||
bad_t = [[0, 1], [2, 3]]
|
||||
assert_raises(ValueError, odeint, sys1, [10.0], bad_t)
|
||||
|
||||
# y0 is 10, but badrhs(x, t) returns [1, -1].
|
||||
assert_raises(RuntimeError, odeint, badrhs, 10, [0, 1])
|
||||
|
||||
# shape of array returned by badjac(x, t) is not correct.
|
||||
assert_raises(RuntimeError, odeint, sys1, [10, 10], [0, 1], Dfun=badjac)
|
||||
|
||||
|
||||
def test_repeated_t_values():
|
||||
"""Regression test for gh-8217."""
|
||||
|
||||
def func(x, t):
|
||||
return -0.25*x
|
||||
|
||||
t = np.zeros(10)
|
||||
sol = odeint(func, [1.], t)
|
||||
assert_array_equal(sol, np.ones((len(t), 1)))
|
||||
|
||||
tau = 4*np.log(2)
|
||||
t = [0]*9 + [tau, 2*tau, 2*tau, 3*tau]
|
||||
sol = odeint(func, [1, 2], t, rtol=1e-12, atol=1e-12)
|
||||
expected_sol = np.array([[1.0, 2.0]]*9 +
|
||||
[[0.5, 1.0],
|
||||
[0.25, 0.5],
|
||||
[0.25, 0.5],
|
||||
[0.125, 0.25]])
|
||||
assert_allclose(sol, expected_sol)
|
||||
|
||||
# Edge case: empty t sequence.
|
||||
sol = odeint(func, [1.], [])
|
||||
assert_array_equal(sol, np.array([], dtype=np.float64).reshape((0, 1)))
|
||||
|
||||
# t values are not monotonic.
|
||||
assert_raises(ValueError, odeint, func, [1.], [0, 1, 0.5, 0])
|
||||
assert_raises(ValueError, odeint, func, [1, 2, 3], [0, -1, -2, 3])
|
||||
@ -0,0 +1,74 @@
|
||||
import numpy as np
|
||||
from numpy.testing import assert_equal, assert_allclose
|
||||
from scipy.integrate import odeint
|
||||
import scipy.integrate._test_odeint_banded as banded5x5
|
||||
|
||||
|
||||
def rhs(y, t):
|
||||
dydt = np.zeros_like(y)
|
||||
banded5x5.banded5x5(t, y, dydt)
|
||||
return dydt
|
||||
|
||||
|
||||
def jac(y, t):
|
||||
n = len(y)
|
||||
jac = np.zeros((n, n), order='F')
|
||||
banded5x5.banded5x5_jac(t, y, 1, 1, jac)
|
||||
return jac
|
||||
|
||||
|
||||
def bjac(y, t):
|
||||
n = len(y)
|
||||
bjac = np.zeros((4, n), order='F')
|
||||
banded5x5.banded5x5_bjac(t, y, 1, 1, bjac)
|
||||
return bjac
|
||||
|
||||
|
||||
JACTYPE_FULL = 1
|
||||
JACTYPE_BANDED = 4
|
||||
|
||||
|
||||
def check_odeint(jactype):
|
||||
if jactype == JACTYPE_FULL:
|
||||
ml = None
|
||||
mu = None
|
||||
jacobian = jac
|
||||
elif jactype == JACTYPE_BANDED:
|
||||
ml = 2
|
||||
mu = 1
|
||||
jacobian = bjac
|
||||
else:
|
||||
raise ValueError(f"invalid jactype: {jactype!r}")
|
||||
|
||||
y0 = np.arange(1.0, 6.0)
|
||||
# These tolerances must match the tolerances used in banded5x5.f.
|
||||
rtol = 1e-11
|
||||
atol = 1e-13
|
||||
dt = 0.125
|
||||
nsteps = 64
|
||||
t = dt * np.arange(nsteps+1)
|
||||
|
||||
sol, info = odeint(rhs, y0, t,
|
||||
Dfun=jacobian, ml=ml, mu=mu,
|
||||
atol=atol, rtol=rtol, full_output=True)
|
||||
yfinal = sol[-1]
|
||||
odeint_nst = info['nst'][-1]
|
||||
odeint_nfe = info['nfe'][-1]
|
||||
odeint_nje = info['nje'][-1]
|
||||
|
||||
y1 = y0.copy()
|
||||
# Pure Fortran solution. y1 is modified in-place.
|
||||
nst, nfe, nje = banded5x5.banded5x5_solve(y1, nsteps, dt, jactype)
|
||||
|
||||
# It is likely that yfinal and y1 are *exactly* the same, but
|
||||
# we'll be cautious and use assert_allclose.
|
||||
assert_allclose(yfinal, y1, rtol=1e-12)
|
||||
assert_equal((odeint_nst, odeint_nfe, odeint_nje), (nst, nfe, nje))
|
||||
|
||||
|
||||
def test_odeint_full_jac():
|
||||
check_odeint(JACTYPE_FULL)
|
||||
|
||||
|
||||
def test_odeint_banded_jac():
|
||||
check_odeint(JACTYPE_BANDED)
|
||||
@ -0,0 +1,680 @@
|
||||
import sys
|
||||
import math
|
||||
import numpy as np
|
||||
from numpy import sqrt, cos, sin, arctan, exp, log, pi
|
||||
from numpy.testing import (assert_,
|
||||
assert_allclose, assert_array_less, assert_almost_equal)
|
||||
import pytest
|
||||
|
||||
from scipy.integrate import quad, dblquad, tplquad, nquad
|
||||
from scipy.special import erf, erfc
|
||||
from scipy._lib._ccallback import LowLevelCallable
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
from scipy._lib._ccallback_c import sine_ctypes
|
||||
|
||||
import scipy.integrate._test_multivariate as clib_test
|
||||
|
||||
|
||||
def assert_quad(value_and_err, tabled_value, error_tolerance=1.5e-8):
|
||||
value, err = value_and_err
|
||||
assert_allclose(value, tabled_value, atol=err, rtol=0)
|
||||
if error_tolerance is not None:
|
||||
assert_array_less(err, error_tolerance)
|
||||
|
||||
|
||||
def get_clib_test_routine(name, restype, *argtypes):
|
||||
ptr = getattr(clib_test, name)
|
||||
return ctypes.cast(ptr, ctypes.CFUNCTYPE(restype, *argtypes))
|
||||
|
||||
|
||||
class TestCtypesQuad:
|
||||
def setup_method(self):
|
||||
if sys.platform == 'win32':
|
||||
files = ['api-ms-win-crt-math-l1-1-0.dll']
|
||||
elif sys.platform == 'darwin':
|
||||
files = ['libm.dylib']
|
||||
else:
|
||||
files = ['libm.so', 'libm.so.6']
|
||||
|
||||
for file in files:
|
||||
try:
|
||||
self.lib = ctypes.CDLL(file)
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
# This test doesn't work on some Linux platforms (Fedora for
|
||||
# example) that put an ld script in libm.so - see gh-5370
|
||||
pytest.skip("Ctypes can't import libm.so")
|
||||
|
||||
restype = ctypes.c_double
|
||||
argtypes = (ctypes.c_double,)
|
||||
for name in ['sin', 'cos', 'tan']:
|
||||
func = getattr(self.lib, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
|
||||
def test_typical(self):
|
||||
assert_quad(quad(self.lib.sin, 0, 5), quad(math.sin, 0, 5)[0])
|
||||
assert_quad(quad(self.lib.cos, 0, 5), quad(math.cos, 0, 5)[0])
|
||||
assert_quad(quad(self.lib.tan, 0, 1), quad(math.tan, 0, 1)[0])
|
||||
|
||||
def test_ctypes_sine(self):
|
||||
quad(LowLevelCallable(sine_ctypes), 0, 1)
|
||||
|
||||
def test_ctypes_variants(self):
|
||||
sin_0 = get_clib_test_routine('_sin_0', ctypes.c_double,
|
||||
ctypes.c_double, ctypes.c_void_p)
|
||||
|
||||
sin_1 = get_clib_test_routine('_sin_1', ctypes.c_double,
|
||||
ctypes.c_int, ctypes.POINTER(ctypes.c_double),
|
||||
ctypes.c_void_p)
|
||||
|
||||
sin_2 = get_clib_test_routine('_sin_2', ctypes.c_double,
|
||||
ctypes.c_double)
|
||||
|
||||
sin_3 = get_clib_test_routine('_sin_3', ctypes.c_double,
|
||||
ctypes.c_int, ctypes.POINTER(ctypes.c_double))
|
||||
|
||||
sin_4 = get_clib_test_routine('_sin_3', ctypes.c_double,
|
||||
ctypes.c_int, ctypes.c_double)
|
||||
|
||||
all_sigs = [sin_0, sin_1, sin_2, sin_3, sin_4]
|
||||
legacy_sigs = [sin_2, sin_4]
|
||||
legacy_only_sigs = [sin_4]
|
||||
|
||||
# LowLevelCallables work for new signatures
|
||||
for j, func in enumerate(all_sigs):
|
||||
callback = LowLevelCallable(func)
|
||||
if func in legacy_only_sigs:
|
||||
pytest.raises(ValueError, quad, callback, 0, pi)
|
||||
else:
|
||||
assert_allclose(quad(callback, 0, pi)[0], 2.0)
|
||||
|
||||
# Plain ctypes items work only for legacy signatures
|
||||
for j, func in enumerate(legacy_sigs):
|
||||
if func in legacy_sigs:
|
||||
assert_allclose(quad(func, 0, pi)[0], 2.0)
|
||||
else:
|
||||
pytest.raises(ValueError, quad, func, 0, pi)
|
||||
|
||||
|
||||
class TestMultivariateCtypesQuad:
|
||||
def setup_method(self):
|
||||
restype = ctypes.c_double
|
||||
argtypes = (ctypes.c_int, ctypes.c_double)
|
||||
for name in ['_multivariate_typical', '_multivariate_indefinite',
|
||||
'_multivariate_sin']:
|
||||
func = get_clib_test_routine(name, restype, *argtypes)
|
||||
setattr(self, name, func)
|
||||
|
||||
def test_typical(self):
|
||||
# 1) Typical function with two extra arguments:
|
||||
assert_quad(quad(self._multivariate_typical, 0, pi, (2, 1.8)),
|
||||
0.30614353532540296487)
|
||||
|
||||
def test_indefinite(self):
|
||||
# 2) Infinite integration limits --- Euler's constant
|
||||
assert_quad(quad(self._multivariate_indefinite, 0, np.inf),
|
||||
0.577215664901532860606512)
|
||||
|
||||
def test_threadsafety(self):
|
||||
# Ensure multivariate ctypes are threadsafe
|
||||
def threadsafety(y):
|
||||
return y + quad(self._multivariate_sin, 0, 1)[0]
|
||||
assert_quad(quad(threadsafety, 0, 1), 0.9596976941318602)
|
||||
|
||||
|
||||
class TestQuad:
|
||||
def test_typical(self):
|
||||
# 1) Typical function with two extra arguments:
|
||||
def myfunc(x, n, z): # Bessel function integrand
|
||||
return cos(n*x-z*sin(x))/pi
|
||||
assert_quad(quad(myfunc, 0, pi, (2, 1.8)), 0.30614353532540296487)
|
||||
|
||||
def test_indefinite(self):
|
||||
# 2) Infinite integration limits --- Euler's constant
|
||||
def myfunc(x): # Euler's constant integrand
|
||||
return -exp(-x)*log(x)
|
||||
assert_quad(quad(myfunc, 0, np.inf), 0.577215664901532860606512)
|
||||
|
||||
def test_singular(self):
|
||||
# 3) Singular points in region of integration.
|
||||
def myfunc(x):
|
||||
if 0 < x < 2.5:
|
||||
return sin(x)
|
||||
elif 2.5 <= x <= 5.0:
|
||||
return exp(-x)
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
assert_quad(quad(myfunc, 0, 10, points=[2.5, 5.0]),
|
||||
1 - cos(2.5) + exp(-2.5) - exp(-5.0))
|
||||
|
||||
def test_sine_weighted_finite(self):
|
||||
# 4) Sine weighted integral (finite limits)
|
||||
def myfunc(x, a):
|
||||
return exp(a*(x-1))
|
||||
|
||||
ome = 2.0**3.4
|
||||
assert_quad(quad(myfunc, 0, 1, args=20, weight='sin', wvar=ome),
|
||||
(20*sin(ome)-ome*cos(ome)+ome*exp(-20))/(20**2 + ome**2))
|
||||
|
||||
def test_sine_weighted_infinite(self):
|
||||
# 5) Sine weighted integral (infinite limits)
|
||||
def myfunc(x, a):
|
||||
return exp(-x*a)
|
||||
|
||||
a = 4.0
|
||||
ome = 3.0
|
||||
assert_quad(quad(myfunc, 0, np.inf, args=a, weight='sin', wvar=ome),
|
||||
ome/(a**2 + ome**2))
|
||||
|
||||
def test_cosine_weighted_infinite(self):
|
||||
# 6) Cosine weighted integral (negative infinite limits)
|
||||
def myfunc(x, a):
|
||||
return exp(x*a)
|
||||
|
||||
a = 2.5
|
||||
ome = 2.3
|
||||
assert_quad(quad(myfunc, -np.inf, 0, args=a, weight='cos', wvar=ome),
|
||||
a/(a**2 + ome**2))
|
||||
|
||||
def test_algebraic_log_weight(self):
|
||||
# 6) Algebraic-logarithmic weight.
|
||||
def myfunc(x, a):
|
||||
return 1/(1+x+2**(-a))
|
||||
|
||||
a = 1.5
|
||||
assert_quad(quad(myfunc, -1, 1, args=a, weight='alg',
|
||||
wvar=(-0.5, -0.5)),
|
||||
pi/sqrt((1+2**(-a))**2 - 1))
|
||||
|
||||
def test_cauchypv_weight(self):
|
||||
# 7) Cauchy prinicpal value weighting w(x) = 1/(x-c)
|
||||
def myfunc(x, a):
|
||||
return 2.0**(-a)/((x-1)**2+4.0**(-a))
|
||||
|
||||
a = 0.4
|
||||
tabledValue = ((2.0**(-0.4)*log(1.5) -
|
||||
2.0**(-1.4)*log((4.0**(-a)+16) / (4.0**(-a)+1)) -
|
||||
arctan(2.0**(a+2)) -
|
||||
arctan(2.0**a)) /
|
||||
(4.0**(-a) + 1))
|
||||
assert_quad(quad(myfunc, 0, 5, args=0.4, weight='cauchy', wvar=2.0),
|
||||
tabledValue, error_tolerance=1.9e-8)
|
||||
|
||||
def test_b_less_than_a(self):
|
||||
def f(x, p, q):
|
||||
return p * np.exp(-q*x)
|
||||
|
||||
val_1, err_1 = quad(f, 0, np.inf, args=(2, 3))
|
||||
val_2, err_2 = quad(f, np.inf, 0, args=(2, 3))
|
||||
assert_allclose(val_1, -val_2, atol=max(err_1, err_2))
|
||||
|
||||
def test_b_less_than_a_2(self):
|
||||
def f(x, s):
|
||||
return np.exp(-x**2 / 2 / s) / np.sqrt(2.*s)
|
||||
|
||||
val_1, err_1 = quad(f, -np.inf, np.inf, args=(2,))
|
||||
val_2, err_2 = quad(f, np.inf, -np.inf, args=(2,))
|
||||
assert_allclose(val_1, -val_2, atol=max(err_1, err_2))
|
||||
|
||||
def test_b_less_than_a_3(self):
|
||||
def f(x):
|
||||
return 1.0
|
||||
|
||||
val_1, err_1 = quad(f, 0, 1, weight='alg', wvar=(0, 0))
|
||||
val_2, err_2 = quad(f, 1, 0, weight='alg', wvar=(0, 0))
|
||||
assert_allclose(val_1, -val_2, atol=max(err_1, err_2))
|
||||
|
||||
def test_b_less_than_a_full_output(self):
|
||||
def f(x):
|
||||
return 1.0
|
||||
|
||||
res_1 = quad(f, 0, 1, weight='alg', wvar=(0, 0), full_output=True)
|
||||
res_2 = quad(f, 1, 0, weight='alg', wvar=(0, 0), full_output=True)
|
||||
err = max(res_1[1], res_2[1])
|
||||
assert_allclose(res_1[0], -res_2[0], atol=err)
|
||||
|
||||
def test_double_integral(self):
|
||||
# 8) Double Integral test
|
||||
def simpfunc(y, x): # Note order of arguments.
|
||||
return x+y
|
||||
|
||||
a, b = 1.0, 2.0
|
||||
assert_quad(dblquad(simpfunc, a, b, lambda x: x, lambda x: 2*x),
|
||||
5/6.0 * (b**3.0-a**3.0))
|
||||
|
||||
def test_double_integral2(self):
|
||||
def func(x0, x1, t0, t1):
|
||||
return x0 + x1 + t0 + t1
|
||||
def g(x):
|
||||
return x
|
||||
def h(x):
|
||||
return 2 * x
|
||||
args = 1, 2
|
||||
assert_quad(dblquad(func, 1, 2, g, h, args=args),35./6 + 9*.5)
|
||||
|
||||
def test_double_integral3(self):
|
||||
def func(x0, x1):
|
||||
return x0 + x1 + 1 + 2
|
||||
assert_quad(dblquad(func, 1, 2, 1, 2),6.)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"x_lower, x_upper, y_lower, y_upper, expected",
|
||||
[
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-inf, 0] for all n.
|
||||
(-np.inf, 0, -np.inf, 0, np.pi / 4),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-inf, -1] for each n (one at a time).
|
||||
(-np.inf, -1, -np.inf, 0, np.pi / 4 * erfc(1)),
|
||||
(-np.inf, 0, -np.inf, -1, np.pi / 4 * erfc(1)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-inf, -1] for all n.
|
||||
(-np.inf, -1, -np.inf, -1, np.pi / 4 * (erfc(1) ** 2)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-inf, 1] for each n (one at a time).
|
||||
(-np.inf, 1, -np.inf, 0, np.pi / 4 * (erf(1) + 1)),
|
||||
(-np.inf, 0, -np.inf, 1, np.pi / 4 * (erf(1) + 1)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-inf, 1] for all n.
|
||||
(-np.inf, 1, -np.inf, 1, np.pi / 4 * ((erf(1) + 1) ** 2)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain Dx = [-inf, -1] and Dy = [-inf, 1].
|
||||
(-np.inf, -1, -np.inf, 1, np.pi / 4 * ((erf(1) + 1) * erfc(1))),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain Dx = [-inf, 1] and Dy = [-inf, -1].
|
||||
(-np.inf, 1, -np.inf, -1, np.pi / 4 * ((erf(1) + 1) * erfc(1))),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [0, inf] for all n.
|
||||
(0, np.inf, 0, np.inf, np.pi / 4),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [1, inf] for each n (one at a time).
|
||||
(1, np.inf, 0, np.inf, np.pi / 4 * erfc(1)),
|
||||
(0, np.inf, 1, np.inf, np.pi / 4 * erfc(1)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [1, inf] for all n.
|
||||
(1, np.inf, 1, np.inf, np.pi / 4 * (erfc(1) ** 2)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-1, inf] for each n (one at a time).
|
||||
(-1, np.inf, 0, np.inf, np.pi / 4 * (erf(1) + 1)),
|
||||
(0, np.inf, -1, np.inf, np.pi / 4 * (erf(1) + 1)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-1, inf] for all n.
|
||||
(-1, np.inf, -1, np.inf, np.pi / 4 * ((erf(1) + 1) ** 2)),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain Dx = [-1, inf] and Dy = [1, inf].
|
||||
(-1, np.inf, 1, np.inf, np.pi / 4 * ((erf(1) + 1) * erfc(1))),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain Dx = [1, inf] and Dy = [-1, inf].
|
||||
(1, np.inf, -1, np.inf, np.pi / 4 * ((erf(1) + 1) * erfc(1))),
|
||||
# Multiple integration of a function in n = 2 variables: f(x, y, z)
|
||||
# over domain D = [-inf, inf] for all n.
|
||||
(-np.inf, np.inf, -np.inf, np.inf, np.pi)
|
||||
]
|
||||
)
|
||||
def test_double_integral_improper(
|
||||
self, x_lower, x_upper, y_lower, y_upper, expected
|
||||
):
|
||||
# The Gaussian Integral.
|
||||
def f(x, y):
|
||||
return np.exp(-x ** 2 - y ** 2)
|
||||
|
||||
assert_quad(
|
||||
dblquad(f, x_lower, x_upper, y_lower, y_upper),
|
||||
expected,
|
||||
error_tolerance=3e-8
|
||||
)
|
||||
|
||||
def test_triple_integral(self):
|
||||
# 9) Triple Integral test
|
||||
def simpfunc(z, y, x, t): # Note order of arguments.
|
||||
return (x+y+z)*t
|
||||
|
||||
a, b = 1.0, 2.0
|
||||
assert_quad(tplquad(simpfunc, a, b,
|
||||
lambda x: x, lambda x: 2*x,
|
||||
lambda x, y: x - y, lambda x, y: x + y,
|
||||
(2.,)),
|
||||
2*8/3.0 * (b**4.0 - a**4.0))
|
||||
|
||||
@pytest.mark.xslow
|
||||
@pytest.mark.parametrize(
|
||||
"x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, expected",
|
||||
[
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, 0] for all n.
|
||||
(-np.inf, 0, -np.inf, 0, -np.inf, 0, (np.pi ** (3 / 2)) / 8),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, -1] for each n (one at a time).
|
||||
(-np.inf, -1, -np.inf, 0, -np.inf, 0,
|
||||
(np.pi ** (3 / 2)) / 8 * erfc(1)),
|
||||
(-np.inf, 0, -np.inf, -1, -np.inf, 0,
|
||||
(np.pi ** (3 / 2)) / 8 * erfc(1)),
|
||||
(-np.inf, 0, -np.inf, 0, -np.inf, -1,
|
||||
(np.pi ** (3 / 2)) / 8 * erfc(1)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, -1] for each n (two at a time).
|
||||
(-np.inf, -1, -np.inf, -1, -np.inf, 0,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 2)),
|
||||
(-np.inf, -1, -np.inf, 0, -np.inf, -1,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 2)),
|
||||
(-np.inf, 0, -np.inf, -1, -np.inf, -1,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 2)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, -1] for all n.
|
||||
(-np.inf, -1, -np.inf, -1, -np.inf, -1,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 3)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = [-inf, -1] and Dy = Dz = [-inf, 1].
|
||||
(-np.inf, -1, -np.inf, 1, -np.inf, 1,
|
||||
(np.pi ** (3 / 2)) / 8 * (((erf(1) + 1) ** 2) * erfc(1))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dy = [-inf, -1] and Dz = [-inf, 1].
|
||||
(-np.inf, -1, -np.inf, -1, -np.inf, 1,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) * (erfc(1) ** 2))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dz = [-inf, -1] and Dy = [-inf, 1].
|
||||
(-np.inf, -1, -np.inf, 1, -np.inf, -1,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) * (erfc(1) ** 2))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = [-inf, 1] and Dy = Dz = [-inf, -1].
|
||||
(-np.inf, 1, -np.inf, -1, -np.inf, -1,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) * (erfc(1) ** 2))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dy = [-inf, 1] and Dz = [-inf, -1].
|
||||
(-np.inf, 1, -np.inf, 1, -np.inf, -1,
|
||||
(np.pi ** (3 / 2)) / 8 * (((erf(1) + 1) ** 2) * erfc(1))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dz = [-inf, 1] and Dy = [-inf, -1].
|
||||
(-np.inf, 1, -np.inf, -1, -np.inf, 1,
|
||||
(np.pi ** (3 / 2)) / 8 * (((erf(1) + 1) ** 2) * erfc(1))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, 1] for each n (one at a time).
|
||||
(-np.inf, 1, -np.inf, 0, -np.inf, 0,
|
||||
(np.pi ** (3 / 2)) / 8 * (erf(1) + 1)),
|
||||
(-np.inf, 0, -np.inf, 1, -np.inf, 0,
|
||||
(np.pi ** (3 / 2)) / 8 * (erf(1) + 1)),
|
||||
(-np.inf, 0, -np.inf, 0, -np.inf, 1,
|
||||
(np.pi ** (3 / 2)) / 8 * (erf(1) + 1)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, 1] for each n (two at a time).
|
||||
(-np.inf, 1, -np.inf, 1, -np.inf, 0,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 2)),
|
||||
(-np.inf, 1, -np.inf, 0, -np.inf, 1,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 2)),
|
||||
(-np.inf, 0, -np.inf, 1, -np.inf, 1,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 2)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, 1] for all n.
|
||||
(-np.inf, 1, -np.inf, 1, -np.inf, 1,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 3)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [0, inf] for all n.
|
||||
(0, np.inf, 0, np.inf, 0, np.inf, (np.pi ** (3 / 2)) / 8),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [1, inf] for each n (one at a time).
|
||||
(1, np.inf, 0, np.inf, 0, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * erfc(1)),
|
||||
(0, np.inf, 1, np.inf, 0, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * erfc(1)),
|
||||
(0, np.inf, 0, np.inf, 1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * erfc(1)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [1, inf] for each n (two at a time).
|
||||
(1, np.inf, 1, np.inf, 0, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 2)),
|
||||
(1, np.inf, 0, np.inf, 1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 2)),
|
||||
(0, np.inf, 1, np.inf, 1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 2)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [1, inf] for all n.
|
||||
(1, np.inf, 1, np.inf, 1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (erfc(1) ** 3)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-1, inf] for each n (one at a time).
|
||||
(-1, np.inf, 0, np.inf, 0, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (erf(1) + 1)),
|
||||
(0, np.inf, -1, np.inf, 0, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (erf(1) + 1)),
|
||||
(0, np.inf, 0, np.inf, -1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (erf(1) + 1)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-1, inf] for each n (two at a time).
|
||||
(-1, np.inf, -1, np.inf, 0, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 2)),
|
||||
(-1, np.inf, 0, np.inf, -1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 2)),
|
||||
(0, np.inf, -1, np.inf, -1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 2)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-1, inf] for all n.
|
||||
(-1, np.inf, -1, np.inf, -1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) ** 3)),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = [1, inf] and Dy = Dz = [-1, inf].
|
||||
(1, np.inf, -1, np.inf, -1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (((erf(1) + 1) ** 2) * erfc(1))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dy = [1, inf] and Dz = [-1, inf].
|
||||
(1, np.inf, 1, np.inf, -1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) * (erfc(1) ** 2))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dz = [1, inf] and Dy = [-1, inf].
|
||||
(1, np.inf, -1, np.inf, 1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) * (erfc(1) ** 2))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = [-1, inf] and Dy = Dz = [1, inf].
|
||||
(-1, np.inf, 1, np.inf, 1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * ((erf(1) + 1) * (erfc(1) ** 2))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dy = [-1, inf] and Dz = [1, inf].
|
||||
(-1, np.inf, -1, np.inf, 1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (((erf(1) + 1) ** 2) * erfc(1))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain Dx = Dz = [-1, inf] and Dy = [1, inf].
|
||||
(-1, np.inf, 1, np.inf, -1, np.inf,
|
||||
(np.pi ** (3 / 2)) / 8 * (((erf(1) + 1) ** 2) * erfc(1))),
|
||||
# Multiple integration of a function in n = 3 variables: f(x, y, z)
|
||||
# over domain D = [-inf, inf] for all n.
|
||||
(-np.inf, np.inf, -np.inf, np.inf, -np.inf, np.inf,
|
||||
np.pi ** (3 / 2)),
|
||||
],
|
||||
)
|
||||
def test_triple_integral_improper(
|
||||
self,
|
||||
x_lower,
|
||||
x_upper,
|
||||
y_lower,
|
||||
y_upper,
|
||||
z_lower,
|
||||
z_upper,
|
||||
expected
|
||||
):
|
||||
# The Gaussian Integral.
|
||||
def f(x, y, z):
|
||||
return np.exp(-x ** 2 - y ** 2 - z ** 2)
|
||||
|
||||
assert_quad(
|
||||
tplquad(f, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper),
|
||||
expected,
|
||||
error_tolerance=6e-8
|
||||
)
|
||||
|
||||
def test_complex(self):
|
||||
def tfunc(x):
|
||||
return np.exp(1j*x)
|
||||
|
||||
assert np.allclose(
|
||||
quad(tfunc, 0, np.pi/2, complex_func=True)[0],
|
||||
1+1j)
|
||||
|
||||
# We consider a divergent case in order to force quadpack
|
||||
# to return an error message. The output is compared
|
||||
# against what is returned by explicit integration
|
||||
# of the parts.
|
||||
kwargs = {'a': 0, 'b': np.inf, 'full_output': True,
|
||||
'weight': 'cos', 'wvar': 1}
|
||||
res_c = quad(tfunc, complex_func=True, **kwargs)
|
||||
res_r = quad(lambda x: np.real(np.exp(1j*x)),
|
||||
complex_func=False,
|
||||
**kwargs)
|
||||
res_i = quad(lambda x: np.imag(np.exp(1j*x)),
|
||||
complex_func=False,
|
||||
**kwargs)
|
||||
|
||||
np.testing.assert_equal(res_c[0], res_r[0] + 1j*res_i[0])
|
||||
np.testing.assert_equal(res_c[1], res_r[1] + 1j*res_i[1])
|
||||
|
||||
assert len(res_c[2]['real']) == len(res_r[2:]) == 3
|
||||
assert res_c[2]['real'][2] == res_r[4]
|
||||
assert res_c[2]['real'][1] == res_r[3]
|
||||
assert res_c[2]['real'][0]['lst'] == res_r[2]['lst']
|
||||
|
||||
assert len(res_c[2]['imag']) == len(res_i[2:]) == 1
|
||||
assert res_c[2]['imag'][0]['lst'] == res_i[2]['lst']
|
||||
|
||||
|
||||
class TestNQuad:
|
||||
@pytest.mark.fail_slow(2)
|
||||
def test_fixed_limits(self):
|
||||
def func1(x0, x1, x2, x3):
|
||||
val = (x0**2 + x1*x2 - x3**3 + np.sin(x0) +
|
||||
(1 if (x0 - 0.2*x3 - 0.5 - 0.25*x1 > 0) else 0))
|
||||
return val
|
||||
|
||||
def opts_basic(*args):
|
||||
return {'points': [0.2*args[2] + 0.5 + 0.25*args[0]]}
|
||||
|
||||
res = nquad(func1, [[0, 1], [-1, 1], [.13, .8], [-.15, 1]],
|
||||
opts=[opts_basic, {}, {}, {}], full_output=True)
|
||||
assert_quad(res[:-1], 1.5267454070738635)
|
||||
assert_(res[-1]['neval'] > 0 and res[-1]['neval'] < 4e5)
|
||||
|
||||
@pytest.mark.fail_slow(2)
|
||||
def test_variable_limits(self):
|
||||
scale = .1
|
||||
|
||||
def func2(x0, x1, x2, x3, t0, t1):
|
||||
val = (x0*x1*x3**2 + np.sin(x2) + 1 +
|
||||
(1 if x0 + t1*x1 - t0 > 0 else 0))
|
||||
return val
|
||||
|
||||
def lim0(x1, x2, x3, t0, t1):
|
||||
return [scale * (x1**2 + x2 + np.cos(x3)*t0*t1 + 1) - 1,
|
||||
scale * (x1**2 + x2 + np.cos(x3)*t0*t1 + 1) + 1]
|
||||
|
||||
def lim1(x2, x3, t0, t1):
|
||||
return [scale * (t0*x2 + t1*x3) - 1,
|
||||
scale * (t0*x2 + t1*x3) + 1]
|
||||
|
||||
def lim2(x3, t0, t1):
|
||||
return [scale * (x3 + t0**2*t1**3) - 1,
|
||||
scale * (x3 + t0**2*t1**3) + 1]
|
||||
|
||||
def lim3(t0, t1):
|
||||
return [scale * (t0 + t1) - 1, scale * (t0 + t1) + 1]
|
||||
|
||||
def opts0(x1, x2, x3, t0, t1):
|
||||
return {'points': [t0 - t1*x1]}
|
||||
|
||||
def opts1(x2, x3, t0, t1):
|
||||
return {}
|
||||
|
||||
def opts2(x3, t0, t1):
|
||||
return {}
|
||||
|
||||
def opts3(t0, t1):
|
||||
return {}
|
||||
|
||||
res = nquad(func2, [lim0, lim1, lim2, lim3], args=(0, 0),
|
||||
opts=[opts0, opts1, opts2, opts3])
|
||||
assert_quad(res, 25.066666666666663)
|
||||
|
||||
def test_square_separate_ranges_and_opts(self):
|
||||
def f(y, x):
|
||||
return 1.0
|
||||
|
||||
assert_quad(nquad(f, [[-1, 1], [-1, 1]], opts=[{}, {}]), 4.0)
|
||||
|
||||
def test_square_aliased_ranges_and_opts(self):
|
||||
def f(y, x):
|
||||
return 1.0
|
||||
|
||||
r = [-1, 1]
|
||||
opt = {}
|
||||
assert_quad(nquad(f, [r, r], opts=[opt, opt]), 4.0)
|
||||
|
||||
def test_square_separate_fn_ranges_and_opts(self):
|
||||
def f(y, x):
|
||||
return 1.0
|
||||
|
||||
def fn_range0(*args):
|
||||
return (-1, 1)
|
||||
|
||||
def fn_range1(*args):
|
||||
return (-1, 1)
|
||||
|
||||
def fn_opt0(*args):
|
||||
return {}
|
||||
|
||||
def fn_opt1(*args):
|
||||
return {}
|
||||
|
||||
ranges = [fn_range0, fn_range1]
|
||||
opts = [fn_opt0, fn_opt1]
|
||||
assert_quad(nquad(f, ranges, opts=opts), 4.0)
|
||||
|
||||
def test_square_aliased_fn_ranges_and_opts(self):
|
||||
def f(y, x):
|
||||
return 1.0
|
||||
|
||||
def fn_range(*args):
|
||||
return (-1, 1)
|
||||
|
||||
def fn_opt(*args):
|
||||
return {}
|
||||
|
||||
ranges = [fn_range, fn_range]
|
||||
opts = [fn_opt, fn_opt]
|
||||
assert_quad(nquad(f, ranges, opts=opts), 4.0)
|
||||
|
||||
def test_matching_quad(self):
|
||||
def func(x):
|
||||
return x**2 + 1
|
||||
|
||||
res, reserr = quad(func, 0, 4)
|
||||
res2, reserr2 = nquad(func, ranges=[[0, 4]])
|
||||
assert_almost_equal(res, res2)
|
||||
assert_almost_equal(reserr, reserr2)
|
||||
|
||||
def test_matching_dblquad(self):
|
||||
def func2d(x0, x1):
|
||||
return x0**2 + x1**3 - x0 * x1 + 1
|
||||
|
||||
res, reserr = dblquad(func2d, -2, 2, lambda x: -3, lambda x: 3)
|
||||
res2, reserr2 = nquad(func2d, [[-3, 3], (-2, 2)])
|
||||
assert_almost_equal(res, res2)
|
||||
assert_almost_equal(reserr, reserr2)
|
||||
|
||||
def test_matching_tplquad(self):
|
||||
def func3d(x0, x1, x2, c0, c1):
|
||||
return x0**2 + c0 * x1**3 - x0 * x1 + 1 + c1 * np.sin(x2)
|
||||
|
||||
res = tplquad(func3d, -1, 2, lambda x: -2, lambda x: 2,
|
||||
lambda x, y: -np.pi, lambda x, y: np.pi,
|
||||
args=(2, 3))
|
||||
res2 = nquad(func3d, [[-np.pi, np.pi], [-2, 2], (-1, 2)], args=(2, 3))
|
||||
assert_almost_equal(res, res2)
|
||||
|
||||
def test_dict_as_opts(self):
|
||||
try:
|
||||
nquad(lambda x, y: x * y, [[0, 1], [0, 1]], opts={'epsrel': 0.0001})
|
||||
except TypeError:
|
||||
assert False
|
||||
|
||||
@ -0,0 +1,721 @@
|
||||
# mypy: disable-error-code="attr-defined"
|
||||
import pytest
|
||||
import numpy as np
|
||||
from numpy import cos, sin, pi
|
||||
from numpy.testing import (assert_equal, assert_almost_equal, assert_allclose,
|
||||
assert_, suppress_warnings)
|
||||
from hypothesis import given
|
||||
import hypothesis.strategies as st
|
||||
import hypothesis.extra.numpy as hyp_num
|
||||
|
||||
from scipy.integrate import (quadrature, romberg, romb, newton_cotes,
|
||||
cumulative_trapezoid, trapezoid,
|
||||
quad, simpson, fixed_quad, AccuracyWarning,
|
||||
qmc_quad, cumulative_simpson)
|
||||
from scipy.integrate._quadrature import _cumulative_simpson_unequal_intervals
|
||||
from scipy import stats, special
|
||||
|
||||
|
||||
class TestFixedQuad:
|
||||
def test_scalar(self):
|
||||
n = 4
|
||||
expected = 1/(2*n)
|
||||
got, _ = fixed_quad(lambda x: x**(2*n - 1), 0, 1, n=n)
|
||||
# quadrature exact for this input
|
||||
assert_allclose(got, expected, rtol=1e-12)
|
||||
|
||||
def test_vector(self):
|
||||
n = 4
|
||||
p = np.arange(1, 2*n)
|
||||
expected = 1/(p + 1)
|
||||
got, _ = fixed_quad(lambda x: x**p[:, None], 0, 1, n=n)
|
||||
assert_allclose(got, expected, rtol=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings('ignore::DeprecationWarning')
|
||||
class TestQuadrature:
|
||||
def quad(self, x, a, b, args):
|
||||
raise NotImplementedError
|
||||
|
||||
def test_quadrature(self):
|
||||
# Typical function with two extra arguments:
|
||||
def myfunc(x, n, z): # Bessel function integrand
|
||||
return cos(n*x-z*sin(x))/pi
|
||||
val, err = quadrature(myfunc, 0, pi, (2, 1.8))
|
||||
table_val = 0.30614353532540296487
|
||||
assert_almost_equal(val, table_val, decimal=7)
|
||||
|
||||
def test_quadrature_rtol(self):
|
||||
def myfunc(x, n, z): # Bessel function integrand
|
||||
return 1e90 * cos(n*x-z*sin(x))/pi
|
||||
val, err = quadrature(myfunc, 0, pi, (2, 1.8), rtol=1e-10)
|
||||
table_val = 1e90 * 0.30614353532540296487
|
||||
assert_allclose(val, table_val, rtol=1e-10)
|
||||
|
||||
def test_quadrature_miniter(self):
|
||||
# Typical function with two extra arguments:
|
||||
def myfunc(x, n, z): # Bessel function integrand
|
||||
return cos(n*x-z*sin(x))/pi
|
||||
table_val = 0.30614353532540296487
|
||||
for miniter in [5, 52]:
|
||||
val, err = quadrature(myfunc, 0, pi, (2, 1.8), miniter=miniter)
|
||||
assert_almost_equal(val, table_val, decimal=7)
|
||||
assert_(err < 1.0)
|
||||
|
||||
def test_quadrature_single_args(self):
|
||||
def myfunc(x, n):
|
||||
return 1e90 * cos(n*x-1.8*sin(x))/pi
|
||||
val, err = quadrature(myfunc, 0, pi, args=2, rtol=1e-10)
|
||||
table_val = 1e90 * 0.30614353532540296487
|
||||
assert_allclose(val, table_val, rtol=1e-10)
|
||||
|
||||
def test_romberg(self):
|
||||
# Typical function with two extra arguments:
|
||||
def myfunc(x, n, z): # Bessel function integrand
|
||||
return cos(n*x-z*sin(x))/pi
|
||||
val = romberg(myfunc, 0, pi, args=(2, 1.8))
|
||||
table_val = 0.30614353532540296487
|
||||
assert_almost_equal(val, table_val, decimal=7)
|
||||
|
||||
def test_romberg_rtol(self):
|
||||
# Typical function with two extra arguments:
|
||||
def myfunc(x, n, z): # Bessel function integrand
|
||||
return 1e19*cos(n*x-z*sin(x))/pi
|
||||
val = romberg(myfunc, 0, pi, args=(2, 1.8), rtol=1e-10)
|
||||
table_val = 1e19*0.30614353532540296487
|
||||
assert_allclose(val, table_val, rtol=1e-10)
|
||||
|
||||
def test_romb(self):
|
||||
assert_equal(romb(np.arange(17)), 128)
|
||||
|
||||
def test_romb_gh_3731(self):
|
||||
# Check that romb makes maximal use of data points
|
||||
x = np.arange(2**4+1)
|
||||
y = np.cos(0.2*x)
|
||||
val = romb(y)
|
||||
val2, err = quad(lambda x: np.cos(0.2*x), x.min(), x.max())
|
||||
assert_allclose(val, val2, rtol=1e-8, atol=0)
|
||||
|
||||
# should be equal to romb with 2**k+1 samples
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(AccuracyWarning, "divmax .4. exceeded")
|
||||
val3 = romberg(lambda x: np.cos(0.2*x), x.min(), x.max(), divmax=4)
|
||||
assert_allclose(val, val3, rtol=1e-12, atol=0)
|
||||
|
||||
def test_non_dtype(self):
|
||||
# Check that we work fine with functions returning float
|
||||
import math
|
||||
valmath = romberg(math.sin, 0, 1)
|
||||
expected_val = 0.45969769413185085
|
||||
assert_almost_equal(valmath, expected_val, decimal=7)
|
||||
|
||||
def test_newton_cotes(self):
|
||||
"""Test the first few degrees, for evenly spaced points."""
|
||||
n = 1
|
||||
wts, errcoff = newton_cotes(n, 1)
|
||||
assert_equal(wts, n*np.array([0.5, 0.5]))
|
||||
assert_almost_equal(errcoff, -n**3/12.0)
|
||||
|
||||
n = 2
|
||||
wts, errcoff = newton_cotes(n, 1)
|
||||
assert_almost_equal(wts, n*np.array([1.0, 4.0, 1.0])/6.0)
|
||||
assert_almost_equal(errcoff, -n**5/2880.0)
|
||||
|
||||
n = 3
|
||||
wts, errcoff = newton_cotes(n, 1)
|
||||
assert_almost_equal(wts, n*np.array([1.0, 3.0, 3.0, 1.0])/8.0)
|
||||
assert_almost_equal(errcoff, -n**5/6480.0)
|
||||
|
||||
n = 4
|
||||
wts, errcoff = newton_cotes(n, 1)
|
||||
assert_almost_equal(wts, n*np.array([7.0, 32.0, 12.0, 32.0, 7.0])/90.0)
|
||||
assert_almost_equal(errcoff, -n**7/1935360.0)
|
||||
|
||||
def test_newton_cotes2(self):
|
||||
"""Test newton_cotes with points that are not evenly spaced."""
|
||||
|
||||
x = np.array([0.0, 1.5, 2.0])
|
||||
y = x**2
|
||||
wts, errcoff = newton_cotes(x)
|
||||
exact_integral = 8.0/3
|
||||
numeric_integral = np.dot(wts, y)
|
||||
assert_almost_equal(numeric_integral, exact_integral)
|
||||
|
||||
x = np.array([0.0, 1.4, 2.1, 3.0])
|
||||
y = x**2
|
||||
wts, errcoff = newton_cotes(x)
|
||||
exact_integral = 9.0
|
||||
numeric_integral = np.dot(wts, y)
|
||||
assert_almost_equal(numeric_integral, exact_integral)
|
||||
|
||||
def test_simpson(self):
|
||||
y = np.arange(17)
|
||||
assert_equal(simpson(y), 128)
|
||||
assert_equal(simpson(y, dx=0.5), 64)
|
||||
assert_equal(simpson(y, x=np.linspace(0, 4, 17)), 32)
|
||||
|
||||
# integral should be exactly 21
|
||||
x = np.linspace(1, 4, 4)
|
||||
def f(x):
|
||||
return x**2
|
||||
|
||||
assert_allclose(simpson(f(x), x=x), 21.0)
|
||||
|
||||
# integral should be exactly 114
|
||||
x = np.linspace(1, 7, 4)
|
||||
assert_allclose(simpson(f(x), dx=2.0), 114)
|
||||
|
||||
# test multi-axis behaviour
|
||||
a = np.arange(16).reshape(4, 4)
|
||||
x = np.arange(64.).reshape(4, 4, 4)
|
||||
y = f(x)
|
||||
for i in range(3):
|
||||
r = simpson(y, x=x, axis=i)
|
||||
it = np.nditer(a, flags=['multi_index'])
|
||||
for _ in it:
|
||||
idx = list(it.multi_index)
|
||||
idx.insert(i, slice(None))
|
||||
integral = x[tuple(idx)][-1]**3 / 3 - x[tuple(idx)][0]**3 / 3
|
||||
assert_allclose(r[it.multi_index], integral)
|
||||
|
||||
# test when integration axis only has two points
|
||||
x = np.arange(16).reshape(8, 2)
|
||||
y = f(x)
|
||||
r = simpson(y, x=x, axis=-1)
|
||||
|
||||
integral = 0.5 * (y[:, 1] + y[:, 0]) * (x[:, 1] - x[:, 0])
|
||||
assert_allclose(r, integral)
|
||||
|
||||
# odd points, test multi-axis behaviour
|
||||
a = np.arange(25).reshape(5, 5)
|
||||
x = np.arange(125).reshape(5, 5, 5)
|
||||
y = f(x)
|
||||
for i in range(3):
|
||||
r = simpson(y, x=x, axis=i)
|
||||
it = np.nditer(a, flags=['multi_index'])
|
||||
for _ in it:
|
||||
idx = list(it.multi_index)
|
||||
idx.insert(i, slice(None))
|
||||
integral = x[tuple(idx)][-1]**3 / 3 - x[tuple(idx)][0]**3 / 3
|
||||
assert_allclose(r[it.multi_index], integral)
|
||||
|
||||
# Tests for checking base case
|
||||
x = np.array([3])
|
||||
y = np.power(x, 2)
|
||||
assert_allclose(simpson(y, x=x, axis=0), 0.0)
|
||||
assert_allclose(simpson(y, x=x, axis=-1), 0.0)
|
||||
|
||||
x = np.array([3, 3, 3, 3])
|
||||
y = np.power(x, 2)
|
||||
assert_allclose(simpson(y, x=x, axis=0), 0.0)
|
||||
assert_allclose(simpson(y, x=x, axis=-1), 0.0)
|
||||
|
||||
x = np.array([[1, 2, 4, 8], [1, 2, 4, 8], [1, 2, 4, 8]])
|
||||
y = np.power(x, 2)
|
||||
zero_axis = [0.0, 0.0, 0.0, 0.0]
|
||||
default_axis = [170 + 1/3] * 3 # 8**3 / 3 - 1/3
|
||||
assert_allclose(simpson(y, x=x, axis=0), zero_axis)
|
||||
# the following should be exact
|
||||
assert_allclose(simpson(y, x=x, axis=-1), default_axis)
|
||||
|
||||
x = np.array([[1, 2, 4, 8], [1, 2, 4, 8], [1, 8, 16, 32]])
|
||||
y = np.power(x, 2)
|
||||
zero_axis = [0.0, 136.0, 1088.0, 8704.0]
|
||||
default_axis = [170 + 1/3, 170 + 1/3, 32**3 / 3 - 1/3]
|
||||
assert_allclose(simpson(y, x=x, axis=0), zero_axis)
|
||||
assert_allclose(simpson(y, x=x, axis=-1), default_axis)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('droplast', [False, True])
|
||||
def test_simpson_2d_integer_no_x(self, droplast):
|
||||
# The inputs are 2d integer arrays. The results should be
|
||||
# identical to the results when the inputs are floating point.
|
||||
y = np.array([[2, 2, 4, 4, 8, 8, -4, 5],
|
||||
[4, 4, 2, -4, 10, 22, -2, 10]])
|
||||
if droplast:
|
||||
y = y[:, :-1]
|
||||
result = simpson(y, axis=-1)
|
||||
expected = simpson(np.array(y, dtype=np.float64), axis=-1)
|
||||
assert_equal(result, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('func', [romberg, quadrature])
|
||||
def test_deprecate_integrator(func):
|
||||
message = f"`scipy.integrate.{func.__name__}` is deprecated..."
|
||||
with pytest.deprecated_call(match=message):
|
||||
func(np.exp, 0, 1)
|
||||
|
||||
|
||||
class TestCumulative_trapezoid:
|
||||
def test_1d(self):
|
||||
x = np.linspace(-2, 2, num=5)
|
||||
y = x
|
||||
y_int = cumulative_trapezoid(y, x, initial=0)
|
||||
y_expected = [0., -1.5, -2., -1.5, 0.]
|
||||
assert_allclose(y_int, y_expected)
|
||||
|
||||
y_int = cumulative_trapezoid(y, x, initial=None)
|
||||
assert_allclose(y_int, y_expected[1:])
|
||||
|
||||
def test_y_nd_x_nd(self):
|
||||
x = np.arange(3 * 2 * 4).reshape(3, 2, 4)
|
||||
y = x
|
||||
y_int = cumulative_trapezoid(y, x, initial=0)
|
||||
y_expected = np.array([[[0., 0.5, 2., 4.5],
|
||||
[0., 4.5, 10., 16.5]],
|
||||
[[0., 8.5, 18., 28.5],
|
||||
[0., 12.5, 26., 40.5]],
|
||||
[[0., 16.5, 34., 52.5],
|
||||
[0., 20.5, 42., 64.5]]])
|
||||
|
||||
assert_allclose(y_int, y_expected)
|
||||
|
||||
# Try with all axes
|
||||
shapes = [(2, 2, 4), (3, 1, 4), (3, 2, 3)]
|
||||
for axis, shape in zip([0, 1, 2], shapes):
|
||||
y_int = cumulative_trapezoid(y, x, initial=0, axis=axis)
|
||||
assert_equal(y_int.shape, (3, 2, 4))
|
||||
y_int = cumulative_trapezoid(y, x, initial=None, axis=axis)
|
||||
assert_equal(y_int.shape, shape)
|
||||
|
||||
def test_y_nd_x_1d(self):
|
||||
y = np.arange(3 * 2 * 4).reshape(3, 2, 4)
|
||||
x = np.arange(4)**2
|
||||
# Try with all axes
|
||||
ys_expected = (
|
||||
np.array([[[4., 5., 6., 7.],
|
||||
[8., 9., 10., 11.]],
|
||||
[[40., 44., 48., 52.],
|
||||
[56., 60., 64., 68.]]]),
|
||||
np.array([[[2., 3., 4., 5.]],
|
||||
[[10., 11., 12., 13.]],
|
||||
[[18., 19., 20., 21.]]]),
|
||||
np.array([[[0.5, 5., 17.5],
|
||||
[4.5, 21., 53.5]],
|
||||
[[8.5, 37., 89.5],
|
||||
[12.5, 53., 125.5]],
|
||||
[[16.5, 69., 161.5],
|
||||
[20.5, 85., 197.5]]]))
|
||||
|
||||
for axis, y_expected in zip([0, 1, 2], ys_expected):
|
||||
y_int = cumulative_trapezoid(y, x=x[:y.shape[axis]], axis=axis,
|
||||
initial=None)
|
||||
assert_allclose(y_int, y_expected)
|
||||
|
||||
def test_x_none(self):
|
||||
y = np.linspace(-2, 2, num=5)
|
||||
|
||||
y_int = cumulative_trapezoid(y)
|
||||
y_expected = [-1.5, -2., -1.5, 0.]
|
||||
assert_allclose(y_int, y_expected)
|
||||
|
||||
y_int = cumulative_trapezoid(y, initial=0)
|
||||
y_expected = [0, -1.5, -2., -1.5, 0.]
|
||||
assert_allclose(y_int, y_expected)
|
||||
|
||||
y_int = cumulative_trapezoid(y, dx=3)
|
||||
y_expected = [-4.5, -6., -4.5, 0.]
|
||||
assert_allclose(y_int, y_expected)
|
||||
|
||||
y_int = cumulative_trapezoid(y, dx=3, initial=0)
|
||||
y_expected = [0, -4.5, -6., -4.5, 0.]
|
||||
assert_allclose(y_int, y_expected)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial", [1, 0.5]
|
||||
)
|
||||
def test_initial_warning(self, initial):
|
||||
"""If initial is not None or 0, a ValueError is raised."""
|
||||
y = np.linspace(0, 10, num=10)
|
||||
with pytest.deprecated_call(match="`initial`"):
|
||||
res = cumulative_trapezoid(y, initial=initial)
|
||||
assert_allclose(res, [initial, *np.cumsum(y[1:] + y[:-1])/2])
|
||||
|
||||
def test_zero_len_y(self):
|
||||
with pytest.raises(ValueError, match="At least one point is required"):
|
||||
cumulative_trapezoid(y=[])
|
||||
|
||||
|
||||
class TestTrapezoid:
|
||||
def test_simple(self):
|
||||
x = np.arange(-10, 10, .1)
|
||||
r = trapezoid(np.exp(-.5 * x ** 2) / np.sqrt(2 * np.pi), dx=0.1)
|
||||
# check integral of normal equals 1
|
||||
assert_allclose(r, 1)
|
||||
|
||||
def test_ndim(self):
|
||||
x = np.linspace(0, 1, 3)
|
||||
y = np.linspace(0, 2, 8)
|
||||
z = np.linspace(0, 3, 13)
|
||||
|
||||
wx = np.ones_like(x) * (x[1] - x[0])
|
||||
wx[0] /= 2
|
||||
wx[-1] /= 2
|
||||
wy = np.ones_like(y) * (y[1] - y[0])
|
||||
wy[0] /= 2
|
||||
wy[-1] /= 2
|
||||
wz = np.ones_like(z) * (z[1] - z[0])
|
||||
wz[0] /= 2
|
||||
wz[-1] /= 2
|
||||
|
||||
q = x[:, None, None] + y[None,:, None] + z[None, None,:]
|
||||
|
||||
qx = (q * wx[:, None, None]).sum(axis=0)
|
||||
qy = (q * wy[None, :, None]).sum(axis=1)
|
||||
qz = (q * wz[None, None, :]).sum(axis=2)
|
||||
|
||||
# n-d `x`
|
||||
r = trapezoid(q, x=x[:, None, None], axis=0)
|
||||
assert_allclose(r, qx)
|
||||
r = trapezoid(q, x=y[None,:, None], axis=1)
|
||||
assert_allclose(r, qy)
|
||||
r = trapezoid(q, x=z[None, None,:], axis=2)
|
||||
assert_allclose(r, qz)
|
||||
|
||||
# 1-d `x`
|
||||
r = trapezoid(q, x=x, axis=0)
|
||||
assert_allclose(r, qx)
|
||||
r = trapezoid(q, x=y, axis=1)
|
||||
assert_allclose(r, qy)
|
||||
r = trapezoid(q, x=z, axis=2)
|
||||
assert_allclose(r, qz)
|
||||
|
||||
def test_masked(self):
|
||||
# Testing that masked arrays behave as if the function is 0 where
|
||||
# masked
|
||||
x = np.arange(5)
|
||||
y = x * x
|
||||
mask = x == 2
|
||||
ym = np.ma.array(y, mask=mask)
|
||||
r = 13.0 # sum(0.5 * (0 + 1) * 1.0 + 0.5 * (9 + 16))
|
||||
assert_allclose(trapezoid(ym, x), r)
|
||||
|
||||
xm = np.ma.array(x, mask=mask)
|
||||
assert_allclose(trapezoid(ym, xm), r)
|
||||
|
||||
xm = np.ma.array(x, mask=mask)
|
||||
assert_allclose(trapezoid(y, xm), r)
|
||||
|
||||
|
||||
class TestQMCQuad:
|
||||
def test_input_validation(self):
|
||||
message = "`func` must be callable."
|
||||
with pytest.raises(TypeError, match=message):
|
||||
qmc_quad("a duck", [0, 0], [1, 1])
|
||||
|
||||
message = "`func` must evaluate the integrand at points..."
|
||||
with pytest.raises(ValueError, match=message):
|
||||
qmc_quad(lambda: 1, [0, 0], [1, 1])
|
||||
|
||||
def func(x):
|
||||
assert x.ndim == 1
|
||||
return np.sum(x)
|
||||
message = "Exception encountered when attempting vectorized call..."
|
||||
with pytest.warns(UserWarning, match=message):
|
||||
qmc_quad(func, [0, 0], [1, 1])
|
||||
|
||||
message = "`n_points` must be an integer."
|
||||
with pytest.raises(TypeError, match=message):
|
||||
qmc_quad(lambda x: 1, [0, 0], [1, 1], n_points=1024.5)
|
||||
|
||||
message = "`n_estimates` must be an integer."
|
||||
with pytest.raises(TypeError, match=message):
|
||||
qmc_quad(lambda x: 1, [0, 0], [1, 1], n_estimates=8.5)
|
||||
|
||||
message = "`qrng` must be an instance of scipy.stats.qmc.QMCEngine."
|
||||
with pytest.raises(TypeError, match=message):
|
||||
qmc_quad(lambda x: 1, [0, 0], [1, 1], qrng="a duck")
|
||||
|
||||
message = "`qrng` must be initialized with dimensionality equal to "
|
||||
with pytest.raises(ValueError, match=message):
|
||||
qmc_quad(lambda x: 1, [0, 0], [1, 1], qrng=stats.qmc.Sobol(1))
|
||||
|
||||
message = r"`log` must be boolean \(`True` or `False`\)."
|
||||
with pytest.raises(TypeError, match=message):
|
||||
qmc_quad(lambda x: 1, [0, 0], [1, 1], log=10)
|
||||
|
||||
def basic_test(self, n_points=2**8, n_estimates=8, signs=np.ones(2)):
|
||||
|
||||
ndim = 2
|
||||
mean = np.zeros(ndim)
|
||||
cov = np.eye(ndim)
|
||||
|
||||
def func(x):
|
||||
return stats.multivariate_normal.pdf(x.T, mean, cov)
|
||||
|
||||
rng = np.random.default_rng(2879434385674690281)
|
||||
qrng = stats.qmc.Sobol(ndim, seed=rng)
|
||||
a = np.zeros(ndim)
|
||||
b = np.ones(ndim) * signs
|
||||
res = qmc_quad(func, a, b, n_points=n_points,
|
||||
n_estimates=n_estimates, qrng=qrng)
|
||||
ref = stats.multivariate_normal.cdf(b, mean, cov, lower_limit=a)
|
||||
atol = special.stdtrit(n_estimates-1, 0.995) * res.standard_error # 99% CI
|
||||
assert_allclose(res.integral, ref, atol=atol)
|
||||
assert np.prod(signs)*res.integral > 0
|
||||
|
||||
rng = np.random.default_rng(2879434385674690281)
|
||||
qrng = stats.qmc.Sobol(ndim, seed=rng)
|
||||
logres = qmc_quad(lambda *args: np.log(func(*args)), a, b,
|
||||
n_points=n_points, n_estimates=n_estimates,
|
||||
log=True, qrng=qrng)
|
||||
assert_allclose(np.exp(logres.integral), res.integral, rtol=1e-14)
|
||||
assert np.imag(logres.integral) == (np.pi if np.prod(signs) < 0 else 0)
|
||||
assert_allclose(np.exp(logres.standard_error),
|
||||
res.standard_error, rtol=1e-14, atol=1e-16)
|
||||
|
||||
@pytest.mark.parametrize("n_points", [2**8, 2**12])
|
||||
@pytest.mark.parametrize("n_estimates", [8, 16])
|
||||
def test_basic(self, n_points, n_estimates):
|
||||
self.basic_test(n_points, n_estimates)
|
||||
|
||||
@pytest.mark.parametrize("signs", [[1, 1], [-1, -1], [-1, 1], [1, -1]])
|
||||
def test_sign(self, signs):
|
||||
self.basic_test(signs=signs)
|
||||
|
||||
@pytest.mark.parametrize("log", [False, True])
|
||||
def test_zero(self, log):
|
||||
message = "A lower limit was equal to an upper limit, so"
|
||||
with pytest.warns(UserWarning, match=message):
|
||||
res = qmc_quad(lambda x: 1, [0, 0], [0, 1], log=log)
|
||||
assert res.integral == (-np.inf if log else 0)
|
||||
assert res.standard_error == 0
|
||||
|
||||
def test_flexible_input(self):
|
||||
# check that qrng is not required
|
||||
# also checks that for 1d problems, a and b can be scalars
|
||||
def func(x):
|
||||
return stats.norm.pdf(x, scale=2)
|
||||
|
||||
res = qmc_quad(func, 0, 1)
|
||||
ref = stats.norm.cdf(1, scale=2) - stats.norm.cdf(0, scale=2)
|
||||
assert_allclose(res.integral, ref, 1e-2)
|
||||
|
||||
|
||||
def cumulative_simpson_nd_reference(y, *, x=None, dx=None, initial=None, axis=-1):
|
||||
# Use cumulative_trapezoid if length of y < 3
|
||||
if y.shape[axis] < 3:
|
||||
if initial is None:
|
||||
return cumulative_trapezoid(y, x=x, dx=dx, axis=axis, initial=None)
|
||||
else:
|
||||
return initial + cumulative_trapezoid(y, x=x, dx=dx, axis=axis, initial=0)
|
||||
|
||||
# Ensure that working axis is last axis
|
||||
y = np.moveaxis(y, axis, -1)
|
||||
x = np.moveaxis(x, axis, -1) if np.ndim(x) > 1 else x
|
||||
dx = np.moveaxis(dx, axis, -1) if np.ndim(dx) > 1 else dx
|
||||
initial = np.moveaxis(initial, axis, -1) if np.ndim(initial) > 1 else initial
|
||||
|
||||
# If `x` is not present, create it from `dx`
|
||||
n = y.shape[-1]
|
||||
x = dx * np.arange(n) if dx is not None else x
|
||||
# Similarly, if `initial` is not present, set it to 0
|
||||
initial_was_none = initial is None
|
||||
initial = 0 if initial_was_none else initial
|
||||
|
||||
# `np.apply_along_axis` accepts only one array, so concatenate arguments
|
||||
x = np.broadcast_to(x, y.shape)
|
||||
initial = np.broadcast_to(initial, y.shape[:-1] + (1,))
|
||||
z = np.concatenate((y, x, initial), axis=-1)
|
||||
|
||||
# Use `np.apply_along_axis` to compute result
|
||||
def f(z):
|
||||
return cumulative_simpson(z[:n], x=z[n:2*n], initial=z[2*n:])
|
||||
res = np.apply_along_axis(f, -1, z)
|
||||
|
||||
# Remove `initial` and undo axis move as needed
|
||||
res = res[..., 1:] if initial_was_none else res
|
||||
res = np.moveaxis(res, -1, axis)
|
||||
return res
|
||||
|
||||
|
||||
class TestCumulativeSimpson:
|
||||
x0 = np.arange(4)
|
||||
y0 = x0**2
|
||||
|
||||
@pytest.mark.parametrize('use_dx', (False, True))
|
||||
@pytest.mark.parametrize('use_initial', (False, True))
|
||||
def test_1d(self, use_dx, use_initial):
|
||||
# Test for exact agreement with polynomial of highest
|
||||
# possible order (3 if `dx` is constant, 2 otherwise).
|
||||
rng = np.random.default_rng(82456839535679456794)
|
||||
n = 10
|
||||
|
||||
# Generate random polynomials and ground truth
|
||||
# integral of appropriate order
|
||||
order = 3 if use_dx else 2
|
||||
dx = rng.random()
|
||||
x = (np.sort(rng.random(n)) if order == 2
|
||||
else np.arange(n)*dx + rng.random())
|
||||
i = np.arange(order + 1)[:, np.newaxis]
|
||||
c = rng.random(order + 1)[:, np.newaxis]
|
||||
y = np.sum(c*x**i, axis=0)
|
||||
Y = np.sum(c*x**(i + 1)/(i + 1), axis=0)
|
||||
ref = Y if use_initial else (Y-Y[0])[1:]
|
||||
|
||||
# Integrate with `cumulative_simpson`
|
||||
initial = Y[0] if use_initial else None
|
||||
kwarg = {'dx': dx} if use_dx else {'x': x}
|
||||
res = cumulative_simpson(y, **kwarg, initial=initial)
|
||||
|
||||
# Compare result against reference
|
||||
if not use_dx:
|
||||
assert_allclose(res, ref, rtol=2e-15)
|
||||
else:
|
||||
i0 = 0 if use_initial else 1
|
||||
# all terms are "close"
|
||||
assert_allclose(res, ref, rtol=0.0025)
|
||||
# only even-interval terms are "exact"
|
||||
assert_allclose(res[i0::2], ref[i0::2], rtol=2e-15)
|
||||
|
||||
@pytest.mark.parametrize('axis', np.arange(-3, 3))
|
||||
@pytest.mark.parametrize('x_ndim', (1, 3))
|
||||
@pytest.mark.parametrize('x_len', (1, 2, 7))
|
||||
@pytest.mark.parametrize('i_ndim', (None, 0, 3,))
|
||||
@pytest.mark.parametrize('dx', (None, True))
|
||||
def test_nd(self, axis, x_ndim, x_len, i_ndim, dx):
|
||||
# Test behavior of `cumulative_simpson` with N-D `y`
|
||||
rng = np.random.default_rng(82456839535679456794)
|
||||
|
||||
# determine shapes
|
||||
shape = [5, 6, x_len]
|
||||
shape[axis], shape[-1] = shape[-1], shape[axis]
|
||||
shape_len_1 = shape.copy()
|
||||
shape_len_1[axis] = 1
|
||||
i_shape = shape_len_1 if i_ndim == 3 else ()
|
||||
|
||||
# initialize arguments
|
||||
y = rng.random(size=shape)
|
||||
x, dx = None, None
|
||||
if dx:
|
||||
dx = rng.random(size=shape_len_1) if x_ndim > 1 else rng.random()
|
||||
else:
|
||||
x = (np.sort(rng.random(size=shape), axis=axis) if x_ndim > 1
|
||||
else np.sort(rng.random(size=shape[axis])))
|
||||
initial = None if i_ndim is None else rng.random(size=i_shape)
|
||||
|
||||
# compare results
|
||||
res = cumulative_simpson(y, x=x, dx=dx, initial=initial, axis=axis)
|
||||
ref = cumulative_simpson_nd_reference(y, x=x, dx=dx, initial=initial, axis=axis)
|
||||
np.testing.assert_allclose(res, ref, rtol=1e-15)
|
||||
|
||||
@pytest.mark.parametrize(('message', 'kwarg_update'), [
|
||||
("x must be strictly increasing", dict(x=[2, 2, 3, 4])),
|
||||
("x must be strictly increasing", dict(x=[x0, [2, 2, 4, 8]], y=[y0, y0])),
|
||||
("x must be strictly increasing", dict(x=[x0, x0, x0], y=[y0, y0, y0], axis=0)),
|
||||
("At least one point is required", dict(x=[], y=[])),
|
||||
("`axis=4` is not valid for `y` with `y.ndim=1`", dict(axis=4)),
|
||||
("shape of `x` must be the same as `y` or 1-D", dict(x=np.arange(5))),
|
||||
("`initial` must either be a scalar or...", dict(initial=np.arange(5))),
|
||||
("`dx` must either be a scalar or...", dict(x=None, dx=np.arange(5))),
|
||||
])
|
||||
def test_simpson_exceptions(self, message, kwarg_update):
|
||||
kwargs0 = dict(y=self.y0, x=self.x0, dx=None, initial=None, axis=-1)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
cumulative_simpson(**dict(kwargs0, **kwarg_update))
|
||||
|
||||
def test_special_cases(self):
|
||||
# Test special cases not checked elsewhere
|
||||
rng = np.random.default_rng(82456839535679456794)
|
||||
y = rng.random(size=10)
|
||||
res = cumulative_simpson(y, dx=0)
|
||||
assert_equal(res, 0)
|
||||
|
||||
# Should add tests of:
|
||||
# - all elements of `x` identical
|
||||
# These should work as they do for `simpson`
|
||||
|
||||
def _get_theoretical_diff_between_simps_and_cum_simps(self, y, x):
|
||||
"""`cumulative_simpson` and `simpson` can be tested against other to verify
|
||||
they give consistent results. `simpson` will iteratively be called with
|
||||
successively higher upper limits of integration. This function calculates
|
||||
the theoretical correction required to `simpson` at even intervals to match
|
||||
with `cumulative_simpson`.
|
||||
"""
|
||||
d = np.diff(x, axis=-1)
|
||||
sub_integrals_h1 = _cumulative_simpson_unequal_intervals(y, d)
|
||||
sub_integrals_h2 = _cumulative_simpson_unequal_intervals(
|
||||
y[..., ::-1], d[..., ::-1]
|
||||
)[..., ::-1]
|
||||
|
||||
# Concatenate to build difference array
|
||||
zeros_shape = (*y.shape[:-1], 1)
|
||||
theoretical_difference = np.concatenate(
|
||||
[
|
||||
np.zeros(zeros_shape),
|
||||
(sub_integrals_h1[..., 1:] - sub_integrals_h2[..., :-1]),
|
||||
np.zeros(zeros_shape),
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
# Differences only expected at even intervals. Odd intervals will
|
||||
# match exactly so there is no correction
|
||||
theoretical_difference[..., 1::2] = 0.0
|
||||
# Note: the first interval will not match from this correction as
|
||||
# `simpson` uses the trapezoidal rule
|
||||
return theoretical_difference
|
||||
|
||||
@pytest.mark.slow
|
||||
@given(
|
||||
y=hyp_num.arrays(
|
||||
np.float64,
|
||||
hyp_num.array_shapes(max_dims=4, min_side=3, max_side=10),
|
||||
elements=st.floats(-10, 10, allow_nan=False).filter(lambda x: abs(x) > 1e-7)
|
||||
)
|
||||
)
|
||||
def test_cumulative_simpson_against_simpson_with_default_dx(
|
||||
self, y
|
||||
):
|
||||
"""Theoretically, the output of `cumulative_simpson` will be identical
|
||||
to `simpson` at all even indices and in the last index. The first index
|
||||
will not match as `simpson` uses the trapezoidal rule when there are only two
|
||||
data points. Odd indices after the first index are shown to match with
|
||||
a mathematically-derived correction."""
|
||||
def simpson_reference(y):
|
||||
return np.stack(
|
||||
[simpson(y[..., :i], dx=1.0) for i in range(2, y.shape[-1]+1)], axis=-1,
|
||||
)
|
||||
|
||||
res = cumulative_simpson(y, dx=1.0)
|
||||
ref = simpson_reference(y)
|
||||
theoretical_difference = self._get_theoretical_diff_between_simps_and_cum_simps(
|
||||
y, x=np.arange(y.shape[-1])
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
res[..., 1:], ref[..., 1:] + theoretical_difference[..., 1:]
|
||||
)
|
||||
|
||||
@pytest.mark.slow
|
||||
@given(
|
||||
y=hyp_num.arrays(
|
||||
np.float64,
|
||||
hyp_num.array_shapes(max_dims=4, min_side=3, max_side=10),
|
||||
elements=st.floats(-10, 10, allow_nan=False).filter(lambda x: abs(x) > 1e-7)
|
||||
)
|
||||
)
|
||||
def test_cumulative_simpson_against_simpson(
|
||||
self, y
|
||||
):
|
||||
"""Theoretically, the output of `cumulative_simpson` will be identical
|
||||
to `simpson` at all even indices and in the last index. The first index
|
||||
will not match as `simpson` uses the trapezoidal rule when there are only two
|
||||
data points. Odd indices after the first index are shown to match with
|
||||
a mathematically-derived correction."""
|
||||
interval = 10/(y.shape[-1] - 1)
|
||||
x = np.linspace(0, 10, num=y.shape[-1])
|
||||
x[1:] = x[1:] + 0.2*interval*np.random.uniform(-1, 1, len(x) - 1)
|
||||
|
||||
def simpson_reference(y, x):
|
||||
return np.stack(
|
||||
[simpson(y[..., :i], x=x[..., :i]) for i in range(2, y.shape[-1]+1)],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
res = cumulative_simpson(y, x=x)
|
||||
ref = simpson_reference(y, x)
|
||||
theoretical_difference = self._get_theoretical_diff_between_simps_and_cum_simps(
|
||||
y, x
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
res[..., 1:], ref[..., 1:] + theoretical_difference[..., 1:]
|
||||
)
|
||||
@ -0,0 +1,947 @@
|
||||
# mypy: disable-error-code="attr-defined"
|
||||
import os
|
||||
import pytest
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose, assert_equal
|
||||
|
||||
import scipy._lib._elementwise_iterative_method as eim
|
||||
from scipy import special, stats
|
||||
from scipy.integrate import quad_vec
|
||||
from scipy.integrate._tanhsinh import _tanhsinh, _pair_cache, _nsum
|
||||
from scipy.stats._discrete_distns import _gen_harmonic_gt1
|
||||
|
||||
class TestTanhSinh:
|
||||
|
||||
# Test problems from [1] Section 6
|
||||
def f1(self, t):
|
||||
return t * np.log(1 + t)
|
||||
|
||||
f1.ref = 0.25
|
||||
f1.b = 1
|
||||
|
||||
def f2(self, t):
|
||||
return t ** 2 * np.arctan(t)
|
||||
|
||||
f2.ref = (np.pi - 2 + 2 * np.log(2)) / 12
|
||||
f2.b = 1
|
||||
|
||||
def f3(self, t):
|
||||
return np.exp(t) * np.cos(t)
|
||||
|
||||
f3.ref = (np.exp(np.pi / 2) - 1) / 2
|
||||
f3.b = np.pi / 2
|
||||
|
||||
def f4(self, t):
|
||||
a = np.sqrt(2 + t ** 2)
|
||||
return np.arctan(a) / ((1 + t ** 2) * a)
|
||||
|
||||
f4.ref = 5 * np.pi ** 2 / 96
|
||||
f4.b = 1
|
||||
|
||||
def f5(self, t):
|
||||
return np.sqrt(t) * np.log(t)
|
||||
|
||||
f5.ref = -4 / 9
|
||||
f5.b = 1
|
||||
|
||||
def f6(self, t):
|
||||
return np.sqrt(1 - t ** 2)
|
||||
|
||||
f6.ref = np.pi / 4
|
||||
f6.b = 1
|
||||
|
||||
def f7(self, t):
|
||||
return np.sqrt(t) / np.sqrt(1 - t ** 2)
|
||||
|
||||
f7.ref = 2 * np.sqrt(np.pi) * special.gamma(3 / 4) / special.gamma(1 / 4)
|
||||
f7.b = 1
|
||||
|
||||
def f8(self, t):
|
||||
return np.log(t) ** 2
|
||||
|
||||
f8.ref = 2
|
||||
f8.b = 1
|
||||
|
||||
def f9(self, t):
|
||||
return np.log(np.cos(t))
|
||||
|
||||
f9.ref = -np.pi * np.log(2) / 2
|
||||
f9.b = np.pi / 2
|
||||
|
||||
def f10(self, t):
|
||||
return np.sqrt(np.tan(t))
|
||||
|
||||
f10.ref = np.pi * np.sqrt(2) / 2
|
||||
f10.b = np.pi / 2
|
||||
|
||||
def f11(self, t):
|
||||
return 1 / (1 + t ** 2)
|
||||
|
||||
f11.ref = np.pi / 2
|
||||
f11.b = np.inf
|
||||
|
||||
def f12(self, t):
|
||||
return np.exp(-t) / np.sqrt(t)
|
||||
|
||||
f12.ref = np.sqrt(np.pi)
|
||||
f12.b = np.inf
|
||||
|
||||
def f13(self, t):
|
||||
return np.exp(-t ** 2 / 2)
|
||||
|
||||
f13.ref = np.sqrt(np.pi / 2)
|
||||
f13.b = np.inf
|
||||
|
||||
def f14(self, t):
|
||||
return np.exp(-t) * np.cos(t)
|
||||
|
||||
f14.ref = 0.5
|
||||
f14.b = np.inf
|
||||
|
||||
def f15(self, t):
|
||||
return np.sin(t) / t
|
||||
|
||||
f15.ref = np.pi / 2
|
||||
f15.b = np.inf
|
||||
|
||||
def error(self, res, ref, log=False):
|
||||
err = abs(res - ref)
|
||||
|
||||
if not log:
|
||||
return err
|
||||
|
||||
with np.errstate(divide='ignore'):
|
||||
return np.log10(err)
|
||||
|
||||
def test_input_validation(self):
|
||||
f = self.f1
|
||||
|
||||
message = '`f` must be callable.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(42, 0, f.b)
|
||||
|
||||
message = '...must be True or False.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, log=2)
|
||||
|
||||
message = '...must be real numbers.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 1+1j, f.b)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, atol='ekki')
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, rtol=pytest)
|
||||
|
||||
message = '...must be non-negative and finite.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, rtol=-1)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, atol=np.inf)
|
||||
|
||||
message = '...may not be positive infinity.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, rtol=np.inf, log=True)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, atol=np.inf, log=True)
|
||||
|
||||
message = '...must be integers.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, maxlevel=object())
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, maxfun=1+1j)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, minlevel="migratory coconut")
|
||||
|
||||
message = '...must be non-negative.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, maxlevel=-1)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, maxfun=-1)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, minlevel=-1)
|
||||
|
||||
message = '...must be True or False.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, preserve_shape=2)
|
||||
|
||||
message = '...must be callable.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_tanhsinh(f, 0, f.b, callback='elderberry')
|
||||
|
||||
@pytest.mark.parametrize("limits, ref", [
|
||||
[(0, np.inf), 0.5], # b infinite
|
||||
[(-np.inf, 0), 0.5], # a infinite
|
||||
[(-np.inf, np.inf), 1], # a and b infinite
|
||||
[(np.inf, -np.inf), -1], # flipped limits
|
||||
[(1, -1), stats.norm.cdf(-1) - stats.norm.cdf(1)], # flipped limits
|
||||
])
|
||||
def test_integral_transforms(self, limits, ref):
|
||||
# Check that the integral transforms are behaving for both normal and
|
||||
# log integration
|
||||
dist = stats.norm()
|
||||
|
||||
res = _tanhsinh(dist.pdf, *limits)
|
||||
assert_allclose(res.integral, ref)
|
||||
|
||||
logres = _tanhsinh(dist.logpdf, *limits, log=True)
|
||||
assert_allclose(np.exp(logres.integral), ref)
|
||||
# Transformation should not make the result complex unnecessarily
|
||||
assert (np.issubdtype(logres.integral.dtype, np.floating) if ref > 0
|
||||
else np.issubdtype(logres.integral.dtype, np.complexfloating))
|
||||
|
||||
assert_allclose(np.exp(logres.error), res.error, atol=1e-16)
|
||||
|
||||
# 15 skipped intentionally; it's very difficult numerically
|
||||
@pytest.mark.parametrize('f_number', range(1, 15))
|
||||
def test_basic(self, f_number):
|
||||
f = getattr(self, f"f{f_number}")
|
||||
rtol = 2e-8
|
||||
res = _tanhsinh(f, 0, f.b, rtol=rtol)
|
||||
assert_allclose(res.integral, f.ref, rtol=rtol)
|
||||
if f_number not in {14}: # mildly underestimates error here
|
||||
true_error = abs(self.error(res.integral, f.ref)/res.integral)
|
||||
assert true_error < res.error
|
||||
|
||||
if f_number in {7, 10, 12}: # succeeds, but doesn't know it
|
||||
return
|
||||
|
||||
assert res.success
|
||||
assert res.status == 0
|
||||
|
||||
@pytest.mark.parametrize('ref', (0.5, [0.4, 0.6]))
|
||||
@pytest.mark.parametrize('case', stats._distr_params.distcont)
|
||||
def test_accuracy(self, ref, case):
|
||||
distname, params = case
|
||||
if distname in {'dgamma', 'dweibull', 'laplace', 'kstwo'}:
|
||||
# should split up interval at first-derivative discontinuity
|
||||
pytest.skip('tanh-sinh is not great for non-smooth integrands')
|
||||
if (distname in {'studentized_range', 'levy_stable'}
|
||||
and not int(os.getenv('SCIPY_XSLOW', 0))):
|
||||
pytest.skip('This case passes, but it is too slow.')
|
||||
dist = getattr(stats, distname)(*params)
|
||||
x = dist.interval(ref)
|
||||
res = _tanhsinh(dist.pdf, *x)
|
||||
assert_allclose(res.integral, ref)
|
||||
|
||||
@pytest.mark.parametrize('shape', [tuple(), (12,), (3, 4), (3, 2, 2)])
|
||||
def test_vectorization(self, shape):
|
||||
# Test for correct functionality, output shapes, and dtypes for various
|
||||
# input shapes.
|
||||
rng = np.random.default_rng(82456839535679456794)
|
||||
a = rng.random(shape)
|
||||
b = rng.random(shape)
|
||||
p = rng.random(shape)
|
||||
n = np.prod(shape)
|
||||
|
||||
def f(x, p):
|
||||
f.ncall += 1
|
||||
f.feval += 1 if (x.size == n or x.ndim <=1) else x.shape[-1]
|
||||
return x**p
|
||||
f.ncall = 0
|
||||
f.feval = 0
|
||||
|
||||
@np.vectorize
|
||||
def _tanhsinh_single(a, b, p):
|
||||
return _tanhsinh(lambda x: x**p, a, b)
|
||||
|
||||
res = _tanhsinh(f, a, b, args=(p,))
|
||||
refs = _tanhsinh_single(a, b, p).ravel()
|
||||
|
||||
attrs = ['integral', 'error', 'success', 'status', 'nfev', 'maxlevel']
|
||||
for attr in attrs:
|
||||
ref_attr = [getattr(ref, attr) for ref in refs]
|
||||
res_attr = getattr(res, attr)
|
||||
assert_allclose(res_attr.ravel(), ref_attr, rtol=1e-15)
|
||||
assert_equal(res_attr.shape, shape)
|
||||
|
||||
assert np.issubdtype(res.success.dtype, np.bool_)
|
||||
assert np.issubdtype(res.status.dtype, np.integer)
|
||||
assert np.issubdtype(res.nfev.dtype, np.integer)
|
||||
assert np.issubdtype(res.maxlevel.dtype, np.integer)
|
||||
assert_equal(np.max(res.nfev), f.feval)
|
||||
# maxlevel = 2 -> 3 function calls (2 initialization, 1 work)
|
||||
assert np.max(res.maxlevel) >= 2
|
||||
assert_equal(np.max(res.maxlevel), f.ncall)
|
||||
|
||||
def test_flags(self):
|
||||
# Test cases that should produce different status flags; show that all
|
||||
# can be produced simultaneously.
|
||||
def f(xs, js):
|
||||
f.nit += 1
|
||||
funcs = [lambda x: np.exp(-x**2), # converges
|
||||
lambda x: np.exp(x), # reaches maxiter due to order=2
|
||||
lambda x: np.full_like(x, np.nan)[()]] # stops due to NaN
|
||||
res = [funcs[j](x) for x, j in zip(xs, js.ravel())]
|
||||
return res
|
||||
f.nit = 0
|
||||
|
||||
args = (np.arange(3, dtype=np.int64),)
|
||||
res = _tanhsinh(f, [np.inf]*3, [-np.inf]*3, maxlevel=5, args=args)
|
||||
ref_flags = np.array([0, -2, -3])
|
||||
assert_equal(res.status, ref_flags)
|
||||
|
||||
def test_flags_preserve_shape(self):
|
||||
# Same test as above but using `preserve_shape` option to simplify.
|
||||
def f(x):
|
||||
return [np.exp(-x[0]**2), # converges
|
||||
np.exp(x[1]), # reaches maxiter due to order=2
|
||||
np.full_like(x[2], np.nan)[()]] # stops due to NaN
|
||||
|
||||
res = _tanhsinh(f, [np.inf]*3, [-np.inf]*3, maxlevel=5, preserve_shape=True)
|
||||
ref_flags = np.array([0, -2, -3])
|
||||
assert_equal(res.status, ref_flags)
|
||||
|
||||
def test_preserve_shape(self):
|
||||
# Test `preserve_shape` option
|
||||
def f(x):
|
||||
return np.asarray([[x, np.sin(10 * x)],
|
||||
[np.cos(30 * x), x * np.sin(100 * x)]])
|
||||
|
||||
ref = quad_vec(f, 0, 1)
|
||||
res = _tanhsinh(f, 0, 1, preserve_shape=True)
|
||||
assert_allclose(res.integral, ref[0])
|
||||
|
||||
def test_convergence(self):
|
||||
# demonstrate that number of accurate digits doubles each iteration
|
||||
f = self.f1
|
||||
last_logerr = 0
|
||||
for i in range(4):
|
||||
res = _tanhsinh(f, 0, f.b, minlevel=0, maxlevel=i)
|
||||
logerr = self.error(res.integral, f.ref, log=True)
|
||||
assert (logerr < last_logerr * 2 or logerr < -15.5)
|
||||
last_logerr = logerr
|
||||
|
||||
def test_options_and_result_attributes(self):
|
||||
# demonstrate that options are behaving as advertised and status
|
||||
# messages are as intended
|
||||
def f(x):
|
||||
f.calls += 1
|
||||
f.feval += np.size(x)
|
||||
return self.f2(x)
|
||||
f.ref = self.f2.ref
|
||||
f.b = self.f2.b
|
||||
default_rtol = 1e-12
|
||||
default_atol = f.ref * default_rtol # effective default absolute tol
|
||||
|
||||
# Test default options
|
||||
f.feval, f.calls = 0, 0
|
||||
ref = _tanhsinh(f, 0, f.b)
|
||||
assert self.error(ref.integral, f.ref) < ref.error < default_atol
|
||||
assert ref.nfev == f.feval
|
||||
ref.calls = f.calls # reference number of function calls
|
||||
assert ref.success
|
||||
assert ref.status == 0
|
||||
|
||||
# Test `maxlevel` equal to required max level
|
||||
# We should get all the same results
|
||||
f.feval, f.calls = 0, 0
|
||||
maxlevel = ref.maxlevel
|
||||
res = _tanhsinh(f, 0, f.b, maxlevel=maxlevel)
|
||||
res.calls = f.calls
|
||||
assert res == ref
|
||||
|
||||
# Now reduce the maximum level. We won't meet tolerances.
|
||||
f.feval, f.calls = 0, 0
|
||||
maxlevel -= 1
|
||||
assert maxlevel >= 2 # can't compare errors otherwise
|
||||
res = _tanhsinh(f, 0, f.b, maxlevel=maxlevel)
|
||||
assert self.error(res.integral, f.ref) < res.error > default_atol
|
||||
assert res.nfev == f.feval < ref.nfev
|
||||
assert f.calls == ref.calls - 1
|
||||
assert not res.success
|
||||
assert res.status == eim._ECONVERR
|
||||
|
||||
# `maxfun` is currently not enforced
|
||||
|
||||
# # Test `maxfun` equal to required number of function evaluations
|
||||
# # We should get all the same results
|
||||
# f.feval, f.calls = 0, 0
|
||||
# maxfun = ref.nfev
|
||||
# res = _tanhsinh(f, 0, f.b, maxfun = maxfun)
|
||||
# assert res == ref
|
||||
#
|
||||
# # Now reduce `maxfun`. We won't meet tolerances.
|
||||
# f.feval, f.calls = 0, 0
|
||||
# maxfun -= 1
|
||||
# res = _tanhsinh(f, 0, f.b, maxfun=maxfun)
|
||||
# assert self.error(res.integral, f.ref) < res.error > default_atol
|
||||
# assert res.nfev == f.feval < ref.nfev
|
||||
# assert f.calls == ref.calls - 1
|
||||
# assert not res.success
|
||||
# assert res.status == 2
|
||||
|
||||
# Take this result to be the new reference
|
||||
ref = res
|
||||
ref.calls = f.calls
|
||||
|
||||
# Test `atol`
|
||||
f.feval, f.calls = 0, 0
|
||||
# With this tolerance, we should get the exact same result as ref
|
||||
atol = np.nextafter(ref.error, np.inf)
|
||||
res = _tanhsinh(f, 0, f.b, rtol=0, atol=atol)
|
||||
assert res.integral == ref.integral
|
||||
assert res.error == ref.error
|
||||
assert res.nfev == f.feval == ref.nfev
|
||||
assert f.calls == ref.calls
|
||||
# Except the result is considered to be successful
|
||||
assert res.success
|
||||
assert res.status == 0
|
||||
|
||||
f.feval, f.calls = 0, 0
|
||||
# With a tighter tolerance, we should get a more accurate result
|
||||
atol = np.nextafter(ref.error, -np.inf)
|
||||
res = _tanhsinh(f, 0, f.b, rtol=0, atol=atol)
|
||||
assert self.error(res.integral, f.ref) < res.error < atol
|
||||
assert res.nfev == f.feval > ref.nfev
|
||||
assert f.calls > ref.calls
|
||||
assert res.success
|
||||
assert res.status == 0
|
||||
|
||||
# Test `rtol`
|
||||
f.feval, f.calls = 0, 0
|
||||
# With this tolerance, we should get the exact same result as ref
|
||||
rtol = np.nextafter(ref.error/ref.integral, np.inf)
|
||||
res = _tanhsinh(f, 0, f.b, rtol=rtol)
|
||||
assert res.integral == ref.integral
|
||||
assert res.error == ref.error
|
||||
assert res.nfev == f.feval == ref.nfev
|
||||
assert f.calls == ref.calls
|
||||
# Except the result is considered to be successful
|
||||
assert res.success
|
||||
assert res.status == 0
|
||||
|
||||
f.feval, f.calls = 0, 0
|
||||
# With a tighter tolerance, we should get a more accurate result
|
||||
rtol = np.nextafter(ref.error/ref.integral, -np.inf)
|
||||
res = _tanhsinh(f, 0, f.b, rtol=rtol)
|
||||
assert self.error(res.integral, f.ref)/f.ref < res.error/res.integral < rtol
|
||||
assert res.nfev == f.feval > ref.nfev
|
||||
assert f.calls > ref.calls
|
||||
assert res.success
|
||||
assert res.status == 0
|
||||
|
||||
@pytest.mark.parametrize('rtol', [1e-4, 1e-14])
|
||||
def test_log(self, rtol):
|
||||
# Test equivalence of log-integration and regular integration
|
||||
dist = stats.norm()
|
||||
|
||||
test_tols = dict(atol=1e-18, rtol=1e-15)
|
||||
|
||||
# Positive integrand (real log-integrand)
|
||||
res = _tanhsinh(dist.logpdf, -1, 2, log=True, rtol=np.log(rtol))
|
||||
ref = _tanhsinh(dist.pdf, -1, 2, rtol=rtol)
|
||||
assert_allclose(np.exp(res.integral), ref.integral, **test_tols)
|
||||
assert_allclose(np.exp(res.error), ref.error, **test_tols)
|
||||
assert res.nfev == ref.nfev
|
||||
|
||||
# Real integrand (complex log-integrand)
|
||||
def f(x):
|
||||
return -dist.logpdf(x)*dist.pdf(x)
|
||||
|
||||
def logf(x):
|
||||
return np.log(dist.logpdf(x) + 0j) + dist.logpdf(x) + np.pi * 1j
|
||||
|
||||
res = _tanhsinh(logf, -np.inf, np.inf, log=True)
|
||||
ref = _tanhsinh(f, -np.inf, np.inf)
|
||||
# In gh-19173, we saw `invalid` warnings on one CI platform.
|
||||
# Silencing `all` because I can't reproduce locally and don't want
|
||||
# to risk the need to run CI again.
|
||||
with np.errstate(all='ignore'):
|
||||
assert_allclose(np.exp(res.integral), ref.integral, **test_tols)
|
||||
assert_allclose(np.exp(res.error), ref.error, **test_tols)
|
||||
assert res.nfev == ref.nfev
|
||||
|
||||
def test_complex(self):
|
||||
# Test integration of complex integrand
|
||||
# Finite limits
|
||||
def f(x):
|
||||
return np.exp(1j * x)
|
||||
|
||||
res = _tanhsinh(f, 0, np.pi/4)
|
||||
ref = np.sqrt(2)/2 + (1-np.sqrt(2)/2)*1j
|
||||
assert_allclose(res.integral, ref)
|
||||
|
||||
# Infinite limits
|
||||
dist1 = stats.norm(scale=1)
|
||||
dist2 = stats.norm(scale=2)
|
||||
def f(x):
|
||||
return dist1.pdf(x) + 1j*dist2.pdf(x)
|
||||
|
||||
res = _tanhsinh(f, np.inf, -np.inf)
|
||||
assert_allclose(res.integral, -(1+1j))
|
||||
|
||||
@pytest.mark.parametrize("maxlevel", range(4))
|
||||
def test_minlevel(self, maxlevel):
|
||||
# Verify that minlevel does not change the values at which the
|
||||
# integrand is evaluated or the integral/error estimates, only the
|
||||
# number of function calls
|
||||
def f(x):
|
||||
f.calls += 1
|
||||
f.feval += np.size(x)
|
||||
f.x = np.concatenate((f.x, x.ravel()))
|
||||
return self.f2(x)
|
||||
f.feval, f.calls, f.x = 0, 0, np.array([])
|
||||
|
||||
ref = _tanhsinh(f, 0, self.f2.b, minlevel=0, maxlevel=maxlevel)
|
||||
ref_x = np.sort(f.x)
|
||||
|
||||
for minlevel in range(0, maxlevel + 1):
|
||||
f.feval, f.calls, f.x = 0, 0, np.array([])
|
||||
options = dict(minlevel=minlevel, maxlevel=maxlevel)
|
||||
res = _tanhsinh(f, 0, self.f2.b, **options)
|
||||
# Should be very close; all that has changed is the order of values
|
||||
assert_allclose(res.integral, ref.integral, rtol=4e-16)
|
||||
# Difference in absolute errors << magnitude of integral
|
||||
assert_allclose(res.error, ref.error, atol=4e-16 * ref.integral)
|
||||
assert res.nfev == f.feval == len(f.x)
|
||||
assert f.calls == maxlevel - minlevel + 1 + 1 # 1 validation call
|
||||
assert res.status == ref.status
|
||||
assert_equal(ref_x, np.sort(f.x))
|
||||
|
||||
def test_improper_integrals(self):
|
||||
# Test handling of infinite limits of integration (mixed with finite limits)
|
||||
def f(x):
|
||||
x[np.isinf(x)] = np.nan
|
||||
return np.exp(-x**2)
|
||||
a = [-np.inf, 0, -np.inf, np.inf, -20, -np.inf, -20]
|
||||
b = [np.inf, np.inf, 0, -np.inf, 20, 20, np.inf]
|
||||
ref = np.sqrt(np.pi)
|
||||
res = _tanhsinh(f, a, b)
|
||||
assert_allclose(res.integral, [ref, ref/2, ref/2, -ref, ref, ref, ref])
|
||||
|
||||
@pytest.mark.parametrize("limits", ((0, 3), ([-np.inf, 0], [3, 3])))
|
||||
@pytest.mark.parametrize("dtype", (np.float32, np.float64))
|
||||
def test_dtype(self, limits, dtype):
|
||||
# Test that dtypes are preserved
|
||||
a, b = np.asarray(limits, dtype=dtype)[()]
|
||||
|
||||
def f(x):
|
||||
assert x.dtype == dtype
|
||||
return np.exp(x)
|
||||
|
||||
rtol = 1e-12 if dtype == np.float64 else 1e-5
|
||||
res = _tanhsinh(f, a, b, rtol=rtol)
|
||||
assert res.integral.dtype == dtype
|
||||
assert res.error.dtype == dtype
|
||||
assert np.all(res.success)
|
||||
assert_allclose(res.integral, np.exp(b)-np.exp(a), rtol=rtol)
|
||||
|
||||
def test_maxiter_callback(self):
|
||||
# Test behavior of `maxiter` parameter and `callback` interface
|
||||
a, b = -np.inf, np.inf
|
||||
def f(x):
|
||||
return np.exp(-x*x)
|
||||
|
||||
minlevel, maxlevel = 0, 2
|
||||
maxiter = maxlevel - minlevel + 1
|
||||
kwargs = dict(minlevel=minlevel, maxlevel=maxlevel, rtol=1e-15)
|
||||
res = _tanhsinh(f, a, b, **kwargs)
|
||||
assert not res.success
|
||||
assert res.maxlevel == maxlevel
|
||||
|
||||
def callback(res):
|
||||
callback.iter += 1
|
||||
callback.res = res
|
||||
assert hasattr(res, 'integral')
|
||||
assert res.status == 1
|
||||
if callback.iter == maxiter:
|
||||
raise StopIteration
|
||||
callback.iter = -1 # callback called once before first iteration
|
||||
callback.res = None
|
||||
|
||||
del kwargs['maxlevel']
|
||||
res2 = _tanhsinh(f, a, b, **kwargs, callback=callback)
|
||||
# terminating with callback is identical to terminating due to maxiter
|
||||
# (except for `status`)
|
||||
for key in res.keys():
|
||||
if key == 'status':
|
||||
assert callback.res[key] == 1
|
||||
assert res[key] == -2
|
||||
assert res2[key] == -4
|
||||
else:
|
||||
assert res2[key] == callback.res[key] == res[key]
|
||||
|
||||
def test_jumpstart(self):
|
||||
# The intermediate results at each level i should be the same as the
|
||||
# final results when jumpstarting at level i; i.e. minlevel=maxlevel=i
|
||||
a, b = -np.inf, np.inf
|
||||
def f(x):
|
||||
return np.exp(-x*x)
|
||||
|
||||
def callback(res):
|
||||
callback.integrals.append(res.integral)
|
||||
callback.errors.append(res.error)
|
||||
callback.integrals = []
|
||||
callback.errors = []
|
||||
|
||||
maxlevel = 4
|
||||
_tanhsinh(f, a, b, minlevel=0, maxlevel=maxlevel, callback=callback)
|
||||
|
||||
integrals = []
|
||||
errors = []
|
||||
for i in range(maxlevel + 1):
|
||||
res = _tanhsinh(f, a, b, minlevel=i, maxlevel=i)
|
||||
integrals.append(res.integral)
|
||||
errors.append(res.error)
|
||||
|
||||
assert_allclose(callback.integrals[1:], integrals, rtol=1e-15)
|
||||
assert_allclose(callback.errors[1:], errors, rtol=1e-15, atol=1e-16)
|
||||
|
||||
def test_special_cases(self):
|
||||
# Test edge cases and other special cases
|
||||
|
||||
# Test that integers are not passed to `f`
|
||||
# (otherwise this would overflow)
|
||||
def f(x):
|
||||
assert np.issubdtype(x.dtype, np.floating)
|
||||
return x ** 99
|
||||
|
||||
res = _tanhsinh(f, 0, 1)
|
||||
assert res.success
|
||||
assert_allclose(res.integral, 1/100)
|
||||
|
||||
# Test levels 0 and 1; error is NaN
|
||||
res = _tanhsinh(f, 0, 1, maxlevel=0)
|
||||
assert res.integral > 0
|
||||
assert_equal(res.error, np.nan)
|
||||
res = _tanhsinh(f, 0, 1, maxlevel=1)
|
||||
assert res.integral > 0
|
||||
assert_equal(res.error, np.nan)
|
||||
|
||||
# Tes equal left and right integration limits
|
||||
res = _tanhsinh(f, 1, 1)
|
||||
assert res.success
|
||||
assert res.maxlevel == -1
|
||||
assert_allclose(res.integral, 0)
|
||||
|
||||
# Test scalar `args` (not in tuple)
|
||||
def f(x, c):
|
||||
return x**c
|
||||
|
||||
res = _tanhsinh(f, 0, 1, args=99)
|
||||
assert_allclose(res.integral, 1/100)
|
||||
|
||||
# Test NaNs
|
||||
a = [np.nan, 0, 0, 0]
|
||||
b = [1, np.nan, 1, 1]
|
||||
c = [1, 1, np.nan, 1]
|
||||
res = _tanhsinh(f, a, b, args=(c,))
|
||||
assert_allclose(res.integral, [np.nan, np.nan, np.nan, 0.5])
|
||||
assert_allclose(res.error[:3], np.nan)
|
||||
assert_equal(res.status, [-3, -3, -3, 0])
|
||||
assert_equal(res.success, [False, False, False, True])
|
||||
assert_equal(res.nfev[:3], 1)
|
||||
|
||||
# Test complex integral followed by real integral
|
||||
# Previously, h0 was of the result dtype. If the `dtype` were complex,
|
||||
# this could lead to complex cached abscissae/weights. If these get
|
||||
# cast to real dtype for a subsequent real integral, we would get a
|
||||
# ComplexWarning. Check that this is avoided.
|
||||
_pair_cache.xjc = np.empty(0)
|
||||
_pair_cache.wj = np.empty(0)
|
||||
_pair_cache.indices = [0]
|
||||
_pair_cache.h0 = None
|
||||
res = _tanhsinh(lambda x: x*1j, 0, 1)
|
||||
assert_allclose(res.integral, 0.5*1j)
|
||||
res = _tanhsinh(lambda x: x, 0, 1)
|
||||
assert_allclose(res.integral, 0.5)
|
||||
|
||||
# Test zero-size
|
||||
shape = (0, 3)
|
||||
res = _tanhsinh(lambda x: x, 0, np.zeros(shape))
|
||||
attrs = ['integral', 'error', 'success', 'status', 'nfev', 'maxlevel']
|
||||
for attr in attrs:
|
||||
assert_equal(res[attr].shape, shape)
|
||||
|
||||
|
||||
class TestNSum:
|
||||
rng = np.random.default_rng(5895448232066142650)
|
||||
p = rng.uniform(1, 10, size=10)
|
||||
|
||||
def f1(self, k):
|
||||
# Integers are never passed to `f1`; if they were, we'd get
|
||||
# integer to negative integer power error
|
||||
return k**(-2)
|
||||
|
||||
f1.ref = np.pi**2/6
|
||||
f1.a = 1
|
||||
f1.b = np.inf
|
||||
f1.args = tuple()
|
||||
|
||||
def f2(self, k, p):
|
||||
return 1 / k**p
|
||||
|
||||
f2.ref = special.zeta(p, 1)
|
||||
f2.a = 1
|
||||
f2.b = np.inf
|
||||
f2.args = (p,)
|
||||
|
||||
def f3(self, k, p):
|
||||
return 1 / k**p
|
||||
|
||||
f3.a = 1
|
||||
f3.b = rng.integers(5, 15, size=(3, 1))
|
||||
f3.ref = _gen_harmonic_gt1(f3.b, p)
|
||||
f3.args = (p,)
|
||||
|
||||
def test_input_validation(self):
|
||||
f = self.f1
|
||||
|
||||
message = '`f` must be callable.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(42, f.a, f.b)
|
||||
|
||||
message = '...must be True or False.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, log=2)
|
||||
|
||||
message = '...must be real numbers.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, 1+1j, f.b)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, None)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, step=object())
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, atol='ekki')
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, rtol=pytest)
|
||||
|
||||
with np.errstate(all='ignore'):
|
||||
res = _nsum(f, [np.nan, -np.inf, np.inf], 1)
|
||||
assert np.all((res.status == -1) & np.isnan(res.sum)
|
||||
& np.isnan(res.error) & ~res.success & res.nfev == 1)
|
||||
res = _nsum(f, 10, [np.nan, 1])
|
||||
assert np.all((res.status == -1) & np.isnan(res.sum)
|
||||
& np.isnan(res.error) & ~res.success & res.nfev == 1)
|
||||
res = _nsum(f, 1, 10, step=[np.nan, -np.inf, np.inf, -1, 0])
|
||||
assert np.all((res.status == -1) & np.isnan(res.sum)
|
||||
& np.isnan(res.error) & ~res.success & res.nfev == 1)
|
||||
|
||||
message = '...must be non-negative and finite.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, rtol=-1)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, atol=np.inf)
|
||||
|
||||
message = '...may not be positive infinity.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, rtol=np.inf, log=True)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, atol=np.inf, log=True)
|
||||
|
||||
message = '...must be a non-negative integer.'
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, maxterms=3.5)
|
||||
with pytest.raises(ValueError, match=message):
|
||||
_nsum(f, f.a, f.b, maxterms=-2)
|
||||
|
||||
@pytest.mark.parametrize('f_number', range(1, 4))
|
||||
def test_basic(self, f_number):
|
||||
f = getattr(self, f"f{f_number}")
|
||||
res = _nsum(f, f.a, f.b, args=f.args)
|
||||
assert_allclose(res.sum, f.ref)
|
||||
assert_equal(res.status, 0)
|
||||
assert_equal(res.success, True)
|
||||
|
||||
with np.errstate(divide='ignore'):
|
||||
logres = _nsum(lambda *args: np.log(f(*args)),
|
||||
f.a, f.b, log=True, args=f.args)
|
||||
assert_allclose(np.exp(logres.sum), res.sum)
|
||||
assert_allclose(np.exp(logres.error), res.error)
|
||||
assert_equal(logres.status, 0)
|
||||
assert_equal(logres.success, True)
|
||||
|
||||
@pytest.mark.parametrize('maxterms', [0, 1, 10, 20, 100])
|
||||
def test_integral(self, maxterms):
|
||||
# test precise behavior of integral approximation
|
||||
f = self.f1
|
||||
|
||||
def logf(x):
|
||||
return -2*np.log(x)
|
||||
|
||||
def F(x):
|
||||
return -1 / x
|
||||
|
||||
a = np.asarray([1, 5])[:, np.newaxis]
|
||||
b = np.asarray([20, 100, np.inf])[:, np.newaxis, np.newaxis]
|
||||
step = np.asarray([0.5, 1, 2]).reshape((-1, 1, 1, 1))
|
||||
nsteps = np.floor((b - a)/step)
|
||||
b_original = b
|
||||
b = a + nsteps*step
|
||||
|
||||
k = a + maxterms*step
|
||||
# partial sum
|
||||
direct = f(a + np.arange(maxterms)*step).sum(axis=-1, keepdims=True)
|
||||
integral = (F(b) - F(k))/step # integral approximation of remainder
|
||||
low = direct + integral + f(b) # theoretical lower bound
|
||||
high = direct + integral + f(k) # theoretical upper bound
|
||||
ref_sum = (low + high)/2 # _nsum uses average of the two
|
||||
ref_err = (high - low)/2 # error (assuming perfect quadrature)
|
||||
|
||||
# correct reference values where number of terms < maxterms
|
||||
a, b, step = np.broadcast_arrays(a, b, step)
|
||||
for i in np.ndindex(a.shape):
|
||||
ai, bi, stepi = a[i], b[i], step[i]
|
||||
if (bi - ai)/stepi + 1 <= maxterms:
|
||||
direct = f(np.arange(ai, bi+stepi, stepi)).sum()
|
||||
ref_sum[i] = direct
|
||||
ref_err[i] = direct * np.finfo(direct).eps
|
||||
|
||||
rtol = 1e-12
|
||||
res = _nsum(f, a, b_original, step=step, maxterms=maxterms, rtol=rtol)
|
||||
assert_allclose(res.sum, ref_sum, rtol=10*rtol)
|
||||
assert_allclose(res.error, ref_err, rtol=100*rtol)
|
||||
assert_equal(res.status, 0)
|
||||
assert_equal(res.success, True)
|
||||
|
||||
i = ((b_original - a)/step + 1 <= maxterms)
|
||||
assert_allclose(res.sum[i], ref_sum[i], rtol=1e-15)
|
||||
assert_allclose(res.error[i], ref_err[i], rtol=1e-15)
|
||||
|
||||
logres = _nsum(logf, a, b_original, step=step, log=True,
|
||||
rtol=np.log(rtol), maxterms=maxterms)
|
||||
assert_allclose(np.exp(logres.sum), res.sum)
|
||||
assert_allclose(np.exp(logres.error), res.error)
|
||||
assert_equal(logres.status, 0)
|
||||
assert_equal(logres.success, True)
|
||||
|
||||
@pytest.mark.parametrize('shape', [tuple(), (12,), (3, 4), (3, 2, 2)])
|
||||
def test_vectorization(self, shape):
|
||||
# Test for correct functionality, output shapes, and dtypes for various
|
||||
# input shapes.
|
||||
rng = np.random.default_rng(82456839535679456794)
|
||||
a = rng.integers(1, 10, size=shape)
|
||||
# when the sum can be computed directly or `maxterms` is large enough
|
||||
# to meet `atol`, there are slight differences (for good reason)
|
||||
# between vectorized call and looping.
|
||||
b = np.inf
|
||||
p = rng.random(shape) + 1
|
||||
n = np.prod(shape)
|
||||
|
||||
def f(x, p):
|
||||
f.feval += 1 if (x.size == n or x.ndim <= 1) else x.shape[-1]
|
||||
return 1 / x ** p
|
||||
|
||||
f.feval = 0
|
||||
|
||||
@np.vectorize
|
||||
def _nsum_single(a, b, p, maxterms):
|
||||
return _nsum(lambda x: 1 / x**p, a, b, maxterms=maxterms)
|
||||
|
||||
res = _nsum(f, a, b, maxterms=1000, args=(p,))
|
||||
refs = _nsum_single(a, b, p, maxterms=1000).ravel()
|
||||
|
||||
attrs = ['sum', 'error', 'success', 'status', 'nfev']
|
||||
for attr in attrs:
|
||||
ref_attr = [getattr(ref, attr) for ref in refs]
|
||||
res_attr = getattr(res, attr)
|
||||
assert_allclose(res_attr.ravel(), ref_attr, rtol=1e-15)
|
||||
assert_equal(res_attr.shape, shape)
|
||||
|
||||
assert np.issubdtype(res.success.dtype, np.bool_)
|
||||
assert np.issubdtype(res.status.dtype, np.integer)
|
||||
assert np.issubdtype(res.nfev.dtype, np.integer)
|
||||
assert_equal(np.max(res.nfev), f.feval)
|
||||
|
||||
def test_status(self):
|
||||
f = self.f2
|
||||
|
||||
p = [2, 2, 0.9, 1.1]
|
||||
a = [0, 0, 1, 1]
|
||||
b = [10, np.inf, np.inf, np.inf]
|
||||
ref = special.zeta(p, 1)
|
||||
|
||||
with np.errstate(divide='ignore'): # intentionally dividing by zero
|
||||
res = _nsum(f, a, b, args=(p,))
|
||||
|
||||
assert_equal(res.success, [False, False, False, True])
|
||||
assert_equal(res.status, [-3, -3, -2, 0])
|
||||
assert_allclose(res.sum[res.success], ref[res.success])
|
||||
|
||||
def test_nfev(self):
|
||||
def f(x):
|
||||
f.nfev += np.size(x)
|
||||
return 1 / x**2
|
||||
|
||||
f.nfev = 0
|
||||
res = _nsum(f, 1, 10)
|
||||
assert_equal(res.nfev, f.nfev)
|
||||
|
||||
f.nfev = 0
|
||||
res = _nsum(f, 1, np.inf, atol=1e-6)
|
||||
assert_equal(res.nfev, f.nfev)
|
||||
|
||||
def test_inclusive(self):
|
||||
# There was an edge case off-by one bug when `_direct` was called with
|
||||
# `inclusive=True`. Check that this is resolved.
|
||||
res = _nsum(lambda k: 1 / k ** 2, [1, 4], np.inf, maxterms=500, atol=0.1)
|
||||
ref = _nsum(lambda k: 1 / k ** 2, [1, 4], np.inf)
|
||||
assert np.all(res.sum > (ref.sum - res.error))
|
||||
assert np.all(res.sum < (ref.sum + res.error))
|
||||
|
||||
def test_special_case(self):
|
||||
# test equal lower/upper limit
|
||||
f = self.f1
|
||||
a = b = 2
|
||||
res = _nsum(f, a, b)
|
||||
assert_equal(res.sum, f(a))
|
||||
|
||||
# Test scalar `args` (not in tuple)
|
||||
res = _nsum(self.f2, 1, np.inf, args=2)
|
||||
assert_allclose(res.sum, self.f1.ref) # f1.ref is correct w/ args=2
|
||||
|
||||
# Test 0 size input
|
||||
a = np.empty((3, 1, 1)) # arbitrary broadcastable shapes
|
||||
b = np.empty((0, 1)) # could use Hypothesis
|
||||
p = np.empty(4) # but it's overkill
|
||||
shape = np.broadcast_shapes(a.shape, b.shape, p.shape)
|
||||
res = _nsum(self.f2, a, b, args=(p,))
|
||||
assert res.sum.shape == shape
|
||||
assert res.status.shape == shape
|
||||
assert res.nfev.shape == shape
|
||||
|
||||
# Test maxterms=0
|
||||
def f(x):
|
||||
with np.errstate(divide='ignore'):
|
||||
return 1 / x
|
||||
|
||||
res = _nsum(f, 0, 10, maxterms=0)
|
||||
assert np.isnan(res.sum)
|
||||
assert np.isnan(res.error)
|
||||
assert res.status == -2
|
||||
|
||||
res = _nsum(f, 0, 10, maxterms=1)
|
||||
assert np.isnan(res.sum)
|
||||
assert np.isnan(res.error)
|
||||
assert res.status == -3
|
||||
|
||||
# Test NaNs
|
||||
# should skip both direct and integral methods if there are NaNs
|
||||
a = [np.nan, 1, 1, 1]
|
||||
b = [np.inf, np.nan, np.inf, np.inf]
|
||||
p = [2, 2, np.nan, 2]
|
||||
res = _nsum(self.f2, a, b, args=(p,))
|
||||
assert_allclose(res.sum, [np.nan, np.nan, np.nan, self.f1.ref])
|
||||
assert_allclose(res.error[:3], np.nan)
|
||||
assert_equal(res.status, [-1, -1, -3, 0])
|
||||
assert_equal(res.success, [False, False, False, True])
|
||||
# Ideally res.nfev[2] would be 1, but `tanhsinh` has some function evals
|
||||
assert_equal(res.nfev[:2], 1)
|
||||
|
||||
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
|
||||
def test_dtype(self, dtype):
|
||||
def f(k):
|
||||
assert k.dtype == dtype
|
||||
return 1 / k ** np.asarray(2, dtype=dtype)[()]
|
||||
|
||||
a = np.asarray(1, dtype=dtype)
|
||||
b = np.asarray([10, np.inf], dtype=dtype)
|
||||
res = _nsum(f, a, b)
|
||||
assert res.sum.dtype == dtype
|
||||
assert res.error.dtype == dtype
|
||||
|
||||
rtol = 1e-12 if dtype == np.float64 else 1e-6
|
||||
ref = _gen_harmonic_gt1(b, 2)
|
||||
assert_allclose(res.sum, ref, rtol=rtol)
|
||||
15
venv/lib/python3.12/site-packages/scipy/integrate/vode.py
Normal file
15
venv/lib/python3.12/site-packages/scipy/integrate/vode.py
Normal file
@ -0,0 +1,15 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
__all__: list[str] = []
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="integrate", module="vode",
|
||||
private_modules=["_vode"], all=__all__,
|
||||
attribute=name)
|
||||
Reference in New Issue
Block a user