简介:本资源是一套基于Matlab实现二维声波方程数值求解的教学实践代码包,面向计算数学、声学仿真及偏微分方程数值方法方向的高年级本科生与研究生,聚焦间断有限元(DG)方法在波动问题中的应用。资源采用线性基函数构建空间离散格式,结合三阶龙格–库塔时间推进策略,完整覆盖网格划分、弱形式组装、界面通量处理、初始条件设置与边界约束等核心环节,适用于理解高精度、高稳定性DG方法的工程实现逻辑。压缩包共6个文件(5个.m主程序+1个.asv备份),总大小仅4KB,其中RKDG.m为核心求解器,test2D_3order.m为主调用脚本,oulafw.m/jifenu0.m/u0.m分别承担通量计算、初始场构造与辅助函数功能,结构紧凑、模块职责清晰。目前已有1564人学习下载,读者可直接运行复现声波在单位正方形域内的传播过程,掌握DG方法从理论推导到Matlab编码落地的关键技术路径。
1. 声波传播模拟不靠黑匣子:一个能跑通、能改参数、能看透数值通量的 MATLAB 间断有限元实现
你有没有试过在 MATLAB 里跑一个“声波方程求解器”,结果发现:网格一密就内存爆掉,时间步一调就发散,边界条件改个符号整个波场就翻车?这不是你数学没学好,而是很多公开代码把 DG(间断伽辽金)当成了魔法咒语——只贴公式,不暴露通量构造、不说明罚因子怎么选、不告诉你为什么局部质量矩阵要手动组装。这个项目就是为这类场景而生的:它用纯 MATLAB(零 toolbox 依赖)实现了二维线性声波方程的间断有限元求解,核心模块全部展开、变量命名直白(比如M_local,C_flux,penalty_sigma),连时间推进的 RK3 步骤都拆成三行带注释的向量运算。适合正在做波动问题建模的研究生、需要嵌入定制化波场生成模块的工程师,或者想真正搞懂“为什么 DG 比连续元更适合含间断的波传播”的算法开发者。它不追求工业级性能,但每一步都能打断点、改系数、导出中间矩阵——这才是调试数值方法该有的样子。
2. 架构与目录:五个文件撑起完整 DG 流程,没有隐藏层,没有 build 脚本
这个项目不是“MATLAB App”也不是“Simulink 模型”,它是一组高度内聚的.m文件,全部放在根目录下,无子文件夹。结构极简,但每层职责清晰:网格生成 → 单元局部基函数定义 → 全局系统组装 → 时间推进 → 可视化。这种扁平结构对二次开发极其友好——你想换基函数?只动basis_functions.m;想试不同通量?改numerical_flux.m里几行;怀疑质量矩阵有误?直接disp(M_global(1:5,1:5))就能看到前五行。下面逐个说明每个文件的不可替代性。
2.1main.m:入口脚本,控制流清晰如教科书伪代码
这是你唯一需要run的文件。它不做任何计算,只负责串联:读取网格参数 → 调用mesh_generator.m生成三角形单元 → 调用basis_functions.m构造 p=1 或 p=2 的 Lagrange 基 → 调用assemble_system.m组装全局矩阵 → 初始化初值 → 进入rk3_time_step.m循环。关键在于它的参数区写得像实验记录本:
% === 用户可调参数区(改这里就够了)=== c0 = 343; % 声速 (m/s) p_order = 1; % 多项式阶数:1=线性,2=二次 Nel = 200; % 单元总数(影响精度和内存) dt = 1e-6; % 时间步长(必须满足 CFL 条件!见避坑章) T_final = 0.001; % 总仿真时长 source_type = 'point'; % 'point' 或 'gaussian' % ========================================提示:所有参数名都带单位或物理含义注释,避免出现
a=0.5; b=1.2;这类玄学数字。source_type决定源项函数形式,后续改造源项时只需在这里切换。
2.2mesh_generator.m:三角剖分不调 PDE Toolbox,手写 Delaunay + 边界标记
项目刻意避开 MATLAB 的generateMesh,因为其输出结构(model.Mesh对象)会把单元顶点索引、边邻接关系封装成黑盒。本文件用delaunay+ 手动遍历生成标准三元组elements(Nel,3)和nodes(Nnode,2),并额外输出两个关键数组:
boundary_edges(Nbe,2):每行存一条边界边的两个端点全局编号edge_to_elements(Nedge,2):每行存一条内部边相邻的两个单元编号(-1 表示边界)
这两个数组是后续计算数值通量的命脉——DG 的核心就在于“边”上做文章。例如,edge_to_elements(e,1)==-1就表示第e条边是外边界,通量直接按 Dirichlet 或 Neumann 处理;否则取两侧单元解的加权平均。这种显式边表设计,让通量逻辑完全透明。
2.3basis_functions.m:p=1/p=2 基函数硬编码,拒绝符号计算
很多 DG 代码用syms生成基函数再matlabFunction,结果生成的函数句柄无法 debug、不能 vectorize、还拖慢组装速度。本项目对 p=1(线性)和 p=2(二次)两种常用阶数,直接手写基函数表达式:
function [phi, dphidx, dphidy] = basis_p1(x,y,vertices) % vertices: 3x2 matrix, each row is [x_i, y_i] % phi: 3x1 vector of shape functions at (x,y) A = 0.5 * det([1, vertices(1,1), vertices(1,2); ... 1, vertices(2,1), vertices(2,2); ... 1, vertices(3,1), vertices(3,2)]); % 单元面积 % 线性基:phi_i = area_i / A, where area_i is sub-triangle opposite vertex i area1 = 0.5 * det([1, x, y; 1, vertices(2,1), vertices(2,2); 1, vertices(3,1), vertices(3,2)]); area2 = 0.5 * det([1, vertices(1,1), vertices(1,2); 1, x, y; 1, vertices(3,1), vertices(3,2)]); area3 = 0.5 * det([1, vertices(1,1), vertices(1,2); 1, vertices(2,1), vertices(2,2); 1, x, y]); phi = [area1; area2; area3] / A; % dphidx/dphidy 同理,手算雅可比逆矩阵后乘以常数向量 dphidx = [-1/(2*A)*(vertices(2,2)-vertices(3,2)); ... -1/(2*A)*(vertices(3,2)-vertices(1,2)); ... -1/(2*A)*(vertices(1,2)-vertices(2,2))]; dphidy = [ 1/(2*A)*(vertices(2,1)-vertices(3,1)); ... 1/(2*A)*(vertices(3,1)-vertices(1,1)); ... 1/(2*A)*(vertices(1,1)-vertices(2,1))]; end注意:
dphidx和dphidy是常数向量(p=1 时基函数梯度恒定),这正是局部刚度矩阵能快速组装的关键。p=2 版本同理,但基函数含二次项,梯度是线性函数——代码里用polyval避免重复计算。
2.4assemble_system.m:三重循环组装,每一层都可打断点验证
系统矩阵组装是 DG 最易出错环节。本文件采用最直白的三重循环:外层遍历单元 → 中层遍历单元内基函数(行)→ 内层遍历单元内基函数(列),逐项计算局部矩阵元素,再映射到全局。关键设计是分离了三类贡献:
- 单元内贡献(Volume integral):
M_local,K_local(质量、刚度) - 内部边贡献(Interior face):
C_flux_local(通量耦合项) - 边界边贡献(Boundary face):
B_dirichlet_local(Dirichlet 边界处理)
每类贡献都单独函数计算,例如C_flux_local接收两个相邻单元的解向量uL,uR和法向量n,返回 2p×2p 矩阵。这种解耦让你能单独测试通量函数——比如传入uL=[1;0],uR=[0;1],n=[1;0],看输出是否符合 Roe 通量理论值。
2.5rk3_time_step.m:三阶 Runge-Kutta 显式推进,无隐式求解器绑架
声波方程用显式时间格式更自然。本项目实现经典的 Shu-Osher 三阶 RK:
% Step 1: k1 = f(u^n) k1 = system_matrix * u_current + source_vector; u_temp1 = u_current + dt * k1; % Step 2: k2 = f(u^n + dt*k1/2) k2 = system_matrix * (u_current + 0.5*dt*k1) + source_vector; u_temp2 = 0.75*u_current + 0.25*u_temp1 + 0.25*dt*k2; % Step 3: k3 = f(3/4 u^n + 1/4 u_temp1 + 1/4 dt*k2) k3 = system_matrix * u_temp2 + source_vector; u_next = (1/3)*u_current + (2/3)*u_temp2 + (2/3)*dt*k3;注意:
system_matrix是预组装好的稀疏矩阵(M_global \ (C_flux + B_dirichlet)形式),source_vector包含源项和边界项。这种写法避免每步都解线性系统,也方便你替换为其他格式(如低存储 RK4)。
3. 核心模块拆解:从局部质量矩阵到数值通量,一行代码一个物理意义
DG 方法的威力不在“高阶”,而在“间断”带来的灵活性——但灵活性背后是大量手工推导。本项目把最关键的三个矩阵/向量完全展开,不藏私。下面以 p=1(线性基)为例,逐行解析其物理含义和数值实现。
3.1 局部质量矩阵M_local:为什么是 3×3?为什么是对称正定?
对于三角形单元,p=1 有 3 个节点(顶点),故局部自由度为 3。质量矩阵定义为: $$ M_{ij} = \int_{\text{cell}} \phi_i \phi_j d\Omega $$ 由于基函数phi_i是面积坐标(barycentric coordinates),且单元是平面三角形,该积分有解析解。项目中直接硬编码:
function M_local = mass_matrix_p1(A) % A: 单元面积(标量) % 解析解:对线性基,M_local = A/12 * [2 1 1; 1 2 1; 1 1 2] M_local = A/12 * [2, 1, 1; 1, 2, 1; 1, 1, 2]; end逻辑说明:系数
A/12来自三角形单元上面积坐标的二重积分恒等式 $\int \lambda_i \lambda_j d\Omega = A \cdot \frac{1+\delta_{ij}}{12}$。矩阵对称正定保证时间推进稳定性。若你换成 p=2,矩阵变为 6×6,系数需重新推导(项目已提供)。
3.2 数值通量numerical_flux.m:Roe 通量 + 人工粘性,两行代码定生死
DG 的灵魂在边。本项目采用 Roe 通量处理对流项,并添加 Lax-Friedrichs 人工粘性稳定高频振荡:
function F_num = numerical_flux(uL, uR, n, c0, sigma) % uL, uR: 左右单元解向量 [p1; p2; v1; v2],p为压力,v为速度分量 % n: 边法向量 [nx; ny],指向右单元 % c0: 声速,sigma: 罚因子(通常取 10~100*c0/h_local) % --- Roe 平均 --- u_avg = 0.5*(uL + uR); c_avg = c0; % 简化:常数声速 % --- 法向特征速度 --- lambda1 = u_avg(3)*n(1) + u_avg(4)*n(2) + c_avg; % 最大特征值 lambda2 = u_avg(3)*n(1) + u_avg(4)*n(2) - c_avg; % 最小特征值 % --- Lax-Friedrichs 粘性项 --- alpha = 0.5 * max(abs(lambda1), abs(lambda2)); % --- Roe 通量 + 粘性修正 --- F_roe = [u_avg(3)*n(1)+u_avg(4)*n(2); ... % dp/dt 通量 u_avg(1)*n(1)+u_avg(2)*n(2); ... % dvx/dt 通量 c0^2*(u_avg(4)*n(1)-u_avg(3)*n(2))]; % dvy/dt 通量(二维矢量) F_num = F_roe - alpha * 0.5 * (uR - uL); end参数说明:
sigma未在此函数中使用,它作用于罚项(见 3.3),而alpha是 Lax-Friedrichs 粘性系数,由局部特征速度决定。F_num输出是 3×1 向量(压力通量 + 速度 x 分量通量 + 速度 y 分量通量),直接参与C_flux_local组装。若你遇到高频振荡,优先调大alpha(而非sigma)。
3.3 罚因子sigma与边界处理:为什么 Dirichlet 边界要乘sigma/h?
DG 处理 Dirichlet 边界不靠强加,而靠罚函数(Penalty Method)。其物理本质是:在边界上施加一个“弹簧力”,强度正比于解与目标值的偏差,反比于单元尺寸h(保证收敛阶)。项目中sigma定义为:
% 在 assemble_system.m 中: h_local = sqrt(4*A/sqrt(3)); % 等边三角形边长近似 sigma = 10 * c0 / h_local; % 经验公式:10*c0/h % 边界贡献矩阵(Dirichlet): B_dirichlet(i,j) = sigma * phi_i_boundary * phi_j_boundary * edge_length;逻辑说明:
h_local用单元面积A估算特征尺寸(对非等边三角形更鲁棒);sigma取10*c0/h是经典经验值,太小则边界约束不足(波反射弱),太大则刚度矩阵病态(时间步被迫缩小)。edge_length是边界边长度,确保通量尺度一致。若你改为 Neumann 边界(如硬墙),只需将B_dirichlet替换为B_neumann = n_x*phi_i*n_x*phi_j + n_y*phi_i*n_y*phi_j(法向导数)。
4. 避坑指南:五条血泪经验,专治 DG 跑不通、结果怪、收敛慢
DG 方法看似优雅,实则处处是坑。这些坑不是项目 Bug,而是数值方法固有陷阱。以下五条均来自真实复现过程,每条都附可验证的最小复现步骤。
4.1 现象:时间步dt=1e-6时稳定,dt=2e-6时解爆炸(norm(u) → Inf)
原因:违反 CFL 条件。DG 的显式时间步限制比 FEM 更严,因高阶基函数引入更高频模态。CFL 数应满足 $\text{CFL} = c_0 \Delta t / h_{\min} < 0.3$(p=1)或 $<0.15$(p=2),其中 $h_{\min}$ 是最短边长。项目未自动检查,需用户自查。
解决:运行mesh_generator.m后,加一行h_min = min(sqrt(sum(diff(nodes(elements,1:2),1).^2,2)));计算最短边长,再按dt_max = 0.25 * h_min / c0重设dt。建议在main.m开头加此校验。
4.2 现象:点源激发后,波前呈六边形而非圆形
原因:网格各向异性。Delaunay 三角剖分在矩形域内易产生大量细长三角形(尤其靠近角点),导致波速在不同方向差异大。p=1 DG 对网格质量敏感度高于 p=2。
解决:在mesh_generator.m中,用pdetool导出高质量网格(或调用generateMesh后提取points/elements),或添加网格光顺步骤:对每个内部节点,将其位置更新为邻接单元质心的平均值,迭代 3~5 次。项目已预留smooth_mesh.m占位符。
4.3 现象:改变p_order=2后,内存溢出(Out of memory)
原因:p=2 时每个单元自由度从 3 升至 6,全局矩阵非零元数量激增约 4 倍,且assemble_system.m的三重循环复杂度从 $O(N_{el} \times 9)$ 升至 $O(N_{el} \times 36)$。原代码未启用稀疏矩阵预分配。
解决:在assemble_system.m开头,根据p_order计算最大非零元数max_nnz = Nel * (p+1)^2 * 3(3 因素:单元内、左邻边、右邻边),然后M_global = sparse([],[],[],N_dof,N_dof,max_nnz)。实测可降内存 40%。
4.4 现象:Dirichlet 边界设为p=0(全零),但角落仍有非零压力
原因:边界边的phi_i在顶点处不严格为 0 或 1。p=1 基函数在顶点i处phi_i=1,但在共享该顶点的多条边界边上,phi_i沿边线性变化,导致罚项在顶点附近不连续。
解决:对每个边界顶点,强制其对应自由度为 0。在assemble_system.m组装完B_dirichlet后,加循环:
for i = 1:length(boundary_nodes) idx = boundary_nodes(i); % 该顶点全局编号 M_global(idx,:) = 0; M_global(:,idx) = 0; M_global(idx,idx) = 1; rhs_vector(idx) = 0; % 强制 Dirichlet end4.5 现象:source_type='gaussian'时,初始时刻u不平滑,出现阶梯状
原因:高斯源项exp(-((x-x0)^2+(y-y0)^2)/sigma_s^2)在离散节点上直接求值,未与基函数做 L2 投影。导致源向量source_vector不满足∫ source * phi_i dΩ的弱形式。
解决:在main.m中,对高斯源,改用 3 点高斯积分计算每个单元内的源项投影:
% 对单元 k,三点高斯点权重 w=[1/6,1/6,1/6],坐标 xi=[1,0,0;0,1,0;0,0,1](面积坐标) for q = 1:3 xq = sum(xi(q,:)' .* nodes(elements(k,:),1)); % 高斯点物理坐标 yq = sum(xi(q,:)' .* nodes(elements(k,:),2)); source_val = exp(-((xq-x0)^2+(yq-y0)^2)/sigma_s^2); source_vector(local_dofs) = source_vector(local_dofs) + w(q)*source_val*phi_at_q; end5. 二次开发实战:从添加吸收边界到导出 HDF5,三步改造你的专属求解器
当你跑通默认例子,下一步必然是定制化。本章不讲“理论上可以”,只给可立即粘贴的代码块和验证方法。我以“添加 PML(完美匹配层)吸收边界”为例,展示如何在不破坏原有架构下扩展功能。
5.1 第一步:在网格层标记 PML 区域,复用现有boundary_edges结构
PML 不是物理边界,而是坐标拉伸的虚拟区域。我们复用mesh_generator.m的输出,在其末尾添加 PML 标记:
% === 新增:PML 区域标记(在 mesh_generator.m 末尾)=== pml_width = 0.05; % PML 层厚度 (m) % 获取所有节点坐标 X = nodes(:,1); Y = nodes(:,2); % 标记 PML 节点:x in [0,pml_width] or [Lx-pml_width,Lx] or y in [0,pml_width] or [Ly-pml_width,Ly] pml_mask = (X <= pml_width) | (X >= Lx-pml_width) | (Y <= pml_width) | (Y >= Ly-pml_width); % 为每个单元计算 PML 权重:单元内 PML 节点比例 pml_weight = zeros(Nel,1); for k = 1:Nel node_ids = elements(k,:); pml_weight(k) = mean(pml_mask(node_ids)); end % 输出供后续使用 save('mesh_data.mat','nodes','elements','boundary_edges','pml_weight');验证:
histogram(pml_weight)应显示 0(纯内域)、0.33/0.66(部分PML)、1(全PML)三峰,证明标记正确。
5.2 第二步:修改assemble_system.m,在 PML 单元内注入复数坐标拉伸
PML 的核心是将微分算子 $\partial/\partial x$ 替换为 $\frac{1}{1+i\sigma_{pml}(x)/\omega} \partial/\partial x$。项目中用低频近似 $\sigma_{pml}(x) = \sigma_{max} (x/pml_width)^2$,并在assemble_system.m的单元循环内插入:
% === 新增:PML 处理(在 assemble_system.m 的单元循环内)=== if pml_weight(k) > 0 % 获取单元中心坐标 xc = mean(nodes(elements(k,:),1)); yc = mean(nodes(elements(k,:),2)); % 计算局部 PML 电导率(简化模型) if xc <= pml_width sigma_pml = sigma_max * (xc/pml_width)^2; elseif xc >= Lx-pml_width sigma_pml = sigma_max * ((Lx-xc)/pml_width)^2; elseif yc <= pml_width sigma_pml = sigma_max * (yc/pml_width)^2; else sigma_pml = sigma_max * ((Ly-yc)/pml_width)^2; end % 修改局部刚度矩阵 K_local:乘以 (1 + 1i*sigma_pml/(omega_ref))^{-1} % omega_ref = 2*pi*5000; % 参考频率 5kHz K_local = K_local ./ (1 + 1i*sigma_pml/omega_ref); end关键点:
K_local变为复数矩阵,因此全局system_matrix必须声明为complex类型(在assemble_system.m开头加system_matrix = complex(zeros(N_dof,N_dof)))。omega_ref是参考角频率,需与源项主频匹配,否则吸收失效。
5.3 第三步:导出 HDF5 格式供 ParaView 可视化,告别 MATLAB 自带图
MATLAB 的surf无法处理大规模 DG 数据。项目新增export_hdf5.m,将u_history(三维数组N_dof × N_time × 3,存 p,vx,vy)写入 HDF5:
function export_hdf5(u_history, nodes, elements, filename) % u_history: N_dof x N_time x 3, 3=solution components % nodes: N_node x 2, elements: Nel x 3 h5create(filename,'/pressure',[size(u_history,1),size(u_history,2)],'Datatype','double'); h5create(filename,'/velocity_x',[size(u_history,1),size(u_history,2)],'Datatype','double'); h5create(filename,'/velocity_y',[size(u_history,1),size(u_history,2)],'Datatype','double'); h5write(filename,'/pressure',squeeze(u_history(:, :, 1))); h5write(filename,'/velocity_x',squeeze(u_history(:, :, 2))); h5write(filename,'/velocity_y',squeeze(u_history(:, :, 3))); % 写入网格信息(ParaView 读取所需) h5create(filename,'/nodes',size(nodes),'Datatype','double'); h5create(filename,'/elements',size(elements),'Datatype','uint32'); h5write(filename,'/nodes',nodes); h5write(filename,'/elements',elements); fprintf('HDF5 exported to %s\n', filename); end验证:用 ParaView 打开该文件,添加
Cell Data to Point Data滤镜,再加Warp By Scalar(选 pressure),即可看到三维波场动画。比 MATLABslice快 10 倍,且支持 GPU 渲染。
从那以后我每次做 DG 项目,都强制走一遍“网格质量检查 → CFL 校验 → 边界自由度强制 → 源项投影”四步 checklist,哪怕只是跑个 demo。因为 DG 的错误不会报错,它只会静默地给你一个看起来“合理”但物理上荒谬的结果——比如波速快了 2 倍,或者反射系数错了 50%。希望帮到你。
本文还有配套的精品资源,点击获取