跳到正文
格致开物MATHWIKI

克兰克—尼科尔森格式

克兰克—尼科尔森格式(Crank–Nicolson scheme)将空间离散算子在新旧两个时刻的作用取平均,是热方程等演化问题常用的二阶隐式时间格式。它对线性扩散问题具有无条件能量稳定性,但高频误差在大步长下可能换号振荡;无条件稳定并不等于无条件非负或任意步长都准确。

只有一个内部网格点的热传导

考虑 0<x<1 上的热方程 ut=κuxx,边界温度为零,κ>0。令网格间距 Δx=1/2,只有中点一个未知温度 U(t)。二阶中心空间差分给出 U′=κ0−2U+0(1/2)2=−8κU. 取 κ=1,U(0)=1,Δt=0.1,新旧时刻平均导数得到 Un+1−Un0.1=−4(Un+1+Un),Un+1=37Un. 所以前两步为 U1=3/7,U2=9/49。用来比较时间误差的精确对象是空间离散系统的解 e−8t;它仍与连续热方程有空间离散误差,不能混作一个误差来源。

一般离散与二阶精度

在均匀网格的内部节点定义 (ΔhU)i=(Ui−1−2Ui+Ui+1)/Δx2。格式为 Un+1−UnΔt=κ2Δh(Un+1+Un). 令 r=κΔt/(2Δx2),逐行写成 −rUi−1n+1+(1+2r)Uin+1−rUi+1n+1=rUi−1n+(1−2r)Uin+rUi+1n. 零边界值直接省去;非零或随时间变化的边界还应加入两时刻的相应项。每一步需解一个三对角线性系统。

对光滑半离散真解 Y′=AY,区间积分使用梯形求积:Y(t+Δt)−Y(t)=Δt[AY(t)+AY(t+Δt)]/2+O(Δt3)。这说明单步状态缺陷为三阶,稳定后固定时间的全局时间误差为二阶;中心空间差分在四阶空间导数有界等条件下给出二阶空间误差。非光滑初值、初边值不相容或缺少所需正则性时,不能直接沿用同一误差常数和阶数。

该时间离散也是亚当斯—莫尔顿法的梯形公式应用到空间离散方程。数值 PDE 教材常以热方程比较它与显式、后向 Euler 方法。[1]

无条件能量稳定性的证明

取离散内积 ⟨U,V⟩h=Δx∑iUiVi,并将边界零值补齐。设中点状态 W=(Un+1+Un)/2。把格式与 W 作内积,左边利用差平方得到 ‖Un+1‖h2−‖Un‖h22Δt=κ⟨ΔhW,W⟩h. 将二阶差分展开,移位相邻求和并用端点为零,得离散分部求和恒等式 ⟨ΔhW,W⟩h=−Δx∑i=0M(Wi+1−WiΔx)2. 右边非正,故任意正时间步长下 ‖Un+1‖h≤‖Un‖h。证明针对这个对称扩散算子和边界条件;非线性、反应项、非对称离散或近似线性求解器需要另作分析。

高频振荡与步长选择

若离散特征模式满足 Y′=−λY、λ>0,其放大因子为 R=1−λΔt/21+λΔt/2. 模始终小于一,但当 λΔt>2 时为负;λΔt→∞ 时趋于 −1,很快的真实衰减模式在数值上只缓慢交替。

小步长梯形推进平滑衰减,大步长结果正负交替但绝对值下降
单内部节点的 λ=8。小步长 Δt=0.1 的因子为 3/7;大步长 Δt=1 的因子为 −3/5。能量下降仍允许温度出现负值。

上文模型若改用 Δt=1,首步就是 −3/5,而真实解为正。这是完整的有解变式,也给出误用“稳定”的反例。对初始不连续或尖峰数据,可先用几个较小的后向 Euler 子步消减高频,再使用二阶梯形推进;具体起步与误差容限仍须核查。

三对角实现

下面对零 Dirichlet 边界求解一次时间步。输入内部节点值、扩散率、空间步长和时间步长,返回新列表,不修改输入。Thomas 消元在本问题的严格对角占优正定系统中没有零主元。每步时间与空间均为 O(M);固定参数时可预先分解,避免重复消元。

Python 3
import math


def crank_nicolson_heat(interior, kappa, dx, dt):
    u = list(map(float, interior))
    kappa, dx, dt = map(float, (kappa, dx, dt))
    if not u or not all(math.isfinite(v) for v in u + [kappa, dx, dt]):
        raise ValueError("nonempty finite data required")
    if kappa <= 0 or dx <= 0 or dt <= 0:
        raise ValueError("positive parameters required")
    r = kappa * dt / (2 * dx * dx)
    if not math.isfinite(r) or not math.isfinite(1+2*r):
        raise OverflowError("coefficient outside floating-point range")
    n = len(u)
    old = [0.0] + u + [0.0]
    rhs = [r*old[i] + (1-2*r)*old[i+1] + r*old[i+2] for i in range(n)]
    diag = [1+2*r] * n
    for i in range(1, n):
        multiplier = -r / diag[i-1]
        diag[i] += multiplier*r
        rhs[i] -= multiplier*rhs[i-1]
    result = [0.0] * n
    result[-1] = rhs[-1] / diag[-1]
    for i in range(n-2, -1, -1):
        result[i] = (rhs[i] + r*result[i+1]) / diag[i]
    if not all(math.isfinite(v) for v in result):
        raise OverflowError("nonfinite linear solve")
    return result


u = crank_nicolson_heat([1], 1, 0.5, 0.1)
print(u)  # [0.4285714285714286]
print(crank_nicolson_heat(u, 1, 0.5, 0.1))  # [0.1836734693877551]
print(crank_nicolson_heat([1], 1, 0.5, 1))    # [-0.6]

参考资料

  1. ↑ Tobin A. Driscoll, Richard J. Braun. Fundamentals of Numerical Computation: Stiffness,扩散的空间离散与时间积分。