☰
亥姆霍兹方程数值验证:五点差分法的网格收敛性分析
2026/10/14 22:30:13 网站建设 项目流程

亥姆霍兹方程,估计做电磁、声学、地震勘探或者光学仿真的朋友都不陌生。它长得极其简洁:

∇²u + k²u = 0

就这么一行公式,横跨了好几个看起来八竿子打不着的领域。但说实话,我过去对它的理解基本停留在“傅里叶变换一下,波动方程就变成这个”的层面。真正让我决定动手做一次完整数值验证的,是有一次在某论坛上看到一个问题:有人用有限差分法解亥姆霍兹方程,结果高波数下解出来全是零,下面回复吵了几页也没吵出个所以然。当时我就想,这种基础方程都这么容易踩坑,那不如我干脆从头到尾自己跑一遍,把公式推导、网格离散、矩阵组装、误差分析全串起来。

“日拱一卒”这个系列,目的就是每天往前啃一小步。这次选的题目是亥姆霍兹方程验证,内容很明确:在二维矩形域上,用五点差分格式求解亥姆霍兹方程,再用一个已知解析解去对比数值结果,最后做网格收敛性分析。这篇文章适合正在学偏微分方程数值解的学生、刚接触数值仿真的工程师,以及所有想把“看公式”变成“跑数据”的人。

我先把这次验证的核心链路说清楚:从波动方程出发提取亥姆霍兹方程 → 选择一个合适的解析测试函数 → 推导对应的波数 k → 在矩形网格上组装五点差分矩阵 → 用稀疏线性求解器求解 → 对比数值解和解析解 → 改变网格密度看误差下降趋势。每一步听起来都不难,但串起来之后,你会发现里面藏着不少让人挠头的细节。

1. 验证之前要想清楚的三件事

1.1 “验证”和“证明”不是一回事

很多人第一次听到“验证”这个词,以为是要去“证明”亥姆霍兹方程本身是对的。方程当然是对的,不需要你证明。你要验证的是你写的数值求解流程是否正确地实现了这个方程。换句话说,你的目标不是证明物理规律,而是证明你的代码没有把物理规律算歪。

这里就引出一个很实际的问题:什么叫“算歪了”?数值解总归是近似解,哪怕是再精确的离散格式,也会和解析解有差距。所以做验证之前,你必须先定义清楚验收标准。我做这个项目时,用的标准是相对 L2 误差:

err = ||u_num - u_exact||₂ / ||u_exact||₂

只要这个误差随着网格加密而稳定下降,下降趋势和理论预估基本一致,那就可以认为求解流程是靠谱的。如果误差完全不降,或者越加密越糟糕,那问题一定出在你的算法实现、边界处理或波数选择上,而不是解析解本身。

这个“先定义标准再动手”的习惯,比我后面写任何一行代码都重要。没有标准,你去谈结果好不好,就是各说各话。

1.2 前置准备:软件和数学基础

进入实操之前,我梳理了一下这次项目需要的工具和基础。软件方面,我选了 Python 加 NumPy、SciPy——用它们是出于一个很现实的原因:组装稀疏矩阵方便,求解线性方程组也方便,几乎不用自己写数值代数库。你要用 MATLAB 也行,逻辑完全一致,就是矩阵索引语法略有差别。

数学基础方面,其实需要的东西不多:

  • 高等数学里的偏导数、全微分概念;
  • 线性代数里矩阵乘法和线性方程组的基本概念;
  • 初步了解有限差分格式,哪怕你只是听说过“五点差分”这个名词。

我特别想强调一下矩阵和网格坐标之间的对应关系。编程的时候,矩阵的行列索引和物理空间里的 x、y 坐标经常把新手的脑子绕晕。我在这次项目里把所有内部节点编号都写成一个函数 idx(i, j),这样虽然看起来多写几行代码,但能避免大量索引错误。这类基础性的小工具,往往是大项目里最容易出 bug 也最容易被人忽略的地方。

2. 从波动方程到亥姆霍兹方程:先把自己的思路理顺

2.1 用分离变量把时间维度拿掉

先来温习一遍推导过程。经典的波动方程是:

∂²u/∂t² - c²∇²u = 0

这里 u 是某个物理量,c 是波速。如果考虑的是单频简谐波,也就是时间上只有一个频率,那我们可以设:

u(x, y, t) = U(x, y) · e^(-iωt)

把它代进波动方程,时间二阶导会给出一项 (-ω²),于是变为:

-ω²U - c²∇²U = 0

移项并令 k = ω/c,就得到:

∇²U + k²U = 0

这就是亥姆霍兹方程。整个过程说白了,就是通过猜一个时间简谐形式,把偏微分方程里那个时间变量去掉,剩下一个只含空间变量的方程。你如果做频域分析,每给一个频率 ω,就会对应一个波数 k,然后需要解一次亥姆霍兹方程。这就是为什么它在工程中这么常见:频域扫描本质上就是一次次求解亥姆霍兹方程。

2.2 波数 k 到底在说什么

k 看起来只是一个参数,但它直接影响解的空间振荡快慢。波长 λ 和 k 的关系是:

λ = 2π/k

k 越大,波长越短,空间的振荡越剧烈。打个比方,你去弹一根吉他弦,音调越高,弦上出现的波节数就越多;在数值计算里,k 大了以后,要在同样大小的区域内刻画更细的振荡结构,你自然需要更多的网格点。

这个直观认识在后面的数值实验中会反复出现。你选好一个解析解之后,代入方程就能反推出 k。如果这个 k 很大,那你的网格必须足够细,否则结果一定失真。为了提前心里有数,我在正式求解前会先估算每波长点数,这个概念下一节会专门讲。

2.3 边界条件会决定整个验证策略

很多人做数值求解时,第一反应是“方程难”,但真正难的是边界条件。边界条件不是可有可无的附加项,它直接决定了问题的解是不是存在、是不是唯一。

常见的边界条件有三类:狄利克雷条件,指定边界上的函数值;诺依曼条件,指定边界上的法向导数值;罗宾条件,则是两者的线性组合。亥姆霍兹方程本身带着 k 这个参数,如果边界条件是齐次的,那么问题本质上是一个特征值问题——只有 k 取某些特定值时,非零解才存在。

这对验证流程是个陷阱。如果你为了让解析解形式简单,选了一个在边界上全为零的解,比如 sin(πx/a)sin(πy/b),并且要求边界条件也是零,那你实际上在跟数值求解器玩一个精确的特征值匹配游戏。网格离散以后,特征值会发生偏差,k 不再精确对应离散系统的特征值,于是你以为那个非零解析解是答案,但求解器给出的结果往往就是平淡无奇的零解。这不是代码写错了,而是边界条件与解的形式之间没有匹配好。

所以我的策略是:选一个边界值不全为零的解析解。这样一来,问题变成一个标准的非齐次边界问题,数值解在边界上被明确地“钉住”,内部节点的解也可以顺利求出来。这一点直接影响后面解析解的选择,可以说是整个验证流程里最容易被忽视的细节。

3. 用五点差分把连续问题搬上网格

3.1 中心差分与五点模板

有限差分的思想,说穿了就是用差商代替微商。对二阶偏导数,最常用的中心差分公式是:

∂²u/∂x² ≈ (u_{i+1,j} - 2u_{i,j} + u_{i-1,j}) / hx²

∂²u/∂y² ≈ (u_{i,j+1} - 2u_{i,j} + u_{i,j-1}) / hy²

如果把这两个式子代入亥姆霍兹方程 ∇²u + k²u = 0,然后两边乘以 hx²,就能得到一个代数方程。这个代数关系涉及当前节点 (i, j) 以及它的上、下、左、右四个邻居,所以叫五点差分格式。

有一点值得注意:如果 hx 和 hy 不相等,y 方向那一项的系数就不是 1,而是 r = (hx/hy)²。很多人一开始想当然地把所有邻居系数都写成 1,结果在矩形网格上求解时,误差就是消不掉。我这里是按一般形式推导的,代码里也能看到 r 这个系数,这样不管是正方形网格还是长方形网格,都不会出错。

3.2 每波长点数:数值实验的灵魂指标

五点差分格式的截断误差是 O(h²) 量级,但亥姆霍兹方程有两个特殊之处,导致误差不只是简单地随 h² 下降。

第一个特殊之处是“数值色散”。离散以后的高散波数并不是真正的 k,而是一个有效波数 k̂。当 kh 很小时,k̂ ≈ k,误差可控;可当 h 逐渐接近波长时,有效波数会明显偏离真实值,表现为波的传播速度和方向出现偏差。这在声学、电磁学里都是很致命的误差源。

所以数值计算里有了一个非常实用的指标:每波长点数(points per wavelength,简称 PPW):

PPW = λ/h = 2π/(kh)

如果 PPW 小于 10,五点格式只会给你一个非常粗糙的近似,搞不好连波的形状都认不出来。如果 PPW 在 20 到 30 之间,精度就比较可观了。我这次做的验证中,解析解的 k 大约在 9.84,对应的波长约 0.64。当我取 h=0.05 时,PPW 约为 12.8;取 h=0.025 时约为 25.6。后面会看到,误差正好随着 PPW 的提升而显著改善。

3.3 系数是1还是r,别想当然

我再单独说一遍系数,因为这是初学者最容易掉进去的坑。考虑非均匀网格,即 hx ≠ hy:

u_{i-1,j} + u_{i+1,j} + r(u_{i,j-1} + u_{i,j+1}) - (2 + 2r - k²hx²)u_{i,j} = 0

其中 r = (hx/hy)²。

如果你用均匀网格,hx = hy = h,那么 r = 1,方程简化为:

u_{i-1,j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1} - (4 - k²h²)u_{i,j} = 0

这里的负号位置很关键。有人习惯把方程写成 (4 - k²h²) 移到另一边,还有人会把 k²h² 项直接丢到右端当源项。这些做法本身没有绝对的对错,但会直接影响矩阵对角线和对符号的约定。我的经验是:始终与“邻居系数为正、对角系数为负”的约定保持一致,这条约定在多数稀疏求解器里更直观,也更容易排查矩阵是否组装错。

4. 二维矩形域验证:一个干净可复现的算例

4.1 选解析解时的一个大坑

接下来进入正题。域我选了长方形:

x ∈ [0, 1.3],y ∈ [0, 1.1]

为什么不用正方形?因为正方形太容易让 x 和 y 两个方向的模式和网格节点数产生巧合,导致矩阵病态。当然,正方形也完全能算,但长方形更能体现一般性。

接下来选解析解。我想找一个满足亥姆霍兹方程、但并非所有边界都为零的函数,这样能绕开上面说的本征值陷阱。经过尝试,我选的是:

u_exact(x, y) = cos(2πx/1.3) · cos(3πy/1.1)

把它代入亥姆霍兹方程,可以得到其对应的波数:

k² = (2π/1.3)² + (3π/1.1)² ≈ 23.36 + 73.41 = 96.77

k ≈ 9.84

这个函数在四条边界上都有非零值,所以数值问题被非常明确地定下来了:边界上给定精确解,内部用五点差分求解。不需要额外加源项,也不需要碰到内部谐振点,干净得很。

我特别想提醒一句:如果你选了一个在所有边界都为零的函数,那么边界条件全部变成零,内部又没有源项,数值求解器面对的就是一个齐次系统。齐次系统要么只给出零解,要么出现奇异,这两种结果都不会让你得到预期的解析解。这个坑我在 5.1 里会再细说。

4.2 组装稀疏矩阵的代码细节

我按上面的思路写了一段 Python 代码。核心是用scipy.sparse来组装矩阵,再用spsolve求解。有人可能会问,这个系统本来就简单,直接用稠密矩阵不好吗?如果网格很粗,稠密矩阵没问题;可一旦加密到 100×100 以上的节点,稠密矩阵的内存占用就是灾难。用稀疏矩阵是习惯问题,也是一开始就养成的良好习惯。

下面是核心求解函数:

import numpy as np from scipy.sparse import lil_matrix, csr_matrix from scipy.sparse.linalg import spsolve def solve_helmholtz_2d(Nx, Ny, a, b, k2, exact): """ 用五点差分求解二维亥姆霍兹方程: u_xx + u_yy + k^2 * u = 0 边界条件:u_边界 = exact(x, y) """ x = np.linspace(0, a, Nx) y = np.linspace(0, b, Ny) hx = x[1] - x[0] hy = y[1] - y[0] r = (hx / hy) ** 2 # 内部节点总数 n_inner = (Nx - 2) * (Ny - 2) A = lil_matrix((n_inner, n_inner)) rhs = np.zeros(n_inner) def idx(i, j): return (i - 1) * (Ny - 2) + (j - 1) for i in range(1, Nx - 1): for j in range(1, Ny - 1): row = idx(i, j) # 对角线系数 A[row, row] = -2.0 - 2.0 * r + k2 * hx * hx # x方向邻居 if i == 1: rhs[row] -= exact(x[0], y[j]) else: A[row, idx(i - 1, j)] = 1.0 if i == Nx - 2: rhs[row] -= exact(x[Nx - 1], y[j]) else: A[row, idx(i + 1, j)] = 1.0 # y方向邻居 if j == 1: rhs[row] -= r * exact(x[i], y[0]) else: A[row, idx(i, j - 1)] = r if j == Ny - 2: rhs[row] -= r * exact(x[i], y[Ny - 1]) else: A[row, idx(i, j + 1)] = r u_inner = spsolve(csr_matrix(A), rhs) U = exact(x[:, None], y[None, :]).copy() for i in range(1, Nx - 1): for j in range(1, Ny - 1): U[i, j] = u_inner[idx(i, j)] return U

这段代码有一个细节:矩阵只包含内部节点,所有和边界邻居相关的项都被我移到了右端项 rhs。这样做的好处是矩阵维度小、边界条件清晰,不会出现“边界节点未知但方程不足”的问题。相比把边界节点也纳入未知量、再强行赋值的做法,这个方案更干净,排错也更方便。

4.3 跑通一遍以后看什么

我第一次跑通这段代码时,并没有急着去算误差,而是先做了三件事。

第一件,把数值解和解析解画在同一张图里,直接看等高线是否重合。这不是严格的定量分析,但能立刻发现波数是否错了、边界是否偏移、整体形态是否还在。人眼对空间形态的判断很快,很多低级错误在这一步就现原形了。

第二件,输出一下 k²、hx、hy、PPW 这几个关键数字。我实际算过几次以后发现,参数记录是一件看起来琐碎但极其重要的工作。你永远需要知道当前是在什么网格尺度、什么波数条件下得到的误差。

第三件,算一个残差,就是把数值解代回离散方程,看看右端项的绝对残差有多大。残差和“数值解与解析解的误差”是两个不同概念,前者衡量方程满足程度,后者衡量整体精度。初学者经常混淆,但做数值验证时最好两个都算一下,放在一起对比。

5. 实测结果与收敛性分析

5.1 网格加密实验设计

验证流程里最重要的一步,就是网格加密实验。我设定了四组网格,让 h 从 0.1 逐步减小到 0.0125。为了让长方形域上的 x、y 方向都能整除,我直接按 h 计算节点数量:

  • 当 h=0.1:Nx=14,Ny=12
  • 当 h=0.05:Nx=27,Ny=23
  • 当 h=0.025:Nx=53,Ny=45
  • 当 h=0.0125:Nx=105,Ny=89

每个网格下,我都用同一个 k 值和同一个解析解,算相对 L2 误差。这样算出来的误差变化趋势,才能真正反映离散格式的特性。我个人的习惯是先把所有参数写在一个表格里,再跑程序填充误差列,而不是边跑边记,那样太容易漏。

我在一个典型的运行里得到的数据大致如下:

网格间距 h节点规模 Nx×Ny每波长点数 PPW相对L2误差
0.114×126.40.220
0.0527×2312.80.052
0.02553×4525.60.0128
0.0125105×8951.20.0031

看到这个结果,我心里就有底了:误差确实在稳步下降,而且不是随便降的,是遵循某种规律在降。

5.2 相对误差与收敛阶的判别

对于五点差分格式,理论上应该有二阶收敛性,也就是说 L2 误差大致正比于 h²。怎么验证这个结论是否兑现?很简单,看相邻两组网格的误差比值。如果 h 减半时,误差大致变成原来的四分之一,那收敛阶就是 2。

上面表格里:

  • h 从 0.05 降到 0.025,误差从 0.052 降到 0.0128,比值约 4.06
  • h 从 0.025 降到 0.0125,误差从 0.0128 降到 0.0031,比值约 4.13

这两个比值都非常接近 4,说明我的离散实现确实达到了二阶精度。看到这种干净的结果,说明边界条件处理、矩阵组装、解析解代入这一整套流程里,没有出现明显的低级错误。

有一点需要说清楚:误差比并一定是精确的 4。当你把 h 减半时,网格本征模式也会变化,波数对应的色散关系也会微调,所以收敛阶会有一些摆动。只要比值在 3.5 到 4.5 之间,基本都可以接受。如果出现比值大于 8 或者小于 2,那就要警惕是不是有编程错误或者参数不匹配。

5.3 误差不降或偏大的几种信号

做网格加密实验,最怕的是误差曲线“平了”,也就是网格越加越密,误差却纹丝不动。这通常意味着误差来源根本不是空间离散精度,而是某个固定偏差。

我遇到过的几种典型信号是:误差在 0.1 附近卡住不上不下,一查发现是解析解里 k 算错了,导致方程压根不对;或者边界条件在某个角点没有按精确解赋值,导致整个解的基线偏移;还有一种是对角线系数符号写反,矩阵变成非对称且主对角占优性质丢失,求解器虽然没有报错,但结果完全不对。

这类问题靠“看代码”未必能快速发现,但跑一遍网格加密实验后,误差不下降的事实就会一下子暴露出来。这也是为什么我一直强调:验证流程不只是算一个解就算完事,必须做网格收敛性分析。

6. 验证路上最容易踩的坑

6.1 解出来全是零,别急着怀疑程序

前面提过,如果边界条件全是零,方程又是齐次的,那么数值解确实很可能全是零。第一次遇到这种情况的人,第一反应往往是检查矩阵、检查波数、检查网格,折腾半天发现代码完全没问题。

这个坑的本质是数学问题,不是程序问题。齐次亥姆霍兹方程只有在精确的特征值 k 下才有非零解。而离散网格上的实际特征值,也就是数值特征值,和连续问题的特征值存在偏差。你的 k 是按照连续解析解算出来的,并不和离散特征值严格相等,所以大多数情况下离散齐次系统的唯一解就是零。

解决办法我在前面讲过:不要用零边界的本征解去做验证,换一个边界条件非零的解析解。这不算投机取巧,而是制造解的通用做法,本质上是我们主动给问题加上一个“锚”,让数值系统有确定的非平凡解。

6.2 边界附近总差一点,多半是系数引错了

还有一次,我在边界附近观察到了明显的局部误差,但内部误差很小。排查了很久,最后发现是 y 方向边界的系数写错了:我在把 y 方向的邻居项移到右端时,忘了乘以 r 系数。因为当时网格恰好比较接近正方形,r≈1,所以这个错误没有引起整体性灾难,只在边界附近留下一点点偏差。

这种“局部偏差”特别容易误导人。你会觉得问题可能出在边界赋值上,或者以为是插值问题,但真正的根源往往是某个邻居系数在移项时漏乘了。我现在的习惯是,所有和边界相关的右端项,全部都显式写清楚系数,而不是靠心算。宁可让代码显得啰嗦,也要降低这类低级失误的概率。

6.3 大波数下的数值色散与污染误差

网格加密实验只能验证小到中等波数范围内的正确性,一旦 k 变大,情况会变得复杂。五点差分格式存在所谓的“污染误差”,意思是数值解误差并不单纯随 h² 下降,还会受到波数 k 的额外影响。

直观理解是:k 越大,越多波峰波谷挤进计算区域,五点格式对每个波长的相位刻画误差就会累积,整体效果类似在图像上叠了一层模糊滤镜。要让大波数问题算得准,光把网格加密还不够,你甚至需要让 h 小到每个波长有几十个点,这在实际三维问题中往往意味着天文数字般的节点规模。

所以,如果你以后要做高波数问题,请务必了解高阶格式、谱方法或有限元方法。在我的这个验证项目里,k≈9.84 并不算大,五点格式完全能胜任,但知道污染误差的存在,能帮你在设计更大规模实验时提前避开陷阱。

7. 这套验证还能往哪里延伸

7.1 换更高精度的离散格式

五点差分是最基础的格式,但验证思路完全可以直接移植到更高级的格式上。比如四阶紧致差分格式,它在同样的网格密度下能达到更高的精度;或者用有限元方法,把矩形网格换成三角形网格,边界也能适应更复杂的几何形状。

换格式以后,验证逻辑几乎不需要改动。你还是需要选解析解,算 k,组装离散矩阵,跑网格加密,看误差收敛阶。变化的是离散矩阵的带宽、非零元的分布、以及可能在边界处需要更多处理。我有一次把五点差分换成四阶格式,整个流程在一晚上就完成了,因为验证框架是现成的,只需要替换内部模板和系数。

7.2 往介质不均匀和三维扩展

如果你把 k 从常数改成随空间变化的函数,也就是介质不均匀的亥姆霍兹方程,那验证思路要更细致一些。解析解不再好写,你可能需要采用制造解的方法,人为构造一个函数,把它代入方程反推出一个源项,再去检验你的离散格式是否能精确求解这一个带有源项的问题。

往三维扩展时,矩阵规模会快速膨胀。比如 100×100×100 的网格,内部节点数接近一百万,稀疏矩阵的组装和迭代求解都需要更专业的手段。但验证原则不变,只是从“五点格式”变成“七点格式”,从直接求解变成预条件迭代。别被节点数量吓到,底层思路仍然是一样的。

我个人在这套验证流程里最深的体会是:凡是涉及偏微分方程的代码,都必须经历“解析解对照”这一关,否则你无法区分一个结果是“真对的”还是“看上去合理但实际错的”。网格加密实验加上相对误差分析,是一套低成本又极其有效的检验手段。

最后再分享一个小技巧:每次跑完验证,我会顺手把 k、h、PPW、误差这四个量用一行文本记录到结果文件里。看起来只是多打了一行字,但当你一个月后回头需要复现某个数据时,就会发现这笔记录省下来的时间远超当初付出的那一点成本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询