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 combines a steady linear drift with a sudden jump near . If a uniform step size is chosen very small, the jump is resolved accurately, but thousands of evaluations are wasted along the quasi-linear regions. Conversely, if is too large, the transient can be missed or distorted.
import numpy as np
from scipy.integrate import solve_ivp
try:
import executable_engineering as exe
except ImportError:
%pip install -q executable_engineering
import executable_engineering as exeAdaptive Step-Size Control¶
Adaptive time steppers automatically adjust their step size based on a local estimate of the truncation error.
If the current step of size produces an estimated error and we aim for a target error tolerance , we can update the step size via:
where is a scaling exponent tied to the method’s order ( 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 and recompute from with a smaller ?
Should we include a meta-analysis of the solver to detect non-convergent or stiff behaviour?
To estimate without excessive computational burden, we typically use one of three strategies:
Step-halving: compare a coarse step to two half-steps.
Embedded Runge-Kutta pairs: compare two integration orders over the same step.
Direct comparison: monitor solution increments or solver convergence metrics.
exe.plot_transient_function(t_range=(0.0, 10.0), n_coarse=12)Step-Halving Methods¶
Error analysis of particular schemes tell you how the error scales with step size , 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 () against two successive half-steps of size ().
For the fourth-order Classical RK4 scheme, the difference between the two approximations should scale as , the local truncation error of the more accurate two-step result is approximately
Example: Integrate with from to (exact value ). Note that evaluating one full step and two half-steps requires (or 12 unshared) evaluations of .
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
exe.display_source(exe.rk4_step)
f = lambda t, y: 4.0 * np.exp(0.8 * t) - 0.5 * y
y_exact_2 = 14.84392190704581
y_onestep = exe.rk4_step(f, 0.0, 2.0, h=2.0)
y_mid = exe.rk4_step(f, 0.0, 2.0, h=1.0)
y_twostep = exe.rk4_step(f, 1.0, y_mid, h=1.0)
est_error = (y_twostep - y_onestep) / 15.0
true_error = y_exact_2 - y_twostep
print(f"One step (h = 2): {y_onestep:.6f}")
print(f"Two steps (h = 1): {y_twostep:.6f}")
print(f"Estimated error: {est_error:.6f} vs. True error: {true_error:.6f}")Runge-Kutta-Fehlberg (RKF45)¶
A more efficient strategy is to compare two different integration orders over the same step by reusing intermediate stage evaluations . The classic Runge-Kutta-Fehlberg (RKF45) method uses 6 stages to construct both a 4th-order and a 5th-order approximation:
The two bottom rows yield the 4th-order and 5th-order updates:
The local error estimate is simply . Embedded RK45 pairs are the default workhorse in scientific libraries such as scipy.integrate.solve_ivp.
f = lambda t, y: 4.0 * np.exp(0.8 * t) - 0.5 * y
y_true = lambda t: (4.0 / 1.3) * (np.exp(0.8 * t) - np.exp(-0.5 * t)) + 2.0 * np.exp(-0.5 * t)
sol = solve_ivp(f, (0.0, 2.0), [2.0], method="RK45")
print(f"Adaptive time nodes: {np.round(sol.t, 4)} ({sol.nfev} function evaluations)")
print(f"RK45 y(2) = {sol.y[0, -1]:.6f} | Absolute Error = { abs(sol.y[0, -1] - y_true(2.0)):.6e}")
exe.plot_rk45_adaptive(sol, y_true=y_true)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:
from which a bound on (or on the relative increment ) can be enforced using the Euclidean or -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 is close to , taking too many Newton iterations signals that the new state has changed too drastically and the step size should be reduced.