Stability and Stiffness of ODEs#
Teng-Jui Lin
Content adapted from UW AMATH 301, Beginning Scientific Computing, in Spring 2020.
Stability
ODEs
ODE integration methods
Stiff ODEs
The code presented here is not for reference purpose but for plotting demonstration figures.
Stability of ODEs and integration methods#
Here we explore the stability of ODEs and integration methods. The stability of ODEs is dependent on its solution, whereas the stability of integration methods depend on the time step and the nature of the ODE being integrated. To get optimal numerical solution, the stability of ODE and the integration method should match.
Stability of ODEs#
An ODE is unstable when
two solutions obtained by two close initial conditions diverges (gets apart) over time
solution is unbounded, approaching
over time
An ODE is stable when
two solutions obtained by two close initial conditions converges (gets closer) over time
solution is bounded, decaying to a finite value over time
Problem Statement. Consider the linear first-order ODE
whose solution is
Determine the stability of the ODE for all
Solution. When
Stability of ODE integration methods#
A method is unstable when the error magnifies over time. A method is stable when the error decreases over time. The region of stability of method varies for different ODEs and time steps.
Problem Statement. Determine the region of stability of forward and backward Euler on the ODE
Solution. We limit our scope to real
at each step. The method is unstable over time (
Backward Euler method combined with the ODE gives
at each step. The method is unstable over time (
To summarize,
Stability |
Forward Euler |
Backward Euler |
---|---|---|
Stable for |
||
Unstable for |
The stability of the methods therefore depends both on the coefficient
Application of stability#
To get optimal numerical solution for an ODE, the stability of the method should match the stability of the ODE:
a stable method should be used for stable ODE
an unstable method should be used for unstable ODE.
Note that the stability of a method depends both on the coefficient
Problem Statement. Demonstrate (in)stability of forward and backward Euler methods for
where
import numpy as np
import matplotlib.pyplot as plt
import scipy
from scipy import integrate
def forward_euler(ode, initial_val, t):
t_len = len(t)
dt = np.diff(t[:2])
x_forward = np.zeros(t_len)
x_forward[0] = initial_val
for i in range(t_len - 1):
x_forward[i+1] = x_forward[i] + ode(t[i], x_forward[i])*dt
return x_forward
def backward_euler(ode, initial_val, t):
t_len = len(t)
dt = np.diff(t[:2])
x_backward = np.zeros(t_len)
x_backward[0] = initial_val
for i in range(t_len - 1):
g = lambda y : y - x_backward[i] - ode(t[i+1], y)*dt
x_backward[i+1] = scipy.optimize.root(g, x_backward[i]).x
return x_backward
# 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
})
def stability_demo(a, initial_val):
'''
Plot forward Euler, backward Euler, and RK4 ("exact") solution for given time steps,
with user input coefficient of the ODE and initial condition.
The plot demonstrates the stability of ODE and integration method
:param a: stiffness parameter of ODE
:param initial_val: initial condition of ODE
'''
# initial condition
initial_val = np.array([initial_val])
# time array
exact_t = np.linspace(0, 10, 1000)
t_initial = 0
t_final = 10
dt = np.linspace(0.1, 1.1, 6)
# linear ode with coefficient a
dxdt = lambda t, x : a*x
# slope field
tvec = np.linspace(0, 10, 30)
xvec = np.linspace(-2, 10, 15)
T, X = np.meshgrid(tvec, xvec)
# initialize figure
row = 3
col = 2
fig, axs = plt.subplots(row, col, figsize=(col*5, row*3), sharex=True, sharey=True)
# shared x and y label
fig.add_subplot(111, frameon=False)
plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False) # hide tick and tick label of the big axis
plt.xlabel('$t$')
plt.ylabel('$x$')
# plot soln curve
for row_i in range(row):
for col_i in range(col):
i = row_i*col + col_i
# time array
t = np.arange(t_initial, t_final+dt[i]/2, dt[i])
# solve ode
forward_soln = forward_euler(dxdt, initial_val, t)
backward_soln = backward_euler(dxdt, initial_val, t)
exact_soln = scipy.integrate.solve_ivp(dxdt, [t_initial, t_final], initial_val, t_eval=exact_t).y[0]
# plot field and soln
scale = np.sqrt(1**2 + dxdt(T, X)**2)
axs[row_i, col_i].quiver(T, X, np.ones_like(dxdt(T, X))/scale, dxdt(T, X)/scale, scale, cmap='winter_r', scale=25, width=0.004)
axs[row_i, col_i].plot(0, initial_val, 'o', color='black')
axs[row_i, col_i].plot(exact_t, exact_soln, '-', color='black', label='RK4')
axs[row_i, col_i].plot(t, forward_soln, '.-', color='red', label='Forward Euler')
axs[row_i, col_i].plot(t, backward_soln, '.-', color='blue', label='Backward Euler')
# plot setting
axs[row_i, col_i].set_xlim(0, 10)
axs[row_i, col_i].set_ylim(-2, 10)
axs[row_i, col_i].set_title('$\dot{x} = ' + f'{a}x$' + f' $(\Delta t = {dt[i] :.1f})$')
# compromised legend for common xylabels
if row_i == 0 and col_i == (col-1):
axs[row_i, col_i].legend(loc='upper right')
Stability of #
stability_demo(a=2, initial_val=0.5)

▲ The figure above shows the RK4, forward Euler, and backward Euler solutions to
Stability of #
stability_demo(a=-2, initial_val=9)

▲ The figure above shows the RK4, forward Euler, and backward Euler solutions to
Stiff ODE#
Stiff ODE is an ODE that is unstable for numerical methods unless the step size is taken to be extremely small. The stiffness is caused by rapid changing derivative that requires small step size to be captured by numerical methods. Here, we demonstrate the need of small step size for forward Euler and the stability of backward Euler.
Problem Statement. Consider the ODE
where
def stiff_demo(a):
'''
Plot forward Euler, backward Euler, and RK4 ("exact") solution for given time steps,
with user input stiffness of the ODE.
The plot demonstrates the need of small time step for stiff ODEs.
:param a: stiffness parameter of ODE
'''
# initial condition
initial_val = np.array([0])
# time array
exact_t = np.linspace(0, 10, 1000)
t_initial = 0
t_final = 10
dt = np.array([0.1, 0.3, 0.9, 1.1])
# slope field
tvec = np.linspace(0, 10, 30)
xvec = np.linspace(-3, 3, 10)
T, X = np.meshgrid(tvec, xvec)
# ode with stiffness a
dxdt = lambda t, x : a*(np.sin(t)-x)
# initialize figure
row = 2
col = 2
fig, axs = plt.subplots(row, col, figsize=(col*5, row*3), sharex=True, sharey=True)
# shared x and y label
fig.add_subplot(111, frameon=False)
plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False) # hide tick and tick label of the big axis
plt.xlabel('$t$')
plt.ylabel('$x$')
# plot soln curve
for row_i in range(row):
for col_i in range(col):
i = row_i*col + col_i
# time array
t = np.arange(t_initial, t_final+dt[i]/2, dt[i])
# solve ode
forward_soln = forward_euler(dxdt, initial_val, t)
backward_soln = backward_euler(dxdt, initial_val, t)
exact_soln = scipy.integrate.solve_ivp(dxdt, [t_initial, t_final], initial_val, t_eval=exact_t).y[0]
# plot field and soln
scale = np.sqrt(1**2 + dxdt(T, X)**2)
axs[row_i, col_i].quiver(T, X, np.ones_like(dxdt(T, X))/scale, dxdt(T, X)/scale, scale, cmap='winter_r', scale=25, width=0.004)
axs[row_i, col_i].plot(0, initial_val, 'o', color='black')
axs[row_i, col_i].plot(exact_t, exact_soln, '-', color='black', label='RK4')
axs[row_i, col_i].plot(t, forward_soln, '.-', color='red', label='Forward Euler')
axs[row_i, col_i].plot(t, backward_soln, '.-', color='blue', label='Backward Euler')
# # plot setting
axs[row_i, col_i].set_xlim(0, 10)
axs[row_i, col_i].set_ylim(-3, 3)
axs[row_i, col_i].set_title('$\dot{x} = a(\sin(t) - x)$' + f' $(\Delta t = {dt[i]})$')
# compromised legend for common xylabels
if row_i == 0 and col_i == (col-1):
axs[row_i, col_i].legend(loc='upper left', bbox_to_anchor=(1, 1))
Stiffness for #
stiff_demo(a=2)

▲ The figure above shows the numerical solutions of the ODE with
Stiffness for #
stiff_demo(a=10)

▲ The figure above shows the numerical solutions of the ODE with
Stiffness for #
stiff_demo(a=100)

▲ The figure above shows the numerical solutions of the ODE with
The situations for