ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于MATLAB的VTI介质弹性波高阶交错网格正演及PML吸收边界实现

基于MATLAB的VTI介质弹性波高阶交错网格正演及PML吸收边界实现 简介本资源是一套面向地球物理勘探与计算地球科学方向的MATLAB正演模拟工具专为电子信息工程、数学及计算机专业本科生课程设计、毕业设计等实践环节开发解决VTI介质中弹性波传播的高精度数值模拟问题。压缩包仅含2个文件1个核心MATLAB脚本.m 1张运行结果图.png总大小16KB轻量易用适配MATLAB 2014a至2021a多个版本。已有154人学习下载说明其在教学实践与算法验证场景中具备较强实用性。用户可直接运行主程序获得含PML吸收边界条件的高阶交错网格有限差分正演结果代码采用参数化设计介质参数、网格步长、时间步进等关键变量均集中定义并附详细中文注释逻辑清晰、易于修改与拓展特别适合初学者理解弹性波方程离散化原理及边界处理技术。 前两天从网盘里拉下来一个压缩包名字叫“基于matlab模拟有限差分VTI介质弹性波方程的高阶交错网格正演pml吸收边界条件.zip”。我本来觉得这类代码包十个里有八个是跑不起来的课程作业结果解压之后花了点时间梳理发现这套程序把地震波正演里的几块硬骨头——VTI各向异性介质、高阶交错网格有限差分、PML吸收边界——串得很完整。这篇文章就把这套程序的原理、实现和调参细节拆开聊聊给正在做地震波正演、各向异性介质模拟或者打算用MATLAB做数值模拟的同学一个参考。作为地震正演里最常用的数值手段之一有限差分法在波动方程求解中的地位不用多讲。但真正上手写一套能用的程序和上课考试完全是两码事。很多人卡在几个地方为什么介质要分各向同性和VTI交错网格到底好在哪高阶系数怎么算PML层为什么总在地边界反射这几点搞不明白程序跑出来的结果就没法信。这篇文章会把这些都铺开说清楚。1. 这个项目到底在解决什么问题1.1 VTI介质是什么为什么值得专门做正演VTIVertical Transversely Isotropic垂直横向各向同性介质是地震勘探里最常见的各向异性介质模型。你可以把它想象成一本叠起来的书水平方向书页排列都一样垂直方向则有明显的层理结构。页岩、薄互层、裂缝发育地层在地震波传播上都会表现出这种“水平方向性质相同、垂直方向性质不同”的特征。在VTI介质里弹性波传播速度和方向有关。纵波速度不再是单独一个数值而是由垂直方向和水平方向两个基准速度加上各向异性参数共同决定。如果做正演的时候还拿各向同性公式硬套炮集记录里的走时会出现系统性偏差偏移成像位置会错位这是实际生产中不能接受的误差。这套程序里用的参数体系通常不是直接拿刚度矩阵C11、C13、C33、C44硬算而是用Thomsen提出的各向异性参数。Vp0是垂直方向纵波速度Vs0是垂直方向横波速度ε描述纵波各向异性强度δ控制近垂直方向的速度变化率。理解这几个参数是读懂整个程序的第一步。1.2 为什么选弹性波而不是声波很多初学正演的人第一反应是先做声波方程简单跑得快。但声波方程对VTI介质是先天不足的。VTI介质中P波和SV波是耦合的两者在边界和界面处互相转化声波方程根本描述不了这种转换现象。如果目标是研究转换波、横波分裂或者各向异性参数反演就必须上弹性波方程。代价也很直观弹性波方程涉及的速度场分量vx、vz和应力场分量τxx、τxz、τzz加起来五个波场变量内存和计算量大概比声波方程高一个量级。这个程序的做法是用速度-应力一阶方程组也就是把弹性波方程的二阶位移形式拆成速度和应力的一阶偏微分方程组这样方便用交错网格进行时间和空间的同时推进。二维VTI介质中平面内只有P-SV波系统弹性刚度矩阵里C66其实用不到。三维情况下SH波才会牵出C66。所以你看程序里刚度系数更新只操作C11、C13、C33、C44四个量就是这个道理。这个细节很多人会漏以为二维程序也要给六个刚度系数。2. 交错网格与高阶有限差分的核心原理2.1 交错网格的空间配置交错网格Staggered Grid的思路说穿了就是“你有的我正好没有我有的你正好没有”。速度分量和应力分量在空间上错开半个网格点放置。以二维模型为例vx定义在格点(nz, nx)的右边界vz定义在上边界法向应力τxx、τzz定义在格点中心切向应力τxz定义在格点的左上角。这样安排的最大好处是每个空间导数都只需要做一次半个网格点的差分不需要插值。比如∂τxx/∂xτxx在格点中心而vx正好在两个τxx的中点所以这个导数天然就是(vx右边减去vx左边)。差分模板的误差是O(h²)起步如果用高阶差分系数还可以进一步压低误差。对比一下普通网格如果所有变量都在同一位置要计算一个变量在另一个变量位置上的值就不得不做插值。插值本身会引入额外的数值误差还会扩大差分模板的有效长度在边界处更麻烦。所以地震波正演现在几乎清一色用交错网格不是没有原因的。在时间推进上交错网格同样采用半时间步速度场在n1/2时刻更新应力场在n1时刻更新两者互相利用对方在“中间时刻”的旧值。这样只需要存储相邻时刻的波场不需要像龙格库塔那样存多级中间量内存压力小很多。2.2 高阶差分系数怎么算普通二阶差分就是对导数用一个三点模板近似。高阶有限差分则用更多点让每个点贡献一个加权系数组合起来使得截断误差达到更小的阶数。程序里最常见的配置是时间二阶、空间十阶或者时间二阶、空间十二阶。空间阶数提高之后同样的网格剖分下频散更小模拟出来的波前面更加光滑。交错网格的高阶差分系数形式上是用泰勒展开求出来的。以x方向一阶导数为例假设用2M个半网格点的加权和来逼近中心点导数系数a_m满足Σ a_m * (m - 0.5) 1Σ a_m * (m - 0.5)^(2j-1) 0j 2, 3, ..., M由于交错网格的采样点只在半整数位置偶数阶矩自动为零所以只需要解一个M×M的线性方程组。这个方程组完全可以在MATLAB里用一行代码解出来function a fd_coeff(M) A zeros(M, M); b zeros(M, 1); for j 1:M b(j) (j 1); for k 1:M A(j, k) (k - 0.5)^(2*j - 1); end end a A \ b; end调用一下fd_coeff(2)得到四阶系数[1.125, -0.0417]fd_coeff(5)得到十阶系数。把这个函数放到程序里就不用查表了想换几阶精度随时改。系数求出来之后空间导数算子就是把这些系数应用到更新公式里比如速度对x的偏导∂vx/∂x ≈ (1/dh) * Σ_{m1}^{M} a_m * (vx_at_x_plus - vx_at_x_minus)注意半网格点的索引关系写矩阵切片的时候特别容易错位。我在调试这套程序的时候至少有两次跑出来波场像长了毛的刺猬最后检查都是索引方向写反了。2.3 时间推进与稳定性条件时间上一般用二阶精度也就是常规的前向/后向差分蛙跳格式。每个时间步里先由旧应力场更新速度场再由新速度场更新应力场。虽然时间只有二阶但因为时间步长dt通常取得比空间步长小一个量级实际数值误差主要来自空间差分。稳定性条件是正演模拟绕不开的硬约束。交错网格下二维弹性波方程的CFL条件近似写作v_max * dt / dh ≤ 1/√(2)这里的v_max是模型中的最大波速dh是空间步长。对于各向异性介质v_max至少要取最大相速度方向的准纵波速度比垂直方向的Vp0可能高出不少。实际操作里我习惯留足余量把CFL数控制在0.5左右。比如模型最大速度v_max3000 m/s空间步长dh5 m那么dt ≤ 0.5 * dh / v_max 0.5 * 5 / 3000 ≈ 0.000833 s取dt0.5 ms保证稳定。程序默认参数如果跑发散第一件事就是检查这个比值而不是去调整什么边界条件。3. PML吸收边界从公式到代码3.1 拉伸坐标系的思想人工边界反射是有限差分正演里最让人头痛的问题。模型四周截断了波传到边界如果不做处理会原路弹回来把真实波场盖得什么都看不清。PMLPerfectly Matched Layer完美匹配层的原理是在模型外部加一圈特殊介质让波进入这个区域之后被指数衰减并且在PML与内部介质的分界面上尽量不产生反射。PML在数学上的解释有几种最具操作性的是复坐标拉伸。简单说把偏微分方程中的空间坐标x换成它的复函数x̃ x - (i/ω) ∫ d(x) dxi是虚数单位ω是角频率d(x)是随位置变化的衰减系数。这样做的效果是进入PML区域的波场在频域上自然带上一个指数衰减因子波还没到达截断边界能量已经基本耗尽。在时域实现里这个拉伸会引入辅助变量。程序里最常见的版本是CPML卷积PML它对不同方向的导数分别做记忆变量卷积更新。比如x方向的导数项∂/∂x 的空间差分结果叠加一个记忆变量ψψ的更新公式为ψ^{n1} b_x * ψ^n a_x * (∂/∂x的差分结果)b_x exp(-(d_x α_x) * dt)a_x d_x * (b_x - 1) / (d_x α_x)其中d_x是衰减函数α_x是一个很小的正数用来保证极低频情况下的稳定性。每个方向各自维护一组记忆变量最后应力更新的导数项等于原始差分加上记忆变量PML就实现了。3.2 衰减函数怎么选衰减函数d(x)通常不是常数而是在PML层内从分界面处的0逐渐增大到外边界处的最大值d_max。程序里最常用的是二次渐变和三次渐变二次渐变已经够用三次渐变更稳一点。d(x) d_max * (x / L_pml)^2这里x是当前点到PML内边界的距离L_pml是PML层的物理厚度。d_max的经验公式d_max - (M 1) * v_max * ln(R) / (2 * L_pml)M是渐变幂次R是理论反射系数通常取0.001。举个例子v_max3000 m/sL_pml100 m20个5 m网格M2R0.001d_max 3 * 3000 * ln(1000) / 200 ≈ 3 * 3000 * 6.9 / 200 ≈ 310.5这个值大概在几百量级是正常的。如果d_max给得太大PML区和内部区域的分界面会有虚假反射给得太小边界吸收不干净波会从截断边界反弹回来。3.3 PML更新实现要点实现PML的时候最容易踩的坑是角点处两个方向的衰减同时生效。在PML的角部区域x方向和z方向都要施加阻尼所以那里需要同时维护两组记忆变量不能只做单方向处理。有些简化程序为了省事把角点直接置零波到了角落还是会被反射效果很差。程序里通常配置的PML厚度是10到30个网格点。厚度太小吸收效果不理想厚度太大计算量白白增加。我一般用20个点左右乘以空间步长就是实际的吸收层宽度。检查PML是否有效最简单的方法是跑一个均匀介质模型观察波场快照的边界区域有没有明显的二次弧状反射波或者把边界处的道集振幅调出来看它们是不是比内部道逐级衰减到很小。PML衰减函数要从零开始渐变千万不要在PML入口处直接给一个大的d值。这种突变等同于人为设置了一个反射界面边界反射比不设置PML还严重。4. 完整正演流程拆解4.1 模型参数与网格设计一套典型参数的二维VTI模型可以这样定模型大小400×400网格点dh5 mdt0.5 ms总时间2 s中心频率f025 Hz。介质参数用Thomsen参数表示时先给一组基准值Vp02500 m/sVs01400 m/sε0.2δ0.05ρ2200 kg/m³。这组参数需要转换成波场更新用的刚度系数。在MATLAB里可以这样算Vp0 2500; Vs0 1400; epsilon 0.2; delta 0.05; rho 2200; C33 rho * Vp0^2; C44 rho * Vs0^2; C11 C33 * (1 2 * epsilon); C13 sqrt((C33 - C44)^2 2 * delta * C33 * (C33 - C44)) - C44;算出来的值大致在以下量级C33约1.375×10^10 PaC44约4.312×10^9 PaC11约1.925×10^10 PaC13约5.79×10^9 Pa。给刚度系数赋值的顺序直接影响程序的运行效率MATLAB里把这些系数先构建成矩阵每个网格点一份再参与向量化运算比在循环里反复计算要快得多。网格参数设计要同时考虑稳定性和频散。最大波速是各向异性介质中沿水平方向传播的准P波速度取Vp_horiz ≈ Vp0 * sqrt(1 2ε) 2500 * sqrt(1.4) ≈ 2958 m/s。CFL条件检查下来0.5 ms的dt是安全的。4.2 震源设置震源加载方式直接影响波场的激励类型。程序里一般用雷克子波Ricker wavelet作为时间函数s(t) (1 - 2π² f0² (t - t0)²) * exp(-π² f0² (t - t0)²)t0取1.2/f0左右保证子波起始段接近零。中心频率f025 Hz时t00.048 s对应时间步在约96步时开始发力。在交错网格里震源的加载位置也要注意分量对应关系。如果是压力型震源就加在应力分量τxx、τzz上如果是力源就加在速度分量vx或vz上如果做的是爆炸源模拟一般是在σxx和σzz上同时加一个负的震源时间函数。程序里常见的是加载在vz分量上模拟垂直力源或者加载在τxx、τzz上模拟爆炸源具体看你要对比什么资料。加载格式上要注意震源项加到波场更新公式的右端时单位要换算对。速度-应力方程中力的单位是加速度所以震源项需要除以密度再乘以dt。如果直接拿子波值加进去振幅比例全乱了后期做波场快照会看不出有效信息。模拟中我用到的做法是src (1 - 2*pi^2*f0^2*(t - t0)^2) * exp(-pi^2*f0^2*(t - t0)^2); vz(isrc, jsrc) vz(isrc, jsrc) src * dt / rho(isrc, jsrc);这样源项的量纲才正确。4.3 主循环实现主循环是程序的灵魂结构上是“先速度后应力先内部网格后边界PML”。更新每个变量时先用内部区域的交错差分公式在二维矩阵切片上做向量化运算再单独处理PML区域。下面是一个简化的速度场vx更新代码片段省略PML部分% 更新 vx % Dx_tauxx: x方向 tauxx 导数 Dx_tauxx (1/dh) * ( sum_a * (tauxx(nz, 2:nx1) - tauxx(nz, 1:nx)) ); % 这里 sum_a 表示用高阶系数做加权叠加需要展开成多阶循环或矩阵乘法 % Dz_tauxz: z方向 tauxz 导数 vx(2:nz1, 2:nx) vx(2:nz1, 2:nx) ... dt/rho(2:nz1, 2:nx) .* (Dx_tauxx Dz_tauxz);实际写的时候高阶差分系数展开成M个相邻点的加权求和代码会比较长。一个实用的做法是写一个通用函数diff_staggered(field, axis, coeff)内部用circshift或者矩阵切片完成半网格点差分但需要注意边界范围。用circshift有个麻烦它会循环填充边界必须在循环之后把PML区域正确覆盖掉。应力更新公式与之对称。比如τxx的更新需要vx对x的导数和vz对z的导数∂τxx/∂t C11 ∂vx/∂x C13 ∂vz/∂z注意C11和C13在VTI介质里是不同的刚度值各向同性情况下C11C33λ2μC13λ程序就退化到各向同性了。这也是验证程序正确性的一个重要手段。PML区域的更新建议单独用一个循环块处理。因为PML内部的记忆变量更新和主区域的差分格式不同如果把两者混在一个向量化表达式里代码可读性差也容易出错。我的做法是主区内层用向量化PML边界用循环或者部分向量化两者分得很清楚。4.4 波场快照与炮集输出正演的结果一般有两种输出方式一是动态波场快照即固定时刻观察整个模型区域的波场分布二是固定检波器位置记录时间序列形成单炮记录。程序里这两者都要做。动态波场快照在MATLAB里很简单每若干时间步用imagesc画一帧稍作停顿再更新。这样你就能亲眼看到P波和SV波在界面的转换、波前在PML区域的衰减过程非常直观。我习惯每20个时间步存一帧最后合成GIF或者AVI方便反复观察。单炮记录则是把每个时间步在预设检波器位置上的vz或者vx分量抽出来存成一个nt × nrec的矩阵。对于二维VTI模型检波器一般布设在模型表层间距与网格间距一致。输出前要注意把有效分量选择好——如果震源是垂直力源vz记录里的P波和SV波到时信息比较清晰vx则可以观察横波偏振特征。5. 常见问题与调参经验5.1 波场发散、频散这些老问题正演跑着跑着波场突然变成满屏噪声通常是CFL条件被打破dt超出稳定上限。此时把dt调小一半再观察是否恢复稳定。另一个可能原因是刚度系数赋值错误比如C13算出来是负数或者各向异性参数过大导致相速度出现非物理数值。检查方法是在均匀介质中跑一次小的模型看波前是否光滑波形低频是否正常。网格频散表现为波场尾部拖着一条条振铃状尾巴看起来像“梳子齿”。解决思路是提高空间差分阶数或者加密空间网格。如果程序本来就是十阶差分还是明显频散那大概率是空间步长太大——一个波长至少要覆盖5到10个网格点。观测最小波长λ_min v_min / f_maxf_max按震源主频的两三倍估算然后反推dh。5.2 PML参数没调好的表现PML调参问题比稳定性问题更隐蔽。边界反射的典型表现是在波场快照的四周能看到与主波前同心但晚到的圆弧状波场振幅逐渐向内扩散。如果出现这种情况优先检查PML层厚度和d_max。PML厚度太小比如只有5个点时即使d_max再大也不容易压住低频段的反射。另一个常见问题是PML区和内部区域分界处参数不连续在分界线上产生一条明显的亮线。这是d值一开始就给得太大造成的。把渐变函数改为从0开始的三次幂函数情况会好很多。5.3 MATLAB性能优化MATLAB的循环是出了名的慢主循环如果写成双层for循环400×400网格、4000个时间步可能要跑到天荒地老。实测经验是先把模型网格double类型改成single类型内存占用减半计算速度也有提升。然后把内部区域的波场更新全都改成矩阵切片运算MATLAB对这类向量化操作优化得非常好。如果要做多炮正演可以在外部对每一炮使用parfor并行此时每炮内部的循环仍用普通for或者向量化。但要注意parfor下随机数生成和文件写入的顺序问题最好每炮独立保存最后再合并。在MATLAB R2022b上如果运行报错可以先检查是不是并行池占用内存过高关掉并行池再跑很多时候“Error 9”一类的报错都跟资源不足有关。虚拟机上跑MATLAB的性能损失真的很大一个物理机半分钟完成的模型虚拟机能拖到好几分钟。如果这只是偶尔跑一次忍一忍也就算了如果要做参数扫描和批量模拟建议直接装原生Linux或者Windows版本不要再叠一层虚拟化。5.4 验证程序正确性程序跑通只是第一步跑得对不对才是关键。我的调试顺序是这样的先把各向异性参数全部设为0εδ0让程序退化成各向同性弹性波正演。在均匀各向同性介质中P波波前是一个完美的圆SV波波前是另一个小圆两个圆同心嵌套。如果快照上看到这两个圆说明应力-速度更新和震源加载基本正确。接着只在模型左半部分设置一个垂直界面右侧改成另一种介质看反射波和透射波能不能正确产生界面处有没有数值伪影。最后再加入VTI各向异性参数观察准P波波前是否从圆形变成椭圆形状。椭圆长轴方向和振幅大小随ε、δ的变化趋势要和各向异性介质理论吻合。在做这些验证的时候记录每一组实验的固定时间步波场快照累积成一组“基准图”。以后程序改动引入新bug拿新的快照和基准图对比能很快定位问题。这套方法论比跑到一半对着数值发呆高效得多。根据我个人实际跑这套程序的体会最值得改进的地方是PML区域的代码结构。程序里把PML更新和主区更新混在一起读起来费劲调试时也不够灵活。如果你只是拿这套程序做基础研究或者课程作业建议先保持原样跑通再按自己的需求把PML部分拆出来单独维护。这个压缩包里的代码给了一个很好的起步框架剩下的可以按你的模型和地质场景去扩展——比如加上自由地表边界、加入地形起伏替换成TTI介质的倾斜对称轴一步步把正演工具往前推。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进