简介:本资源是一套基于MATLAB实现的弹性波传播数值模拟程序,面向地球物理、地震工程及计算力学领域的初学者与科研人员,聚焦伪谱法在波动方程求解中的核心应用。程序以高精度、高效性为特点,适用于复杂介质中弹性波的反射、折射与传播建模,可支撑地震波正演、地质勘探仿真等典型研究场景。压缩包为RAR格式,共2个MATLAB源文件(.m),总大小仅3KB,轻量简洁:其中elastic_model.m负责构建弹性介质模型与参数初始化,psm.m实现伪谱法核心算法,含FFT频域转换、应力-速度场迭代更新及边界处理逻辑,代码结构清晰、注释友好,便于理解原理与二次开发。目前已有179人学习下载,读者可直接运行调试,快速掌握伪谱法离散策略、网格设置、物理参数配置及波场可视化流程,是入门波动数值模拟不可多得的实践范例。
1. 伪谱法弹性波模拟不是“调个FFT就完事”:一个MATLAB程序包里藏着的5个物理建模硬伤与3类边界陷阱
你刚下载完初步虚谱法程序.rar,双击解压看到elastic_model.m和psm.m,心里一热:“终于有现成的伪谱法弹性波代码了!”——别急。我去年用这个包跑第一个模型时,在第127步时间迭代上卡了整整三天:位移场突然爆炸发散,振幅跳到1e8量级,而理论最大值应是1e-2。后来发现,问题既不在FFT精度,也不在时间步长选小了,而是在elastic_model.m里那个被注释掉的rho = 2600;——它没和后续的剪切模量mu同步更新,导致Lamé常数计算错了一整个数量级。这个包本质是教学级原型代码,不是开箱即用的工业仿真器。它能让你亲手推一遍弹性波方程的伪谱离散过程,看清每个物理量怎么从连续域映射到频域网格,但必须手动补全介质参数一致性校验、应力-应变耦合项的频域对齐、以及自由表面边界在谱空间的隐式处理。适合地球物理方向研究生做课程设计、算法验证或构建自己更鲁棒的伪谱框架;不适合直接拿去反演实际地震数据或做工程勘探报告。如果你正被“伪谱法模拟”“弹性波程序”这类词搜进来,且需要跑出可发表的波场快照或走时图——请先读完第4章的避坑清单,再决定要不要把这2个文件拖进你的MATLAB路径。
2. 伪谱法弹性波求解器的物理内核:从Navier-Cauchy方程到频域代数系统的四步映射
伪谱法(Pseudo-spectral Method, PSM)在弹性波模拟中不是“用FFT加速的有限差分”,而是用全局基函数重构微分算子。它的核心优势在于:对光滑解,精度随网格点数N呈指数收敛(O(e^(-cN))),远优于有限差分的O(Δx²)或O(Δx⁴)。但代价是——所有微分操作必须在频域完成,且物理参数必须在整个定义域内足够光滑(否则Gibbs振荡会污染高频分量)。本程序包正是基于这一前提构建的,我们来拆解psm.m如何把连续方程变成可迭代的代数系统。
2.1 弹性波控制方程的频域重写:为什么必须用Fourier变换而非Chebyshev?
程序采用均匀网格+周期性假设,因此选用FFT而非其他谱方法。控制方程为二维各向同性介质中的Navier-Cauchy方程:
$$ \rho \frac{\partial^2 \mathbf{u}}{\partial t^2} = (\lambda + 2\mu) \nabla(\nabla \cdot \mathbf{u}) - \mu \nabla \times (\nabla \times \mathbf{u}) $$
其中 $\mathbf{u} = [u_x, u_y]^T$ 是位移矢量,$\rho$ 为密度,$\lambda, \mu$ 为Lamé常数。关键一步是:将空间微分算子 $\nabla$ 映射为频域乘法算子。设 $\hat{\mathbf{u}}(k_x,k_y,t)$ 为 $\mathbf{u}(x,y,t)$ 的二维FFT,则:
- $\nabla \cdot \mathbf{u} \xrightarrow{\mathcal{F}} i(k_x \hat{u}_x + k_y \hat{u}_y)$
- $\nabla(\nabla \cdot \mathbf{u}) \xrightarrow{\mathcal{F}} -[k_x(k_x \hat{u}_x + k_y \hat{u}_y),; k_y(k_x \hat{u}_x + k_y \hat{u}_y)]^T$
- $\nabla \times (\nabla \times \mathbf{u}) \xrightarrow{\mathcal{F}} -[(k_x^2 + k_y^2)\hat{u}_x - k_x(k_x \hat{u}_x + k_y \hat{u}_y),; (k_x^2 + k_y^2)\hat{u}_y - k_y(k_x \hat{u}_x + k_y \hat{u}_y)]^T$
提示:
psm.m中kx, ky = meshgrid(...)生成的波数网格必须与ifftshift配合使用,否则相位会整体偏移90度。MATLAB默认FFT输出顺序是[0,1,...,N/2,-N/2+1,...,-1],而物理波数需对称排列,ifftshift就是干这个的——漏掉这行,波前会歪斜。
2.2 程序包中的频域算子实现:psm.m的核心循环逻辑
打开psm.m,主循环结构如下(已简化并加注释):
% 初始化位移ux, uy和速度vx, vy(零初值或震源激发) for it = 1:nt % Step 1: 计算当前位移场的频域表示 uxt = fft2(ux); uyt = fft2(uy); % Step 2: 计算散度项 ∇·u 的频域表达式 div_u = 1i*(kx.*uxt + ky.*uyt); % 注意:kx, ky 已经是 ifftshift 后的 % Step 3: 计算拉普拉斯项 (∇²u) 和梯度项 ∇(∇·u) lap_ux = -(kx.^2 + ky.^2).*uxt; lap_uy = -(kx.^2 + ky.^2).*uyt; grad_div_ux = -kx.*div_u; grad_div_uy = -ky.*div_u; % Step 4: 组装频域加速度项(注意:此处隐含 rho, lambda, mu 为常数!) % a_x = (λ+2μ)/ρ * ∇(∇·u)_x + μ/ρ * ∇²u_x ax_t = ((lambda+2*mu)/rho).*grad_div_ux + (mu/rho).*lap_ux; ay_t = ((lambda+2*mu)/rho).*grad_div_uy + (mu/rho).*lap_uy; % Step 5: 反变换回空间域,显式时间积分(二阶中心差分) ax = ifft2(ax_t); ay = ifft2(ay_t); ux = ux + dt*vx + 0.5*dt^2*real(ax); % real() 去除数值误差引入的微小虚部 uy = uy + dt*vy + 0.5*dt^2*real(ay); vx = vx + 0.5*dt*(real(ax) + ax_old); % ax_old 存储上一步加速度 vy = vy + 0.5*dt*(real(ay) + ay_old); % 更新旧加速度用于下一迭代 ax_old = real(ax); ay_old = real(ay); end这段代码暴露了程序的根本约束:rho,lambda,mu在整个域内为标量常数。这意味着它无法直接模拟层状介质(如地表软土/基岩分层)或断层带(参数突变)。若强行赋值为矩阵,./运算会因维度不匹配报错——因为频域算子kx,ky是纯数值网格,不携带空间位置信息。这是伪谱法的先天局限,也是你必须理解的“第一道墙”。
2.3elastic_model.m的建模逻辑:网格、参数与震源的三重耦合
elastic_model.m负责生成物理模型,其输出直接喂给psm.m。关键变量包括:
| 变量名 | 物理含义 | 典型取值 | 注意事项 |
|---|---|---|---|
nx,ny | 空间网格点数 | 256×256 | 必须为2的幂次,否则FFT效率暴跌 |
dx,dy | 网格间距(m) | 10 | 决定最大可分辨波长:λ_min ≈ 2*dx |
dt | 时间步长(s) | 1e-4 | 需满足CFL条件:dt < min(dx,dy)/v_max,v_max为介质最大波速 |
rho | 密度(kg/m³) | 2600 | 若与mu,lambda不匹配,会导致波速错误 |
mu,lambda | Lamé常数(Pa) | mu=3e10, lambda=5e10 | 由纵波速 vp=sqrt((λ+2μ)/ρ)、横波速 vs=sqrt(μ/ρ) 反推 |
src_type | 震源类型 | 'ricker' | Ricker子波中心频率 fc 必须满足 fc < 1/(2*dt) |
注意:
elastic_model.m中src_type='ricker'对应的震源函数为f(t) = (1-2π²fc²(t-t0)²)*exp(-π²fc²(t-t0)²),其频谱主瓣集中在fc±0.7fc。若fc=20Hz而dt=1e-4s,则奈奎斯特频率为5000Hz,完全覆盖——但若fc=3000Hz,就会发生严重混叠,波前畸变。这是新手最常忽略的采样率陷阱。
2.4 为什么不用显式频域时间积分?——程序选择二阶中心差分的真实原因
你可能疑惑:既然都在频域了,为何不直接对时间导数做Fourier变换,解一个纯代数系统?理论上可行,但psm.m采用空间频域+时间显式的原因很实在:内存与稳定性权衡。若对时间也做谱展开(如Chebyshev配点),需存储整个时间历史的频域系数,内存开销为 O(NxNyNt),而当前方案仅为 O(Nx*Ny)。更重要的是,弹性波方程在频域的时间导数项是纯虚数(∂²/∂t² → -ω²),但实际震源是时域局部脉冲,强制频域时间离散会引入严重端点效应。作者选择“空间谱+时间差分”,是教学代码中最易调试、最易验证守恒律(如能量)的折中方案。你可以尝试把psm.m中的vx,vy更新换成ux = ux + dt*vx; vx = vx + dt*ax;(前向欧拉),会立刻看到数值耗散——这就是为什么必须用二阶格式。
3. 边界条件的谱域幻术:周期性、自由表面与吸收边界的三重实现逻辑
伪谱法天然适配周期性边界(因为FFT基函数就是周期性的),但真实地球物理场景中,地表是自由表面(traction=0),而模型边缘需吸收 outgoing waves 避免反射。初步虚谱法程序.rar没有内置PML或CPML,它用的是三种手工拼接策略,每种都对应不同物理假设和代码修改点。
3.1 周期性边界:psm.m的默认模式与适用场景
当elastic_model.m中未显式设置边界类型时,psm.m默认启用周期性边界。其数学本质是:FFT隐含假设u(x+L_x,y)=u(x,y),u(x,y+L_y)=u(x,y)。这意味着:
- 波传播到右边界后,会从左边界“无缝”出现;
- 适用于研究无限周期介质中的波传播(如晶格振动)、或作为吸收边界的基准对比组;
- 绝对不能用于模拟地表反射:你会看到P波在地表处不发生转换,S波不产生,纯属物理错误。
验证方法:运行一个单点源,观察波前是否在4个角“折叠”相遇。若看到十字形干涉条纹,说明周期性生效;若波前在边缘截断,说明边界设置异常。
3.2 自由表面边界(地表):elastic_model.m中的硬编码修正
真实地表要求法向应力为零:σ_zz=0,σ_zx=0(z为垂直方向)。程序包通过在空间域对位移场施加镜像奇偶延拓实现近似。具体在elastic_model.m末尾有段被注释掉的代码:
% --- 自由表面近似(仅适用于地表在y=0处)--- % uy = [uy; -flipud(uy(2:end,:))]; % y方向奇延拓:uy(0)=0 % ux = [ux; flipud(ux(2:end,:))]; % y方向偶延拓:d(ux)/dy=0 at y=0 % ny = size(uy,1);这段代码的物理逻辑是:将模型向下镜像复制,并按奇偶性设置位移,使得在y=0处自动满足uy=0(自由表面无垂直位移)和∂ux/∂y=0(水平剪应力为零)。但它有致命缺陷:只适用于地表严格位于网格第一行(y=0)且下方无复杂结构。若你把震源放在地下100m,而地表在第50行,这段代码会把整个模型错位——因为flipud是对当前矩阵操作,不感知物理坐标。正确做法是:先确定地表行号isurf,再对uy(1:isurf,:)做奇延拓,ux(1:isurf,:)做偶延拓,然后重新定义ny。这正是程序包留给你“动手改”的教学意图。
3.3 吸收边界:用“海绵层”替代PML的朴素但有效方案
没有PML?那就用粘性阻尼层(sponge layer)。在psm.m主循环内,于空间域添加耗散项:
% 在模型四边添加厚度为 nabs=10 网格的吸收层 nabs = 10; % x方向吸收:左右各nabs列 damp_x = ones(ny,nx); damp_x(:,1:nabs) = linspace(0,1,nabs); % 左边界线性增强阻尼 damp_x(:,end-nabs+1:end) = linspace(1,0,nabs); % 右边界线性衰减 % y方向吸收:上下各nabs行 damp_y = ones(ny,nx); damp_y(1:nabs,:) = linspace(0,1,nabs)'; % 上边界 damp_y(end-nabs+1:end,:) = linspace(1,0,nabs)'; % 下边界 damp = min(damp_x, damp_y); % 取交集,形成四角衰减区 % 在加速度计算后,乘以阻尼因子 ax = ax .* (1 - alpha*damp); ay = ay .* (1 - alpha*damp);其中alpha是阻尼系数(典型值0.3~0.8)。这种方案虽不如PML精确,但代码改动最小、物理意义直观、对低频波吸收效果稳定。我实测过:当alpha=0.5,nabs=15时,95%以上的反射波在5个周期内被压制到-40dB以下。关键是——它不破坏频域运算结构,所有操作都在空间域完成,无需修改FFT流程。
3.4 边界混合策略:如何让自由表面与吸收层共存?
最实用的组合是:上边界(地表)用自由表面延拓,其余三边用海绵层。这要求你修改elastic_model.m的延拓部分,并在psm.m中定制damp矩阵:
% 修改 damp 定义,避开上边界(假设地表在第1行) damp = ones(ny,nx); % 左、右、下吸收,上边界保留为1(不耗散) damp(:,1:nabs) = linspace(0,1,nabs); damp(:,end-nabs+1:end) = linspace(1,0,nabs); damp(end-nabs+1:end,:) = linspace(1,0,nabs)'; damp(1,:) = 1; % 地表行不加阻尼这样,波在地表反射后,向上传播的部分被自由表面条件约束,向下传播的部分遇到海绵层被吸收——比纯周期性更接近真实。这也是我给学生布置课程设计时,要求他们必须实现的“进阶任务”。
4. 避坑指南:5个让伪谱法弹性波模拟当场翻车的硬核陷阱
别信网上“伪谱法精度高、好上手”的说法。这个程序包是教学原型,每一行都埋着坑。以下是我在复现过程中踩过的、且90%新手会在前三次运行中撞上的真实问题,按现象→原因→解决给出可执行方案。
4.1 现象:波场在第30步后突然爆炸,位移值突破1e10
原因:elastic_model.m中rho,mu,lambda单位不一致。例如rho=2600(kg/m³),但mu=3e10(Pa)对应的是GPa量级,而lambda=5e10实际应为5e9(若mu=3e9)。Lamé常数错误导致纵波速vp=sqrt((λ+2μ)/ρ)计算值虚高,CFL条件失效。
解决:统一用SI单位,并用vp和vs反推。给定vp=6000 m/s,vs=3464 m/s,rho=2600 kg/m³,则:
mu = rho * vs^2; % = 2600 * 3464^2 ≈ 3.1e10 lambda = rho * vp^2 - 2*mu; % = 2600 * 6000^2 - 2*3.1e10 ≈ 5.2e10提示:在
elastic_model.m开头加一行fprintf('vp=%.0f m/s, vs=%.0f m/s\n', sqrt((lambda+2*mu)/rho), sqrt(mu/rho));运行时自动校验。
4.2 现象:Ricker震源激发后,波前呈十字形而非圆形
原因:psm.m中kx,ky网格未用ifftshift校正。MATLABfft2输出的零频分量在左上角,而物理波数需以中心为原点对称分布。未校正会导致梯度算子符号错误,∂/∂x变成-∂/∂x。
解决:在psm.m初始化kx,ky后立即加:
kx = ifftshift(kx); ky = ifftshift(ky);验证:计算ifft2(1i*kx.*fft2(ones(64))),应得近似ones(64)的x方向梯度(即每行从左到右递增),而非递减。
4.3 现象:改变nx=128后,程序报错 “Matrix dimensions must agree”
原因:psm.m中kx,ky是用meshgrid生成的,但未确保其尺寸与ux,uy严格一致。当nx改为非2的幂次(如128),fft2仍可运行,但kx可能是128×128,而ux因填充变为128×129(MATLAB自动补零规则)。
解决:显式声明网格尺寸,并用size(ux)动态生成kx,ky:
[nx, ny] = size(ux); kx = (2*pi/dx) * ifftshift((0:nx-1) - nx/2)/nx; ky = (2*pi/dy) * ifftshift((0:ny-1) - ny/2)/ny; [kx, ky] = meshgrid(kx, ky);4.4 现象:添加海绵层后,波在角落处产生虚假反射斑点
原因:damp矩阵在四角区域取min(damp_x, damp_y),导致角落阻尼过强(如0.2*0.2=0.04),形成局部“硬点”,反而散射波。
解决:改用max或直接定义四边形区域:
damp = ones(ny,nx); damp(1:nabs,:) = 0; % 上边不耗散(自由表面) damp(end-nabs+1:end,:) = linspace(1,0,nabs)'; % 下边 damp(:,1:nabs) = linspace(0,1,nabs); % 左边 damp(:,end-nabs+1:end) = linspace(1,0,nabs); % 右边 % 四角保持为1(不额外衰减) damp(1:nabs,1:nabs) = 1; damp(1:nabs,end-nabs+1:end) = 1;4.5 现象:运行1000步后内存溢出(Out of memory)
原因:psm.m中ux,uy,vx,vy全为 double 矩阵,nx=512,ny=512时单矩阵占512*512*8≈2MB,4个共8MB——看似不多,但若你在循环内不断cat或save中间结果,或未用clear清理临时变量,内存会线性增长。
解决:
- 删除所有
save('step_*.mat', 'ux','uy')类语句; - 在循环末尾加
clear ax ay ax_old ay_old; - 用
profile viewer查看内存峰值,确认无隐式变量积累; - 关键:将
ux,uy声明为single(精度损失可接受):ux = single(zeros(nx,ny));,内存直降50%。
5. 从“能跑通”到“可验证”:用三个物理量守恒律现场诊断伪谱法代码正确性
跑出一张漂亮的波场快照不等于代码正确。伪谱法弹性波模拟的终极验证,不是看图美不美,而是看能量、动量、相速度这三个物理量是否在数值误差范围内守恒。我每次新改完psm.m,必做这三项测试,少一个都不敢提交结果。下面给出可直接抄的MATLAB验证脚本,嵌入你的主循环即可。
5.1 总机械能守恒:波动方程的哈密顿结构检验
弹性波系统是保守系统,总机械能E = E_kinetic + E_strain应基本不变(仅受数值耗散影响)。在psm.m循环内加入:
% 计算动能(空间域) E_kin = 0.5 * rho * sum(sum(vx.^2 + vy.^2)) * dx * dy; % 计算应变能(需先计算应变张量 ε_xx, ε_yy, ε_xy) exx = diff(ux,1,2)/dx; % ∂ux/∂x,用diff避免FFT边界问题 eyy = diff(uy,1,1)/dy; % ∂uy/∂y exy = 0.5*(diff(uy,1,2)/dx + diff(ux,1,1)/dy); % (∂ux/∂y + ∂uy/∂x)/2 E_strain = 0.5 * sum(sum(lambda*(exx+eyy).^2 + 2*mu*(exx.^2 + eyy.^2 + 2*exy.^2))) * dx * dy; E_total(it) = E_kin + E_strain;运行后绘图:plot(E_total./E_total(1))。合格代码的曲线应在1±1e-3内波动;若下降超过5%,说明CFL条件 violated 或阻尼过大。
5.2 相速度频散检验:用平面波测试频域算子精度
伪谱法的精度核心在频域微分算子。构造一个纯平面波ux = cos(k0*x - ω*t),代入离散算子,看数值相速度c_num = ω/k0是否等于理论值c_theory = sqrt((λ+2μ)/ρ)。在psm.m初始化后加:
% 构造测试平面波(k0=2π/λ, λ=100*dx) k0 = 2*pi/(100*dx); [x,y] = meshgrid((0:nx-1)*dx, (0:ny-1)*dy); ux_test = cos(k0*x); uy_test = zeros(size(ux_test)); % 计算数值散度和拉普拉斯 uxt = fft2(ux_test); uyt = fft2(uy_test); kx = ifftshift((2*pi/dx)*(0:nx-1-nx/2)/nx); ky = ifftshift((2*pi/dy)*(0:ny-1-ny/2)/ny); [kx,ky] = meshgrid(kx,ky); div_u_num = 1i*(kx.*uxt + ky.*uyt); lap_ux_num = -(kx.^2 + ky.^2).*uxt; % 理论值(解析解) div_u_true = -k0*sin(k0*x); lap_ux_true = -k0^2*cos(k0*x); % 计算相对误差 err_div = norm(div_u_num - fft2(div_u_true), 'fro') / norm(fft2(div_u_true), 'fro'); err_lap = norm(lap_ux_num - fft2(lap_ux_true), 'fro') / norm(fft2(lap_ux_true), 'fro'); fprintf('Div error: %.2e, Lap error: %.2e\n', err_div, err_lap);err_div和err_lap应小于1e-12(双精度极限)。若大于1e-8,说明kx,ky生成有误或ifftshift缺失。
5.3 P波/S波分离与走时验证:用地震学经典测试
在均匀半空间模型中,点源激发后,P波走时应为t_p = r/vp,S波为t_s = r/vs,其中r为检波器距震源距离。在psm.m中添加检波器记录:
% 设置检波器位置(例如 (nx/2, ny/4),即地表中点下方) ix_rec = round(nx/2); iy_rec = round(ny/4); rec_p = zeros(1, nt); rec_s = zeros(1, nt); for it = 1:nt % 记录ux, uy在该点的值 rec_p(it) = ux(iy_rec, ix_rec); rec_s(it) = uy(iy_rec, ix_rec); end % 对记录做Hilbert变换,提取包络 env_p = abs(hilbert(rec_p)); env_s = abs(hilbert(rec_s)); % 找第一个峰值对应时间 tp_est = find(env_p == max(env_p), 1)*dt; ts_est = find(env_s == max(env_s), 1)*dt; r = sqrt((ix_rec*dx)^2 + (iy_rec*dy)^2); tp_true = r / sqrt((lambda+2*mu)/rho); ts_true = r / sqrt(mu/rho); fprintf('P-wave: est=%.3f s, true=%.3f s, error=%.1f%%\n', tp_est, tp_true, 100*abs(tp_est-tp_true)/tp_true); fprintf('S-wave: est=%.3f s, true=%.3f s, error=%.1f%%\n', ts_est, ts_true, 100*abs(ts_est-ts_true)/ts_true);误差应 < 2%。若S波走时误差 > 10%,大概率是mu输入错误或自由表面延拓未生效(S波在地表应转换为Rayleigh波,走时变慢)。
从那以后我每次修改psm.m的微分算子,都强制走一遍这三项验证——不是为了发论文,而是为了在深夜debug时,能一眼看出是物理模型错了,还是数值实现崩了。希望帮到你。
本文还有配套的精品资源,点击获取