Initial Value Problems for Ordinary Differential Equations, Part 4: Systems of ODEs and Higher Order ODEs
Contents
32. Initial Value Problems for Ordinary Differential Equations, Part 4: Systems of ODEs and Higher Order ODEs#
The short version of this section is that the numerical methods and algorithms developed so for for the initial value problem
all also work for system of first order ODEs by simply letting \(u\) and \(f\) be vector-valued, and the Python code requires only one very small change.
Also, higher order ODE’s (and syrem of them) can be converted into system of first order ODEs.
The second point is seen in a typical first course in differential equations, so I will just illutrate the procedure for a single famous example.
import numpy as np
from matplotlib import pyplot as plt
# Shortcuts for some favorite commands:
from numpy import linspace
32.1. Motion of a Damped Mass-Spring System in One Dimension#
A simple mathematical model of a damped mass-spring system is
where \(k\) is the spring constant and \(D\) is the coefficient of friction, or drag.
To convert to a first order system, introduction the two functions
(Aside: this is one of many places where the Pythonic system of counting from \(0\) fits the mathematics better!)
Then with \(d^2 y/dt^2 = d u_1/dt\) the above equation becomes
Combined with the definition of \(u_1\) give the system
Next this can be put into vector form. Defining the vector-valued functions
and initial data vector
puts the ODE IVP into the form
The Euler’s method code from before:
def euler(f, a, b, u_0, n=100):
"""Use Euler's Method to solve du/dt = f(t, u) for t in [a, b], with initial value u(a) = u_0"""
h = (b-a)/n
t = np.linspace(a, b, n+1) # Note: "n" counts steps, so there are n+1 values for t.
U = np.empty_like(t)
U[0] = u_0
for i in range(n):
U[i+1] = U[i] + f(t[i], U[i])*h
return (t, U)
does not quite work, but only slgiht modificsit is needed:
def euler_system(f, a, b, u_0, n=100):
"""Use Euler's Method to solve du/dt = f(t, u) for t in [a, b], with initial value u(a) = u_0
Modified from function euler to handle systems."""
h = (b-a)/n
t = np.linspace(a, b, n+1) # Note: "n" counts steps, so there are n+1 values for t.
# Only the following three lines change for the systems version
n_unknowns = len(u_0)
U = np.zeros([n+1, n_unknowns])
U[0] = np.array(u_0)
for i in range(n):
U[i+1] = U[i] + f(t[i], U[i])*h
return (t, U)
def f(t, u):
return np.array([ u[1], -(k/M)*u[0] - (D/M)*u[1]])
M = 1.0
k = 1.0
D = 0.0
u_0 = [1.0, 0.0]
a = 0.0
b = 8 * np.pi # Four periods
n=400
(t, U) = euler_system(f, a, b, u_0, n)
y = U[:,0]
Dy = U[:,1]
plt.figure(figsize=[12,6])
plt.title(f"U_0, = y with {k=}, {D=} — by Euler with {n} steps")
plt.plot(t, y)
plt.xlabel('t')
plt.ylabel('y')
plt.grid(True)
plt.figure(figsize=[12,6])
plt.title(f"U_1, = dy/dt with {k=}, {D=} — by Euler with {n} steps")
plt.plot(t, Dy)
plt.xlabel('t')
plt.ylabel('dy/dt')
plt.grid(True)
plt.figure(figsize=[12,6])
plt.title(f"U_0=y and U_1=dy/dt with {k=}, {D=} — by Euler with {n} steps")
plt.plot(t, U)
plt.xlabel('t')
plt.ylabel('y and dy/dt')
plt.grid(True)
plt.figure(figsize=[8,8]) # Make axes equal length; orbits should be circular or "circular spirals"
if D == 0.:
plt.title(f"The orbits of the undamped mass-spring system, {k=} — by Euler with {n} steps")
else:
plt.title(f"The orbits of the damped mass-spring system, k={k}, D={D} — by Euler with {n} steps")
plt.plot(y, Dy)
plt.xlabel('y')
plt.ylabel('dy/dt')
plt.plot(y[0], Dy[0], "g*", label="start")
plt.plot(y[-1], Dy[-1], "r*", label="end")
plt.legend()
plt.grid(True)
32.2. The “Classical” Runge-Kutta Method, Extended to Systems of Equations#
As above, the previous function for this method needs just three lines of code modified.
Before:
def RungeKutta(f, a, b, u_0, n=100):
"""Use the (classical) Runge-Kutta Method
to solve du/dt = f(t, u) for t in [a, b], with initial value u(a) = u_0"""
h = (b-a)/n
t = np.linspace(a, b, n+1) # Note: "n" counts steps, so there are n+1 values for t.
U = np.empty_like(t)
U[0] = u_0
for i in range(n):
K_1 = f(t[i], U[i])*h
K_2 = f(t[i]+h/2, U[i]+K_1/2)*h
K_3 = f(t[i]+h/2, U[i]+K_2/2)*h
K_4 = f(t[i]+h, U[i]+K_3)*h
U[i+1] = U[i] + (K_1 + 2*K_2 + 2*K_3 + K_4)/6
return (t, U)
After:
def RungeKutta_system(f, a, b, u_0, n=100):
"""Use the (classical) Runge-Kutta Method
to solve du/dt = f(t, u) for t in [a, b], with initial value u(a) = u_0"""
h = (b-a)/n
t = np.linspace(a, b, n+1) # Note: "n" counts steps, so there are n+1 values for t.
# Only the following three lines change for the systems version — the same lines as for euler_system and so on.
n_unknowns = len(u_0)
U = np.zeros([n+1, n_unknowns])
U[0] = np.array(u_0)
for i in range(n):
K_1 = f(t[i], U[i])*h
K_2 = f(t[i]+h/2, U[i]+K_1/2)*h
K_3 = f(t[i]+h/2, U[i]+K_2/2)*h
K_4 = f(t[i]+h, U[i]+K_3)*h
U[i+1] = U[i] + (K_1 + 2*K_2 + 2*K_3 + K_4)/6
return (t, U)
M = 1.0
k = 1.0
D = 0.0
u_0 = [1.0, 0.0]
a = 0.0
b = 8 * np.pi # Four periods
n_RK = 100
n = n_RK
(t, U) = RungeKutta_system(f, a, b, u_0, n)
y = U[:,0]
Dy = U[:,1]
plt.figure(figsize=[12,6])
plt.title(f"U_0, = y with {k=}, {D=} — by Runge-Kutta with {n} steps")
plt.plot(t, y)
plt.xlabel('t')
plt.ylabel('y')
plt.grid(True)
plt.figure(figsize=[12,6])
plt.title(f"U_1, = dy/dt with {k=}, {D=} — by Runge-Kutta with {n} steps")
plt.plot(t, Dy)
plt.xlabel('t')
plt.ylabel('dy/dt')
plt.grid(True)
plt.figure(figsize=[8,8]) # Make axes equal length; orbits should be circular or "circular spirals"
if D == 0.:
plt.title(f"The orbits of the undamped mass-spring system, {k=} — by Runge-Kutta with {n} steps")
else:
plt.title(f"The orbits of the damped mass-spring system, k={k}, D={D} — by Runge-Kutta with {n} steps")
plt.plot(y, Dy)
plt.xlabel('y')
plt.ylabel('dy/dt')
plt.plot(y[0], Dy[0], "g*", label="start")
plt.plot(y[-1], Dy[-1], "r*", label="end")
plt.legend()
plt.grid(True)
32.3. Appendix: the Explicit Trapezoid and Midpoint Methods for systems#
Yet again, the previous functions for these methods need just three lines of code modified.
The demos are just for the non-dissipative case, where the solution is known to be \(y = \cos t\), \(dt/dt = -\sin t\).
For a fairer comparison of “accuracy vs computtional effort” to the Runge-Kutta method, twice as many time steps are used so that the same number of function evaluations are used for these three methods.
def explicitTrapezoid_system(f, a, b, u_0, n=100):
"""Use the Explict Trapezoid Method (a.k.a Improved Euler)
to solve du/dt = f(t, u) for t in [a, b], with initial value u(a) = u_0"""
h = (b-a)/n
t = np.linspace(a, b, n+1) # Note: "n" counts steps, so there are n+1 values for t.
# Only the following three lines change for the systems version — the same lines as for euler_system and so on.
n_unknowns = len(u_0)
U = np.zeros([n+1, n_unknowns])
U[0] = np.array(u_0)
for i in range(n):
K_1 = f(t[i], U[i])*h
K_2 = f(t[i]+h, U[i]+K_1)*h
U[i+1] = U[i] + (K_1 + K_2)/2.
return (t, U)
M = 1.0
k = 1.0
D = 0.0
u_0 = [1.0, 0.0]
a = 0.0
b = 8 * np.pi # Four periods
n = n_RK * 2
(t, U) = explicitTrapezoid_system(f, a, b, u_0, n)
y = U[:,0]
Dy = U[:,1]
plt.figure(figsize=[12,6])
plt.title(f"U_0, = y with {k=}, {D=} — by Explict Trapezoid with {n} steps")
plt.plot(t, y)
plt.xlabel('t')
plt.ylabel('y')
plt.grid(True)
plt.figure(figsize=[8,8]) # Make axes equal length; orbits should be circular or "circular spirals"
if D == 0.:
plt.title(f"The orbits of the undamped mass-spring system, {k=} — by Explict Trapezoid with {n} steps")
else:
plt.title(f"The orbits of the damped mass-spring system, k={k}, D={D} — by Explict Trapezoid with {n} steps")
plt.plot(y, Dy)
plt.xlabel('y')
plt.ylabel('dy/dt')
plt.plot(y[0], Dy[0], "g*", label="start")
plt.plot(y[-1], Dy[-1], "r*", label="end")
plt.legend()
plt.grid(True)
def explicitMidpoint_system(f, a, b, u_0, n=100):
"""Use the Explicit Midpoint Method (a.k.a Modified Euler)
to solve du/dt = f(t, u) for t in [a, b], with initial value u(a) = u_0"""
h = (b-a)/n
t = np.linspace(a, b, n+1) # Note: "n" counts steps, so there are n+1 values for t.
# Only the following three lines change for the systems version — the same lines as for euler_system and so on.
n_unknowns = len(u_0)
U = np.zeros([n+1, n_unknowns])
U[0] = np.array(u_0)
for i in range(n):
K_1 = f(t[i], U[i])*h
K_2 = f(t[i]+h/2, U[i]+K_1/2)*h
U[i+1] = U[i] + K_2
return (t, U)
M = 1.0
k = 1.0
D = 0.0
u_0 = [1.0, 0.0]
a = 0.0
b = 8 * np.pi # Four periods
n = n_RK * 2
(t, U) = explicitMidpoint_system(f, a, b, u_0, n)
y = U[:,0]
Dy = U[:,1]
plt.figure(figsize=[12,6])
plt.title(f"U_0, = y with {k=}, {D=} — by Explict Midpoint with {n} steps")
plt.plot(t, y)
plt.xlabel('t')
plt.ylabel('y')
plt.grid(True)
plt.figure(figsize=[8,8]) # Make axes equal length; orbits should be circular or "circular spirals"
if D == 0.:
plt.title(f"The orbits of the undamped mass-spring system, {k=} — by Explict Midpoint with {n} steps")
else:
plt.title(f"The orbits of the damped mass-spring system, k={k}, D={D} — by Explict Midpoint with {n} steps")
plt.plot(y, Dy)
plt.xlabel('y')
plt.ylabel('dy/dt')
plt.plot(y[0], Dy[0], "g*", label="start")
plt.plot(y[-1], Dy[-1], "r*", label="end")
plt.legend()
plt.grid(True)
This work is licensed under Creative Commons Attribution-ShareAlike 4.0 International