数值求解波动方程 [4]
这篇里讲述了另外一种求得 PML 的方法以及简单实现二维波动方程的数值模拟.

另一个 PML 求解
在上一篇求一维 PML 时, PML 变换为 , 以及在假设
时得到关系
, 并且在最后恰好全部虚数单位
都被抵消, 留下一个纯实数 (复数也可以) 的方程. 在部分情况下, 需要施加 PML 变换的波动方程比较复杂 (比如强迫振动或粘度等), 造成虚数单位无法恰好被消除, 留下一个需要在复数域上求解的方程. 也不是说数值求解复数方程不可以, 只不过考虑到内存和计算开销, 复数真的不太行.
实际上, 考虑傅里叶变换 , 无论
如何都有
, 这与上面提到的关系是等价的. 另外再考虑拉普拉斯变换
, 除了积分域和定义域不一样外, 它跟傅里叶变换是相同的. 在波动方程里, 不需要太关心
部分 (这里只讨论正演而忽略反演), 那么把
替换掉所有出现的频率得到新的 PML 变换
和关系
. 下面继续以一维波动方程做例子:
对一维波动方程 施加拉普拉斯变换得到
, 即
(偏微分算符和拉普拉斯变换的线性), 这个就是波动方程在 s 域的形式. 对 s 域的波动方程施加 PML 变换得
, 两边同乘
得
. 引入新量
使得
, 即得到
, 把方程组从 s 域转为时域得
, 可以看到这与之前得到的一模一样.
尽管两个方法得到的结果都是一样的, 但在 s 域上处理 PML 变换是非常通用的, 至少不需要假设一些条件.



二维波动方程
二维波动方程为 , 其中
为所求波的解. 习惯上使用
作为二维笛卡尔坐标, 但是 unicode 没有下标 y, 所以这里使用
作为坐标了 (方便以后的编程实现). 跟一维波动方程类似, 二维波动方程离散化为
有一说一, 确实有点长了. 整理后得到
其中 . 在离散化后还需要截断计算区域, 截断方式与一维的一样, 但只不过现在不管是截断一个维度, 而是两个. 但是值得留意的是, 边界围成的形状不一定只能是正方形长方形, 但奇形怪状的边界很多时候需要额外计算边界处的离散化, 所以这里只展示正方形的边界了.

二维实现
在模拟二维波动方程里, 有一个比较大的问题就是计算速度, 因为网格大小会随着空间精度和大小的增大呈平方上升, 所以与其放在 cpu 上面跑, 不如放在 gpu 上面.
julia 的 cuda 支持在 https://cuda.juliagpu.org/stable 有详细记述. 一般来说有显卡驱动时直接在 REPL 里 `]add CUDA` 就可以安装了, 并且 julia 函数可以编译为 cuda 代码, 而不用像 python 那样在字符串里写 C++ CUDA 函数. 下面是 Wave2D 相关的实现, 在代码里上下边界使用了 Neumann 边界, 而左右边界则是循环边界.

不过在渲染 gif 的时候无论如何都不能把文件大小降下来, 明明隔壁 mp4 都超级小了, 怎么 gif 就那么大 (mp4: 211K, gif: 13.8M). 所以下面贴图片算了, 但是代码是直接渲染视频的.

观察到图中, 波包在空间扩散时, 在后面留下了不为零的"尾迹", 并且尾迹不会消失 (可以参考《数理方程》) , 亦即不符合 Huggens 原理, 这种现象称为波的弥散或有后效现象, 并且这是偶数维度空间特有的. 下图是沿轴方向的切片, 可以更直观地看到这个现象:


在上面的代码实现里, 已经包括了不均匀介质, 下图为空间中心有一个半径为 0.1 的实心玻璃圆.

渲染的代码稍微有点长, 所以这里单独拿出来贴一份:


数值稳定性
因为二维波动方程有三个维度 (两个空间一个时间), 那么应该讨论三个维度的数值稳定性. 据自己的观察经验, 当 时,
会造成空间数值不稳定, 否则
是空间数值不稳定 (? 我也不知道为何). 在空间维度上,
是衡量时间不稳定的指标, 与一维波动方程类似, 这两个数值在
时会在相应维度上产生时间不稳定, 但必须确保全局上不能有空间不稳定, 即
, 所以任何维度都不能不产生时间不稳定.
尽管在任何维度上必定是时间不稳定的, 但是当 时, 在空间对角线上是数值稳定的, 如下图所示:


为了应对这种情况, 可以选择具有复杂网格的实现方式, 比如旋转网格或自适应网格等. 但这些技术的轮子实在不太好搓, 这里就 pass 了.