The problem
All ordinary differential equations are created equal:
\[ \frac{d \vec y}{dt} = \vec f(\vec y, t) \]
This is the same to saying that the only possible difference between ordinary differential equations is the function \(\vec f\). That is, all the properties of the dynamical system are coded in that function.
The vector notation allows for using the same formula for different number of dimensions, such as:
\[ \frac{dy}{dt} = f(y, t) \]
or:
\[ \begin{cases} \frac{du}{dt} = f(u, v, t) \\ \frac{dv}{dt} = g(u, v, t) \end{cases} \]
Higher order differential equations
Higher order differential equations can also be adapted to our formula by using the trick of introducing new variables. We can illustrate this with an example. Consider the equation below:
\[ \frac{d^2y}{dt^2} = f(\frac{dy}{dt}, y, t) \]
if we define:
\[ \omega \equiv \frac{dy}{dt} \]
we can rewrite our equation as a first-order equation:
\[ \frac{d \omega}{dt} = f(\omega, y, t) \]
Putting all together, we end up with a two-dimensional first order equation:
\[ \begin{cases} \frac{dy}{dt} = \omega \\ \frac{d\omega}{dt} = f(\omega, y, t) \end{cases} \]
Examples
Exponential decay
Formula
\[ f(y, t) = -r y \]
Implementation
«odes»=
import numpy as np
def exponential_decay(r = 1):
""" Implementation of an exponential decay """
def f(y, t):
return -r * y
return fHarmonic oscillator
Formula
\[ \begin{cases} \frac{d\theta}{dt} = \omega \\ \frac{d\omega}{dt} = -2 \zeta \omega_0 \omega - \omega_0^2 \theta \end{cases} \]
Implementation
«odes»+
def harmonic_oscillator(omega_0, zeta = 0):
""" Implementation of a harmonic oscillator with damping """
def f(y, t):
return np.r_[y[1],
-2 * zeta * omega_0 * y[1] - omega_0**2 * y[0]]
return fSolving a differential equation
The purpose of any method for solving an ordinary differential equation is to forecast the future. More technically, the purpose is to take an initial state \(\vec y_0, t_0\) and a future time \(t_1\), and return the state \(\vec y_1\) corresponding to the new time:
\[ \lbrace \vec y_0, t_0, t_1; \vec f \rbrace \longrightarrow \vec y_1 \]
Our approach consists on building an integrator method
that actually performs this operation. integrator has to be
a functional, in the sense that its input has to be the function \(f\):
\[ (\vec y_0, t_0, t_1) \xrightarrow {integrator(\vec f)} \vec y_1 \]
Analytical example: one-dimensional state-independent differential equation
A one-dimensional state-independent differential equation can be obtained by making:
\[ f(y, t) = F(t) \]
the differential equation can be rewritten as:
\[ dy = F(t) dt \]
and solved by direct integration:
\[ (y_0, t_0, t_1) \xrightarrow {integrator(F)} y_1 = y_0 + \int_{t_0}^{t_1} F(s) ds \]
Numerical example: Euler integration
Euler’s integration method is the most basic numerical method for approximating a future state \(y_1\). It takes advantage of the fact that a derivative can be approximated by a non-infinitesimal ratio of variation, i.e.:
\[ \frac{d\vec y}{dt} \approx \frac{\Delta \vec y}{\Delta t} \]
and thus:
\[ \Delta \vec y \approx \vec f(\vec y, t) \Delta t \]
We can thus write:
\[ (\vec y_0, t_0, t_1) \xrightarrow {integrator(\vec f)} \vec y_1 \approx \vec y_0 + (t_1 - t_0) \cdot \vec f(\vec y_0, t_0) \]
Implementation
«integrators»=
def forward_euler(f):
""" Forward Euler method
Parameters
----------
f : function
Function to solve.
"""
def updater(y_0, t_0, t_1):
""" Implementation of a single step in the forward Euler method. """
y_1 = y_0 + (t_1 - t_0) * f(y_0, t_0)
return y_1
return updater«integrators»+
def predcorr_euler(f):
""" Prediction-correction Euler method
Parameters
----------
f : function
Function to solve.
"""
def updater(y_0, t_0, t_1):
""" Implementation of a single step in the prediction-correction Euler method. """
y_a = forward_euler(f)(y_0, t_0, t_1)
y_1 = y_0 + (t_1 - t_0) / 2 * (f(y_0, t_0) + f(y_a, t_0))
return y_1
return updaterIterate
Numerical integrators are usually applied iteratively:
\[ \vec y_n = \vec y_{n-1} + integrator(\vec f)(\vec y_{n-1}, t_{n-1}, t_n) \]
The code below takes care of this in a practical way:
«iterators»=
def tabulate(y_0, t, integrator):
""" Iterates the integrator function over the time interval t
and returns the result as a list.
Parameters
----------
y_0 : array_like
Initial condition.
t : array_like
Time points.
integrator : function
Integrator function.
"""
y = [y_0]
for i in range(1, t.size):
y.append(integrator(y[i-1], t[i-1], t[i]))
return yNow we are ready to solve:
«src/funcode/funcode.py»=
from funcode.iterators import tabulate
def solve(f, y0, t, integrator):
""" Solve and ODE
Using the given initial condition, time points, and integrator.
Parameters
----------
f : function
Function to solve.
y0 : array_like
Initial condition.
t : array_like
Time points.
"""
return tabulate(y0, t, integrator(f))