|
|
|
""" |
|
First-order ODE integrators. |
|
|
|
User-friendly interface to various numerical integrators for solving a |
|
system of first order ODEs with prescribed initial conditions:: |
|
|
|
d y(t)[i] |
|
--------- = f(t,y(t))[i], |
|
d t |
|
|
|
y(t=0)[i] = y0[i], |
|
|
|
where:: |
|
|
|
i = 0, ..., len(y0) - 1 |
|
|
|
class ode |
|
--------- |
|
|
|
A generic interface class to numeric integrators. It has the following |
|
methods:: |
|
|
|
integrator = ode(f, jac=None) |
|
integrator = integrator.set_integrator(name, **params) |
|
integrator = integrator.set_initial_value(y0, t0=0.0) |
|
integrator = integrator.set_f_params(*args) |
|
integrator = integrator.set_jac_params(*args) |
|
y1 = integrator.integrate(t1, step=False, relax=False) |
|
flag = integrator.successful() |
|
|
|
class complex_ode |
|
----------------- |
|
|
|
This class has the same generic interface as ode, except it can handle complex |
|
f, y and Jacobians by transparently translating them into the equivalent |
|
real-valued system. It supports the real-valued solvers (i.e., not zvode) and is |
|
an alternative to ode with the zvode solver, sometimes performing better. |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = ['ode', 'complex_ode'] |
|
|
|
import re |
|
import threading |
|
import warnings |
|
|
|
from numpy import asarray, array, zeros, isscalar, real, imag, vstack |
|
|
|
from . import _vode |
|
from . import _dop |
|
from . import _lsoda |
|
|
|
|
|
_dop_int_dtype = _dop.types.intvar.dtype |
|
_vode_int_dtype = _vode.types.intvar.dtype |
|
_lsoda_int_dtype = _lsoda.types.intvar.dtype |
|
|
|
|
|
|
|
|
|
LSODA_LOCK = threading.Lock() |
|
VODE_LOCK = threading.Lock() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ode: |
|
""" |
|
A generic interface class to numeric integrators. |
|
|
|
Solve an equation system :math:`y'(t) = f(t,y)` with (optional) ``jac = df/dy``. |
|
|
|
*Note*: The first two arguments of ``f(t, y, ...)`` are in the |
|
opposite order of the arguments in the system definition function used |
|
by `scipy.integrate.odeint`. |
|
|
|
Parameters |
|
---------- |
|
f : callable ``f(t, y, *f_args)`` |
|
Right-hand side of the differential equation. t is a scalar, |
|
``y.shape == (n,)``. |
|
``f_args`` is set by calling ``set_f_params(*args)``. |
|
`f` should return a scalar, array or list (not a tuple). |
|
jac : callable ``jac(t, y, *jac_args)``, optional |
|
Jacobian of the right-hand side, ``jac[i,j] = d f[i] / d y[j]``. |
|
``jac_args`` is set by calling ``set_jac_params(*args)``. |
|
|
|
Attributes |
|
---------- |
|
t : float |
|
Current time. |
|
y : ndarray |
|
Current variable values. |
|
|
|
See also |
|
-------- |
|
odeint : an integrator with a simpler interface based on lsoda from ODEPACK |
|
quad : for finding the area under a curve |
|
|
|
Notes |
|
----- |
|
Available integrators are listed below. They can be selected using |
|
the `set_integrator` method. |
|
|
|
"vode" |
|
|
|
Real-valued Variable-coefficient Ordinary Differential Equation |
|
solver, with fixed-leading-coefficient implementation. It provides |
|
implicit Adams method (for non-stiff problems) and a method based on |
|
backward differentiation formulas (BDF) (for stiff problems). |
|
|
|
Source: http://www.netlib.org/ode/vode.f |
|
|
|
.. warning:: |
|
|
|
This integrator is not re-entrant. You cannot have two `ode` |
|
instances using the "vode" integrator at the same time. |
|
|
|
This integrator accepts the following parameters in `set_integrator` |
|
method of the `ode` class: |
|
|
|
- atol : float or sequence |
|
absolute tolerance for solution |
|
- rtol : float or sequence |
|
relative tolerance for solution |
|
- lband : None or int |
|
- uband : None or int |
|
Jacobian band width, jac[i,j] != 0 for i-lband <= j <= i+uband. |
|
Setting these requires your jac routine to return the jacobian |
|
in packed format, jac_packed[i-j+uband, j] = jac[i,j]. The |
|
dimension of the matrix must be (lband+uband+1, len(y)). |
|
- method: 'adams' or 'bdf' |
|
Which solver to use, Adams (non-stiff) or BDF (stiff) |
|
- with_jacobian : bool |
|
This option is only considered when the user has not supplied a |
|
Jacobian function and has not indicated (by setting either band) |
|
that the Jacobian is banded. In this case, `with_jacobian` specifies |
|
whether the iteration method of the ODE solver's correction step is |
|
chord iteration with an internally generated full Jacobian or |
|
functional iteration with no Jacobian. |
|
- nsteps : int |
|
Maximum number of (internally defined) steps allowed during one |
|
call to the solver. |
|
- first_step : float |
|
- min_step : float |
|
- max_step : float |
|
Limits for the step sizes used by the integrator. |
|
- order : int |
|
Maximum order used by the integrator, |
|
order <= 12 for Adams, <= 5 for BDF. |
|
|
|
"zvode" |
|
|
|
Complex-valued Variable-coefficient Ordinary Differential Equation |
|
solver, with fixed-leading-coefficient implementation. It provides |
|
implicit Adams method (for non-stiff problems) and a method based on |
|
backward differentiation formulas (BDF) (for stiff problems). |
|
|
|
Source: http://www.netlib.org/ode/zvode.f |
|
|
|
.. warning:: |
|
|
|
This integrator is not re-entrant. You cannot have two `ode` |
|
instances using the "zvode" integrator at the same time. |
|
|
|
This integrator accepts the same parameters in `set_integrator` |
|
as the "vode" solver. |
|
|
|
.. note:: |
|
|
|
When using ZVODE for a stiff system, it should only be used for |
|
the case in which the function f is analytic, that is, when each f(i) |
|
is an analytic function of each y(j). Analyticity means that the |
|
partial derivative df(i)/dy(j) is a unique complex number, and this |
|
fact is critical in the way ZVODE solves the dense or banded linear |
|
systems that arise in the stiff case. For a complex stiff ODE system |
|
in which f is not analytic, ZVODE is likely to have convergence |
|
failures, and for this problem one should instead use DVODE on the |
|
equivalent real system (in the real and imaginary parts of y). |
|
|
|
"lsoda" |
|
|
|
Real-valued Variable-coefficient Ordinary Differential Equation |
|
solver, with fixed-leading-coefficient implementation. It provides |
|
automatic method switching between implicit Adams method (for non-stiff |
|
problems) and a method based on backward differentiation formulas (BDF) |
|
(for stiff problems). |
|
|
|
Source: http://www.netlib.org/odepack |
|
|
|
.. warning:: |
|
|
|
This integrator is not re-entrant. You cannot have two `ode` |
|
instances using the "lsoda" integrator at the same time. |
|
|
|
This integrator accepts the following parameters in `set_integrator` |
|
method of the `ode` class: |
|
|
|
- atol : float or sequence |
|
absolute tolerance for solution |
|
- rtol : float or sequence |
|
relative tolerance for solution |
|
- lband : None or int |
|
- uband : None or int |
|
Jacobian band width, jac[i,j] != 0 for i-lband <= j <= i+uband. |
|
Setting these requires your jac routine to return the jacobian |
|
in packed format, jac_packed[i-j+uband, j] = jac[i,j]. |
|
- with_jacobian : bool |
|
*Not used.* |
|
- nsteps : int |
|
Maximum number of (internally defined) steps allowed during one |
|
call to the solver. |
|
- first_step : float |
|
- min_step : float |
|
- max_step : float |
|
Limits for the step sizes used by the integrator. |
|
- max_order_ns : int |
|
Maximum order used in the nonstiff case (default 12). |
|
- max_order_s : int |
|
Maximum order used in the stiff case (default 5). |
|
- max_hnil : int |
|
Maximum number of messages reporting too small step size (t + h = t) |
|
(default 0) |
|
- ixpr : int |
|
Whether to generate extra printing at method switches (default False). |
|
|
|
"dopri5" |
|
|
|
This is an explicit runge-kutta method of order (4)5 due to Dormand & |
|
Prince (with stepsize control and dense output). |
|
|
|
Authors: |
|
|
|
E. Hairer and G. Wanner |
|
Universite de Geneve, Dept. de Mathematiques |
|
CH-1211 Geneve 24, Switzerland |
|
e-mail: [email protected], [email protected] |
|
|
|
This code is described in [HNW93]_. |
|
|
|
This integrator accepts the following parameters in set_integrator() |
|
method of the ode class: |
|
|
|
- atol : float or sequence |
|
absolute tolerance for solution |
|
- rtol : float or sequence |
|
relative tolerance for solution |
|
- nsteps : int |
|
Maximum number of (internally defined) steps allowed during one |
|
call to the solver. |
|
- first_step : float |
|
- max_step : float |
|
- safety : float |
|
Safety factor on new step selection (default 0.9) |
|
- ifactor : float |
|
- dfactor : float |
|
Maximum factor to increase/decrease step size by in one step |
|
- beta : float |
|
Beta parameter for stabilised step size control. |
|
- verbosity : int |
|
Switch for printing messages (< 0 for no messages). |
|
|
|
"dop853" |
|
|
|
This is an explicit runge-kutta method of order 8(5,3) due to Dormand |
|
& Prince (with stepsize control and dense output). |
|
|
|
Options and references the same as "dopri5". |
|
|
|
Examples |
|
-------- |
|
|
|
A problem to integrate and the corresponding jacobian: |
|
|
|
>>> from scipy.integrate import ode |
|
>>> |
|
>>> y0, t0 = [1.0j, 2.0], 0 |
|
>>> |
|
>>> def f(t, y, arg1): |
|
... return [1j*arg1*y[0] + y[1], -arg1*y[1]**2] |
|
>>> def jac(t, y, arg1): |
|
... return [[1j*arg1, 1], [0, -arg1*2*y[1]]] |
|
|
|
The integration: |
|
|
|
>>> r = ode(f, jac).set_integrator('zvode', method='bdf') |
|
>>> r.set_initial_value(y0, t0).set_f_params(2.0).set_jac_params(2.0) |
|
>>> t1 = 10 |
|
>>> dt = 1 |
|
>>> while r.successful() and r.t < t1: |
|
... print(r.t+dt, r.integrate(r.t+dt)) |
|
1 [-0.71038232+0.23749653j 0.40000271+0.j ] |
|
2.0 [0.19098503-0.52359246j 0.22222356+0.j ] |
|
3.0 [0.47153208+0.52701229j 0.15384681+0.j ] |
|
4.0 [-0.61905937+0.30726255j 0.11764744+0.j ] |
|
5.0 [0.02340997-0.61418799j 0.09523835+0.j ] |
|
6.0 [0.58643071+0.339819j 0.08000018+0.j ] |
|
7.0 [-0.52070105+0.44525141j 0.06896565+0.j ] |
|
8.0 [-0.15986733-0.61234476j 0.06060616+0.j ] |
|
9.0 [0.64850462+0.15048982j 0.05405414+0.j ] |
|
10.0 [-0.38404699+0.56382299j 0.04878055+0.j ] |
|
|
|
References |
|
---------- |
|
.. [HNW93] E. Hairer, S.P. Norsett and G. Wanner, Solving Ordinary |
|
Differential Equations i. Nonstiff Problems. 2nd edition. |
|
Springer Series in Computational Mathematics, |
|
Springer-Verlag (1993) |
|
|
|
""" |
|
|
|
def __init__(self, f, jac=None): |
|
self.stiff = 0 |
|
self.f = f |
|
self.jac = jac |
|
self.f_params = () |
|
self.jac_params = () |
|
self._y = [] |
|
|
|
@property |
|
def y(self): |
|
return self._y |
|
|
|
def set_initial_value(self, y, t=0.0): |
|
"""Set initial conditions y(t) = y.""" |
|
if isscalar(y): |
|
y = [y] |
|
n_prev = len(self._y) |
|
if not n_prev: |
|
self.set_integrator('') |
|
self._y = asarray(y, self._integrator.scalar) |
|
self.t = t |
|
self._integrator.reset(len(self._y), self.jac is not None) |
|
return self |
|
|
|
def set_integrator(self, name, **integrator_params): |
|
""" |
|
Set integrator by name. |
|
|
|
Parameters |
|
---------- |
|
name : str |
|
Name of the integrator. |
|
**integrator_params |
|
Additional parameters for the integrator. |
|
""" |
|
integrator = find_integrator(name) |
|
if integrator is None: |
|
|
|
|
|
message = f'No integrator name match with {name!r} or is not available.' |
|
warnings.warn(message, stacklevel=2) |
|
else: |
|
self._integrator = integrator(**integrator_params) |
|
if not len(self._y): |
|
self.t = 0.0 |
|
self._y = array([0.0], self._integrator.scalar) |
|
self._integrator.reset(len(self._y), self.jac is not None) |
|
return self |
|
|
|
def integrate(self, t, step=False, relax=False): |
|
"""Find y=y(t), set y as an initial condition, and return y. |
|
|
|
Parameters |
|
---------- |
|
t : float |
|
The endpoint of the integration step. |
|
step : bool |
|
If True, and if the integrator supports the step method, |
|
then perform a single integration step and return. |
|
This parameter is provided in order to expose internals of |
|
the implementation, and should not be changed from its default |
|
value in most cases. |
|
relax : bool |
|
If True and if the integrator supports the run_relax method, |
|
then integrate until t_1 >= t and return. ``relax`` is not |
|
referenced if ``step=True``. |
|
This parameter is provided in order to expose internals of |
|
the implementation, and should not be changed from its default |
|
value in most cases. |
|
|
|
Returns |
|
------- |
|
y : float |
|
The integrated value at t |
|
""" |
|
if step and self._integrator.supports_step: |
|
mth = self._integrator.step |
|
elif relax and self._integrator.supports_run_relax: |
|
mth = self._integrator.run_relax |
|
else: |
|
mth = self._integrator.run |
|
|
|
try: |
|
self._y, self.t = mth(self.f, self.jac or (lambda: None), |
|
self._y, self.t, t, |
|
self.f_params, self.jac_params) |
|
except SystemError as e: |
|
|
|
raise ValueError( |
|
'Function to integrate must not return a tuple.' |
|
) from e |
|
|
|
return self._y |
|
|
|
def successful(self): |
|
"""Check if integration was successful.""" |
|
try: |
|
self._integrator |
|
except AttributeError: |
|
self.set_integrator('') |
|
return self._integrator.success == 1 |
|
|
|
def get_return_code(self): |
|
"""Extracts the return code for the integration to enable better control |
|
if the integration fails. |
|
|
|
In general, a return code > 0 implies success, while a return code < 0 |
|
implies failure. |
|
|
|
Notes |
|
----- |
|
This section describes possible return codes and their meaning, for available |
|
integrators that can be selected by `set_integrator` method. |
|
|
|
"vode" |
|
|
|
=========== ======= |
|
Return Code Message |
|
=========== ======= |
|
2 Integration successful. |
|
-1 Excess work done on this call. (Perhaps wrong MF.) |
|
-2 Excess accuracy requested. (Tolerances too small.) |
|
-3 Illegal input detected. (See printed message.) |
|
-4 Repeated error test failures. (Check all input.) |
|
-5 Repeated convergence failures. (Perhaps bad Jacobian |
|
supplied or wrong choice of MF or tolerances.) |
|
-6 Error weight became zero during problem. (Solution |
|
component i vanished, and ATOL or ATOL(i) = 0.) |
|
=========== ======= |
|
|
|
"zvode" |
|
|
|
=========== ======= |
|
Return Code Message |
|
=========== ======= |
|
2 Integration successful. |
|
-1 Excess work done on this call. (Perhaps wrong MF.) |
|
-2 Excess accuracy requested. (Tolerances too small.) |
|
-3 Illegal input detected. (See printed message.) |
|
-4 Repeated error test failures. (Check all input.) |
|
-5 Repeated convergence failures. (Perhaps bad Jacobian |
|
supplied or wrong choice of MF or tolerances.) |
|
-6 Error weight became zero during problem. (Solution |
|
component i vanished, and ATOL or ATOL(i) = 0.) |
|
=========== ======= |
|
|
|
"dopri5" |
|
|
|
=========== ======= |
|
Return Code Message |
|
=========== ======= |
|
1 Integration successful. |
|
2 Integration successful (interrupted by solout). |
|
-1 Input is not consistent. |
|
-2 Larger nsteps is needed. |
|
-3 Step size becomes too small. |
|
-4 Problem is probably stiff (interrupted). |
|
=========== ======= |
|
|
|
"dop853" |
|
|
|
=========== ======= |
|
Return Code Message |
|
=========== ======= |
|
1 Integration successful. |
|
2 Integration successful (interrupted by solout). |
|
-1 Input is not consistent. |
|
-2 Larger nsteps is needed. |
|
-3 Step size becomes too small. |
|
-4 Problem is probably stiff (interrupted). |
|
=========== ======= |
|
|
|
"lsoda" |
|
|
|
=========== ======= |
|
Return Code Message |
|
=========== ======= |
|
2 Integration successful. |
|
-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). |
|
=========== ======= |
|
""" |
|
try: |
|
self._integrator |
|
except AttributeError: |
|
self.set_integrator('') |
|
return self._integrator.istate |
|
|
|
def set_f_params(self, *args): |
|
"""Set extra parameters for user-supplied function f.""" |
|
self.f_params = args |
|
return self |
|
|
|
def set_jac_params(self, *args): |
|
"""Set extra parameters for user-supplied function jac.""" |
|
self.jac_params = args |
|
return self |
|
|
|
def set_solout(self, solout): |
|
""" |
|
Set callable to be called at every successful integration step. |
|
|
|
Parameters |
|
---------- |
|
solout : callable |
|
``solout(t, y)`` is called at each internal integrator step, |
|
t is a scalar providing the current independent position |
|
y is the current solution ``y.shape == (n,)`` |
|
solout should return -1 to stop integration |
|
otherwise it should return None or 0 |
|
|
|
""" |
|
if self._integrator.supports_solout: |
|
self._integrator.set_solout(solout) |
|
if self._y is not None: |
|
self._integrator.reset(len(self._y), self.jac is not None) |
|
else: |
|
raise ValueError("selected integrator does not support solout," |
|
" choose another one") |
|
|
|
|
|
def _transform_banded_jac(bjac): |
|
""" |
|
Convert a real matrix of the form (for example) |
|
|
|
[0 0 A B] [0 0 0 B] |
|
[0 0 C D] [0 0 A D] |
|
[E F G H] to [0 F C H] |
|
[I J K L] [E J G L] |
|
[I 0 K 0] |
|
|
|
That is, every other column is shifted up one. |
|
""" |
|
|
|
newjac = zeros((bjac.shape[0] + 1, bjac.shape[1])) |
|
newjac[1:, ::2] = bjac[:, ::2] |
|
newjac[:-1, 1::2] = bjac[:, 1::2] |
|
return newjac |
|
|
|
|
|
class complex_ode(ode): |
|
""" |
|
A wrapper of ode for complex systems. |
|
|
|
This functions similarly as `ode`, but re-maps a complex-valued |
|
equation system to a real-valued one before using the integrators. |
|
|
|
Parameters |
|
---------- |
|
f : callable ``f(t, y, *f_args)`` |
|
Rhs of the equation. t is a scalar, ``y.shape == (n,)``. |
|
``f_args`` is set by calling ``set_f_params(*args)``. |
|
jac : callable ``jac(t, y, *jac_args)`` |
|
Jacobian of the rhs, ``jac[i,j] = d f[i] / d y[j]``. |
|
``jac_args`` is set by calling ``set_f_params(*args)``. |
|
|
|
Attributes |
|
---------- |
|
t : float |
|
Current time. |
|
y : ndarray |
|
Current variable values. |
|
|
|
Examples |
|
-------- |
|
For usage examples, see `ode`. |
|
|
|
""" |
|
|
|
def __init__(self, f, jac=None): |
|
self.cf = f |
|
self.cjac = jac |
|
if jac is None: |
|
ode.__init__(self, self._wrap, None) |
|
else: |
|
ode.__init__(self, self._wrap, self._wrap_jac) |
|
|
|
def _wrap(self, t, y, *f_args): |
|
f = self.cf(*((t, y[::2] + 1j * y[1::2]) + f_args)) |
|
|
|
|
|
self.tmp[::2] = real(f) |
|
self.tmp[1::2] = imag(f) |
|
return self.tmp |
|
|
|
def _wrap_jac(self, t, y, *jac_args): |
|
|
|
jac = self.cjac(*((t, y[::2] + 1j * y[1::2]) + jac_args)) |
|
|
|
|
|
|
|
|
|
|
|
jac_tmp = zeros((2 * jac.shape[0], 2 * jac.shape[1])) |
|
jac_tmp[1::2, 1::2] = jac_tmp[::2, ::2] = real(jac) |
|
jac_tmp[1::2, ::2] = imag(jac) |
|
jac_tmp[::2, 1::2] = -jac_tmp[1::2, ::2] |
|
|
|
ml = getattr(self._integrator, 'ml', None) |
|
mu = getattr(self._integrator, 'mu', None) |
|
if ml is not None or mu is not None: |
|
|
|
|
|
|
|
jac_tmp = _transform_banded_jac(jac_tmp) |
|
|
|
return jac_tmp |
|
|
|
@property |
|
def y(self): |
|
return self._y[::2] + 1j * self._y[1::2] |
|
|
|
def set_integrator(self, name, **integrator_params): |
|
""" |
|
Set integrator by name. |
|
|
|
Parameters |
|
---------- |
|
name : str |
|
Name of the integrator |
|
**integrator_params |
|
Additional parameters for the integrator. |
|
""" |
|
if name == 'zvode': |
|
raise ValueError("zvode must be used with ode, not complex_ode") |
|
|
|
lband = integrator_params.get('lband') |
|
uband = integrator_params.get('uband') |
|
if lband is not None or uband is not None: |
|
|
|
|
|
|
|
|
|
integrator_params['lband'] = 2 * (lband or 0) + 1 |
|
integrator_params['uband'] = 2 * (uband or 0) + 1 |
|
|
|
return ode.set_integrator(self, name, **integrator_params) |
|
|
|
def set_initial_value(self, y, t=0.0): |
|
"""Set initial conditions y(t) = y.""" |
|
y = asarray(y) |
|
self.tmp = zeros(y.size * 2, 'float') |
|
self.tmp[::2] = real(y) |
|
self.tmp[1::2] = imag(y) |
|
return ode.set_initial_value(self, self.tmp, t) |
|
|
|
def integrate(self, t, step=False, relax=False): |
|
"""Find y=y(t), set y as an initial condition, and return y. |
|
|
|
Parameters |
|
---------- |
|
t : float |
|
The endpoint of the integration step. |
|
step : bool |
|
If True, and if the integrator supports the step method, |
|
then perform a single integration step and return. |
|
This parameter is provided in order to expose internals of |
|
the implementation, and should not be changed from its default |
|
value in most cases. |
|
relax : bool |
|
If True and if the integrator supports the run_relax method, |
|
then integrate until t_1 >= t and return. ``relax`` is not |
|
referenced if ``step=True``. |
|
This parameter is provided in order to expose internals of |
|
the implementation, and should not be changed from its default |
|
value in most cases. |
|
|
|
Returns |
|
------- |
|
y : float |
|
The integrated value at t |
|
""" |
|
y = ode.integrate(self, t, step, relax) |
|
return y[::2] + 1j * y[1::2] |
|
|
|
def set_solout(self, solout): |
|
""" |
|
Set callable to be called at every successful integration step. |
|
|
|
Parameters |
|
---------- |
|
solout : callable |
|
``solout(t, y)`` is called at each internal integrator step, |
|
t is a scalar providing the current independent position |
|
y is the current solution ``y.shape == (n,)`` |
|
solout should return -1 to stop integration |
|
otherwise it should return None or 0 |
|
|
|
""" |
|
if self._integrator.supports_solout: |
|
self._integrator.set_solout(solout, complex=True) |
|
else: |
|
raise TypeError("selected integrator does not support solouta, " |
|
"choose another one") |
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_integrator(name): |
|
for cl in IntegratorBase.integrator_classes: |
|
if re.match(name, cl.__name__, re.I): |
|
return cl |
|
return None |
|
|
|
|
|
class IntegratorConcurrencyError(RuntimeError): |
|
""" |
|
Failure due to concurrent usage of an integrator that can be used |
|
only for a single problem at a time. |
|
|
|
""" |
|
|
|
def __init__(self, name): |
|
msg = (f"Integrator `{name}` can be used to solve only a single problem " |
|
"at a time. If you want to integrate multiple problems, " |
|
"consider using a different integrator (see `ode.set_integrator`)") |
|
RuntimeError.__init__(self, msg) |
|
|
|
|
|
class IntegratorBase: |
|
runner = None |
|
success = None |
|
istate = None |
|
supports_run_relax = None |
|
supports_step = None |
|
supports_solout = False |
|
integrator_classes = [] |
|
scalar = float |
|
|
|
def acquire_new_handle(self): |
|
|
|
|
|
|
|
self.__class__.active_global_handle += 1 |
|
self.handle = self.__class__.active_global_handle |
|
|
|
def check_handle(self): |
|
if self.handle is not self.__class__.active_global_handle: |
|
raise IntegratorConcurrencyError(self.__class__.__name__) |
|
|
|
def reset(self, n, has_jac): |
|
"""Prepare integrator for call: allocate memory, set flags, etc. |
|
n - number of equations. |
|
has_jac - if user has supplied function for evaluating Jacobian. |
|
""" |
|
|
|
def run(self, f, jac, y0, t0, t1, f_params, jac_params): |
|
"""Integrate from t=t0 to t=t1 using y0 as an initial condition. |
|
Return 2-tuple (y1,t1) where y1 is the result and t=t1 |
|
defines the stoppage coordinate of the result. |
|
""" |
|
raise NotImplementedError('all integrators must define ' |
|
'run(f, jac, t0, t1, y0, f_params, jac_params)') |
|
|
|
def step(self, f, jac, y0, t0, t1, f_params, jac_params): |
|
"""Make one integration step and return (y1,t1).""" |
|
raise NotImplementedError(f'{self.__class__.__name__} ' |
|
'does not support step() method') |
|
|
|
def run_relax(self, f, jac, y0, t0, t1, f_params, jac_params): |
|
"""Integrate from t=t0 to t>=t1 and return (y1,t).""" |
|
raise NotImplementedError(f'{self.__class__.__name__} ' |
|
'does not support run_relax() method') |
|
|
|
|
|
|
|
|
|
def _vode_banded_jac_wrapper(jacfunc, ml, jac_params): |
|
""" |
|
Wrap a banded Jacobian function with a function that pads |
|
the Jacobian with `ml` rows of zeros. |
|
""" |
|
|
|
def jac_wrapper(t, y): |
|
jac = asarray(jacfunc(t, y, *jac_params)) |
|
padded_jac = vstack((jac, zeros((ml, jac.shape[1])))) |
|
return padded_jac |
|
|
|
return jac_wrapper |
|
|
|
|
|
class vode(IntegratorBase): |
|
runner = getattr(_vode, 'dvode', None) |
|
|
|
messages = {-1: 'Excess work done on this call. (Perhaps wrong MF.)', |
|
-2: 'Excess accuracy requested. (Tolerances too small.)', |
|
-3: 'Illegal input detected. (See printed message.)', |
|
-4: 'Repeated error test failures. (Check all input.)', |
|
-5: 'Repeated convergence failures. (Perhaps bad' |
|
' Jacobian supplied or wrong choice of MF or tolerances.)', |
|
-6: 'Error weight became zero during problem. (Solution' |
|
' component i vanished, and ATOL or ATOL(i) = 0.)' |
|
} |
|
supports_run_relax = 1 |
|
supports_step = 1 |
|
active_global_handle = 0 |
|
|
|
def __init__(self, |
|
method='adams', |
|
with_jacobian=False, |
|
rtol=1e-6, atol=1e-12, |
|
lband=None, uband=None, |
|
order=12, |
|
nsteps=500, |
|
max_step=0.0, |
|
min_step=0.0, |
|
first_step=0.0, |
|
): |
|
|
|
if re.match(method, r'adams', re.I): |
|
self.meth = 1 |
|
elif re.match(method, r'bdf', re.I): |
|
self.meth = 2 |
|
else: |
|
raise ValueError(f'Unknown integration method {method}') |
|
self.with_jacobian = with_jacobian |
|
self.rtol = rtol |
|
self.atol = atol |
|
self.mu = uband |
|
self.ml = lband |
|
|
|
self.order = order |
|
self.nsteps = nsteps |
|
self.max_step = max_step |
|
self.min_step = min_step |
|
self.first_step = first_step |
|
self.success = 1 |
|
|
|
self.initialized = False |
|
|
|
def _determine_mf_and_set_bands(self, has_jac): |
|
""" |
|
Determine the `MF` parameter (Method Flag) for the Fortran subroutine `dvode`. |
|
|
|
In the Fortran code, the legal values of `MF` are: |
|
10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, |
|
-11, -12, -14, -15, -21, -22, -24, -25 |
|
but this Python wrapper does not use negative values. |
|
|
|
Returns |
|
|
|
mf = 10*self.meth + miter |
|
|
|
self.meth is the linear multistep method: |
|
self.meth == 1: method="adams" |
|
self.meth == 2: method="bdf" |
|
|
|
miter is the correction iteration method: |
|
miter == 0: Functional iteration; no Jacobian involved. |
|
miter == 1: Chord iteration with user-supplied full Jacobian. |
|
miter == 2: Chord iteration with internally computed full Jacobian. |
|
miter == 3: Chord iteration with internally computed diagonal Jacobian. |
|
miter == 4: Chord iteration with user-supplied banded Jacobian. |
|
miter == 5: Chord iteration with internally computed banded Jacobian. |
|
|
|
Side effects: If either self.mu or self.ml is not None and the other is None, |
|
then the one that is None is set to 0. |
|
""" |
|
|
|
jac_is_banded = self.mu is not None or self.ml is not None |
|
if jac_is_banded: |
|
if self.mu is None: |
|
self.mu = 0 |
|
if self.ml is None: |
|
self.ml = 0 |
|
|
|
|
|
if has_jac: |
|
if jac_is_banded: |
|
miter = 4 |
|
else: |
|
miter = 1 |
|
else: |
|
if jac_is_banded: |
|
if self.ml == self.mu == 0: |
|
miter = 3 |
|
else: |
|
miter = 5 |
|
else: |
|
|
|
|
|
if self.with_jacobian: |
|
miter = 2 |
|
else: |
|
miter = 0 |
|
|
|
mf = 10 * self.meth + miter |
|
return mf |
|
|
|
def reset(self, n, has_jac): |
|
mf = self._determine_mf_and_set_bands(has_jac) |
|
|
|
if mf == 10: |
|
lrw = 20 + 16 * n |
|
elif mf in [11, 12]: |
|
lrw = 22 + 16 * n + 2 * n * n |
|
elif mf == 13: |
|
lrw = 22 + 17 * n |
|
elif mf in [14, 15]: |
|
lrw = 22 + 18 * n + (3 * self.ml + 2 * self.mu) * n |
|
elif mf == 20: |
|
lrw = 20 + 9 * n |
|
elif mf in [21, 22]: |
|
lrw = 22 + 9 * n + 2 * n * n |
|
elif mf == 23: |
|
lrw = 22 + 10 * n |
|
elif mf in [24, 25]: |
|
lrw = 22 + 11 * n + (3 * self.ml + 2 * self.mu) * n |
|
else: |
|
raise ValueError(f'Unexpected mf={mf}') |
|
|
|
if mf % 10 in [0, 3]: |
|
liw = 30 |
|
else: |
|
liw = 30 + n |
|
|
|
rwork = zeros((lrw,), float) |
|
rwork[4] = self.first_step |
|
rwork[5] = self.max_step |
|
rwork[6] = self.min_step |
|
self.rwork = rwork |
|
|
|
iwork = zeros((liw,), _vode_int_dtype) |
|
if self.ml is not None: |
|
iwork[0] = self.ml |
|
if self.mu is not None: |
|
iwork[1] = self.mu |
|
iwork[4] = self.order |
|
iwork[5] = self.nsteps |
|
iwork[6] = 2 |
|
self.iwork = iwork |
|
|
|
self.call_args = [self.rtol, self.atol, 1, 1, |
|
self.rwork, self.iwork, mf] |
|
self.success = 1 |
|
self.initialized = False |
|
|
|
def run(self, f, jac, y0, t0, t1, f_params, jac_params): |
|
if self.initialized: |
|
self.check_handle() |
|
else: |
|
self.initialized = True |
|
self.acquire_new_handle() |
|
|
|
if self.ml is not None and self.ml > 0: |
|
|
|
|
|
|
|
jac = _vode_banded_jac_wrapper(jac, self.ml, jac_params) |
|
|
|
args = ((f, jac, y0, t0, t1) + tuple(self.call_args) + |
|
(f_params, jac_params)) |
|
|
|
with VODE_LOCK: |
|
y1, t, istate = self.runner(*args) |
|
|
|
self.istate = istate |
|
if istate < 0: |
|
unexpected_istate_msg = f'Unexpected istate={istate:d}' |
|
warnings.warn(f'{self.__class__.__name__:s}: ' |
|
f'{self.messages.get(istate, unexpected_istate_msg):s}', |
|
stacklevel=2) |
|
self.success = 0 |
|
else: |
|
self.call_args[3] = 2 |
|
self.istate = 2 |
|
return y1, t |
|
|
|
def step(self, *args): |
|
itask = self.call_args[2] |
|
self.call_args[2] = 2 |
|
r = self.run(*args) |
|
self.call_args[2] = itask |
|
return r |
|
|
|
def run_relax(self, *args): |
|
itask = self.call_args[2] |
|
self.call_args[2] = 3 |
|
r = self.run(*args) |
|
self.call_args[2] = itask |
|
return r |
|
|
|
|
|
if vode.runner is not None: |
|
IntegratorBase.integrator_classes.append(vode) |
|
|
|
|
|
class zvode(vode): |
|
runner = getattr(_vode, 'zvode', None) |
|
|
|
supports_run_relax = 1 |
|
supports_step = 1 |
|
scalar = complex |
|
active_global_handle = 0 |
|
|
|
def reset(self, n, has_jac): |
|
mf = self._determine_mf_and_set_bands(has_jac) |
|
|
|
if mf in (10,): |
|
lzw = 15 * n |
|
elif mf in (11, 12): |
|
lzw = 15 * n + 2 * n ** 2 |
|
elif mf in (-11, -12): |
|
lzw = 15 * n + n ** 2 |
|
elif mf in (13,): |
|
lzw = 16 * n |
|
elif mf in (14, 15): |
|
lzw = 17 * n + (3 * self.ml + 2 * self.mu) * n |
|
elif mf in (-14, -15): |
|
lzw = 16 * n + (2 * self.ml + self.mu) * n |
|
elif mf in (20,): |
|
lzw = 8 * n |
|
elif mf in (21, 22): |
|
lzw = 8 * n + 2 * n ** 2 |
|
elif mf in (-21, -22): |
|
lzw = 8 * n + n ** 2 |
|
elif mf in (23,): |
|
lzw = 9 * n |
|
elif mf in (24, 25): |
|
lzw = 10 * n + (3 * self.ml + 2 * self.mu) * n |
|
elif mf in (-24, -25): |
|
lzw = 9 * n + (2 * self.ml + self.mu) * n |
|
|
|
lrw = 20 + n |
|
|
|
if mf % 10 in (0, 3): |
|
liw = 30 |
|
else: |
|
liw = 30 + n |
|
|
|
zwork = zeros((lzw,), complex) |
|
self.zwork = zwork |
|
|
|
rwork = zeros((lrw,), float) |
|
rwork[4] = self.first_step |
|
rwork[5] = self.max_step |
|
rwork[6] = self.min_step |
|
self.rwork = rwork |
|
|
|
iwork = zeros((liw,), _vode_int_dtype) |
|
if self.ml is not None: |
|
iwork[0] = self.ml |
|
if self.mu is not None: |
|
iwork[1] = self.mu |
|
iwork[4] = self.order |
|
iwork[5] = self.nsteps |
|
iwork[6] = 2 |
|
self.iwork = iwork |
|
|
|
self.call_args = [self.rtol, self.atol, 1, 1, |
|
self.zwork, self.rwork, self.iwork, mf] |
|
self.success = 1 |
|
self.initialized = False |
|
|
|
|
|
if zvode.runner is not None: |
|
IntegratorBase.integrator_classes.append(zvode) |
|
|
|
|
|
class dopri5(IntegratorBase): |
|
runner = getattr(_dop, 'dopri5', None) |
|
name = 'dopri5' |
|
supports_solout = True |
|
|
|
messages = {1: 'computation successful', |
|
2: 'computation successful (interrupted by solout)', |
|
-1: 'input is not consistent', |
|
-2: 'larger nsteps is needed', |
|
-3: 'step size becomes too small', |
|
-4: 'problem is probably stiff (interrupted)', |
|
} |
|
|
|
def __init__(self, |
|
rtol=1e-6, atol=1e-12, |
|
nsteps=500, |
|
max_step=0.0, |
|
first_step=0.0, |
|
safety=0.9, |
|
ifactor=10.0, |
|
dfactor=0.2, |
|
beta=0.0, |
|
method=None, |
|
verbosity=-1, |
|
): |
|
self.rtol = rtol |
|
self.atol = atol |
|
self.nsteps = nsteps |
|
self.max_step = max_step |
|
self.first_step = first_step |
|
self.safety = safety |
|
self.ifactor = ifactor |
|
self.dfactor = dfactor |
|
self.beta = beta |
|
self.verbosity = verbosity |
|
self.success = 1 |
|
self.set_solout(None) |
|
|
|
def set_solout(self, solout, complex=False): |
|
self.solout = solout |
|
self.solout_cmplx = complex |
|
if solout is None: |
|
self.iout = 0 |
|
else: |
|
self.iout = 1 |
|
|
|
def reset(self, n, has_jac): |
|
work = zeros((8 * n + 21,), float) |
|
work[1] = self.safety |
|
work[2] = self.dfactor |
|
work[3] = self.ifactor |
|
work[4] = self.beta |
|
work[5] = self.max_step |
|
work[6] = self.first_step |
|
self.work = work |
|
iwork = zeros((21,), _dop_int_dtype) |
|
iwork[0] = self.nsteps |
|
iwork[2] = self.verbosity |
|
self.iwork = iwork |
|
self.call_args = [self.rtol, self.atol, self._solout, |
|
self.iout, self.work, self.iwork] |
|
self.success = 1 |
|
|
|
def run(self, f, jac, y0, t0, t1, f_params, jac_params): |
|
x, y, iwork, istate = self.runner(*((f, t0, y0, t1) + |
|
tuple(self.call_args) + (f_params,))) |
|
self.istate = istate |
|
if istate < 0: |
|
unexpected_istate_msg = f'Unexpected istate={istate:d}' |
|
warnings.warn(f'{self.__class__.__name__:s}: ' |
|
f'{self.messages.get(istate, unexpected_istate_msg):s}', |
|
stacklevel=2) |
|
self.success = 0 |
|
return y, x |
|
|
|
def _solout(self, nr, xold, x, y, nd, icomp, con): |
|
if self.solout is not None: |
|
if self.solout_cmplx: |
|
y = y[::2] + 1j * y[1::2] |
|
return self.solout(x, y) |
|
else: |
|
return 1 |
|
|
|
|
|
if dopri5.runner is not None: |
|
IntegratorBase.integrator_classes.append(dopri5) |
|
|
|
|
|
class dop853(dopri5): |
|
runner = getattr(_dop, 'dop853', None) |
|
name = 'dop853' |
|
|
|
def __init__(self, |
|
rtol=1e-6, atol=1e-12, |
|
nsteps=500, |
|
max_step=0.0, |
|
first_step=0.0, |
|
safety=0.9, |
|
ifactor=6.0, |
|
dfactor=0.3, |
|
beta=0.0, |
|
method=None, |
|
verbosity=-1, |
|
): |
|
super().__init__(rtol, atol, nsteps, max_step, first_step, safety, |
|
ifactor, dfactor, beta, method, verbosity) |
|
|
|
def reset(self, n, has_jac): |
|
work = zeros((11 * n + 21,), float) |
|
work[1] = self.safety |
|
work[2] = self.dfactor |
|
work[3] = self.ifactor |
|
work[4] = self.beta |
|
work[5] = self.max_step |
|
work[6] = self.first_step |
|
self.work = work |
|
iwork = zeros((21,), _dop_int_dtype) |
|
iwork[0] = self.nsteps |
|
iwork[2] = self.verbosity |
|
self.iwork = iwork |
|
self.call_args = [self.rtol, self.atol, self._solout, |
|
self.iout, self.work, self.iwork] |
|
self.success = 1 |
|
|
|
|
|
if dop853.runner is not None: |
|
IntegratorBase.integrator_classes.append(dop853) |
|
|
|
|
|
class lsoda(IntegratorBase): |
|
runner = getattr(_lsoda, 'lsoda', None) |
|
active_global_handle = 0 |
|
|
|
messages = { |
|
2: "Integration successful.", |
|
-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)." |
|
} |
|
|
|
def __init__(self, |
|
with_jacobian=False, |
|
rtol=1e-6, atol=1e-12, |
|
lband=None, uband=None, |
|
nsteps=500, |
|
max_step=0.0, |
|
min_step=0.0, |
|
first_step=0.0, |
|
ixpr=0, |
|
max_hnil=0, |
|
max_order_ns=12, |
|
max_order_s=5, |
|
method=None |
|
): |
|
|
|
self.with_jacobian = with_jacobian |
|
self.rtol = rtol |
|
self.atol = atol |
|
self.mu = uband |
|
self.ml = lband |
|
|
|
self.max_order_ns = max_order_ns |
|
self.max_order_s = max_order_s |
|
self.nsteps = nsteps |
|
self.max_step = max_step |
|
self.min_step = min_step |
|
self.first_step = first_step |
|
self.ixpr = ixpr |
|
self.max_hnil = max_hnil |
|
self.success = 1 |
|
|
|
self.initialized = False |
|
|
|
def reset(self, n, has_jac): |
|
|
|
if has_jac: |
|
if self.mu is None and self.ml is None: |
|
jt = 1 |
|
else: |
|
if self.mu is None: |
|
self.mu = 0 |
|
if self.ml is None: |
|
self.ml = 0 |
|
jt = 4 |
|
else: |
|
if self.mu is None and self.ml is None: |
|
jt = 2 |
|
else: |
|
if self.mu is None: |
|
self.mu = 0 |
|
if self.ml is None: |
|
self.ml = 0 |
|
jt = 5 |
|
lrn = 20 + (self.max_order_ns + 4) * n |
|
if jt in [1, 2]: |
|
lrs = 22 + (self.max_order_s + 4) * n + n * n |
|
elif jt in [4, 5]: |
|
lrs = 22 + (self.max_order_s + 5 + 2 * self.ml + self.mu) * n |
|
else: |
|
raise ValueError(f'Unexpected jt={jt}') |
|
lrw = max(lrn, lrs) |
|
liw = 20 + n |
|
rwork = zeros((lrw,), float) |
|
rwork[4] = self.first_step |
|
rwork[5] = self.max_step |
|
rwork[6] = self.min_step |
|
self.rwork = rwork |
|
iwork = zeros((liw,), _lsoda_int_dtype) |
|
if self.ml is not None: |
|
iwork[0] = self.ml |
|
if self.mu is not None: |
|
iwork[1] = self.mu |
|
iwork[4] = self.ixpr |
|
iwork[5] = self.nsteps |
|
iwork[6] = self.max_hnil |
|
iwork[7] = self.max_order_ns |
|
iwork[8] = self.max_order_s |
|
self.iwork = iwork |
|
self.call_args = [self.rtol, self.atol, 1, 1, |
|
self.rwork, self.iwork, jt] |
|
self.success = 1 |
|
self.initialized = False |
|
|
|
def run(self, f, jac, y0, t0, t1, f_params, jac_params): |
|
if self.initialized: |
|
self.check_handle() |
|
else: |
|
self.initialized = True |
|
self.acquire_new_handle() |
|
args = [f, y0, t0, t1] + self.call_args[:-1] + \ |
|
[jac, self.call_args[-1], f_params, 0, jac_params] |
|
|
|
with LSODA_LOCK: |
|
y1, t, istate = self.runner(*args) |
|
|
|
self.istate = istate |
|
if istate < 0: |
|
unexpected_istate_msg = f'Unexpected istate={istate:d}' |
|
warnings.warn(f'{self.__class__.__name__:s}: ' |
|
f'{self.messages.get(istate, unexpected_istate_msg):s}', |
|
stacklevel=2) |
|
self.success = 0 |
|
else: |
|
self.call_args[3] = 2 |
|
self.istate = 2 |
|
return y1, t |
|
|
|
def step(self, *args): |
|
itask = self.call_args[2] |
|
self.call_args[2] = 2 |
|
r = self.run(*args) |
|
self.call_args[2] = itask |
|
return r |
|
|
|
def run_relax(self, *args): |
|
itask = self.call_args[2] |
|
self.call_args[2] = 3 |
|
r = self.run(*args) |
|
self.call_args[2] = itask |
|
return r |
|
|
|
|
|
if lsoda.runner: |
|
IntegratorBase.integrator_classes.append(lsoda) |
|
|