
简介这套基于交错网格的三维非定常纳维-斯托克斯求解器使用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.h和preProcessing.h正是用来分配这些偏移量并维护网格尺寸和边界数组。2.2 三维交错网格的内存布局与索引宏处理三维问题时内存布局要仔细设计。一般我会把压力空间定义成(NX1)×(NY1)×(NZ1)个节点速度分量各占一个方向的面中心C语言数组用一维连续内存表示。下面这段代码描述变量声明const int NX 64, NY 64, NZ 64; // 压力与密度存放在网格节点上 double p[(NX1)*(NY1)*(NZ1)]; double rho[(NX1)*(NY1)*(NZ1)]; // 三个速度分量分别存储在三个方向的面中心 double u[(NX1)*NY*NZ]; // u面中心对应x方向 double v[NX*(NY1)*NZ]; // v面中心对应y方向 double w[NX*NY*(NZ1)]; // 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(i1,j,k)] - p[IDX_P(i,j,k)]) / dx; RHS_U[IDX_U(i,j,k)] - dpdx; } } }IDX_P和IDX_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(NX1, j, k)] p[IDX_P(NX, j, k)]; }如果边界是入口则需要把入口面的u设为给定速度分布压力仍然使用法向零梯度。出口边界通常让速度法向梯度为零压力给定为环境值。调试中我发现这类边界条件的数组越界往往发生在j和k的循环上界写错时因此建议把边界处理单独放在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.h和postProcessing.h分别负责初始化和输出variables.h存储全局数组simple.h与simpler.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.3CFL_diff取0.2。如果计算域内存在剪切层或边界层最大速度点往往不在入口而在边界层附近因此打印最大速度的坐标也很有用。对非定常模拟时间步还要兼顾物理时间分辨率不能只满足CFL一般我会要求每个涡翻转周期至少有20到50个时间步。4.3 压力泊松方程的残差监控压力修正方程是不定常求解器的核心瓶颈它要求每一个时间步内都收敛到足够精度。迭代求解时需要监控残差double res 0.0; for (int i0; inx*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 -stdc11 -O2 -Wall -o u3dns U3DSG.cpp simpler.cpp simple.cpp编译报错多半是C11后的头文件路径或C标准库兼容问题。运行前把输出重定向到日志文件避免大量残差滚动刷屏。如果需要修改边界条件或初始速度分布直接在preProcessing.h里改即可改完重新编译一次。如果用了-Wall看到未初始化变量警告必须处理这类问题在非定常计算中常导致压力修正量逐渐偏移。5.2 让三维循环对缓存更友好交错网格天然让速度数组比压力数组少一个维度很容易出现索引错位高速缓存利用率也容易受影响。实践中最有效的调整是把最内层循环放在网格长度最大、数组连续的方向上。假设k方向连续代码结构如下for (int i1; inx; i) for (int j1; jny; j) for (int k1; knz; k) { double uP u[IDX_U(i,j,k)]; double uE u[IDX_U(i,j,k1)]; }同时打开-O3和-marchnative之后向量化比率会明显提高。若使用OpenMP需要把数组声明为共享变量并在每个线程内复制一部分边界值否则交界面上的速度点会被多个线程重复写入产生计算错误。5.3 用不同CFL对比验证时间步分辨率最后一个验证技巧把CFL分别设为0.5、0.2和0.1使用同一初始条件各跑足够长的物理时间在某个固定截面上输出u或压力曲线。如果三条曲线基本重合说明当前时间步长已经能够解析流动如果曲线分离明显则说明数值耗散在起作用需要减小步长或升级对流格式。我在验证非定常涡脱落时就用这个办法比单纯看残差可靠得多也能顺带确认时间推进是否存在过度耗散。本文还有配套的精品资源点击获取