跳到正文
格致开物MATHWIKI

嵌入式龙格—库塔法

嵌入式龙格—库塔法(embedded Runge–Kutta methods)复用同一组阶段斜率,计算两个不同阶数的近似值,并用它们的差作为局部误差指标,决定是否接受当前步以及下一步多长。共享阶段使误差估计不必完整重做一次积分;差值仍是渐近指标,不是无条件的误差证明。

本篇使用两阶段的Heun—Euler对。它的高阶输出为二阶Heun值,低阶输出为一阶Euler值。对 y′=y,y(0)=1,试步 h=0.2 时,两阶段斜率为一和 1.2,Euler值为 1.2,Heun值为 1.22,差为 0.02。若允许指标上限为 0.01,这一步应拒绝并缩短步长。

共享阶段、两个输出

在当前点 (t,y) 计算 k1=f(t,y),k2=f(t+h,y+hk1). 两套权重给出 yL=y+hk1,yH=y+h2(k1+k2),δ=yH−yL=h2(k2−k1). 下标 L,H 分别表示低阶与高阶。两次函数评价同时服务两个输出,额外计算差值只需少量算术。常用RK23、RK45等嵌入对采用更多阶段和更高阶权重,但这种“共享阶段、比较输出”的结构相同。[1]

差值估计的是哪个误差

假设当前 y 为精确解值,f 足够光滑。令 a=ft+fyf,则 k2=f+ha+O(h2),y(t+h)=y+hf+h22a+O(h3). 所以 yH 的一步缺陷为 O(h3),而 yL 缺少二次项;两者差值为 δ=h22a+O(h3). 它渐近反映的是低阶Euler输出的一步误差,而不是把高阶Heun输出的三阶缺陷精确测出来。因此用它控制并传播 yH 通常比较保守。一般高阶为 p、低阶为 p−1 的嵌入对,其差值主阶通常为 hp,控制指数应与这个阶数匹配。

若高低两输出差值的首项消失,或函数在一步内部发生未被阶段点采到的变化,差值就可能低估误差。阶段计算与真解的接近依赖光滑性,不能只因程序返回一个很小的数就跳过条件。

归一化指标和步长更新

对标量状态,使用绝对、相对容差组合 scale=atol+rtolmax⁡(|y|,|yH|),err=|δ|scale. 本文要求 atol>0,rtol≥0,避免零状态导致零分母。若 err≤1,接受新状态;否则保留原时刻与原状态,重新试算。拒绝步只改变候选步长,不能仍把不合要求的值推进到下一时刻。

在差值约为 Ch2 的范围内,要把归一化误差调至一,可取 hnew=hηerr−1/2, 其中 0<η<1 是安全系数,通常还限制步长缩放的最大、最小倍数。本篇用 η=0.9。主例首次 err=2,于是新步长为 0.2⋅0.9/2≈0.127279;从同一初值重试时差值为 h2/2=0.0081,低于 0.01。

差值指标随步长按h平方除以二增长,阈值0.01;步长0.2的差值0.02被拒绝,步长0.1的差值0.005可接受
图比较两个候选步长;自动控制器可以选择并非恰好减半的新步长。

变式。 若手动把步长改试为 0.1,则 yL=1.1,yH=1.105,差值 0.005,同样可接受。这个试步比自动选择的约 0.1273 更短,显示自适应控制的目标是满足指标同时避免不必要的小步。

可运行的自适应标量实现

函数返回接受的状态列表和包含拒绝步的全部试步记录。终点不早于起点,初始步长为正,最多尝试指定次数;不改写用户数据。实现会检查非有限阶段和无法推进的浮点步长,但它使用的仍是上述启发式误差指标。

Python 3
from math import isfinite

def adaptive_heun(function, t0, y0, target, initial_step,
                  atol=1e-6, rtol=1e-3, max_trials=10000):
    t, y, target, h, atol, rtol = map(float,
        (t0, y0, target, initial_step, atol, rtol))
    if not all(isfinite(v) for v in (t, y, target, h, atol, rtol)):
        raise ValueError("finite input required")
    if target < t or h <= 0 or atol <= 0 or rtol < 0:
        raise ValueError("invalid interval, step or tolerances")
    if type(max_trials) is not int or max_trials < 1:
        raise ValueError("max_trials must be positive")
    states, trials = [(t, y)], []
    while t < target:
        if len(trials) >= max_trials:
            raise RuntimeError("trial limit reached")
        remaining = target - t
        h = min(h, remaining)
        endpoint = target if h == remaining else t+h
        if endpoint == t:
            raise ArithmeticError("step cannot advance time")
        k1 = float(function(t, y))
        k2 = float(function(endpoint, y+h*k1))
        low = y+h*k1
        high = y+h*(k1+k2)/2
        if not all(isfinite(v) for v in (k1, k2, low, high)):
            raise ArithmeticError("nonfinite trial")
        delta = abs(high-low)
        error = delta/(atol+rtol*max(abs(y), abs(high)))
        accepted = error <= 1
        trials.append({"t": t, "h": h, "delta": delta,
                       "scaled_error": error, "accepted": accepted})
        if accepted:
            t, y = endpoint, high
            states.append((t, y))
        factor = 2.0 if error == 0 else min(2.0, max(0.2, 0.9*error**-0.5))
        h *= factor
    return states, trials

states, trials = adaptive_heun(lambda t, y: y, 0, 1, 0.2, 0.2,
                               atol=0.01, rtol=0)
assert not trials[0]["accepted"]
assert abs(trials[0]["delta"] - 0.02) < 1e-14
assert trials[1]["t"] == 0
assert states[-1][0] == 0.2
assert all(row["scaled_error"] <= 1 for row in trials if row["accepted"])
assert adaptive_heun(lambda t, y: y, 0, 1, 0, 0.1)[0] == [(0.0, 1.0)]
print(states)

接受时使用的 yH 正是Heun更新,阶段正确性和二阶局部展开见龙格—库塔法;拒绝时状态未变,不变量为“所有保存的推进均通过所声明的指标”。这不变量不等于真误差已被严格界住。每个试步有两次右端评价,N 次尝试耗时 O(N),保存全部记录耗空间 O(N)。若不断拒绝,程序由试步上限或无法推进检查终止报错,而不是假定所有问题都必定成功到达终点。

零指标仍可能漏掉内部变化

取 y′=t(t−1),y(0)=0,一步走到一。两个阶段都评价到零,因此 yL=yH=0,差值也为零;真解却满足 y(1)=∫01(t2−t)dt=−16. 函数在两个采样端点之间的负变化被漏掉了。这个反例说明,误差控制还需合适的初始步长、对事件或快速变化的处理,以及与问题尺度相容的光滑性假设。相对容差很小也不会改变两阶段恰好都采到零的事实。

参考文献

  1. ↑ Tobin A. Driscoll、Richard J. Braun:Fundamentals of Numerical Computation: Adaptive Runge–Kutta,嵌入对、局部误差、接受或拒绝及步长控制。访问日期:2026-10-22。