3. 迭代求解器不收敛
在计算物理中,绝大多数偏微分方程离散后都会得到一个大型线性或非线性方程组,最终交给迭代求解器处理。迭代法不收敛是排障中最常见、也最容易被误判的一类问题:有人看到残差曲线不动就加大迭代步数,有人看到残差上下跳动就换求解器,结果往往徒劳。要高效定位,先要分清两种截然不同的失败形态——残差停滞与发散振荡,它们的成因和处置手段差别很大。
先读懂残差曲线:三种典型形态
迭代求解器的核心观测量是残差范数,通常取相对残差 r_k = ||b - A x_k|| / ||b||。健康收敛时,残差在半对数坐标下近似直线下降;而故障形态主要有三类:
- 停滞:残差快速下降一段后进入平台,几乎不再变化,例如卡在 1e-4 附近。常见于预条件子与矩阵不匹配、边界条件欠约束、存在零空间(如纯 Neumann 问题的常数零模态)。
- 发散振荡:残差在若干量级间来回跳动甚至持续上升。常见于松弛因子过大、矩阵非对角占优、对流项离散导致中心差分失稳、时间步长超过稳定性限制。
- 缓慢收敛:残差单调下降但斜率极缓,迭代上千步仍达不到容差。多与条件数过大、网格长宽比过高、预条件子质量差有关。
下图给出三类残差形态的判读逻辑与对应的首要排查方向,可作为现场诊断的对照参考。
停滞的典型成因与验证手段
停滞最容易被误认为“算得慢”,其实它往往意味着迭代方向已经无法再降低残差。一个高频原因是预条件子与矩阵不匹配:例如对非对称矩阵误用了对称 Gauss-Seidel 预条件,或 ILU 分解的填充级别过低,导致预条件后的系统仍严重病态。验证方法是关闭预条件(P = I)跑几十步,若残差下降趋势明显改善,说明预条件子本身在拖后腿。
第二个高频原因是零空间未被处理。以纯 Neumann 边界的热传导问题为例,解只在相差一个常数的意义下唯一,系数矩阵奇异,残差会稳定停在某个非零水平。此时应固定一个参考点(如令 x[0] = 0)或改用能处理奇异系统的求解器。第三个原因是容差设置不合理:把相对容差设到 1e-14,而双精度舍入误差本身就在 1e-16 量级,残差自然无法继续下降。经验上,相对容差取 1e-8~1e-10 已能满足多数工程精度。
发散振荡的典型成因与稳定化
振荡发散通常指向迭代格式本身不稳定。以 SOR 方法为例,松弛因子 ω 必须满足 0 < ω < 2,且最优值依赖谱半径;ω 取 1.9 对某些矩阵会直接振荡。调试时先把 ω 降到 1.0(退化为 Gauss-Seidel)观察是否恢复单调下降,再逐步上调。
对流占优问题中,若对流项采用中心差分且网格 Péclet 数 Pe = u·h/α > 2,离散矩阵失去对角占优,迭代必然振荡。此时应改用迎风格式或加人工粘性。瞬态问题则要检查时间步长是否超过显式格式的 CFL 限制。下面是一段可复用的诊断代码骨架,用于在迭代循环中自动识别形态:
r_hist = []
for k in range(max_iter):
r = np.linalg.norm(b - A @ x) / norm_b
r_hist.append(r)
if r < tol:
print("收敛于第", k, "步"); break
if k > 20:
recent = r_hist[-10:]
if max(recent) / min(recent) < 1.05:
print("残差停滞,检查预条件子/零空间"); break
if recent[-1] > 10 * recent[0]:
print("发散振荡,检查松弛因子/离散格式"); break
x = x + omega * (solve_precond(b - A @ x))
系统化调试顺序
面对不收敛,建议按“先小后大、先简后繁”的顺序排查:先用一个已知解析解的小规模算例(如 2D Poisson 方程在 16×16 网格上)验证求解器链路是否正常;再逐步放大到真实规模,观察残差形态在哪一步发生质变;然后单独测试预条件子、松弛因子、边界条件三个变量,每次只改一个。若条件数过大导致缓慢收敛,可考虑改用 GMRES 配合 ILU,或引入几何多重网格作为预条件。记录每次调整后的残差曲线与迭代步数,是判断是否真正改善的唯一可靠依据,切勿仅凭“感觉变快了”下结论。