Marmousi地震正演MATLAB实现与有限差分实战

发布时间:2026/10/25 20:18:03
Marmousi地震正演MATLAB实现与有限差分实战

简介:本资源是一套面向数学建模学习者与地球物理方向本科生/研究生的MATLAB地震波模拟实践材料,聚焦Marmousi经典速度模型的数值建模与可视化,解决地震勘探中波场传播仿真、成像算法验证等核心问题。压缩包共13个文件,含5幅JPG格式运行结果图(直观展示不同时刻波场快照)、5个DAT数据文件(存储中间波场与速度模型)、1个主程序marmousi.m、1个FIG图形文件(可直接用MATLAB打开复现绘图)及1个TXT说明文档,整体6.81MB,结构精炼、即开即用。已有1315人学习下载,读者可直接运行源码复现二维波动方程有限差分求解全过程,获得从模型构建、边界设置、时间步进到动态波场可视化的完整技术链路,并配套多组对比结果图与数据文件,便于理解速度非均匀性引发的折射/反射现象,支撑课程设计、竞赛建模或科研入门。

1. 为什么用 Marmousi 模型跑地震波正演,MATLAB 不是“凑数工具”而是关键推演引擎?

你手头有一份标着【含Matlab源码 1977期】的压缩包,解压后看到marmousi_model.mat、forward_simulation.m、plot_wavefield.m这类文件——这不是教学演示动画,也不是课程作业模板。这是真实地震勘探中最经典、最严苛的二维速度建模基准:Marmousi 模型,由法国石油研究院(IFPEN)在1990年代初构建,包含复杂断层、盐丘、褶皱和强速度梯度变化,至今仍是工业界验证偏移成像、全波形反演(FWI)、逆时偏移(RTM)算法的“试金石”。而这份 MATLAB 实现,不是调用几个plot()就完事的玩具代码;它用有限差分法(FD)在时间域显式求解声波方程,每一步都直面数值频散、边界反射、震源激发精度、网格离散误差这四大硬伤。如果你正在做地震数据处理流程验证、算法对比实验,或需要可复现、可调试、可嵌入自定义算子的正演模块——这份源码就是你绕不开的起点。它不依赖任何商业软件许可证,所有物理参数、离散策略、边界条件全部裸露在.m文件里,适合从研究生到油田物探工程师的全链条复现与改造。


2. 从模型加载到波场快照:用 MATLAB 完整跑通 Marmousi 正演最小闭环

Marmousi 模型本质是一张 384×122 的二维速度场矩阵(单位:m/s),对应 6.5 km × 1.9 km 区域,空间采样率 17 m。MATLAB 实现的核心逻辑是:以该速度场为输入,驱动二阶声波方程的 2D 有限差分迭代,生成单炮记录(shot gather)及中间波场快照(wavefield snapshot)。整个流程不依赖任何工具箱,纯原生 MATLAB 数值计算,但对内存管理、循环优化、边界处理有明确要求。

2.1 加载 Marmousi 模型并校验物理维度

模型文件marmousi_model.mat中通常包含变量vp(纵波速度矩阵)和dx,dz(空间步长)。必须先确认其尺寸与物理尺度匹配,否则后续所有模拟将失真:

load('marmousi_model.mat'); % 加载标准Marmousi模型 % 验证基础维度(标准Marmousi为384列×122行,即x方向384点,z方向122点) assert(size(vp, 1) == 122 && size(vp, 2) == 384, 'Marmousi模型尺寸异常:应为122×384'); % 物理尺寸校验(dx=17m, dz=17m为原始设计) if ~exist('dx', 'var') || ~exist('dz', 'var') dx = 17; dz = 17; % 显式设定,避免.mat中缺失 end Lx = (size(vp, 2) - 1) * dx; % x方向总长度(m) Lz = (size(vp, 1) - 1) * dz; % z方向总长度(m) fprintf('Marmousi模型物理尺寸:%.1f m × %.1f m(x × z)\n', Lx, Lz);

逻辑说明:vp矩阵按 MATLAB 默认存储习惯是(z, x)布局(行对应深度z,列对应水平x),因此size(vp,1)是z向点数,size(vp,2)是x向点数。dx/dz必须与模型原始采样一致,否则波速与空间尺度错配,导致波长计算错误——这是后续所有频散问题的根源。

2.2 构建稳定有限差分网格与时间步长控制

声波方程离散化采用二阶中心差分格式,稳定性由 CFL 条件约束:
$$ \Delta t \leq \frac{\min(\Delta x, \Delta z)}{v_{\max} \cdot \sqrt{2}} $$
其中 $v_{\max}$ 是模型中最大纵波速度(Marmousi 中约 5500 m/s)。MATLAB 实现中需显式计算并强制满足:

vmax = max(vp(:)); % 模型最大速度 dt = 0.9 * min(dx, dz) / (vmax * sqrt(2)); % CFL系数取0.9留余量 nt = 2000; % 总时间步数(对应约1.5s,足够覆盖反射波返回) fprintf('采用时间步长 dt = %.4f s(CFL=0.9)\n', dt);

参数说明:dt必须严格按此公式计算,不能随意设为0.001或1e-3。Marmousi 模型中vmax≈5500,dx=dz=17,理论最大dt≈0.00218 s;若设dt=0.0025,CFL > 1,数值解将指数发散。nt需根据最大传播距离估算:最深反射点约 1800 m,往返路径约 3600 m,以 2000 m/s 平均速度计,需约 1.8 s,故nt=2000(dt≈0.0009 s)更稳妥。

2.3 震源注入与接收器布设:位置、类型与离散化对齐

Marmousi 标准正演使用 Ricker 子波作为震源,中心频率通常设为 15–25 Hz。震源位置必须落在网格节点上,且需考虑空间插值精度:

% 震源位置(x=1000m, z=50m),转换为网格索引(注意z向上为0,向下递增) sx = round(1000 / dx) + 1; % x索引(+1因MATLAB索引从1开始) sz = round(50 / dz) + 1; % z索引(地表z=0,向下为正) % Ricker子波参数 fc = 20; % 中心频率(Hz) tvec = (0:dt:(nt-1)*dt)'; src = (1 - 2*(pi*fc.*tvec).^2) .* exp(-(pi*fc.*tvec).^2); % Ricker时域表达式 % 接收器:地表单排,x方向均匀布设(共120道,间距17m) nx_rec = 120; rec_x = 1:dx:Lx; % 地表x坐标 rec_idx = round(rec_x / dx) + 1; % 对应网格x索引 rec_idx = rec_idx(rec_idx <= size(vp,2)); % 截断超界索引

逻辑说明:震源sz必须是整数索引,否则需双线性插值,但标准实现多采用最近邻(round)以保持代码简洁。Ricker 子波公式中tvec必须与dt严格同步,否则频谱偏移。接收器索引rec_idx若超出vp列数(384),会导致数组越界,必须截断——这是新手高频翻车点。


3. 波场演化核心:二阶声波方程有限差分迭代与内存优化技巧

正演模拟的主体是循环迭代更新压力场p。标准二阶显式格式为:
$$ p^{n+1}{i,j} = 2p^n{i,j} - p^{n-1}{i,j} + \left(\frac{v{i,j},\Delta t}{\Delta x}\right)^2 \left(p^n_{i+1,j} - 2p^n_{i,j} + p^n_{i-1,j}\right) + \left(\frac{v_{i,j},\Delta t}{\Delta z}\right)^2 \left(p^n_{i,j+1} - 2p^n_{i,j} + p^n_{i,j-1}\right) $$
但直接三重循环(for n=1:nt, for i=2:ix-1, for j=2:iz-1)在 MATLAB 中极慢。高效做法是预分配三维数组 + 向量化空间差分,并利用 MATLAB 的矩阵索引广播特性。

3.1 三维波场数组预分配与双缓冲机制

为避免内存碎片和动态扩容开销,必须预先分配p为iz × ix × nt三维数组,并采用双缓冲(two-buffer)策略减少内存占用:

% 预分配三维波场(z,x,t),初始化为零 p = zeros(size(vp,1), size(vp,2), nt); % 双缓冲:只存当前步和前一步,节省内存(nt大时至关重要) p_prev = zeros(size(vp)); % t-1时刻 p_curr = zeros(size(vp)); % t时刻 p_next = zeros(size(vp)); % t+1时刻(待更新) % 初始时刻:t=0,p=0;t=1,仅震源处赋值(delta函数近似) p_curr(sz, sx) = src(1); % 第一个时间步注入

逻辑说明:p三维数组若nt=2000,iz=122,ix=384,则总元素约 9300 万,单精度需 372 MB 内存。而双缓冲仅需 3 个122×384矩阵(约 0.56 MB),内存节省 600 倍。p_curr(sz,sx)=src(1)是离散震源建模,src(1)即 Ricker 子波首采样值,非归一化处理——这是保证能量守恒的关键细节。

3.2 向量化空间二阶差分计算(无循环)

核心加速在于用矩阵切片替代for循环计算拉普拉斯算子:

% 计算空间二阶差分:d2p/dx2 和 d2p/dz2 % 注意:MATLAB中p_curr(z,x),所以x方向差分用(:,2:end-1),z方向用(2:end-1,:) d2p_dx2 = p_curr(:,3:end) - 2*p_curr(:,2:end-1) + p_curr(:,1:end-2); d2p_dz2 = p_curr(3:end,:) - 2*p_curr(2:end-1,:) + p_curr(1:end-2,:); % 速度平方缩放(逐元素乘) vx2 = (vp .* dt / dx).^2; % (z,x)矩阵 vz2 = (vp .* dt / dz).^2; % 应用差分(注意维度对齐:d2p_dx2是(z,x-2),需与vx2(:,2:end-1)相乘) % 所以先扩展vx2/vz2到匹配差分区域尺寸 vx2_crop = vx2(:,2:end-1); % 裁剪至x方向有效范围 vz2_crop = vz2(2:end-1,:); % 裁剪至z方向有效范围 % 更新下一时刻波场(核心公式向量化) p_next(2:end-1,2:end-1) = ... 2*p_curr(2:end-1,2:end-1) - p_prev(2:end-1,2:end-1) ... + vx2_crop .* d2p_dx2 ... + vz2_crop .* d2p_dz2;

参数说明:d2p_dx2尺寸为iz × (ix-2),vx2_crop必须裁剪为相同尺寸才能逐元素相乘。p_next(2:end-1,2:end-1)是内部点更新区域,边界点(第1行/列、末行/列)由吸收边界条件单独处理。此写法比三重循环快 15–20 倍,是 MATLAB 正演提速的玄学门槛。

3.3 PML 吸收边界条件:为何不用简单衰减,而选复频移PML?

Marmousi 模型侧边和底边存在强反射干扰,必须抑制。简单指数衰减(如p = p .* exp(-alpha*x))在宽频带下效果差。本实现采用复频移 PML(CPML),其核心是引入复数坐标拉伸因子:

% CPML参数(典型值) alpha_max = 1.0; % 衰减系数最大值 kappa_max = 1.0; % 坐标拉伸系数最大值 npml = 20; % PML层厚度(网格点数) % 构建x方向PML衰减因子(复数) x_pml = (1:npml)'; sigma_x = alpha_max * (x_pml/npml).^2; kappa_x = 1 + (kappa_max-1) * (x_pml/npml).^2; damp_x = (sigma_x + 1i * 2*pi*fc .* kappa_x) ./ (kappa_x .* (sigma_x + 1i * 2*pi*fc)); % 应用于x方向差分(简化版,实际需修改差分算子) % (注:完整CPML实现需重构差分格式,此处仅示意原理)

逻辑说明:CPML 的damp_x是复数,作用于差分算子而非直接乘p,因此需重写差分核。但多数开源 Marmousi MATLAB 实现采用更轻量的Stacey 一阶吸收边界:在边界几行/列添加阻尼项p = p * (1 - damp_rate)。本代码默认启用 Stacey 边界(damp_rate=0.005),CPML 作为进阶选项需手动替换——这是平衡精度与代码复杂度的血泪经验。


4. 常见问题排查:5 个让 Marmousi 正演“静音”或“爆炸”的真实踩坑记录

Marmousi 模型正演失败往往无声无息:要么输出全零(静音),要么数值溢出变Inf/NaN(爆炸)。以下是我在油田项目中反复验证的 5 类高频问题,每条附现象、根因与可执行修复方案。

4.1 现象:波场快照全黑(p全为 0),震源位置无扰动

原因:震源索引sx,sz超出vp矩阵范围,或src(1)为 0(Ricker 子波在t=0时值为 0)
解决:

  • 检查sx = round(1000/dx)+1是否 ≤size(vp,2)(384),sz = round(50/dz)+1是否 ≤size(vp,1)(122);
  • Ricker 子波改用src = (1 - 2*(pi*fc*(tvec-dt)).^2) .* exp(-(pi*fc*(tvec-dt)).^2),将峰值移至t=dt,确保src(1)≠0。

4.2 现象:运行几秒后p出现Inf或NaN,max(abs(p))指数增长

原因:CFL 条件被违反,或vp中存在Inf/NaN值(常见于加载.mat时数据损坏)
解决:

  • 执行dt = 0.9 * min(dx,dz) / (max(vp(:))*sqrt(2))后,打印dt并人工验证;
  • 加载后立即运行assert(isfinite(vp(:)), 'vp含Inf/NaN');若失败,用vp(isnan(vp)|isinf(vp)) = median(vp(:))修补。

4.3 现象:单炮记录(shot_gather)中直达波正常,但反射波微弱或缺失

原因:接收器未布设在地表(z=0行),或rec_idx索引错位(MATLAB 索引从 1 开始,但vp(1,:)是地表)
解决:

  • 确认接收器行索引为1:rec_data(:,t) = p_curr(1, rec_idx);
  • 检查rec_idx是否全部在[1, size(vp,2)]内,用rec_idx = max(1, min(size(vp,2), rec_idx))截断。

4.4 现象:波场快照显示强网格频散(阶梯状波前),尤其在高速区(盐丘)边缘

原因:空间采样不足(dx=dz=17m对 25Hz 子波,波长 λ=v/f≈220m,λ/dx≈13,临界值为 10–12)
解决:

  • 将dx=dz降至 12 m(需重采样vp,用imresize(vp, [122*17/12, 384*17/12], 'bicubic'));
  • 或降低fc至 15 Hz,使λ/dx≈18,频散显著缓解。

4.5 现象:plot_wavefield.m报错Index exceeds matrix dimensions

原因:波场保存步数save_step与nt不匹配,或p未按iz×ix×nt预分配
解决:

  • 在forward_simulation.m中确认p = zeros(iz,ix,nt);
  • 若只保存部分快照,设save_step = 10,则p_save(:,:,k) = p_curr时k从 1 到nt/save_step,需p_save = zeros(iz,ix,nt/save_step)。

提示:所有修复必须在forward_simulation.m主循环前完成。不要在循环中动态调整dt或vp,那会破坏数值一致性。


5. 从单炮到批量:构建可复用的 Marmousi 正演批处理框架与算法验证闭环

单次正演只是起点。真实勘探需跑多炮集(multi-shot)、不同震源深度扫描、参数敏感性分析(如fc、dt、PML厚度)。手工改参数、反复运行太低效。我一般会封装一个marmousi_batch函数,接受结构体参数,自动调度并归档结果:

function results = marmousi_batch(params) % params: struct with fields .model_file, .fc, .dt, .nt, .shots, .output_dir load(params.model_file); vp = double(vp); % 强制double避免single精度损失 % 生成多炮位置(沿地表x方向,间隔200m) x_shots = 200:200:6000; % 30炮 results.shot_gathers = cell(1, length(x_shots)); results.wavefields = cell(1, 5); % 存5个关键时刻快照 for idx = 1:length(x_shots) fprintf('Running shot %d at x=%.0f m...\n', idx, x_shots(idx)); sx = round(x_shots(idx)/params.dx) + 1; sz = round(50/params.dz) + 1; [sg, wf] = forward_simulation(vp, params.dx, params.dz, params.dt, params.nt, ... sx, sz, params.fc, params.pml_thickness); results.shot_gathers{idx} = sg; if idx <= 5, results.wavefields{idx} = wf; end % 仅存前5炮快照 end % 自动保存为 .mat,含参数快照 save(fullfile(params.output_dir, ['batch_fc' num2str(params.fc) '_dt' num2str(params.dt,3) '.mat']), 'results', '-v7.3'); end

5.1 批处理核心:参数化与结果结构化

上述函数将fc(中心频率)、dt(时间步长)、shots(炮点列表)等全部外置为params结构体字段。这样做的好处是:

  • 可复现性:每次运行自动存档params,避免“上次怎么跑的?”疑问;
  • 对比实验:写个循环扫参for fc=[15,20,25],结果自动按fc命名,方便后续compare_gathers.m统一绘图;
  • 故障隔离:某炮失败时,try-catch可捕获并记录idx,不影响其余炮运行。

5.2 验证闭环:用 Marmousi 正演结果反向检验算法正确性

Marmousi 的价值不仅在于生成数据,更在于提供已知地下结构的“黄金标准”。例如验证你的去噪算法:

  1. 用上述批处理生成干净单炮记录sg_clean;
  2. 添加真实噪声(如sg_noisy = sg_clean + 0.1*randn(size(sg_clean)));
  3. 运行你的算法得到sg_denoised;
  4. 计算PSNR = 10*log10(var(sg_clean(:))/mean((sg_clean-sg_denoised).^2))。
    若 PSNR < 20 dB,说明算法在 Marmousi 复杂构造下失效——这比在合成简单模型上测试更有说服力。

5.3 工业级落地:如何把 MATLAB 正演嵌入 Python 处理流水线?

很多团队用 Python 做数据预处理和深度学习,但 MATLAB 在波动方程求解上仍有优势。我的做法是:

  • 将forward_simulation.m编译为独立.exe(MATLAB Compiler),命令行调用:
    system(['marmousi_forward.exe ', num2str(sx), ' ', num2str(sz), ' ', num2str(fc)]);
  • 或用 MATLAB Engine API for Python(需安装 MATLAB Runtime):
    import matlab.engine eng = matlab.engine.start_matlab() sg = eng.forward_simulation(matlab.double(vp.tolist()), sx, sz, fc, nargout=1)
    关键点:vp必须转为 Python list 再转matlab.double,否则传参失败。

最后说句实在话:我第一次跑通 Marmousi 正演花了整整三天,卡在dt计算错误和sz索引越界上。后来发现,所有“玄学”问题,90% 都源于没校验vp尺寸、dx/dz、dt这三个物理量是否自洽。现在我的习惯是:每次打开.m文件,第一件事就是加三行assert——这比读文档快十倍。希望帮到你。

本文还有配套的精品资源,点击获取