交错网格上的三维非定常Navier-Stokes求解器源码解析
2026/9/16 5:07:43 网站建设 项目流程

简介:这套基于交错网格的三维非定常纳维-斯托克斯求解器,使用C/C++编写,面向计算流体力学方向的学生、研究人员及相关工程师,用于模拟激波、涡旋生成等随时间演化的流动问题。压缩包共10个文件,体积27KB,包含8个头文件、1个C++源文件和1份Word说明文档,头文件覆盖预处理、后处理、格式离散、变量声明、SIMPLER算法及延迟校正等核心模块,主程序文件则统筹求解流程。已有187人学习下载,适合作为计算流体力学数值方法的入门范例和二次开发基础;通过阅读源码,可以理解交错网格上速度分量与压力变量的错位存储方式,学习时间推进、差分格式、边界条件处理和压力方程迭代求解等关键实现。代码规模精简而模块清晰,便于对照经典算法逐段分析,也可结合文档进一步扩展非结构网格或并行计算,对深入掌握非定常流动数值模拟具有直接的参考价值。

1. 非定常三维Navier-Stokes解算器,从交错网格源码开始拆解

很多刚接触CFD的分析人员会疑惑:为什么用交错网格?如果直接在同一套网格上存储所有变量,速度与压力的耦合关系很容易被破坏,出现棋盘状压力场;而换成交错网格后,u、v、w分量分别错位半个控制体,差分时能天然感知相邻压力差,数值刚度明显改善。这里要拆的这份源码u3D-NS-SG,就是一个典型的三维非定常Navier-Stokes解算器,文件不多,核心算法集中在U3DSG.cpp以及schemes.h、simple.h等头文件里,源码和模块都容易看清楚。

这份代码没有依赖现成商业库,而是用C和C++从头写了变量存储、压力-速度耦合、时间推进和边界条件,适合想掌握交错网格和投影算法细节的人,也很适合把其中某个模块提取出来移植到自己的研究中。下面我从变量布局、模块主流程、时间积分和部署调试四个方向逐层展开。

2. 交错网格上的变量布局与离散化

2.1 从压力棋盘问题到错位存储

原生同位网格把所有变量存储在控制体中心时,压力场的奇偶网格可以在离散方程中“脱钩”,产生棋盘状压力分布,而压力梯度不再推动流动。交错网格的做法是让三个速度分量分别落在三个方向的控制体表面上,压力仍留在格心。在三维笛卡尔坐标下,u分量位于x方向相邻压力节点的中点,v、w同理;这样一个速度方程天然引入了它两侧的压力差,避免了压力-速度失耦。

存储方式变量位置离散难度压力-速度耦合典型场景
同位网格速度、压力同置于格心易出现棋盘失耦商用软件需配动量插值
交错网格速度在面中心,压力在格心天然避免失耦本求解器采用
非结构网格变量在网格体心或节点上需要特殊压力处理复杂几何CFD

交错网格也有代价:控制体之间相互错开,边界面插值、边界条件实现和索引管理都要比同位网格多一层逻辑。这个项目里的variables.hpreProcessing.h正是用来分配这些偏移量,并维护网格尺寸和边界数组。

2.2 三维交错网格的内存布局与索引宏

处理三维问题时,内存布局要仔细设计。一般我会把压力空间定义成(NX+1)×(NY+1)×(NZ+1)个节点,速度分量各占一个方向的面中心,C语言数组用一维连续内存表示。下面这段代码描述变量声明:

const int NX = 64, NY = 64, NZ = 64; // 压力与密度存放在网格节点上 double p[(NX+1)*(NY+1)*(NZ+1)]; double rho[(NX+1)*(NY+1)*(NZ+1)]; // 三个速度分量分别存储在三个方向的面中心 double u[(NX+1)*NY*NZ]; // u面中心,对应x方向 double v[NX*(NY+1)*NZ]; // v面中心,对应y方向 double w[NX*NY*(NZ+1)]; // w面中心,对应z方向

这里使用一维数组是为了避免多层 vector 带来的内存碎片和寻址开销。在C++里访问u(i,j,k)时,索引可以写成i*NY*NZ + j*NZ + k,我通常在头文件里定义IDX_U宏。交错数组比压力数组少一个维度,所以循环上界要特别注意:求解u动量方程时,i从1循环到NX-1而不是NX,否则会越界读入未定义数据。

2.3 压力梯度在交错网格上的离散化

交错网格下,压力梯度项可以直接用相邻压力差表达,不需要插值。假设网格步长均匀,u动量方程中的压力梯度写成:

for (int i = 1; i <= NX-1; ++i) { for (int j = 1; j <= NY; ++j) { for (int k = 1; k <= NZ; ++k) { double dpdx = (p[IDX_P(i+1,j,k)] - p[IDX_P(i,j,k)]) / dx; RHS_U[IDX_U(i,j,k)] -= dpdx; } } }

IDX_PIDX_U分别是压力与速度的索引宏。注意到u所在位置正是压力节点之间,因此压力梯度的中心差分在交错网格上是严格二阶精度;如果变量同址,这里必须用插值得到压力梯度,插值过程会压低有效精度并引入额外耗散。这也是交错网格在有限体积法中仍然被广泛使用的原因之一。

2.4 边界条件的错位处理

边界条件在交错网格上比同位网格多一道手续。无滑移壁面处,速度分量直接落在壁面边界上,可以直接赋零;而压力在壁面上往往需要法向梯度满足零条件。常见做法是在边界外引入一层虚拟网格,用镜像赋值实现:

// 无滑移壁面:x 方向左侧和右侧 for (int j = 1; j <= NY; ++j) for (int k = 1; k <= NZ; ++k) { u[IDX_U(0, j, k)] = 0.0; u[IDX_U(NX, j, k)] = 0.0; // 虚拟格心压力复制法向值以达到零梯度 p[IDX_P(0, j, k)] = p[IDX_P(1, j, k)]; p[IDX_P(NX+1, j, k)] = p[IDX_P(NX, j, k)]; }

如果边界是入口,则需要把入口面的u设为给定速度分布,压力仍然使用法向零梯度。出口边界通常让速度法向梯度为零,压力给定为环境值。调试中我发现,这类边界条件的数组越界往往发生在jk的循环上界写错时,因此建议把边界处理单独放在preProcessing.h里,方便统一复查。

3. u3D-NS-SG代码库:模块划分与执行流程

3.1 U3DSG.cpp主循环里发生了什么

打开U3DSG.cpp,最先看到的是一段按时间步推进的主循环,结构相当于标准压力投影法。它做的事情可以用下面的伪代码概括:

while (time < tEnd) { preProcessing(); // 更新边界条件和物理参数 for (int it = 0; it < outerIter; it++) { computeMomentum(u, v, w, p); // 由动量方程计算预测速度 solvePressure(p); // 解压力泊松方程 correctVelocity(u, v, w, p); // 用压力修正速度 } postProcessing(); // 输出流场、残差等信息 time += dt; }

这段流程是许多不可压Navier-Stokes解算器共用的骨架,区别在于各函数内部如何取值。preProcessing.hpostProcessing.h分别负责初始化和输出,variables.h存储全局数组,simple.hsimpler.h提供两种压力-速度耦合算法。一般我会把outerIter设为2到3,因为在每个时间步内多迭代几次只是让子问题更收敛,并不会提高时间方向精度,反而让非线性被迭代成稳态,削弱了非定常效果。

3.2 schemes.h:空间差分格式与延迟修正

schemes.h封装了对流项和扩散项的空间离散格式。在非定常计算中,对流项通常用二阶迎风或中心差分,扩散项则用中心差分。项目里出现的deferredCorrection.h实现的是延迟修正策略:先用低阶格式组装系数矩阵,再把高阶格式与低阶格式的差值作为显式源项加入右端项。这样可以保持矩阵对角占优,同时获得高阶精度。一个简单的延迟修正函数如下:

double deferredCorrection(double phiC, double phiE, double flux) { double phiUpwind = (flux > 0) ? phiC : phiE; double phiCentral = 0.5 * (phiC + phiE); return phiCentral - phiUpwind; // 该差值加入显式源项 }

调用时,系数矩阵中只保留一阶迎风部分,右端项额外加上返回值。这里乘一个亚松弛因子通常会更稳,比如0.7。如果因子取1.0,阶数高但显式修正量大会引起高频振荡;取太小又拉低格式有效精度。这个调试经验对任何嵌入高阶格式的求解器都适用。

3.3 simple.h 与 simpler.h:两套压力修正在非定常流动中的取舍

这两个文件分别实现SIMPLE和SIMPLER算法。SIMPLE先由猜测压力场求解动量方程得到预测速度,再由速度偏差构建压力泊松方程,并用修正量同时更新速度和压力;SIMPLER则先通过当前速度场重构一个压力场,再去解动量方程,然后用压力修正量只修正速度。两者在非定常计算中的差异如下:

算法预测后的压力用途速度修正方式单步开销非定常适配性
SIMPLE直接用初始压力压力修正量同时修正速度和压力好,瞬态响应直接
SIMPLER由速度重构压力压力修正量只修正速度略高快收敛但会较强抑制瞬态

在非定常模拟中,我倾向于使用标准SIMPLE。时间步内压力变化本身有限,SIMPLE的单步开销低,而SIMPLER虽然每个时间步内收敛更快,但重构压力相当于额外滤波,可能让时间尺度稍微失真。项目中保留两套正好用来互相验证:用相同初值分别跑几个时间步,对比速度和压力场的差异可以判断算法实现是否正确。

3.4 variables.h与postProcessing.h:数据交换与后处理切入口

variables.h集中放网格尺寸、物性参数和流场数组,避免多个模块重复声明。postProcessing.h负责输出速度、压力和残差,同时可以计算某截面上的流量或平均速度损失。调试时我会在postProcessing里加入一个函数,把每个时间步的最大速度和最小压力输出到日志,一旦最大速度突然增长到原来的十倍以上,基本可以断定流场开始失稳,再逐步回溯到时间步或边界条件上。

4. 非定常流动的时间积分与稳定性控制

4.1 时间离散:显式、隐式还是半隐式

三维Navier-Stokes在时间方向有双重的刚性来源:粘性扩散项对时间步长的限制和对流项的CFL限制。显式格式实现简单,但扩散项的稳定条件要求时间步长与网格间距的平方成正比,三维细网格下几乎无法使用。隐式格式无条件稳定,但每个时间步要解非线性方程组,开销大。半隐式把对流项显式处理,压力与扩散项隐式处理,是中大型问题最常用的折中方案。

格式稳定条件每步计算量典型时间步
显式 Eulerdt ≤ min(CFL·dx/u, 0.5·dx²/ν)
全隐式无条件稳定受精度限制
半隐式对流限制与扩散限制混合中间值

项目的时间循环中,压力通过隐式泊松方程求解,速度的对流项则使用显式积分,配合CFL条件限制时间步。代码实现时,先把显式对流项算好放入右端向量,再调用隐式扩散求解器。

4.2 CFL条件与自适应时间步计算

不合适的步长会让残差在几个时间步内暴涨。对流稳定性条件限制时间步与网格间距成正比,与当地速度成反比;扩散稳定性条件则与粘性系数和网格间距平方成正比。为了保险,我会把全场最大速度和全局最细网格都取进来计算:

double dtConv = CFL_conv * dx / (maxU + 1e-8); double dtDiff = CFL_diff * dx * dx / (nu + 1e-12); double dt = min(dtConv, dtDiff);

这里CFL_conv我通常取0.3,CFL_diff取0.2。如果计算域内存在剪切层或边界层,最大速度点往往不在入口而在边界层附近,因此打印最大速度的坐标也很有用。对非定常模拟,时间步还要兼顾物理时间分辨率,不能只满足CFL;一般我会要求每个涡翻转周期至少有20到50个时间步。

4.3 压力泊松方程的残差监控

压力修正方程是不定常求解器的核心瓶颈,它要求每一个时间步内都收敛到足够精度。迭代求解时需要监控残差:

double res = 0.0; for (int i=0; i<nx*ny*nz; ++i) { double r = b[i] - (A[i]*p[i] + sumNeighbourCoef*pNeighbour); res += r*r; } res = sqrt(res / (nx*ny*nz)); printf("t=%.4f iter=%d pressRes=%.2e\n", time, iter, res);

残差一般降到初始值的千分之一才认为压力场合格。如果出现震荡,需要看残差的下降曲线是在同一个数量级上反复,还是单调下降。单调下降但速度慢时,可以增加迭代次数或使用更快的求解器;反复震荡则多半是时间步过大或边界压力条件给得不合理,这时降低CFL比加大迭代更有效。

4.4 非定常发散时的排查顺序

非定常模拟发散的原因往往是多因素叠加。我按固定顺序排查:先看时间步是否超过CFL限制;再检查初始压力场与边界条件是否相容;然后查压力泊松方程迭代是否达到收敛阈值;最后检查对流格式是否有局部振荡。这四步走下来,差不多能定位95%的问题。特别是在交错网格中,压力点与速度点数量不一致,一旦边界循环上界写错,发散点就会出现在特定方向的面附近,观察残差分布图能快速缩小范围。

5. 编译、性能优化与调试中的关键技巧

5.1 快速编译并运行

源码包内没有预置构建系统,把.cpp.h放在同一目录下直接用命令编译即可:

g++ -std=c++11 -O2 -Wall -o u3dns U3DSG.cpp simpler.cpp simple.cpp

编译报错多半是C++11后的头文件路径或C标准库兼容问题。运行前把输出重定向到日志文件,避免大量残差滚动刷屏。如果需要修改边界条件或初始速度分布,直接在preProcessing.h里改即可,改完重新编译一次。如果用了-Wall看到未初始化变量警告,必须处理,这类问题在非定常计算中常导致压力修正量逐渐偏移。

5.2 让三维循环对缓存更友好

交错网格天然让速度数组比压力数组少一个维度,很容易出现索引错位,高速缓存利用率也容易受影响。实践中最有效的调整是把最内层循环放在网格长度最大、数组连续的方向上。假设k方向连续,代码结构如下:

for (int i=1; i<=nx; i++) for (int j=1; j<=ny; j++) for (int k=1; k<=nz; k++) { double uP = u[IDX_U(i,j,k)]; double uE = u[IDX_U(i,j,k+1)]; }

同时打开-O3-march=native之后,向量化比率会明显提高。若使用OpenMP,需要把数组声明为共享变量,并在每个线程内复制一部分边界值,否则交界面上的速度点会被多个线程重复写入产生计算错误。

5.3 用不同CFL对比验证时间步分辨率

最后一个验证技巧:把CFL分别设为0.5、0.2和0.1,使用同一初始条件,各跑足够长的物理时间,在某个固定截面上输出u或压力曲线。如果三条曲线基本重合,说明当前时间步长已经能够解析流动;如果曲线分离明显,则说明数值耗散在起作用,需要减小步长或升级对流格式。我在验证非定常涡脱落时就用这个办法,比单纯看残差可靠得多,也能顺带确认时间推进是否存在过度耗散。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询