Runge-Kutta method
Contents
Runge-Kutta method#
Runge-Kutta (RK4) is most commonly used method for integrating Ordinary Differential Equations (ODEs). This method takes into account slope at the beginning, middle (twice) and the end of interval to integrate an ODE with a 4th order accuracy.
1st order ODE integration#
Consider a continuous function
Our aim is to find
know the value of
at some initial value ofstep forward from the initial point using finite steps of size
know how much
changes for each step in
In RK4 method, the independent variable is incremented in steps and the new value for the dependent variable is calculated at the end of each step according to -
where
In this notebook, we will learn how to integrate ODEs with this method.
At first, let’s create a RungeKutta
function that will calculate slopes and return
Click to show
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams.update({'font.size': 12})
def RungeKutta(x, y, dx, dydx):
# Calculate slopes
k1 = dx*dydx(x, y)
k2 = dx*dydx(x+dx/2., y+k1/2.)
k3 = dx*dydx(x+dx/2., y+k2/2.)
k4 = dx*dydx(x+dx, y+k3)
# Calculate new x and y
y = y + 1./6*(k1+2*k2+2*k3+k4)
x = x + dx
return x, y
Analytical solution to the ODE
with initial values
Let’s solve this equation numerically with RK4 method, with increment
At first we will define our function dxdy1
and analaytical solution as y1
:
def dydx1(x, y):
return x**2
def y1(x):
return x**3/3.+2/3.
Now we need to set up initial values in the problem - starting positions
x0 = 1.
y0 = 1.
dx = 0.1
x_end = 2.
To find a solution with RK4 method, we will need to loop over
x_rk = [x0]
y_rk = [y0]
y = y0
x = x0
while x <= x_end:
x, y = RungeKutta(x, y, dx, dydx1)
x_rk.append(x)
y_rk.append(y)
As mentioned before, RK4 method is 4th order accurate, while Euler method is 2nd order accurate. To compare the solutions, we will also define Euler
function and solve the ODE accordingly.
def Euler(x, y, dx, dydx):
return x+dx, y+dx*dydx(x, y)
x_eu = [x0]
y_eu = [y0]
y = y0
x = x0
while x <= x_end:
x, y = Euler(x, y, dx, dydx1)
x_eu.append(x)
y_eu.append(y)
We can plot results together on one graph:
Click to show
plt.figure(figsize=(7,5))
plt.plot(np.linspace(1,2,50), y1(np.linspace(1,2,50)),
label="Analytical solution",color="red", lw=2)
plt.plot(x_rk, y_rk, label="Numerical solution:\nRunge-Kutta", dashes=(3,2), color="blue",
lw=3)
plt.plot(x_eu, y_eu, label="Numerical solution:\nEuler", dashes=(3,2), color="green",
lw=3)
plt.legend(loc="best", fontsize=12)
plt.title(r"Solution to ODE: $\quad\frac{dy}{dx}=x^2$")
plt.xlabel("x", fontsize=12)
plt.ylabel("y", fontsize=12)
plt.show()

As we can see, Runge-Kutta is much more accurate than Euler method. That’s the reson why it is more often used when integrating ODEs. In this case Euler method underestimated the analytical solution.
Exercise#
Let’s solve
numerically with Runge-Kutta method. We will assume again that
Define functions
def dydx2(x, y):
return 1/(x*y)
def y2(x):
return np.sqrt(1+2*np.log(x))
Define necessary variables
x0 = 1.
y0 = 1.
dx = 0.1
xend = 2.
x_rk = [x0]
y_rk = [y0]
Solve the ODE with RK4 method
x = x0
y = y0
while x <= xend:
x, y = RungeKutta(x, y, dx, dydx2)
x_rk.append(x)
y_rk.append(y)
Solve the ODE with Euler method
x_eu = [x0]
y_eu = [y0]
x = x0
y = y0
while x <= xend:
x, y = Euler(x, y, dx, dydx2)
x_eu.append(x)
y_eu.append(y)
In this example, Euler method overestimated the solution, while RK4 method proves to be very accurate:

Coupled ODEs integration#
Often systems will have more than one dependent variable. In order to be solved, we will need the same number of differential equations as the number of independent variables.
Let’s assume a system with two coupled ODEs, with two dependent variables
Solving it with Runge-Kutta will be very similar to solving one equation. We will now have to define slopes for two functions.
Initial slopes -
Middle slopes -
Final slopes-
Then,
To solve the ODEs, we will need starting positions
We can define RungeKuttaCoupled
for coupled ODEs solution:
def RungeKuttaCoupled(x, y, z, dx, dydx, dzdx):
k1 = dx*dydx(x, y, z)
h1 = dx*dzdx(x, y, z)
k2 = dx*dydx(x+dx/2., y+k1/2., z+h1/2.)
h2 = dx*dzdx(x+dx/2., y+k1/2., z+h1/2.)
k3 = dx*dydx(x+dx/2., y+k2/2., z+h2/2.)
h3 = dx*dzdx(x+dx/2., y+k2/2., z+h2/2.)
k4 = dx*dydx(x+dx, y+k3, z+h3)
h4 = dx*dzdx(x+dx, y+k3, z+h3)
y = y + 1./6.*(k1+2*k2+2*k3+k4)
z = z + 1./6.*(h1+2*h2+2*h3+h4)
x = x + dx
return x, y, z
Mass hanging from a spring#
We can use RK4 for coupled ODEs to solve for position and velocity of a mass hanging from a spring as a function of time.
The spring obeys Hooke’s law:
where
where
where
This is our first ODE to be solved.
We can also note, that by definition
We will know solve for position and velocity of the mass as a function of time, assuming that the mass is initially at rest (
We need to think what are our variables in terms of RungeKuttaCoupled
funciton. We know that that
def dvdt(t, v, x):
return -1.*x-1.
def dxdt(t, v, x):
return v
We will again define initial values for our variables, increment and
t0 = 0
v0 = 0
x0 = 0
dt = 0.1
t_end = 30
We can now iterate over time increments to obtain position and velocity. Make sure that the order of derivatives matches the order of independent variables taken by RungeKuttaCoupled
:
t_list = [t0]
v_list = [v0]
x_list = [x0]
t = t0
v = v0
x = x0
while t <= t_end:
t, v, x = RungeKuttaCoupled(t, v, x, dt, dvdt, dxdt)
t_list.append(t)
v_list.append(v)
x_list.append(x)
The results show an oscillatory motion, with velocity being out of phase.
Click to show
plt.title("Spring movement without friction")
plt.plot(t_list, v_list, label="Velocity", color="red")
plt.plot(t_list, x_list, label="Position", color="blue")
plt.xlabel("Time")
plt.ylabel("Velocity/position")
plt.legend(loc="best")
plt.show()

The numerical solution looks consistent with behaviour in real world, however, in reality friction dampens the motion.
Therefore, we can solve a new set of ODEs that take into account friction that obeys equation:
Our new system of ODEs is then:
Assuming
Click to show
def dvdt(t, v, x):
return -1*x-1-0.2*v
def dxdt(t, v, x):
return v
t_list = [t0]
v_list = [v0]
x_list = [x0]
t = t0
v = v0
x = x0
while t <= t_end:
t, v, x = RungeKuttaCoupled(t, v, x, dt, dvdt, dxdt)
t_list.append(t)
v_list.append(v)
x_list.append(x)
plt.title("Spring movement with friction")
plt.plot(t_list, v_list, label="Velocity", color="red")
plt.plot(t_list, x_list, label="Position", color="blue")
plt.xlabel("Time")
plt.ylabel("Velocity/position")
plt.legend(loc="best")
plt.show()

Higher order ODEs#
Higher order ODEs are often encountered in nature. For example, diffusion and heat transfer are 2nd order ODEs. The order of an ODE indicates which derivatives it contains. Diffusion and heat transfer equations will therefire include second derivatives.
To solve a higher order ODE with Runge-Kutta method we must break it down into a set of 1st order ODEs. For example, if we have a second order ODE:
we can make the following substitution to solve the system as two coupled 1st order ODEs:
Temperature profile in a rod#
A long thin rod connects two bodies with different temperatures. At steady state, the temperature profile in a long thin rod is governed by the following equation:
where
The heat flux,
We can rewrite the system into two 1st order ODEs by using heat flux
At first we need to change the first equation to involve
Therefore,
Assuming
We can define functions for derivatives:
def dHdx(x, t, H):
# dH/dx = K*(t-t_ext)
# K = 1
# t_ext = 0
return t
def dtdx(x, t, H):
return -H
Define variables:
H0 = 10
t0 = 100
x0 = 0
x_end = 1
dx = 0.01
Solve for temperature and heat flux:
x_list = [x0]
t_list = [t0]
H_list = [H0]
x = x0
t = t0
H = H0
while x <= x_end:
x, t, H = RungeKuttaCoupled(x, t, H, dx, dtdx, dHdx)
x_list.append(x)
t_list.append(t)
H_list.append(H)
Click to show
fig, axes = plt.subplots(1,2,figsize=(7,3))
ax1 = axes[0]
ax2 = axes[1]
ax1.plot(x_list, t_list,
color="blue")
ax1.set_xlabel("Length")
ax1.set_ylabel("Temperature")
ax1.text(0,50,r"$\tau(1)=%.2f^\circ$C" % t_list[-1])
ax2.plot(x_list, H_list,
color="magenta")
ax2.set_xlabel("Length")
ax2.set_ylabel("Heat flux")
ax2.text(0,80,r"$H(1)=%.2f$ W/m$^2$" % H_list[-1])
plt.subplots_adjust(wspace=0.3)
plt.show()

What happens if we change the external temperature between 0 and 100
Click to show
fig, axes = plt.subplots(1,2,figsize=(7,3))
ax1 = axes[0]
ax2 = axes[1]
temp_list = []
flux_list = []
temps = np.linspace(0, 100, 11)
colors = plt.cm.seismic(np.linspace(0,1,len(temps)))
for i in range(len(temps)):
x_list = [x0]
t_list = [t0]
H_list = [H0]
x = x0
t = t0
H = H0
def dHdx(x, t, H):
return t-temps[i]
while x <= x_end:
x, t, H = RungeKuttaCoupled(x, t, H, dx, dtdx, dHdx)
x_list.append(x)
t_list.append(t)
H_list.append(H)
ax1.plot(x_list, t_list, color=colors[i])
ax2.plot(x_list, H_list, color=colors[i], label="%.2f$^\circ$C" % temps[i])
ax1.set_xlabel("Length")
ax1.set_ylabel("Temperature")
ax2.set_xlabel("Length")
ax2.set_ylabel("Heat flux")
ax2.legend(bbox_to_anchor=[1,1], ncol=2,
title="External temperature")
plt.subplots_adjust(wspace=0.3)
plt.suptitle("Effect of external temperature on temperature and heat flux in the rod")
plt.show()

As the external temperature decreases, the temperature profile in the rod decreases as well. Heat flux, however, increases at the other end of the rod as the gradient in temperature becomes sharper.