简介这份资源提供三维泊肃叶流数值模拟的C实现面向具备流体力学基础与C编程能力的学习者和科研人员用于理解纳维-斯托克斯方程在管道层流问题中的离散与求解思路。压缩包共5个文件约9KB包含1个cpp主源码、1个Makefile构建脚本、2个mk配置文件和1个sh启动脚本分别承担数值求解、编译链接、宏定义与运行参数传递等职责结构紧凑便于快速上手。资源围绕网格划分、初始条件设定、无滑移边界处理、时间推进与迭代收敛、流场结果输出等关键环节展开可作为CFD入门实践与代码研读的参考。目前已有700人学习适合希望借助小型可编译工程理解三维泊肃叶流模拟流程的读者。1. 三维泊肃叶流为什么值得用 C 从头写一遍很多人第一次接触计算流体力学CFD都是从二维泊肃叶流开始的两块无限大平板之间流体在压力驱动下做定常层流速度剖面是漂亮的抛物线。这个解析解太经典了经典到很多人写完二维版本就觉得自己懂了流体。但真正到了三维尤其是圆管、方管、任意截面管道里的泊肃叶流事情立刻变得不一样——截面形状决定了速度剖面的形状而速度剖面又直接决定了流量、壁面剪应力和压降之间的关系。如果你正在做微流控芯片设计、管道输运仿真或者只是想找一个有解析解可对照的算例来验证自己写的三维求解器那么用 C 实现一个三维泊肃叶流求解器是一个非常划算的练手项目。这篇文章不讲空泛的 CFD 概论而是把三维泊肃叶流的 C 代码这件事拆开控制方程怎么离散、网格怎么生成、边界条件怎么设、泊松方程怎么迭代求解、结果怎么和解析解对比验证。适合有 C 基础、学过一点数值方法、想动手写第一个三维流动求解器的工程师和学生。全程只依赖标准库不引入任何第三方 CFD 框架保证你能一行行看懂。2. 从 Navier-Stokes 到可求解的泊松方程三维泊肃叶流的数学骨架2.1 为什么三维泊肃叶流能简化成一个标量方程三维不可压缩 Navier-Stokes 方程写成向量形式是ρ(∂u/∂t (u·∇)u) -∇p μ∇²u ∇·u 0对于充分发展的管道泊肃叶流有几个关键假设让方程大幅简化。第一流动是定常的∂u/∂t 0。第二流动是充分发展的意思是速度剖面沿流动方向不再变化只和横截面坐标有关。第三我们取流动方向为 z 轴那么速度只有一个非零分量 w(x, y)即 u v 0w w(x, y)。把这三个条件代进去对流项 (u·∇)u 直接归零因为速度在流动方向没有梯度、在横截面方向分量为零。剩下的就是压力梯度和粘性力的平衡μ(∂²w/∂x² ∂²w/∂y²) ∂p/∂z这里 ∂p/∂z 是一个常数因为压力沿 z 方向线性下降。把它记作 -GG 是单位长度压降正值方程变成∂²w/∂x² ∂²w/∂y² -G/μ这就是一个标准的三维泊松方程只不过未知量 w 只依赖两个空间变量 (x, y)。所谓三维泊肃叶流物理上是三维的流体在三维管道里流动但数学上退化成了一个二维横截面上的泊松问题。这一点非常关键它意味着你不需要写一个完整的三维 Navier-Stokes 求解器只需要写一个二维泊松求解器再把它套在三维管道的横截面上。2.2 离散化五点差分格式与网格编号在矩形截面上我们用均匀网格离散。设 x 方向有 nx 个节点y 方向有 ny 个节点步长分别为 hx 和 hy。对二阶导数用中心差分∂²w/∂x² ≈ (w[i1][j] - 2w[i][j] w[i-1][j]) / hx² ∂²w/∂y² ≈ (w[i][j1] - 2w[i][j] w[i][j-1]) / hy²代入泊松方程整理后得到每个内点的迭代格式w[i][j] ( (w[i1][j] w[i-1][j]) / hx² (w[i][j1] w[i][j-1]) / hy² G/μ ) / (2/hx² 2/hy²)这就是五点差分格式。边界上 w 0无滑移条件。对于方管截面解析解是无穷级数但我们可以用数值解和它对比。网格编号上我用一维数组存二维数据索引 idx i * ny j这样内存连续缓存友好。对于 nx ny 129 的网格数组大小约 1.6 万个 double完全在缓存里跑得动。2.3 求解器选型为什么用 SOR 而不是直接法泊松方程离散后是一个大型稀疏线性系统。对于 129×129 的网格未知量约 1.6 万个直接法如高斯消元的复杂度是 O(N³)大约 4×10¹² 次操作太慢。而且直接法会破坏稀疏性内存也吃不消。迭代法里Jacobi 收敛太慢Gauss-Seidel 快一些但仍然慢SOR逐次超松弛通过引入松弛因子 ω 加速收敛实现简单几行代码就能写出来。对于泊松方程这类对称正定问题最优松弛因子有理论公式ω_opt 2 / (1 sin(π / max(nx, ny)))对于 nx ny 129ω_opt ≈ 1.95。用这个值SOR 的迭代次数大约在几百到一千次量级比 Gauss-Seidel 快一个数量级。这就是我选 SOR 的原因代码短、参数有理论指导、收敛速度够用。提示SOR 的收敛速度对 ω 非常敏感。ω 比最优值大一点会震荡小一点会变慢。实际写代码时先用理论公式算一个初值再在它附近微调。3. 用 C 把三维泊肃叶流求解器写出来网格、边界与 SOR 迭代3.1 网格生成与数据结构定义先定义网格参数和数据结构。我用一个结构体把网格和物理参数打包避免全局变量满天飞。#include iostream #include vector #include cmath #include chrono struct Grid { int nx, ny; // x、y 方向节点数 double Lx, Ly; // 截面物理尺寸 double hx, hy; // 网格步长 std::vectordouble w; // 速度场一维存储索引 i*nyj Grid(int nx_, int ny_, double Lx_, double Ly_) : nx(nx_), ny(ny_), Lx(Lx_), Ly(Ly_) { hx Lx / (nx - 1); hy Ly / (ny - 1); w.assign(nx * ny, 0.0); } // 二维索引转一维 inline int idx(int i, int j) const { return i * ny j; } };这里 nx 和 ny 是节点数不是单元数。比如 nx 129 表示 x 方向有 129 个节点包括两个边界点。hx Lx / (nx - 1) 是节点间距。速度场 w 用一维 vector 存索引规则是 i * ny ji 是 x 方向索引j 是 y 方向索引。物理参数方面取 G 1.0单位长度压降μ 1.0动力粘度这样 G/μ 1.0方程右边是常数 1。截面取正方形 Lx Ly 1.0。这些值不影响算法正确性只是让解析解好算。3.2 边界条件的处理无滑移与索引陷阱边界条件是 w 0在四条边上。写代码时最容易翻车的地方是边界索引。对于 i 0、i nx-1、j 0、j ny-1 的所有点w 都置零。初始化时整个数组已经是零所以只需要保证迭代过程中不更新边界点。void applyBoundary(Grid g) { for (int i 0; i g.nx; i) { g.w[g.idx(i, 0)] 0.0; g.w[g.idx(i, g.ny - 1)] 0.0; } for (int j 0; j g.ny; j) { g.w[g.idx(0, j)] 0.0; g.w[g.idx(g.nx - 1, j)] 0.0; } }这个函数看起来简单但如果你在迭代循环里忘了跳过边界点边界值会被内点公式覆盖导致解完全错误。我一般会在 SOR 循环里显式地从 i 1 到 nx-2、j 1 到 ny-2 遍历这样边界自然不被触碰。3.3 SOR 迭代实现与收敛判据SOR 的核心更新公式是w_new w_old ω * (w_gs - w_old)其中 w_gs 是 Gauss-Seidel 更新值。写成代码double solveSOR(Grid g, double G_over_mu, double omega, double tol, int maxIter) { double hx2 g.hx * g.hx; double hy2 g.hy * g.hy; double denom 2.0 / hx2 2.0 / hy2; double rhs G_over_mu; for (int iter 0; iter maxIter; iter) { double maxDiff 0.0; for (int i 1; i g.nx - 1; i) { for (int j 1; j g.ny - 1; j) { double w_gs ( (g.w[g.idx(i1, j)] g.w[g.idx(i-1, j)]) / hx2 (g.w[g.idx(i, j1)] g.w[g.idx(i, j-1)]) / hy2 rhs ) / denom; double w_old g.w[g.idx(i, j)]; double w_new w_old omega * (w_gs - w_old); g.w[g.idx(i, j)] w_new; double diff std::fabs(w_new - w_old); if (diff maxDiff) maxDiff diff; } } if (maxDiff tol) { std::cout SOR converged at iter iter , maxDiff maxDiff \n; return maxDiff; } } std::cout SOR reached maxIter without convergence\n; return -1.0; }参数说明G_over_mu 是方程右端项这里取 1.0omega 是松弛因子用理论公式算tol 是收敛容差我一般取 1e-8maxIter 是最大迭代次数取 100000 足够。收敛判据用最大更新量 maxDiff比用残差范数更直观也更容易调。注意SOR 的收敛速度对 omega 极其敏感。omega 1.0 退化成 Gauss-Seidelomega 2 会发散。实际调试时先把 omega 设成 1.5 跑一遍确认能收敛再逐步调到最优值附近。3.4 主函数组装与解析解对比把上面几块拼起来主函数里生成网格、算最优 omega、调用 SOR、输出中心点速度和解析解对比。int main() { int nx 129, ny 129; double Lx 1.0, Ly 1.0; Grid g(nx, ny, Lx, Ly); double G_over_mu 1.0; double omega 2.0 / (1.0 std::sin(M_PI / std::max(nx, ny))); double tol 1e-8; int maxIter 100000; auto t0 std::chrono::high_resolution_clock::now(); solveSOR(g, G_over_mu, omega, tol, maxIter); auto t1 std::chrono::high_resolution_clock::now(); double center g.w[g.idx(nx/2, ny/2)]; std::cout Center velocity: center \n; std::cout Elapsed: std::chrono::durationdouble(t1 - t0).count() s\n; // 方管泊肃叶流中心速度解析解无穷级数前几项 double a Lx / 2.0; double exact 0.0; for (int n 1; n 99; n 2) { double beta n * M_PI / (2 * a); exact (1.0 / (n * n * n)) * (1.0 - 1.0 / std::cosh(beta * a)); } exact * 16.0 * G_over_mu * a * a / (M_PI * M_PI * M_PI); std::cout Exact center velocity: exact \n; std::cout Relative error: std::fabs(center - exact) / exact \n; return 0; }解析解这里用的是方管泊肃叶流的经典级数解中心点速度的表达式。级数收敛很快取到 n 99 已经足够精确。运行后你会看到中心速度数值解和解析解的相对误差在 1e-4 量级对于 129×129 的网格这个精度是合理的。如果想更准把网格加密到 257×257误差会降到 1e-5 量级但迭代次数和单次迭代耗时都会增加。4. 三维泊肃叶流 C 代码的避坑与排查清单4.1 迭代不收敛残差震荡或发散现象SOR 跑了几万次迭代maxDiff 一直在 1e-3 附近震荡降不下去或者直接飙到无穷大。原因最常见的是 omega 设得太大。理论最优 omega 是针对理想泊松问题的如果你的边界条件处理有误或者网格长宽比很大实际最优值会偏移。另一个原因是收敛判据写错了比如用绝对残差而不是更新量导致早期就误判收敛。解决先把 omega 降到 1.0退化成 Gauss-Seidel确认能收敛。如果能收敛再以 0.05 为步长往上调观察迭代次数变化。如果 omega 1.0 都不收敛检查边界条件是否真的固定住了以及方程右端项符号是否正确。4.2 中心速度比解析解大很多或小很多现象数值解和解析解差一个数量级或者符号相反。原因方程右端项符号搞反了。泊松方程是 ∂²w/∂x² ∂²w/∂y² -G/μ如果你写成 G/μ解出来速度是负的。另一个常见原因是网格步长算错比如把 hx 写成 Lx / nx 而不是 Lx / (nx - 1)导致离散方程系数整体偏移。解决打印几个内点的离散方程系数手算一个点的更新值和代码输出对比。符号问题最隐蔽建议在代码里把 G_over_mu 定义成 const并在注释里写清楚符号约定。4.3 边界点被意外更新解出现边界层现象速度场在靠近边界的一两层网格上出现异常大或异常小的值中心区域反而正常。原因SOR 循环的索引范围写成了 i 0 到 nx-1把边界点也更新了。边界点的更新公式里会引用越界的邻居或者把边界值覆盖成内点公式算出来的值。解决SOR 循环必须从 i 1 到 nx-2、j 1 到 ny-2。每次迭代结束后可以调用 applyBoundary 强制置零但更稳妥的做法是循环范围本身就排除边界。4.4 网格加密后误差反而变大现象nx 从 65 加到 129中心速度误差从 1e-3 变成 1e-2。原因收敛容差没有随网格加密而收紧。网格越密离散误差越小但迭代误差如果没降下去反而会主导总误差。tol 1e-8 在粗网格上够用在细网格上可能不够。解决把 tol 设成和 hx² 同量级或者直接用 1e-10。同时检查 maxIter 是否够大细网格上 SOR 迭代次数会增加。4.5 程序跑得比预期慢很多现象129×129 网格跑了十几秒还没收敛。原因一维索引计算在双重循环里被反复调用编译器没有内联或者用了 vector 的 at() 而不是 operator[]每次访问都做边界检查。解决把 idx 函数声明为 inline用 operator[] 访问编译时开 -O2。如果还慢可以把内层循环的索引提前算好避免重复乘法。5. 从方管到任意截面把求解器改造成能算真实管道的工具上面写的求解器只能算矩形截面。真实管道可能是圆形、椭圆形、或者更复杂的形状。把求解器改造成能处理任意截面的版本核心思路是用掩码标记哪些点是流体、哪些是固体固体点不参与迭代边界条件通过掩码边界处理。具体做法是加一个 mask 数组和 w 同样大小。mask[i][j] 1 表示流体点0 表示固体点。SOR 循环里只更新 mask 1 的点且邻居如果是固体点就把邻居速度当作 0 处理无滑移。这样圆形截面只需要在初始化时根据 (x - xc)² (y - yc)² r² 来设置 mask。// 圆形截面掩码初始化 double xc Lx / 2.0, yc Ly / 2.0, r 0.4; for (int i 0; i nx; i) { for (int j 0; j ny; j) { double x i * hx, y j * hy; bool inside (x - xc) * (x - xc) (y - yc) * (y - yc) r * r; mask[idx(i, j)] inside ? 1 : 0; w[idx(i, j)] 0.0; } }SOR 更新时对每个流体点检查四个邻居的 mask如果邻居是固体就用 0 代替邻居速度。这样圆形管道的泊肃叶流也能算中心速度的解析解是 w_max G r² / (4μ)可以直接对比验证。这个改造的价值在于你不再需要一个专门的圆管求解器而是用一个统一的框架处理任意截面。微流控里常见的矩形、圆形、梯形截面都可以用同一套代码算。我一般会先用方管验证代码正确性再换成圆形截面最后换成实际设计的截面形状。提示掩码边界处理时固体点邻居的速度取 0 是合理的因为无滑移条件要求壁面速度为零。但如果固体点离流体点超过一个网格这种近似会引入误差所以网格要足够密保证截面边界被至少两三层网格分辨。最后一个习惯每次改完代码先跑方管算例和解析解对比误差在 1e-4 以内再往下做。这个回归测试帮我省了很多次调试时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?