简介:本资源是一套面向地球物理勘探初学者与科研人员的Matlab三维有限元直流电阻率正演模拟实践包,聚焦电阻率法正演建模核心能力训练,适用于水文地质调查、矿产勘查及工程地质建模等实际场景。压缩包含99个文件(1001KB),主体为52个Matlab源码(.m)、8个数据文件(.dat/.mat)、3个可视化结果图(.fig)、2个地质模型图像(.tif)及1份PDF手册,覆盖网格生成、泊松方程求解、电流密度计算与响应可视化全流程;其中FEMIC系列主程序(如FEMIC_forward2D.m、FEMIC_inverse3D.m)与配套数据集(如Real_model.tif、Input_femic.dat)构成完整可运行框架。已有54人学习下载,用户可直接复现论文级正演流程,获得从模型构建、参数设置、数值求解到结果分析的一站式代码支撑,并基于开源结构开展反演调试、灵敏度分析或算法优化二次开发。
1. 为什么用Matlab做3D直流电法正演——看似冷门实则很适合
先交代一下背景。直流电法正演,通俗说就是给定地下电阻率分布,求解地表或井中的电位响应。这是电阻率资料解释的基础,不管是做隧道超前预报、矿区水害探测,还是考古勘察,都离不开正演计算。3D正演因为能处理地形起伏、各向异性、任意形状异常体,一直是研究热点,但也是出了名的难做:模型剖分复杂、矩阵规模大、求解效率低。
在正式进入程序实现之前,我想先有个前置结论,免得你白费力气:如果你只是要做规则模型的教学演示、方法对比、或者验证反演算法,Matlab是足够了;但如果目标是上工业级规模(比如几百万以上网格单元、真实复杂地形),请老老实实回到Fortran/C++ + HPC框架,或者用现成的COMSOL/ANSYS做二次开发。
为什么这么判断?我最早接触3D直流电法正演的时候,第一反应是用Fortran写,因为学院里老一辈传下来的代码都是Fortran。后来发现调试起来实在痛苦,矩阵装配有个索引算错,找bug能找三天。后来换了Matlab,同样的算法三天写完,两天调通,虽然计算慢一些,但对研究阶段的参数测试、算法验证来说完全够用。
这个项目从本质上讲,就是利用有限单元法求解直流电场控制方程。完整的技术链条包括:
- 控制方程的推导:基于电流连续性方程和欧姆定律
- 变分形式推导与边界条件处理:把偏微分方程转化为积分极小值问题
- 三维网格剖分:六面体或四面体离散
- 单元刚度矩阵推导和全局稀疏矩阵组装
- 源项处理:点电源的δ函数离散化
- 线性方程组求解:Matlab中的直接法和迭代法对比
- 结果可视化与精度验证
这些环节每一环都有坑,我一个个拆开讲。
2. 直流电法正演的控制方程与变分推导——把偏微分方程变成计算机语言
2.1 控制方程怎么来的
直流电法正演的物理基础非常简单,就是欧姆定律的微分形式加上电荷守恒。地下介质中,电流密度J满足:
[ \mathbf{J} = \sigma \mathbf{E} = -\sigma abla u ]
电流连续性方程为:
[ abla \cdot \mathbf{J} = I \delta(\mathbf{r} - \mathbf{r}_0) ]
其中I是供电电流强度,(\mathbf{r}_0)是点电源位置,(\delta)是狄拉克δ函数。两个公式合并就得到直流电场的控制方程:
[ abla \cdot (\sigma abla u) = -I \delta(\mathbf{r} - \mathbf{r}_0) ]
这是典型的椭圆型偏微分方程,和第二类边值问题同构。
2.2 变分法推导的完整过程
有限元法不是直接去解偏微分方程,而是先将它转化为变分问题。这个转化的过程很多教材直接跳过了,但实际操作中每一步都会影响后面的实现方式。
先定义一个泛函:
[ F(u) = \frac{1}{2}\int_\Omega \sigma ( abla u)^2 d\Omega + I u(\mathbf{r}_0) ]
可以证明,上面偏微分方程的边值问题等价于在满足边界条件的函数空间中极小化这个泛函。
具体推导过程我精简一下:对泛函取变分δF,利用格林第一恒等式,将体积分转化为面积分,再代入边界条件,最终得到与微分方程等价的变分方程。
实际编程实现时,我们并不需要真的写出泛函的解析形式,而是直接将单元刚度矩阵填充到全局矩阵,将源项填充到右端项。关键在于下面这条思路:有限元本质上就是用一个有限维函数空间去逼近变分问题的解,最终将连续问题离散化为线性方程组Ku = f。
2.3 边界条件:最容易影响精度的一环
直流电法正演的边界条件有三类:
- Dirichlet边界条件(第一类):边界上电位已知,最简单,直接将相应行置零处理即可
- Neumann边界条件(第二类):边界上电流密度法向分量为零,相当于电流不穿过边界,天然满足于自然边界条件,不需要额外处理
- 混合边界条件(第三类):最接近实际物理场景,模拟电流在大地中无限传播的效应,公式为(\frac{\partial u}{\partial n} + \frac{\cos\theta}{r}u = 0)
如果你的模型边界离异常体比较远,用第二类边界条件是够的,问题不大。但如果模型边界离供电点只有几个网格距离,第二类边界条件会导致明显的边界效应——等位线会被压缩、电位梯度异常变大。这种情况必须用混合边界条件,否则算出来的视电阻率曲线在远极距处会翘起来,很丑也很难看。
我在这个项目里用的是混合边界条件。具体实现是在全局刚度矩阵的边界节点上,对角元素累加一项:
[ K_{ii} += \frac{\cos\theta_i}{r_i} \cdot S_i ]
其中S_i是边界面积权重,r_i是边界节点到电源的距离,θ_i是边界节点与电源的连线与边界法向的夹角。这个项是从边界积分推导出来的,感兴趣的可以看徐世浙《地球物理中的有限单元法》。
提示:在对比不同边界条件的效果时,可以在相同网格下分别用Neumann和混合边界跑一遍,然后对比边界附近的电位分布。如果差异超过5%,说明模型边界太小,需要扩大网格范围或改用混合边界。
3. 网格生成与形函数组装——从单元刚度矩阵到全局稀疏系统
3.1 六面体网格剖分:手工剖分方案
我在这个项目中选用了六面体网格。理由很直接:直流电法正演需要处理大尺度背景场加局部细化的场景,六面体网格在规则层状介质中有天然优势——剖分简单、单元数量可控、形函数推导方便。
网格生成策略采用"三向分区剖分法":
- 在目标区域(异常体附近)用细网格(0.5m~1m)
- 在过渡区域用渐变网格(1m~5m)
- 在边界区域用粗网格(10m~50m)
这样既保证目标区域的计算精度,又控制总网格数量。我的测试模型是100m × 100m × 60m的立方体,采用60×60×40的剖分,总单元数14.4万,节点数约15万,这个规模在Matlab里用稀疏矩阵是可以处理的。
如果你不知道怎么安排渐变网格,可以用最简单的比例函数:
% 生成从xmin到xmax的网格坐标,中间区域加密 function x = generateGrid(xmin, xmax, nFine, fineSize, ratio) % 从中心向外扩展生成节点坐标 xLeft = xmin; xRight = xmax; % 中心点 xCenter = (xmin + xmax) / 2; % 向左右两侧按比例递增扩展 x = []; % 左半部分 xTemp = xCenter; step = fineSize; while xTemp > xmin x = [xTemp, x]; xTemp = xTemp - step; step = step * ratio; end x = [xmin, x]; % 右半部分 xTemp = xCenter; step = fineSize; while xTemp < xmax xTemp = xTemp + step; step = step * ratio; x = [x, xTemp]; end x = unique([x, xmax]); end这里的关键参数是ratio,一般取1.2~1.5,太大网格突变会引入数值误差,太小细网格区域外扩太快。我实测下来1.3是比较合理的折中。
3.2 三线性形函数与单元刚度矩阵
对于六面体单元,每个单元有8个节点。采用三线性形函数:
[ N_i(\xi, \eta, \zeta) = \frac{1}{8}(1 + \xi_i\xi)(1 + \eta_i\eta)(1 + \zeta_i\zeta) ]
其中(\xi, \eta, \zeta \in [-1, 1])是局部坐标,(\xi_i, \eta_i, \zeta_i)是第i个节点在局部坐标系中的坐标(都为±1)。
单元刚度矩阵的计算公式为:
[ \mathbf{K}^e_{ij} = \int_{-1}^{1} \int_{-1}^{1} \int_{-1}^{1} \sigma [\frac{\partial N_i}{\partial x}\frac{\partial N_j}{\partial x} + \frac{\partial N_i}{\partial y}\frac{\partial N_j}{\partial y} + \frac{\partial N_i}{\partial z}\frac{\partial N_j}{\partial z}] |\mathbf{J}| d\xi d\eta d\zeta ]
物理含义可以这样理解:它量化了两个节点之间通过单元传递电流的能力。这就像水管系统中两个接口之间的连通能力,电导率越高(水管越粗),连通能力越强;节点间距越远(水管越长),连通能力越弱。
代码实现中用高斯积分(每个方向2个积分点,共8个积分点):
% 2点高斯积分 gaussPoints = [-1/sqrt(3), 1/sqrt(3)]; gaussWeights = [1, 1]; Ke = zeros(8, 8); for i = 1:2 for j = 1:2 for k = 1:2 xi = gaussPoints(i); eta = gaussPoints(j); zeta = gaussPoints(k); w = gaussWeights(i) * gaussWeights(j) * gaussWeights(k); % 计算形函数对局部坐标的导数 [dN_dxi, dN_deta, dN_dzeta] = shapeFunctionDerivatives(xi, eta, zeta); % 计算雅可比矩阵 J = [dN_dxi * xNodes'; dN_deta * yNodes'; dN_dzeta * zNodes']; detJ = det(J); invJ = inv(J); % 形函数对全局坐标的导数 dN_dx = invJ(1,1) * dN_dxi + invJ(1,2) * dN_deta + invJ(1,3) * dN_dzeta; dN_dy = invJ(2,1) * dN_dxi + invJ(2,2) * dN_deta + invJ(2,3) * dN_dzeta; dN_dz = invJ(3,1) * dN_dxi + invJ(3,2) * dN_deta + invJ(3,3) * dN_dzeta; % 单元电导率 sigma = getElementConductivity(element); % 累加到单元刚度矩阵 for a = 1:8 for b = 1:8 gradA = [dN_dx(a), dN_dy(a), dN_dz(a)]; gradB = [dN_dx(b), dN_dy(b), dN_dz(b)]; Ke(a,b) = Ke(a,b) + sigma * (gradA * gradB') * detJ * w; end end end end end3.3 全局稀疏矩阵组装:Matlab实现的关键优化点
全局矩阵组装是Matlab实现中最容易卡壳的地方。最容易犯的错误是用双层循环逐个填充全局矩阵,在15万节点规模下,这种写法会慢到怀疑人生。
正确做法是使用COO格式(坐标列表)一次性构建稀疏矩阵:
% 预分配COO数组 I = zeros(nElements * 64, 1); J = zeros(nElements * 64, 1); V = zeros(nElements * 64, 1); idx = 0; for el = 1:nElements nodes = elementNodes(el, :); % 8个节点的全局编号 Ke = assembleElementStiffness(el); for a = 1:8 for b = 1:8 idx = idx + 1; I(idx) = nodes(a); J(idx) = nodes(b); V(idx) = Ke(a, b); end end end % 一次性构建稀疏矩阵 K = sparse(I, J, V, nNodes, nNodes); K = (K + K') / 2; % 保证对称性这里有一个精度问题要特别注意:Matlab的sparse函数在遇到重复下标时默认是累加的。如果相邻单元共享节点,它们的贡献会自动累加,这正是我们需要的。但必须注意浮点误差导致的矩阵不完全对称,所以组装完成后用(K+K')/2强制对称。
提示:千万不要在循环里用K(nodes(a), nodes(b)) = K(nodes(a), nodes(b)) + Ke(a,b)这种方式。在Matlab里对稀疏矩阵逐元素赋值会频繁触发稀疏矩阵的重新索引,15万节点规模下可能等你半小时都算不完。
4. 点电源源项处理——最容易出错的地方
4.1 δ函数的离散化:不要在节点上直接赋值
这是初学者最容易犯的错误:把源项直接放到供电电极所在节点的右端项上,设成f(i) = I。这样做的问题在于,δ函数的物理意义是单位体积内的源强度,而节点代表的是一个控制体积,直接赋值等于把源强均匀分布在节点控制的体积上,但体积因子没有考虑在内。
正确做法是将电流源按体积加权分配到包含供电点的单元节点上:
[ f_i = I \cdot N_i(\xi_0, \eta_0, \zeta_0) ]
其中(N_i)是形函数在电源局部坐标处的值。
如果电源恰好在节点上,直接(f_i = I)即可。但现实中电源通常不落在节点上,特别是在曲面地形或渐变网格中。这个项目里我很幸运,把电源放在了节点上,但如果要做任意位置的电源,千万不要忘了体积加权。
4.2 供电电极与测量电极的分离处理
直流电法实测中通常有两个供电电极(A、B)和两个测量电极(M、N)。正演时,每个供电点单独求解一次,然后利用叠加原理得到MN之间的电位差:
[ \Delta u_{MN} = (u_A(M) - u_A(N)) + (u_B(M) - u_B(N)) ]
其中u_A表示A点供电时的电位分布,u_B表示B点供电时的电位分布。
这种处理方式的代码实现非常方便,只需循环电源位置求解右侧项,然后按公式组合即可。
4.3 源项处理的抗奇异性方案(singularity removal)
点电源在源点附近会产生电位奇异性——电位趋向无穷大,导致有限元解在源点附近误差很大。处理这个问题有几种方法:
- 在源点附近加密网格(我用的方案)
- 奇异项分离技术(两次求解法):将总电位分解为均匀半空间解析解与异常电位之和
奇异项分离的数学形式为:
[ u = u_p + u_s ]
其中u_p是背景模型(均匀半空间)的解析解,u_s是异常电位。将u带入控制方程,可以转化为求解u_s的方程。这样源点附近的奇异性被解析解吸收了,数值计算只需要求解光滑的异常电位。
但这个方案实现复杂度高,还要处理边界条件的转化,目前还在优化中。如果你的项目不需要高精度源点附近电位,像我这样在源点附近加密网格就够了。
% 二次求解函数实现 function [uTotal, uNormal] = solveDCPotential(sigma, nodes, elements, sourceNode, I) % 跳过背景场直接解总场的常规方法 K = assembleGlobalStiffness(sigma, nodes, elements); f = zeros(size(nodes, 1), 1); f(sourceNode) = I; % 混合边界条件修正边界节点 K = applyMixedBoundary(K, nodes, sourceNode); uNormal = K \ f; uTotal = uNormal; end5. 算法验证——从解析解到复杂模型的差分检验
5.1 均匀半空间模型与解析解对比
正演程序写完后,第一件事不是急着跑复杂模型,而是先用均匀半空间模型验证代码的正确性。均匀半空间地表点电源的电位解析解为:
[ u(r) = \frac{I\rho}{2\pi r} ]
其中(\rho = 1/\sigma)是电阻率,r是距供电点的距离。
这个公式极其简洁,但威力巨大——它能一次性检验刚度矩阵组装、源项处理和求解器的正确性。如果解析解和数值解的相对误差在2%以内,说明主程序通畅。
验证脚本的大致逻辑:
% 均匀半空间模型验证 sigma = 0.01; % 电导率,对应电阻率100 Ohm·m I = 1; model = createHalfSpaceModel(100, 100, 60, [60, 60, 40], sigma); nodes = model.nodes; elements = model.elements; % 供电点在地表中心 sourceNode = findSourceNode(model, 50, 50, 0); uNum = solveDCPotential(model, sourceNode, I); % 计算解析解 dist = sqrt((nodes(:,1)-50).^2 + (nodes(:,2)-50).^2 + nodes(:,3).^2); uAna = I * (1/sigma) ./ (2 * pi * dist); % 对比相对误差(避开源点附近区域) mask = dist > 5 & nodes(:,3) == 0; % 地表且距离源点大于5m relErr = abs(uNum(mask) - uAna(mask)) ./ abs(uAna(mask)); fprintf('最大相对误差: %.2f%%\n', max(relErr)*100); fprintf('平均相对误差: %.2f%%\n', mean(relErr)*100);我在实际测试中,六面体网格在源点外5m处的电位相对误差约1.2%,在20m以外降到0.5%以下。这个精度对直流电法正演来说足够了。
注意:解析解验证时要排除源点附近区域的节点,因为源点附近电位值大且变化剧烈,由源项离散化引入的误差会被放大。我通常取r > 3倍最小网格尺寸的节点做对比。
5.2 两层地电模型与递归解析解对比
均匀半空间通过后,进一步用两层地电模型验证程序对层状介质的响应。两层模型的视电阻率响应有现成的解析公式(基于Cagniard公式或递归反射系数法),可以直接对比。
这里贴一段我在Matlab中实现的两层模型解析视电阻率计算:
function rhoA = twoLayerApparentResistivity(AB, h1, rho1, rho2) % AB: 供电极距(A和B的距离) % h1: 第一层厚度 % rho1, rho2: 两层电阻率 k = (rho2 - rho1) / (rho2 + rho1); % 反射系数 r = AB / 2; % 利用镜像法累加 sum = 1; n = 1; term = 1; while abs(term) > 1e-6 term = k^n * r / sqrt(r^2 + (2*n*h1)^2); sum = sum + 2 * term; n = n + 1; if n > 1000 break; end end rhoA = rho1 * sum; end两层模型的验证通过后,程序对层状介质的分辨能力就确定了。下一步开始做横向不均匀体模型,但是一般推荐先做差分检验。
5.3 三维异常体模型的差分检验
解析解验证完基本正确性之后,我建议不要直接跳到实测数据,先做一个更有挑战性的验证:用加密网格的数值解作为参考标准,检验粗网格解的精度。
做法很简单:
- 对一个含三维异常体的模型,用加密网格(比如90×90×60)算一遍,视为"准解析解"
- 用较粗网格(60×60×40)算一遍
- 将粗网格插值到加密网格坐标上,对比电位差
这个检验能直观地看出网格大小对解的影响。我的经验是,当网格加密一倍后,如果电位解的变化小于1%,说明原网格是合理的;如果变化超过5%,说明网格太粗,需要重新剖分。
% 差分检验结果示例 % 加密网格: 90x90x60, 粗网格: 60x60x40 % 异常体: 10m x 10m x 10m, 电阻率100 Ohm·m, 背景电阻率10 Ohm·m % 最大电位差: 0.75% (位于异常体边界正上方) % 平均电位差: 0.32% % 结论: 60x60x40网格满足精度要求6. 从正演到应用——探测场景建模与结果可视化
6.1 应用场景一:断层含水构造探测模拟
正演程序最直接的应用是模拟不同地电条件下各种装置的观测数据,为野外施工设计提供依据。
我构造了一个断层破碎带含水构造模型:背景电阻率设为500 Ohm·m(灰岩),断层破碎带电阻率20 Ohm·m(含水),断层宽度10m,走向沿Y方向,倾角约60°。
观测装置采用温纳装置(Wenner)和施伦贝谢装置(Schlumberger)两种排列,沿X方向布置测线。对比结果如下表:
| 装置 | 极距因子 | 对低阻体响应 | 信号强度 | 抗干扰能力 |
|---|---|---|---|---|
| 温纳装置 | 1 | 明显,异常幅值约25% | 较强 | 中等 |
| 施伦贝谢装置 | 1 | 明显,异常幅值约32% | 弱 | 较强 |
从计算结果看,施伦贝谢装置对低阻体的水平分辨率略高,但野外信噪比低时不如温纳装置稳定。这和实际经验吻合——高分辨率的代价就是信号弱。
6.2 应用场景二:岩溶溶洞探测模拟
另一个典型应用场景是探地雷达和直流电法联合探测岩溶溶洞。我构造了一个埋深15m、直径5m的溶洞模型,用跨孔电阻率CT排列进行正演模拟。
这个场景的网格设计需要注意:溶洞直径只有5m,如果网格超过2m,溶洞的几何形态就不能被准确刻画。所以我把溶洞附近网格加密到0.5m,远离区域用3m的粗网格。
多极距三极装置CT正演结果能明显看出溶洞的低阻或高阻响应(充水溶洞低阻,充气溶洞高阻),而且多个源距的电位差数据可以同时提供零散的定位信息。
6.3 结果可视化——用Matlab把电位分布画出来
正演结果的展示直接影响论文、报告或汇报的效果。我带网格信息的电位分布配以切片图和俯视图展示:
% 切片图:展示电位在多个截面上的分布 figure; [x3d, y3d, z3d] = meshgrid(unique(nodes(:,1)), unique(nodes(:,2)), unique(nodes(:,3))); u3d = griddata(nodes(:,1), nodes(:,2), nodes(:,3), uNum, x3d, y3d, z3d); % 三个切片 subplot(2,2,1); slice(x3d, y3d, z3d, u3d, [], 50, [0 15 30]); shading interp; colorbar; xlabel('X (m)'); ylabel('Y (m)'); zlabel('Z (m)'); title('电位切片分布'); % 地表电位平面图 subplot(2,2,2); [uSurf, xSurf, ySurf] = griddata(nodes(nodes(:,3)==0,1), nodes(nodes(:,3)==0,2), ... uNum(nodes(:,3)==0), unique(nodes(:,1)), unique(nodes(:,2)), 'cubic'); imagesc(xSurf, ySurf, uSurf); set(gca, 'YDir', 'normal'); colorbar; xlabel('X (m)'); ylabel('Y (m)'); title('地表电位分布');对于视电阻率断面图,通常用伪剖面图来展示,把每个测深点的视电阻率值画在对应的极距-位置坐标上。Matlab的pcolor或contourf函数是常用工具。
6.4 多场景扩展:程序模块化设计思路
为了让程序有更强的扩展性,我在代码结构上做了模块化设计。核心模块包括:
- 网格生成模块(createModel.m):负责生成节点坐标、单元连接关系、边界标记
- 矩阵组装模块(assembleGlobalStiffness.m):负责单元刚度矩阵计算和全局稀疏矩阵组装
- 源项模块(buildSourceTerm.m):负责点电源的离散化
- 边界条件模块(applyBoundaryCondition.m):负责混合边界条件的实现
- 求解模块(solveSystem.m):负责线性方程组求解
- 后处理模块(visualizeResults.m):负责结果可视化
每个模块都写成了独立函数,输入输出清晰。后续如果要扩展为各向异性介质、带地形模型,只需改对应模块,不影响其他部分。
关于程序效率,当前最耗时的部分是全局刚度矩阵的组装循环。如果用parfor并行循环替代普通for循环,可以显著提升速度:
% 并行组装单元刚度矩阵 parfor el = 1:nElements Ke = assembleElementStiffness(el); % 将结果保存到独立数组,最后统一组装 KeList{el} = Ke; end我实测在12核本地机器上,parfor可以将组装时间减少约65%。但要注意,parfor中不能对共享数组I、J、V直接累加,必须用cell数组返回每个单元的结果,最后再汇总组装。
7. 性能优化与常见问题排查——调试、提速与误差修正
7.1 线性求解器选型:直接法 vs 迭代法
Matlab里求解稀疏线性方程组最简单的方式是直接用反斜杠运算符:
u = K \ f;对于15万节点规模的问题,Matlab的Cholesky分解(K是对称正定矩阵,自动选择)能在大约10~30秒内完成。如果模型大于50万节点,直接法会占用几十GB内存,这时候必须转向迭代法。
迭代法中,我用得最顺手的是预处理共轭梯度法(PCG):
% 不完全Cholesky预条件子 L = ichol(K, struct('type', 'ict', 'droptol', 1e-3, 'michol', 'on')); [u, flag, relres, iter] = pcg(K, f, 1e-8, 500, L, L');关键参数解释:
- droptol(丢弃容差)控制预条件子的丰满程度,太小则L太密没有预条件效果,太大会导致收敛慢。我实测1e-3~1e-4比较合适
- michol(修正不完全Cholesky)对于对角占优问题效果更好
- maxiter设500,如果500步不收敛,通常说明预条件子参数需要调整
对比实测结果(15万节点、均匀半空间模型):
| 方法 | 求解时间 | 内存占用 | 相对误差 |
|---|---|---|---|
| 直接法(K\f) | 18s | 约2.5GB | 1.2% |
| PCG + ichol(droptol=1e-3) | 5s | 约0.8GB | 1.3% |
| PCG + ichol(droptol=1e-6) | 8s | 约1.2GB | 1.2% |
| 无预条件CG | 180s | 约0.5GB | 1.5% |
可以明显看出,预条件子的选择对迭代法的效率影响极大,一个合适的ichol预条件子能让求解速度提升30倍以上。
7.2 常见错误排查:看这五个症状就够了
症状一:对角值很大,解全是NaN或Inf
原因几乎都是源项处理错误——电源节点放错了位置,或f向量中有NaN混入。检查方法很简单:
assert(all(isfinite(f)), '右侧项包含非有限值'); assert(all(diag(K) > 0), '刚度矩阵对角线出现非正数');症状二:解出来电位不是单调递减的
均匀介质中电位从源点向外应该单调递减。如果沿某一方向出现震荡或回升,大概率是网格严重畸变或单元连接关系错误。检查手段是画一个切片等值线图,肉眼观察电位云图是否光滑。
症状三:两个不同模型算出来的结果几乎一样
极大概率是参数传递问题——电导率数组没有正确传入装配函数。建议在装配前先把每个单元的电导率统计打印出来,确认异常体单元的电导率值与背景不同。
症状四:求解速度奇慢
先看是否误用了稠密矩阵存储。Matlab中全零矩阵默认是double型稠密矩阵,如果初始化的时候忘了用sparse,后面组装到全局矩阵时就会灾难。
症状五:边界处等值线严重变形
这是边界条件不匹配的典型表现。试着扩大网格范围,看看变形是否减轻。如果减轻,说明是边界截断误差;如果没有,说明边界条件实现有问题。
7.3 不同网格加密策略的对比——如何把握精度与效率的平衡
实际测试中,不同加密策略的效果差异很大。我对比了三种网格方案:
方案一:全局均匀加密(把整体网格分别加密为90×90×60、120×120×80) 方案二:只加密异常体和源点附近(其余区域保持粗网格) 方案三:全域按等比数列渐变(源点最密、边界最疏)
综合对比如下:
| 网格方案 | 总节点数 | 相对精度 | 装配时间 | 求解时间 |
|---|---|---|---|---|
| 全局均匀60³ | 22.7万 | 基准 | 12s | 25s |
| 全局均匀90³ | 75万 | 提升约60% | 45s | 120s |
| 局部加密(推荐) | 15万 | 提升约80% | 8s | 18s |
| 渐变网格 | 18万 | 提升约50% | 9s | 20s |
很有意思的结论:局部加密方案用更少的节点数获得了更高的精度。原因在于直流电法的解在电源附近和异常体附近变化最剧烈,这些区域恰恰需要最细的网格;而远离这些区域的解非常平滑,粗网格完全够用。这几乎是直流电法正演网格设计的黄金法则。
就我个人经验而言最终采用如下操作:先跑一个粗网格模型(45×45×30),确认整体响应形态正确之后,再在关注区域做局部加密,得出最终的高精度结果。
8. 程序扩展思路与后续优化方向
项目做到这里,基本功能已经完整了。但如果你想继续往深走,有几条路可以考虑。
第一,把当前的直流电法正演扩展为频率域电磁法正演。控制方程从椭圆型变为Helmholtz型,需要加入趋肤效应项((i\omega\mu\sigma)),但整体有限元框架完全复用。第二,加入地形起伏——这是野外实测绕不开的需求,核心工作是把网格生成改造为随地形变化的自适应网格。第三,做反演程序——正演是反演的核心底层,把当前程序嵌入Occam反演或高斯-牛顿反演框架中,就构成一个完整的成像系统。
还有一个值得说的方向:把Matlab程序与Python生态结合起来。Matlab负责核心矩阵求解和可视化,Python的TensorFlow/PyTorch负责反演中的深度学习模块。两者之间的数据传递通过.mat文件或CSV文件完成,不需要复杂的接口。
根据我个人的项目经验,Matlab的矩阵运算能力和调试体验对3D直流电法正演这类中大规模数值计算非常合适,但要把效率发挥到极致,必须在稀疏矩阵存储、向量化操作和预条件子上下足功夫。正演问题的最终目的不是算出一个好看的电位分布图,而是为反演、解释和野外设计提供可靠的响应依据,这一点在程序开发的每个环节都值得反复回头确认。
本文还有配套的精品资源,点击获取