4. 显式格式的编程实现与验证

本节把前面推导的FTCS(Forward-Time Central-Space,时间前向、空间中心)离散格式真正落到代码上,并用一个具有解析解的金属棒瞬态导热案例来检验实现是否正确。学习目标不是"跑通一段程序",而是建立一条可复现的验证链:明确物理问题与边界条件 → 按FTCS公式组装更新循环 → 用解析解逐点比对 → 用误差范数给出定量结论 → 用时空演化图观察物理合理性。只有这五步都成立,才能说求解器"可信"。

一、物理问题与解析解基准

取一根长度 L = 1 m 的均质金属棒,热扩散系数 α = 1.0×10⁻⁴ m²/s(量级接近不锈钢),初始温度分布取正弦模态:

T(x,0) = T₀ + A·sin(πx/L),T₀ = 20 ℃,A = 100 ℃

两端保持恒定温度 T(0,t) = T(L,t) = T₀(第一类边界条件)。该问题的解析解为:

T(x,t) = T₀ + A·sin(πx/L)·exp(−α π² t / L²)

选择正弦初值而非阶跃初值,是因为正弦模态是热方程的特征函数,解析解光滑、无奇点,最适合做代码验证;阶跃初值在 x=0 处导数不连续,会引入额外的离散误差,不适合作为第一轮验证对象。注意温度以 T₀ 为基准,程序中可令 u = T − T₀ 简化计算,最后再加回 T₀。

二、离散参数与稳定性约束

FTCS 的更新公式为:

u[i]^(n+1) = u[i]^n + r·(u[i+1]^n − 2u[i]^n + u[i−1]^n),r = α·Δt/Δx²

显式格式的稳定性条件是 r ≤ 0.5。取 Nx = 51 个节点(Δx = 0.02 m),若取 r = 0.4,则 Δt = r·Δx²/α = 0.4×4×10⁻⁴/1×10⁻⁴ = 1.6 s。总模拟时长取 t_end = 200 s,约需 125 步。这个参数组合既满足稳定性,又让时间推进步数适中,便于观察。若误取 r = 0.6,数值解会出现非物理的振荡并迅速发散——这正是验证环节要捕捉的典型错误。

FTCS 求解器:从参数到误差判读 离散参数 L=1 m,α=1e-4 m²/s Nx=51,Δx=0.02 m r=0.4 → Δt=1.6 s t_end=200 s(125 步) 稳定性检查 r = αΔt/Δx² r ≤ 0.5 ? 0.4 ≤ 0.5 ✓ 通过 FTCS 更新循环 u_new[i] = u[i] + r·( u[i+1] − 2u[i] + u[i−1]) 边界固定 u[0]=u[Nx−1]=0 逐时间步推进 解析解基准 T(x,t)=T₀+A·sin(πx/L) ·exp(−απ²t/L²) 在 t=50,100,200 s 取样 误差范数 L∞ = max|T_num − T_exact| L2 = sqrt(Δx·Σ(误差)²) 随 Δt、Δx 减小而收敛 温度时空演化图 横轴 x,纵轴 t,色标 T 正弦峰随时间指数衰减 两端恒为 T₀,无振荡 判据:L∞ 误差随网格加密按 O(Δt)+O(Δx²) 下降,且演化图无振荡 → 实现正确

三、Python 实现要点

核心循环用 NumPy 向量化切片实现,避免逐点 Python 循环。关键写法是:u[1:-1] = u[1:-1] + r*(u[2:] - 2*u[1:-1] + u[:-2]),其中 u[2:] 对应 u[i+1],u[:-2] 对应 u[i−1]。边界节点 u[0] 与 u[-1] 不参与更新,保持为 0(即 T₀)。

import numpy as np
import matplotlib.pyplot as plt

L, alpha, T0, A = 1.0, 1e-4, 20.0, 100.0
Nx = 51
dx = L / (Nx - 1)
x = np.linspace(0, L, Nx)
r = 0.4
dt = r * dx**2 / alpha
t_end = 200.0
nsteps = int(round(t_end / dt))

u = A * np.sin(np.pi * x / L)   # 初始条件(相对温度)
u[0] = u[-1] = 0.0              # 边界条件

snapshots = {0: u.copy()}
for n in range(1, nsteps + 1):
    u[1:-1] = u[1:-1] + r * (u[2:] - 2*u[1:-1] + u[:-2])
    t = n * dt
    if abs(t - 50) < dt/2 or abs(t - 100) < dt/2 or abs(t - 200) < dt/2:
        snapshots[round(t)] = u.copy()

def exact(x, t):
    return A * np.sin(np.pi * x / L) * np.exp(-alpha * np.pi**2 * t / L**2)

for t, un in snapshots.items():
    err = un - exact(x, t)
    Linf = np.max(np.abs(err))
    L2 = np.sqrt(dx * np.sum(err**2))
    print(f"t={t:3d}s  Linf={Linf:.4e}  L2={L2:.4e}")

代码中有两个容易出错的细节。第一,nsteps 用 round 而非 int 截断,避免因浮点误差少走一步;第二,快照判断用 abs(t - 目标) < dt/2,因为 n*dt 未必精确等于整数秒。若直接用 t == 50 判断,可能永远不触发。

四、误差范数与收敛性判读

在 t = 50、100、200 s 三个时刻分别计算 L∞ 与 L2 误差。以本节参数为例,合理预期是:L∞ 误差在 10⁻² ℃ 量级,且随时间增大而略有增长(因为时间累积误差),但不会发散。若出现 L∞ 达到 10¹ 以上或随时间爆炸,几乎可以断定 r > 0.5 或边界处理写错。

更严格的验证是网格收敛性检验:固定 r = 0.4,将 Nx 从 51 加密到 101、201,观察 L∞ 是否按约 4 倍(Δx² 主导)下降。由于 FTCS 的时间误差是一阶、空间误差是二阶,在 r 固定时 Δt ∝ Δx²,时间误差也退化为二阶,因此整体应呈现接近二阶收敛。若加密后误差不降反升,说明实现中存在索引错位或边界泄漏。

五、时空演化图的观察要点

用 plt.imshow 或 plt.pcolormesh 绘制 T(x,t) 的二维色图,横轴 x、纵轴 t、色标为温度。正确的图应呈现:初始时刻中间高、两端低的半正弦拱形;随时间推移,峰值沿时间轴指数衰减并趋于 T₀;两端始终为 T₀ 的竖直等温带;整个场无棋盘状振荡、无负温度。若出现沿 x 方向的高频锯齿,通常是 r 接近 0.5 时的临界振荡;若出现负值,则已越过稳定性边界。

验证顺序建议:先跑 r = 0.4 确认误差量级与图形形态,再故意改成 r = 0.6 观察发散现象,形成"正确—错误"的对照记忆。这种对照比单纯看正确结果更能建立对稳定性条件的直觉。
解析解对比时务必保证数值解与解析解使用同一套坐标与温度基准。若程序中用相对温度 u 推进,比对前要加回 T₀,否则误差会被整体平移 20 ℃ 而完全失真。