10.6: Integrating ODEs with Scipy
- Page ID
- 34862
\( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)
\( \newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\)
( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\)
\( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)
\( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\)
\( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)
\( \newcommand{\Span}{\mathrm{span}}\)
\( \newcommand{\id}{\mathrm{id}}\)
\( \newcommand{\Span}{\mathrm{span}}\)
\( \newcommand{\kernel}{\mathrm{null}\,}\)
\( \newcommand{\range}{\mathrm{range}\,}\)
\( \newcommand{\RealPart}{\mathrm{Re}}\)
\( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)
\( \newcommand{\Argument}{\mathrm{Arg}}\)
\( \newcommand{\norm}[1]{\| #1 \|}\)
\( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)
\( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\AA}{\unicode[.8,0]{x212B}}\)
\( \newcommand{\vectorA}[1]{\vec{#1}} % arrow\)
\( \newcommand{\vectorAt}[1]{\vec{\text{#1}}} % arrow\)
\( \newcommand{\vectorB}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vectorC}[1]{\textbf{#1}} \)
\( \newcommand{\vectorD}[1]{\overrightarrow{#1}} \)
\( \newcommand{\vectorDt}[1]{\overrightarrow{\text{#1}}} \)
\( \newcommand{\vectE}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash{\mathbf {#1}}}} \)
\( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)
\( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)
\(\newcommand{\avec}{\mathbf a}\) \(\newcommand{\bvec}{\mathbf b}\) \(\newcommand{\cvec}{\mathbf c}\) \(\newcommand{\dvec}{\mathbf d}\) \(\newcommand{\dtil}{\widetilde{\mathbf d}}\) \(\newcommand{\evec}{\mathbf e}\) \(\newcommand{\fvec}{\mathbf f}\) \(\newcommand{\nvec}{\mathbf n}\) \(\newcommand{\pvec}{\mathbf p}\) \(\newcommand{\qvec}{\mathbf q}\) \(\newcommand{\svec}{\mathbf s}\) \(\newcommand{\tvec}{\mathbf t}\) \(\newcommand{\uvec}{\mathbf u}\) \(\newcommand{\vvec}{\mathbf v}\) \(\newcommand{\wvec}{\mathbf w}\) \(\newcommand{\xvec}{\mathbf x}\) \(\newcommand{\yvec}{\mathbf y}\) \(\newcommand{\zvec}{\mathbf z}\) \(\newcommand{\rvec}{\mathbf r}\) \(\newcommand{\mvec}{\mathbf m}\) \(\newcommand{\zerovec}{\mathbf 0}\) \(\newcommand{\onevec}{\mathbf 1}\) \(\newcommand{\real}{\mathbb R}\) \(\newcommand{\twovec}[2]{\left[\begin{array}{r}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\ctwovec}[2]{\left[\begin{array}{c}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\threevec}[3]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\cthreevec}[3]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\fourvec}[4]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\cfourvec}[4]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\fivevec}[5]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\cfivevec}[5]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\mattwo}[4]{\left[\begin{array}{rr}#1 \amp #2 \\ #3 \amp #4 \\ \end{array}\right]}\) \(\newcommand{\laspan}[1]{\text{Span}\{#1\}}\) \(\newcommand{\bcal}{\cal B}\) \(\newcommand{\ccal}{\cal C}\) \(\newcommand{\scal}{\cal S}\) \(\newcommand{\wcal}{\cal W}\) \(\newcommand{\ecal}{\cal E}\) \(\newcommand{\coords}[2]{\left\{#1\right\}_{#2}}\) \(\newcommand{\gray}[1]{\color{gray}{#1}}\) \(\newcommand{\lgray}[1]{\color{lightgray}{#1}}\) \(\newcommand{\rank}{\operatorname{rank}}\) \(\newcommand{\row}{\text{Row}}\) \(\newcommand{\col}{\text{Col}}\) \(\renewcommand{\row}{\text{Row}}\) \(\newcommand{\nul}{\text{Nul}}\) \(\newcommand{\var}{\text{Var}}\) \(\newcommand{\corr}{\text{corr}}\) \(\newcommand{\len}[1]{\left|#1\right|}\) \(\newcommand{\bbar}{\overline{\bvec}}\) \(\newcommand{\bhat}{\widehat{\bvec}}\) \(\newcommand{\bperp}{\bvec^\perp}\) \(\newcommand{\xhat}{\widehat{\xvec}}\) \(\newcommand{\vhat}{\widehat{\vvec}}\) \(\newcommand{\uhat}{\widehat{\uvec}}\) \(\newcommand{\what}{\widehat{\wvec}}\) \(\newcommand{\Sighat}{\widehat{\Sigma}}\) \(\newcommand{\lt}{<}\) \(\newcommand{\gt}{>}\) \(\newcommand{\amp}{&}\) \(\definecolor{fillinmathshade}{gray}{0.9}\)Except for educational purposes, it is almost always a bad idea to implement your own ODE solver; instead, you should use a pre-written solver.
10.6.1 The scipy.integrate.odeint
Solver
In Scipy, the simplest ODE solver to use is the scipy.integrate.odeint
function, which is in the scipy.integrate
module. This is actually a wrapper around a low-level numerical library known as LSODE (the Livermore Solver for ODEs"), which is part of a widely-used ODE solver library known as ODEPACK. The most important feature of this solver is that it is "adaptive": it can automatically figure out (i) which integration scheme to use (choosing between either a high-order Adams-Moulton method, or another implicit method known as the Backward Differentiation Formula which we haven't described), and (ii) the size of the discrete time steps, based on the behavior of the solutions as they are being worked out. In other words, the user only needs to specify the derivative function, the initial state, and the desired output times, without having to worry about the internal details of the solution method.
The function takes several inputs, of which the most important ones are:
func
, a function corresponding to the derivative function \(\vec{F}(\vec{y}, t)\).y0
, either a number or 1D array, corresponding to the initial state \(\vec{y}(t_0)\).t
, an array of times at which to output the ODE solution. The first element corresponding to the initial time \(t_{0}\). Note that these are the "output" times only—they do not specify the actual time steps which the solver uses for finding the solutions; those are automatically determined by the solver.- (optional)
args
, a tuple of extra inputs to pass to the derivative functionfunc
. For example, ifargs=(2,3)
, thenfunc
should accept four inputs, and it will be passed 2 and 3 as the last two inputs.
The function then returns an array y
, where y[n]
contains the solution at time t[n]
. Note that y[0]
will be exactly the same as the input y0
, the initial state which you specified.
Here is an example of using odeint
to solve the damped harmonic oscillator problem \(m \ddot{x} = - \lambda \dot{x} - k x(t)\), using the previously-mentioned vectorization trick to cast it into a first-order ODE:
from scipy import * import matplotlib.pyplot as plt from scipy.integrate import odeint def ydot(y, t, m, lambd, k): x, v = y[0], y[1] return array([v, -(lambd/m) * v - k * x / m]) m, lambd, k = 1.0, 0.1, 1.0 # Oscillator parameters y0 = array([1.0, 5.0]) # Initial conditions [x, v] t = linspace(0.0, 50.0, 100) # Output times y = odeint(ydot, y0, t, args=(m, lambd, k)) ## Plot x versus t plt.plot(t, y[:,0], 'b-') plt.xlabel('t') plt.ylabel('x') plt.show()
There is an important limitation of odeint
: it does not handle complex ODEs, and always assumes that \(\vec{y}\) and \(\vec{F}\) are real. However, this is not a problem in practice, because you can always convert a complex first-order ODE into a real one, by replacing the complex vectors \(\vec{y}\) and \(\vec{F}\) with double-length real vectors:
\[\vec{y}' \equiv \begin{bmatrix}\mathrm{Re}(\vec{y})\\ \mathrm{Im}(\vec{y})\end{bmatrix}, \;\; \vec{F}' \equiv \begin{bmatrix}\mathrm{Re}(\vec{F})\\ \mathrm{Im}(\vec{F})\end{bmatrix}.\]
10.6.2 The scipy.integrate.ode
Solvers
Apart from odeint
, Scipy provides a more general interface to a variety of ODE solvers, in the form of the scipy.integrate.ode
class. This is a much more low-level interface; instead of calling a single function, you have to create an ODE "object", then use the methods of this object to specify the type of ODE solver to use, the initial conditions, etc.; then you have to repeatedly call the ODE object's integrate
method, to integrate the solution up to each desired output time step.
The is an extremely aggravating inconsistency between the odeint
function and this ode
class: the expected order of inputs for the derivative functions are reversed! The odeint
function assumes the derivative function has the form F(y,t)
, but the ode
class assumes it has the form F(t,y)
. Watch out for this!
Here is an example of using ode
class with the damped harmonic oscillator problem \(m \ddot{x} = - \lambda \dot{x} - k x(t)\), using a Runge-Kutta solver:
from scipy import * import matplotlib.pyplot as plt from scipy.integrate import ode ## Note the order of inputs (different from odeint)! def ydot(t, y, m, lambd, k): x, v = y[0], y[1] return array([v, -(lambd/m) * v - k * x / m]) m, lambd, k = 1.0, 0.1, 1.0 # Oscillator parameters y0 = array([1.0, 5.0]) # Initial conditions [x, v] t = linspace(0.0, 50.0, 100) # Output times ## Set up the ODE object r = ode(ydot) r.set_integrator('dopri5') # A Runge-Kutta solver r.set_initial_value(y0) r.set_f_params(m, lambd, k) ## Perform the integration. Note that the "integrate" method only integrates ## up to one single final time point, rather than an array of times. x = zeros(len(t)) x[0] = y0[0] for n in range(1,len(t)): r.integrate(t[n]) assert r.successful() x[n] = (r.y)[0] ## Plot x versus t plt.plot(t, x, 'b-') plt.xlabel('t') plt.ylabel('x') plt.show()
See the documentation for a more detailed list of options, including the list of ODE solvers that you can choose from.