Solving Higher-Order ODEs#

Teng-Jui Lin

Content adapted from UW AMATH 301, Beginning Scientific Computing, in Spring 2020.

  • Solving higher-order ODEs (systems of first-order ODEs)

    • Forward Euler

    • Backward Euler

  • scipy implementation

Solving higher-order ODEs (system of ODEs)#

A higher-order ODE can always be written as a system of first-order ODEs. We can then solve the first-order ODE system using forward Euler, backward Euler, or scipy.integrate.solve_ivp().

Forward Euler#

Forward Euler for linear system has a similar form of

xk+1=xk+ΔtAxk=(I+ΔtA)xk

The method is stable when all of the absolute value of eigenvalues of I+ΔtA are less than 1: |λ|<1.

Backward Euler#

Backward Euler for linear system has a similar form of

xk+1=xk+ΔtAxk+1

that can be implemented as

(IΔtA)xk+1=xk

which can be implemented with LU decomposition. The method is stable when all of the absolute value of eigenvalues of (IΔtA)1 are less than 1: |λ|<1.

Implementation#

Problem Statement. Linear pendulum.

The motion of a linear pendulum at small angles can be described by the linear second-order ODE

θ¨=gLθ

where θ is the angle of deflection of the pendulum from the vertical. Given physical parameters L=1 m and g=9.8 m/s2.

The second-order ODE can be converted to a system of two first-order ODEs

{θ˙=ωω˙=gLθ

which is equivalent to

x˙=Ax

where

x=[θω],A=[01gL0]

For the initial conditions θ(0)=0,θ˙(0)=ω(0)=0.5, we will solve the system of ODEs over t[0,10] s using numerical methods, then we compare the results with it analytical solution of

x(t)=[θ(t)ω(t)]=[0.5Lgsin(gLt)0.5cos(gLt)].

(a) Use forward Euler for linear system with Δt=0.005 s to solve the system. Determine the stability of the method and compare the error at final time with the analytical solution.

(b) Use backward Euler for linear system with Δt=0.005 s using LU decomposition. Determine the stability of the method and compare the error at final time with the analytical solution.

(c) Use scipy.integrate.solve_ivp() to solve the system. Compare the error at final time with the analytical solution.

Forward Euler#

import numpy as np
import matplotlib.pyplot as plt
import scipy
from scipy import integrate
# define physical constants
g = 9.8
l = 1
# define time array
t_initial = 0
t_final = 10
dt = 0.005
t = np.arange(t_initial, t_final+dt/2, dt)
t_len = len(t)
# define matrix and initial conditions
A = np.array([[0, 1], [-g/l, 0]])
x0 = np.array([0, 0.5])
x = np.zeros((2, t_len))
x[:, 0] = x0
# forward euler of linear system
for i in range(t_len - 1):
    x[:, i+1] = x[:, i] + dt*A@x[:, i]
# compare with analytical soln
x_exact = lambda t : np.array([0.5 * np.sqrt(l/g) *np.sin(t*np.sqrt(g/l)),
                               0.5 * np.cos(t*np.sqrt(g/l))])
x_error = np.linalg.norm(x[:, -1] - x_exact(t_final))
print(f'Error = {x_error :.2f}')
Error = 0.14
# assess stability
stability_matrix = np.eye(len(A)) + dt*A
abs_eig = abs(np.linalg.eig(stability_matrix)[0])
if max(abs_eig) > 1:
    print(f'Unstable with |lambda_max| = {max(abs_eig) :.4f}')
else:
    print(f'Stable with |lambda_max| = {max(abs_eig) :.4f}')
Unstable with |lambda_max| = 1.0001
# plot settings
%config InlineBackend.figure_format = 'retina'
%matplotlib inline

plt.rcParams.update({
    'font.family': 'Arial',  # Times New Roman, Calibri
    'font.weight': 'normal',
    'mathtext.fontset': 'cm',
    'font.size': 18,
    
    'lines.linewidth': 2,
    
    'axes.linewidth': 2,
    'axes.spines.top': False,
    'axes.spines.right': False,
    'axes.titleweight': 'bold',
    'axes.titlesize': 18,
    'axes.labelweight': 'bold',
    
    'xtick.major.size': 8,
    'xtick.major.width': 2,
    'ytick.major.size': 8,
    'ytick.major.width': 2,
    
    'figure.dpi': 80,
    
    'legend.framealpha': 1, 
    'legend.edgecolor': 'black',
    'legend.fancybox': False,
    'legend.fontsize': 14
})
fig, ax = plt.subplots(figsize=(4, 4))
ax.plot(t, x_exact(t)[0], '--', label='Analytic')
ax.plot(t, x[0], label='Forward Euler')
ax.set_xlabel('$t \ [\mathrm{s}]$')
ax.set_ylabel('$\\theta \ [\mathrm{rad}]$')
ax.set_title('$\\theta(t)$')
ax.set_xlim(t_initial, t_final)
ax.set_ylim(-0.3, 0.3)
ax.legend(loc='upper left', bbox_to_anchor=(1.05, 1.05))
<matplotlib.legend.Legend at 0x2a5d7de2c88>
../../_images/886f13652598b765d0727ce28c69574281efd5f80d10801a54703a92d996f24e.png

▲ Forward Euler has first order error and the amplitude grows over time compared to the analytic solution.

Backward Euler#

# define physical constants
g = 9.8
l = 1
# define time array
t_initial = 0
t_final = 10
dt = 0.005
t = np.arange(t_initial, t_final+dt/2, dt)
t_len = len(t)
# define matrix and initial conditions
A = np.array([[0, 1], [-g/l, 0]])
x0 = np.array([0, 0.5])
x = np.zeros((2, t_len))
x[:, 0] = x0
P, L, U = scipy.linalg.lu(np.eye(len(A)) - dt*A)
# backward euler of linear system
for i in range(t_len - 1):
    y = np.linalg.solve(L, P@x[:, i])
    x[:, i+1] = np.linalg.solve(U, y)
# compare with analytical soln
x_exact = lambda t : np.array([0.5 * np.sqrt(l/g) *np.sin(t*np.sqrt(g/l)),
                               0.5 * np.cos(t*np.sqrt(g/l))])
x_error = np.linalg.norm(x[:, -1] - x_exact(t_final))
print(f'Error = {x_error :.2f}')
Error = 0.11
# assess stability
stability_matrix = np.linalg.inv(np.eye(len(A)) - dt*A)
abs_eig = abs(np.linalg.eig(stability_matrix)[0])
if max(abs_eig) > 1:
    print(f'Unstable with |lambda_max| = {max(abs_eig) :.4f}')
else:
    print(f'Stable with |lambda_max| = {max(abs_eig) :.4f}')
Stable with |lambda_max| = 0.9999
fig, ax = plt.subplots(figsize=(4, 4))
ax.plot(t, x_exact(t)[0], '--', label='Analytic')
ax.plot(t, x[0], label='Backward Euler')
ax.set_xlabel('$t \ [\mathrm{s}]$')
ax.set_ylabel('$\\theta \ [\mathrm{rad}]$')
ax.set_title('$\\theta(t)$')
ax.set_xlim(t_initial, t_final)
ax.set_ylim(-0.3, 0.3)
ax.legend(loc='upper left', bbox_to_anchor=(1.05, 1.05))
<matplotlib.legend.Legend at 0x2a5d80d4108>
../../_images/b0c374b6c0fb558552fbd59bfb25baee973efa735bfdba6a6fa5c388c76c94af.png

▲ Backward Euler has first order error and the amplitude decays over time compared to the analytic solution.

scipy.integrate.solve_ivp()#

# define physical constants
g = 9.8
l = 1
# define time array
t_initial = 0
t_final = 10
dt = 0.005
t = np.arange(t_initial, t_final+dt/2, dt)
t_len = len(t)
# define initial conditions
x0 = np.array([0, 0.5])
# define ode system
dtheta_dt = lambda theta, omega : omega
domega_dt = lambda theta, omega: -g/l*theta
ode_syst = lambda t, z : np.array([dtheta_dt(*z), domega_dt(*z)])
# solve ode system
scipy_soln = scipy.integrate.solve_ivp(ode_syst, [t_initial, t_final], x0, t_eval=t).y
# compare with analytical soln
x_exact = lambda t : np.array([0.5 * np.sqrt(l/g) *np.sin(t*np.sqrt(g/l)),
                               0.5 * np.cos(t*np.sqrt(g/l))])
x_error = np.linalg.norm(scipy_soln[:, -1] - x_exact(t_final))
print(f'Error = {x_error :.4f}')
Error = 0.0015
fig, ax = plt.subplots(figsize=(4, 4))
ax.plot(t, x_exact(t)[0], '--', label='Analytic')
ax.plot(t, scipy_soln[0], label='scipy')
ax.set_xlabel('$t \ [\mathrm{s}]$')
ax.set_ylabel('$\\theta \ [\mathrm{rad}]$')
ax.set_title('$\\theta(t)$')
ax.set_xlim(t_initial, t_final)
ax.set_ylim(-0.3, 0.3)
ax.legend(loc='upper left', bbox_to_anchor=(1.05, 1.05))
<matplotlib.legend.Legend at 0x2a5b61a6ec8>
../../_images/cc6831f67b23ff6b5c47df1a01637cd66080dfb09894fefa9ed431509fba94d2.png

▲ The scipy implementation has fourth order error, agreeing with the analytic solution.