| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409 |
- # Authors: Pearu Peterson, Pauli Virtanen, John Travers
- """
- 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.
- """
- # XXX: Integrators must have:
- # ===========================
- # cvode - C version of vode and vodpk with many improvements.
- # Get it from http://www.netlib.org/ode/cvode.tar.gz.
- # To wrap cvode to Python, one must write the extension module by
- # hand. Its interface is too much 'advanced C' that using f2py
- # would be too complicated (or impossible).
- #
- # How to define a new integrator:
- # ===============================
- #
- # class myodeint(IntegratorBase):
- #
- # runner = <odeint function> or None
- #
- # def __init__(self,...): # required
- # <initialize>
- #
- # def reset(self,n,has_jac): # optional
- # # n - the size of the problem (number of equations)
- # # has_jac - whether user has supplied its own routine for Jacobian
- # <allocate memory,initialize further>
- #
- # def run(self,f,jac,y0,t0,t1,f_params,jac_params): # required
- # # this method is called to integrate from t=t0 to t=t1
- # # with initial condition y0. f and jac are user-supplied functions
- # # that define the problem. f_params,jac_params are additional
- # # arguments
- # # to these functions.
- # <calculate y1>
- # if <calculation was unsuccessful>:
- # self.success = 0
- # return t1,y1
- #
- # # In addition, one can define step() and run_relax() methods (they
- # # take the same arguments as run()) if the integrator can support
- # # these features (see IntegratorBase doc strings).
- #
- # if myodeint.runner:
- # IntegratorBase.integrator_classes.append(myodeint)
- __all__ = ['ode', 'complex_ode']
- import re
- import types
- import warnings
- import numpy as np
- from numpy import asarray, array, zeros, isscalar, real, imag
- from . import _vode
- from . import _dop
- from ._odepack import lsoda as lsoda_step
- # ------------------------------------------------------------------------------
- # User interface
- # ------------------------------------------------------------------------------
- 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
- 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
- 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).
- This integrator uses the C translation of the original Fortran 77 ODEPACK
- library, which can be found at http://www.netlib.org/odepack
- 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: ernst.hairer@math.unige.ch, gerhard.wanner@math.unige.ch
- 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)
- """
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(types.GenericAlias)
- 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('') # find first available integrator
- # NOTE: The C code modifies y in place, hence the copy.
- self._y = asarray(y, self._integrator.scalar).copy()
- 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:
- # FIXME: this really should be raise an exception. Will that break
- # any code?
- 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:
- # f2py issue with tuple returns, see ticket 1187.
- 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.
- """
- # Shift every other column.
- 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
- self.tmp_derivative = None # Work array for derivatives in _wrap
- 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_derivative is a real-valued array containing the interleaved
- # real and imaginary parts of f (the derivative).
- # IMPORTANT: Must NOT use self.tmp here, as it may alias with y!
- self.tmp_derivative[::2] = real(f)
- self.tmp_derivative[1::2] = imag(f)
- return self.tmp_derivative
- def _wrap_jac(self, t, y, *jac_args):
- # jac is the complex Jacobian computed by the user-defined function.
- jac = self.cjac(*((t, y[::2] + 1j * y[1::2]) + jac_args))
- # jac_tmp is the real version of the complex Jacobian. Each complex
- # entry in jac, say 2+3j, becomes a 2x2 block of the form
- # [2 -3]
- # [3 2]
- 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:
- # Jacobian is banded. The user's Jacobian function has computed
- # the complex Jacobian in packed format. The corresponding
- # real-valued version has every other column shifted up.
- 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:
- # The Jacobian is banded. Override the user-supplied bandwidths
- # (which are for the complex Jacobian) with the bandwidths of
- # the corresponding real-valued Jacobian wrapper of the complex
- # Jacobian.
- 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)
- # Create separate work array for derivatives to avoid aliasing issues
- self.tmp_derivative = zeros(y.size * 2, 'float')
- 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")
- # ------------------------------------------------------------------------------
- # ODE integrators
- # ------------------------------------------------------------------------------
- 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 # runner is None => integrator is not available
- success = None # success==1 if integrator was called successfully
- istate = None # istate > 0 means success, istate < 0 means failure
- supports_run_relax = None
- supports_step = None
- supports_solout = False
- integrator_classes = []
- scalar = float
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(types.GenericAlias)
- def acquire_new_handle(self):
- # Some of the integrators have internal state (ancient
- # Fortran...), and so only one instance can use them at a time.
- # We keep track of this, and fail when concurrent usage is tried.
- 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')
- # XXX: __str__ method for getting visual state of the integrator
- 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
- 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, # corresponds to infinite
- min_step=0.0,
- first_step=0.0, # determined by solver
- ):
- 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
- # State persistence arrays for VODE internal state
- # These retain the solver state between calls
- self.state_doubles = zeros(51, dtype=np.float64) # VODE_STATE_DOUBLE_SIZE
- self.state_ints = zeros(41, dtype=np.int32) # VODE_STATE_INT_SIZE
- 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
- # has_jac is True if the user provided a Jacobian function.
- 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 # Chord iteration with internal diagonal Jacobian.
- else:
- miter = 5 # Chord iteration with internal banded Jacobian.
- else:
- # self.with_jacobian is set by the user in
- # the call to ode.set_integrator.
- if self.with_jacobian:
- miter = 2 # Chord iteration with internal full Jacobian.
- else:
- miter = 0 # Functional iteration; no Jacobian involved.
- 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,), dtype=np.int32)
- 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 # mxhnil
- self.iwork = iwork
- self.call_args = [self.rtol, self.atol, 1, 1,
- self.rwork, self.iwork, mf]
- self.success = 1
- self.initialized = False
- # Zero state arrays on reset to avoid contamination from previous problems.
- # State persistence works within a single integration (istate=2), but between
- # different problems (different n etc.), state needs to be cleared.
- self.state_doubles.fill(0.0)
- self.state_ints.fill(0)
- def run(self, f, jac, y0, t0, t1, f_params, jac_params):
- # Note: For banded Jacobians, the user provides the compressed format
- # (ml + mu + 1, n), and the C code handles padding to the expanded
- # format (ml + 2*mu + 1, n) internally. No Python wrapper needed.
- # VODE C wrapper signature:
- # dvode(f, jac, y0, t0, t1, rtol, atol, itask, istate, rwork, iwork, mf,
- # f_params, jac_params, state_doubles, state_ints)
- args = ((f, jac, y0, t0, t1) + tuple(self.call_args) +
- (f_params, jac_params, self.state_doubles, self.state_ints))
- 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 # upgrade istate from 1 to 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
- __class_getitem__ = None
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Override state array sizes for ZVODE (53 doubles vs 51 for VODE)
- self.state_doubles = zeros(53, dtype=np.float64) # ZVODE_STATE_DOUBLE_SIZE
- self.state_ints = zeros(41, dtype=np.int32) # ZVODE_STATE_INT_SIZE
- 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,), np.int32)
- 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 # mxhnil
- 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
- # Zero state arrays on reset to avoid contamination from previous problems.
- # State persistence works within a single integration (istate=2), but between
- # different problems (different n etc.), state needs to be cleared.
- self.state_doubles.fill(0.0)
- self.state_ints.fill(0)
- 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)',
- }
- __class_getitem__ = None
- def __init__(self,
- rtol=1e-6, atol=1e-12,
- nsteps=500,
- max_step=0.0,
- first_step=0.0, # determined by solver
- safety=0.9,
- ifactor=10.0,
- dfactor=0.2,
- beta=0.0,
- method=None,
- verbosity=-1, # no messages if negative
- ):
- 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
- self.iwork = zeros((21,), dtype=np.int32)
- self.call_args = [self.rtol, self.atol, self._solout,
- self.iout, self.work, self.iwork,
- self.nsteps, self.verbosity]
- self.success = 1
- def run(self, f, jac, y0, t0, t1, f_params, jac_params):
- x, y, 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, x, y):
- 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, 'dopri853', None)
- name = 'dop853'
- def __init__(self,
- rtol=1e-6, atol=1e-12,
- nsteps=500,
- max_step=0.0,
- first_step=0.0, # determined by solver
- safety=0.9,
- ifactor=6.0,
- dfactor=0.3,
- beta=0.0,
- method=None,
- verbosity=-1, # no messages if negative
- ):
- 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
- self.iwork = zeros((21,), dtype=np.int32)
- self.call_args = [self.rtol, self.atol, self._solout,
- self.iout, self.work, self.iwork,
- self.nsteps, self.verbosity]
- self.success = 1
- if dop853.runner is not None:
- IntegratorBase.integrator_classes.append(dop853)
- class lsoda(IntegratorBase):
- runner = lsoda_step # Use low-level lsoda wrapper
- 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)."
- }
- __class_getitem__ = None
- def __init__(self,
- with_jacobian=False,
- rtol=1e-6, atol=1e-12,
- lband=None, uband=None,
- nsteps=500,
- max_step=0.0, # corresponds to infinite
- min_step=0.0,
- first_step=0.0, # determined by solver
- 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
- # State persistence arrays for LSODA internal state
- # These retain the solver state between calls
- self.state_doubles = zeros(240, dtype=np.float64) # LSODA_STATE_DOUBLE_SIZE
- self.state_ints = zeros(48, dtype=np.int32) # LSODA_STATE_INT_SIZE
- def reset(self, n, has_jac):
- # Zero state arrays on reset to avoid contamination from previous steps.
- # State persistence works within a single integration (istate=2), but between
- # different problems (different n etc.), state needs to be cleared.
- self.state_doubles.fill(0.0)
- self.state_ints.fill(0)
- # Calculate parameters for lsoda subroutine.
- # jt values: 1=user full, 2=FD full, 4=user banded, 5=FD banded (3=invalid)
- if has_jac:
- if self.mu is None and self.ml is None:
- jt = 1 # User-supplied full Jacobian
- else:
- if self.mu is None:
- self.mu = 0
- if self.ml is None:
- self.ml = 0
- jt = 4 # User-supplied banded Jacobian
- else:
- if self.mu is None and self.ml is None:
- jt = 2 # Internally generated full Jacobian (finite differences)
- else:
- if self.mu is None:
- self.mu = 0
- if self.ml is None:
- self.ml = 0
- jt = 5 # Internally generated banded Jacobian (finite differences)
- # Calculate work array sizes
- 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
- # Create and initialize work arrays
- 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,), dtype=np.int32)
- 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
- def run(self, f, jac, y0, t0, t1, f_params, jac_params):
- # Prepare arguments for low-level lsoda wrapper
- rtol = self.call_args[0]
- atol = self.call_args[1]
- itask = self.call_args[2]
- istate = self.call_args[3]
- rwork = self.call_args[4]
- iwork = self.call_args[5]
- jt = self.call_args[6]
- # Note: For banded Jacobians, the user provides the compressed format
- # (ml + mu + 1, n), and the C code handles padding to the expanded
- # format (2*ml + mu + 1, n) internally. No Python wrapper needed.
- # Signature:
- #
- # lsoda(fun, y0, t, tout, rtol, atol, itask, istate, rwork, iwork,
- # jac, jt, f_params, tfirst, jac_params, state_doubles, state_ints)
- #
- # "state_doubles" and "state_ints" are arrays for passing internal
- # state between Python-C code.
- y1, t, istate = self.runner(
- f, y0, t0, t1, rtol, atol, itask, istate, rwork, iwork,
- jac, jt, f_params, 1, jac_params, # tfirst=1 for (t, y) signature
- self.state_doubles, self.state_ints
- )
- 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 # upgrade istate from 1 to 2
- self.istate = 2
- self.success = 1
- 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)
|