We are now in a position to generalize the Runge-Kutta (RK) methods. Our goal is to use intermediate evaluations of the slope function called stages to better approximate the true integral (via cancelation of higher order corrections in the Taylor series):
For the explicit family of RK methods, each stage depends only on previously computed stages:
RK methods are parameterized by:
nodes - the fraction of the current timestep
coupling coefficients that construct new stages
weights - the final combination for the full time step
These constant parameters are characteristic of the specific RK scheme. Note that RK1 is simply the Forward Euler method.
import numpy as np
import plotly.graph_objects as go
try:
import executable_engineering as exe
except ImportError:
%pip install -q executable_engineering
import executable_engineering as exeButcher Tableaux¶
Noting that , , and form two vectors of dimension and an matrix, RK schemes can be written compactly in a Butcher Tableau. For explicit methods, this takes the form:
Notice that the matrix is strictly lower triangular, allowing each stage to be evaluated sequentially. We will later introduce the implicit family of RK methods, where entries appear on and above the diagonal!
For a given order, the values of , , and are derived to match the Taylor series expansion.
Second-Order Runge-Kutta (RK2) Derivation¶
For RK2 methods:
Performing a bivariate Taylor expansion of around :
Substituting and into the update for :
Comparing this against the exact second-order Taylor expansion using the chain rule :
Matching terms yields three equations for four unknowns (1 degree of freedom):
RK2 Variants: Heun’s, Midpoint, and Ralston’s Methods¶
Any choice of satisfies the second-order conditions and integrates constant, linear, and quadratic trajectories exactly:
Heun’s Method (): Averages endpoint slopes and .
Midpoint Method (): Uses the midpoint slope .
Ralston’s Method (): Provides a minimum bound on the leading local truncation error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18def ralston_method(f, t0, y0, t_end, h): t_points = np.arange(t0, t_end + h / 2, h) y_points = np.zeros_like(t_points, dtype=float) y_points[0] = y0 for i in range(len(t_points) - 1): t_i, y_i = t_points[i], y_points[i] k1 = f(t_i, y_i) k2 = f(t_i + 0.75 * h, y_i + 0.75 * h * k1) y_points[i + 1] = y_points[i] + h * (k1 + 2.0 * k2) / 3.0 return { "t_points": t_points, "y_points": y_points, "h": h, "method": "Ralston's Method (RK2)", }
exe.display_source(exe.ralston_method)def f(t, y):
return 4.0 * np.exp(0.8 * t) - 0.5 * y
def y_true(t):
return (4.0 / 1.3) * (np.exp(0.8 * t) - np.exp(-0.5 * t)) + 2.0 * np.exp(-0.5 * t)
fig = exe.plot_ode_comparison(
(0, 4),
y_true,
{
"Heun's Method (a2 = 1/2)": exe.heun_method(f, 0.0, 2.0, 4.0, 1.0),
"Midpoint Method (a2 = 1)": exe.midpoint_ode(f, 0.0, 2.0, 4.0, 1.0),
"Ralston's Method (a2 = 2/3)": exe.ralston_method(f, 0.0, 2.0, 4.0, 1.0),
},
title="Comparison of RK2 Variants: Heun's, Midpoint, and Ralston's Methods",
)
fig.show()Third-Order Runge-Kutta (RK3) Methods¶
A similar derivation follows for RK3 with additional degrees of freedom. Kutta’s classical third-order choice is:
with
which reduces to Simpson’s Rule when is solely a function of , achieving local and global accuracy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19def rk3_method(f, t0, y0, t_end, h): t_points = np.arange(t0, t_end + h / 2, h) y_points = np.zeros_like(t_points, dtype=float) y_points[0] = y0 for i in range(len(t_points) - 1): t_i, y_i = t_points[i], y_points[i] k1 = f(t_i, y_i) k2 = f(t_i + 0.5 * h, y_i + 0.5 * h * k1) k3 = f(t_i + h, y_i - h * k1 + 2.0 * h * k2) y_points[i + 1] = y_points[i] + h * (k1 + 4.0 * k2 + k3) / 6.0 return { "t_points": t_points, "y_points": y_points, "h": h, "method": "Classical RK3", }
exe.display_source(exe.rk3_method)Fourth-Order Runge-Kutta (Classical RK4)¶
Classical RK4 is the workhorse of fixed-step explicit integration:
with
achieving local and global truncation error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20def rk4_method(f, t0, y0, t_end, h): t_points = np.arange(t0, t_end + h / 2, h) y_points = np.zeros_like(t_points, dtype=float) y_points[0] = y0 for i in range(len(t_points) - 1): t_i, y_i = t_points[i], y_points[i] k1 = f(t_i, y_i) k2 = f(t_i + 0.5 * h, y_i + 0.5 * h * k1) k3 = f(t_i + 0.5 * h, y_i + 0.5 * h * k2) k4 = f(t_i + h, y_i + h * k3) y_points[i + 1] = y_points[i] + h * (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0 return { "t_points": t_points, "y_points": y_points, "h": h, "method": "Classical RK4", }
exe.display_source(exe.rk4_method)
def f(t, y):
return 4.0 * np.exp(0.8 * t) - 0.5 * y
def y_true(t):
return (4.0 / 1.3) * (np.exp(0.8 * t) - np.exp(-0.5 * t)) + 2.0 * np.exp(-0.5 * t)
fig = exe.plot_ode_comparison(
(0, 4),
y_true,
{
"Forward Euler (RK1)": exe.forward_euler(f, 0.0, 2.0, 4.0, 1.0),
"Ralston's (RK2)": exe.ralston_method(f, 0.0, 2.0, 4.0, 1.0),
"Classical RK3": exe.rk3_method(f, 0.0, 2.0, 4.0, 1.0),
"Classical RK4": exe.rk4_method(f, 0.0, 2.0, 4.0, 1.0),
},
)
fig.show()Example: Geometric Anatomy of RK4 Stages¶
Notice how the RK4 algorithm walks its way forward across the interval: partial trial steps inform subsequent slope evaluations, culminating in a highly accurate weighted step.

Because internal stages systematically cancel higher-order Taylor derivatives, a single RK4 step achieves far better accuracy than two RK2 half-steps using the same number of function evaluations. However, this assumes the solution is smooth over the step —when rapid transients occur, we need adaptive time stepping.
fig = exe.plot_rk4_stages(f, y_true, t0=0.0, y0=2.0, h=1.0)
fig.show()