Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

 Open In Colab

It is common for engineering systems to exhibit a combination of fast transients alongside otherwise slow, smooth behaviour. In general, we do not know in advance when a sharp transient will occur or how rapidly it will evolve!

For example, the function y(t)=tanh⁡ ⁣(t−50.1)−ty(t) = \tanh\!\left(\frac{t - 5}{0.1}\right) - t combines a steady linear drift with a sudden jump near t=5t = 5. If a uniform step size hh is chosen very small, the jump is resolved accurately, but thousands of evaluations are wasted along the quasi-linear regions. Conversely, if hh is too large, the transient can be missed or distorted.

Adaptive Step-Size Control

Adaptive time steppers automatically adjust their step size hh based on a local estimate of the truncation error.

If the current step of size hcurrenth_{\text{current}} produces an estimated error EcurrentE_{\text{current}} and we aim for a target error tolerance EgoalE_{\text{goal}}, we can update the step size via:

hnew=hcurrent∣EgoalEcurrent∣αh_{\text{new}} = h_{\text{current}} \left|\frac{E_{\text{goal}}}{E_{\text{current}}}\right|^{\alpha}

where α\alpha is a scaling exponent tied to the method’s order (α≈0.2\alpha \approx 0.2 is a standard choice for fourth/fifth-order schemes).

Two practical design questions arise in any adaptive solver:

  • Should we reject the current time step whenever Ecurrent>EtoleranceE_{\text{current}} > E_{\text{tolerance}} and recompute from tit_i with a smaller hh?

  • Should we include a meta-analysis of the solver to detect non-convergent or stiff behaviour?

To estimate EcurrentE_{\text{current}} without excessive computational burden, we typically use one of three strategies:

  1. Step-halving: compare a coarse step to two half-steps.

  2. Embedded Runge-Kutta pairs: compare two integration orders over the same step.

  3. Direct comparison: monitor solution increments or solver convergence metrics.

Step-Halving Methods

Error analysis of particular schemes tell you how the error scales with step size hh, but we need an estimate of the error itself.

Step-halving (Richardson extrapolation for ODEs) compares the result of a single full step of size hh (ysingle stepy_{\text{single step}}) against two successive half-steps of size h/2h/2 (ydouble stepy_{\text{double step}}).

For the fourth-order Classical RK4 scheme, the difference between the two approximations should scale as O(h5)\mathcal{O}(h^5), the local truncation error of the more accurate two-step result is approximately E≈ydouble step−ysingle step15E \approx \frac{y_{\text{double step}} - y_{\text{single step}}}{15}

Example: Integrate dydt=4e0.8t−0.5y\frac{\mathrm{d}y}{\mathrm{d}t} = 4e^{0.8t} - 0.5y with y(0)=2y(0) = 2 from t=0t = 0 to t=2t = 2 (exact value y(2)≈14.84392y(2) \approx 14.84392). Note that evaluating one full step and two half-steps requires 4+2×4−1=114 + 2 \times 4 - 1 = 11 (or 12 unshared) evaluations of f(t,y)f(t, y).

differential_equations_module.py
def rk4_step(f, t, y, h):
    k1 = h * f(t, y)
    k2 = h * f(t + 0.5 * h, y + 0.5 * k1)
    k3 = h * f(t + 0.5 * h, y + 0.5 * k2)
    k4 = h * f(t + h, y + k3)
    return y + (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0

Runge-Kutta-Fehlberg (RKF45)

A more efficient strategy is to compare two different integration orders over the same step by reusing intermediate stage evaluations k1,…,ksk_1, \dots, k_s. The classic Runge-Kutta-Fehlberg (RKF45) method uses 6 stages to construct both a 4th-order and a 5th-order approximation:

0151531034094035310−910651−115452−702735277816315529617551257513824442751105922534096y(4)37378025062112559405121771y(5)2825276480185754838413525552962771433614\begin{array}{c|cccccc} 0 & & & & & & \\ \frac{1}{5} & \frac{1}{5} & & & & & \\ \frac{3}{10} & \frac{3}{40} & \frac{9}{40} & & & & \\ \frac{3}{5} & \frac{3}{10} & -\frac{9}{10} & \frac{6}{5} & & & \\ 1 & -\frac{11}{54} & \frac{5}{2} & -\frac{70}{27} & \frac{35}{27} & & \\ \frac{7}{8} & \frac{1631}{55296} & \frac{175}{512} & \frac{575}{13824} & \frac{44275}{110592} & \frac{253}{4096} & \\ \hline y^{(4)} & \frac{37}{378} & 0 & \frac{250}{621} & \frac{125}{594} & 0 & \frac{512}{1771} \\ y^{(5)} & \frac{2825}{27648} & 0 & \frac{18575}{48384} & \frac{13525}{55296} & \frac{277}{14336} & \frac{1}{4} \end{array}

The two bottom rows yield the 4th-order and 5th-order updates:

yi+1(4)=yi+h(37378k1+250621k3+125594k4+5121771k6)yi+1(5)=yi+h(282527648k1+1857548384k3+1352555296k4+27714336k5+14k6)\begin{aligned} y_{i+1}^{(4)} &= y_i + h \left( \frac{37}{378} k_1 + \frac{250}{621} k_3 + \frac{125}{594} k_4 + \frac{512}{1771} k_6 \right) \\ y_{i+1}^{(5)} &= y_i + h \left( \frac{2825}{27648} k_1 + \frac{18575}{48384} k_3 + \frac{13525}{55296} k_4 + \frac{277}{14336} k_5 + \frac{1}{4} k_6 \right) \end{aligned}

The local error estimate is simply E=∣yi+1(5)−yi+1(4)∣E = \left|y_{i+1}^{(5)} - y_{i+1}^{(4)}\right|. Embedded RK45 pairs are the default workhorse in scientific libraries such as scipy.integrate.solve_ivp.

Direct Comparison

A less formal, yet often pragmatic, approach uses empirical solution metrics to regulate the step size.

After advancing a time step, the finite-difference rate of change can be evaluated:

dydt≈yi+1−yih\frac{\mathrm{d}y}{\mathrm{d}t} \approx \frac{y_{i+1} - y_i}{h}

from which a bound on ∥dydt∥\left\|\frac{\mathrm{d}y}{\mathrm{d}t}\right\| (or on the relative increment ∥yi+1−yi∥\|y_{i+1} - y_i\|) can be enforced using the Euclidean or ∞\infty-norm. Note that this heuristic bounds the change in the state variable rather than the mathematical truncation error.

In implicit solvers, an even more direct indicator is the convergence rate of the nonlinear root finder at each time step. Because Newton-Raphson converges quadratically when the initial guess yiy_i is close to yi+1y_{i+1}, taking too many Newton iterations signals that the new state has changed too drastically and the step size hh should be reduced.