☰
圆柱绕流LBM仿真实战:从卡门涡街复现到工程参数扫描
2026/10/11 7:41:16 网站建设 项目流程

简介:这份资源面向流体力学数值模拟的学习者与研究者,提供基于格子Boltzmann方法(LBM)的圆柱绕流C++实现,适合具备一定编程与流体基础、希望动手复现经典绕流案例的中高级读者。压缩包内共1个文件,为单个cpp源码,整体约2KB,体量轻巧,便于直接阅读与二次开发。源码围绕LBM核心流程展开,涵盖初始化流场与边界条件、碰撞与传播两阶段的时间演化、圆柱壁面边界处理,以及速度、密度、涡度等流场量的计算与输出,可帮助读者理解分离涡形成、漩涡脱落等典型现象,并与实验或解析解对照验证。目前已有633人学习下载,可作为入门LBM编程、搭建圆柱绕流数值实验的参考实例。

1. 圆柱绕流 LBM 仿真:从卡门涡街到工程落地,这套方案到底值不值得跑

圆柱绕流是流体力学里最经典的验证算例之一,而 LBM(格子玻尔兹曼方法)做圆柱绕流,近年在换热器管束、海洋立管涡激振动、风速仪标定这些场景里被反复提起。很多人第一次搜到LBM-cylinder.rar这类资源时,心里想的其实是同一件事:我能不能用一套现成的格子玻尔兹曼代码,把 Re=100 的卡门涡街稳定复现出来,再往上推到 Re=1000 甚至更高?答案是能,但前提是你得先搞清楚 LBM 的边界条件怎么设、格子单位怎么换算、以及为什么你的涡街会莫名其妙衰减成一条直线。这篇笔记就按「理论先立住、再动手能复现」的路子,把圆柱绕流 LBM 从选型、参数、代码到排错讲透,适合刚接触 LBM 的 CFD 从业者,也适合想拿圆柱绕流当基准算例验证自研求解器的老手。

2. 圆柱绕流 LBM 的理论底座与选型理由

2.1 为什么圆柱绕流偏偏适合用 LBM 来做

圆柱绕流的核心物理是边界层分离、剪切层失稳和涡街脱落,这三个过程对数值格式的耗散和色散非常敏感。传统有限体积法在低马赫数不可压问题上,压力-速度耦合迭代往往吃掉大量时间,而 LBM 的演化方程天然是显式的,压力通过状态方程直接得到,不需要解泊松方程。对于圆柱这种曲面边界,LBM 的反弹格式(bounce-back)实现起来比贴体网格简单得多,尤其是半步反弹(half-way bounce-back)能把曲面边界处理到二阶精度。

另一个现实理由是并行效率。LBM 的碰撞和迁移步骤局部性极强,MPI 或 OpenMP 并行时通信量小,在同样核数下通常比压力修正类求解器更容易跑满。圆柱绕流作为二维问题,单卡或单节点就能把 Re=100 到 Re=1000 的工况扫一遍,这对做参数敏感性分析非常友好。

但 LBM 不是没有代价。它的可压缩性误差随马赫数平方增长,圆柱绕流里如果 Ma 超过 0.1,涡街的形态就会失真。所以选型的第一条铁律是:把格子马赫数压在 0.1 以下,最好在 0.05 附近。

2.2 从 Navier-Stokes 到格子玻尔兹曼:你需要盯住的三个无量纲数

LBM 求解的是离散速度分布函数 $f_i(\mathbf{x},t)$ 的演化方程:

$$f_i(\mathbf{x}+\mathbf{e}_i \Delta t, t+\Delta t) = f_i(\mathbf{x},t) + \Omega_i(f)$$

其中 $\Omega_i$ 是碰撞算子。工程上最常用的是 BGK 近似:

$$\Omega_i = -\frac{1}{\tau}(f_i - f_i^{eq})$$

这里 $\tau$ 是松弛时间,它和运动黏度的关系是:

$$\nu = c_s^2 \left(\tau - \frac{1}{2}\right) \Delta t$$

$c_s = 1/\sqrt{3}$ 是格子声速。这个公式是整篇文章里最重要的一个,因为它把物理黏度和格子参数绑死了。你每改一次 $\tau$,雷诺数就跟着变。

圆柱绕流要盯住的三个无量纲数:

无量纲数定义圆柱绕流典型取值对 LBM 的含义
雷诺数 Re$U D / \nu$100 / 200 / 1000决定涡街是否脱落、脱落频率
马赫数 Ma$U / c_s$< 0.1控制可压缩性误差
阻塞比 B$D / H$< 0.05侧壁对涡街的干扰

阻塞比这一条经常被忽略。很多人在 10D×10D 的域里放一个 D=20 格子的圆柱,阻塞比直接到 0.2,算出来的斯特劳哈尔数 St 比文献值偏高 10% 以上,还以为是代码写错了。常见做法是侧向边界至少离圆柱 10D 以上,阻塞比压到 0.05 以内。

2.3 边界条件选型:反弹格式、Zou-He 与周期性边界怎么配

圆柱绕流 LBM 的边界条件分三块:圆柱表面、入口出口、上下侧壁。

圆柱表面用半步反弹格式。它的逻辑是:当粒子从流体节点迁移到固体节点时,直接沿原方向反弹回流体节点,反弹位置在两者中点。这样曲面边界不需要插值,实现简单且守恒性好。对于静止圆柱,半步反弹的代码就是一句反向索引赋值。

入口用速度入口。Zou-He 格式是经典选择,它根据已知的速度分量反推未知的分布函数。但 Zou-He 在角点处容易出问题,如果入口和侧壁的交角处理不当,会看到入口附近出现非物理的密度振荡。我一般会在入口前留 2D 到 3D 的缓冲区,让流动先发展一段再接触圆柱。

出口用零梯度外推或者对流边界。零梯度最简单,但出口离圆柱太近时会把涡街反射回来。出口位置建议放在圆柱下游 20D 以外,配合对流边界条件效果更稳。

上下侧壁分两种工况:如果是自由来流,用周期性边界或者自由滑移;如果是风洞侧壁,用无滑移反弹。做基准验证时优先用自由滑移,这样阻塞效应最小,方便和文献对比。

3. 用 Python 从零搭一个圆柱绕流 LBM 求解器

3.1 格子布置与圆柱几何的离散化

先确定格子单位。取圆柱直径 D = 20 个格子,计算域 40D × 20D,即 800 × 400。圆柱中心放在 (10D, 10D) = (200, 200)。入口在 x=0,出口在 x=800,上下在 y=0 和 y=400。

圆柱的离散化用距离判断:对每个格子点,计算它到圆心的距离 r,如果 r < D/2 就标记为固体。但这样得到的边界是锯齿状的,半步反弹对锯齿边界的精度会下降。改进做法是用一个更细的标记:把 r 在 D/2 附近的格子单独处理,用插值反弹格式。不过对于 D=20 这个分辨率,直接锯齿边界已经能给出可接受的 St 数,误差在 3% 以内。

import numpy as np # 格子参数 Nx, Ny = 800, 400 D = 20.0 cx, cy = 200.0, 200.0 R = D / 2.0 # 固体标记 solid = np.zeros((Nx, Ny), dtype=bool) for i in range(Nx): for j in range(Ny): if (i - cx)**2 + (j - cy)**2 < R**2: solid[i, j] = True # D2Q9 速度集 c = np.array([[0,0], [1,0], [0,1], [-1,0], [0,-1], [1,1], [-1,1], [-1,-1], [1,-1]]) w = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36])

这段代码做了两件事:标记固体节点、定义 D2Q9 的速度集和权重。solid数组在后续碰撞和迁移时用来判断哪些节点不参与演化。注意圆柱中心取在 (200, 200) 而不是 (400, 200),是为了给下游留出足够的涡街发展空间。

3.2 碰撞与迁移:D2Q9 模型的核心循环

LBM 的主循环就两步:碰撞和迁移。碰撞在本地完成,迁移把分布函数沿速度方向搬到邻居节点。

def equilibrium(rho, ux, uy): """计算 D2Q9 平衡态分布函数""" feq = np.zeros((9, Nx, Ny)) u2 = ux**2 + uy**2 for k in range(9): cu = c[k,0]*ux + c[k,1]*uy feq[k] = w[k] * rho * (1.0 + 3.0*cu + 4.5*cu**2 - 1.5*u2) return feq def collide(f, rho, ux, uy, tau): """BGK 碰撞""" feq = equilibrium(rho, ux, uy) f += -(f - feq) / tau return f def stream(f): """迁移:沿速度方向搬移""" for k in range(9): f[k] = np.roll(f[k], shift=(c[k,0], c[k,1]), axis=(0,1)) return f

equilibrium里那个3.0*cu和4.5*cu**2来自 D2Q9 的泰勒展开,系数是固定的,不要改。collide里的tau由雷诺数反推:先定 Ma = 0.05,则 U = Ma * c_s = 0.05/√3 ≈ 0.0289。再定 D = 20,Re = UD/ν,所以 ν = UD/Re。最后 τ = ν/(c_s²) + 0.5 = 3ν + 0.5。

以 Re=100 为例:ν = 0.028920/100 = 0.00578,τ = 30.00578 + 0.5 = 0.5173。这个值离 0.5 很近,数值稳定性偏紧,如果发现发散,先把 Ma 降到 0.03 再试。

stream用np.roll实现周期性迁移,但圆柱绕流不是全周期性边界,所以迁移后要单独处理边界节点。入口、出口和圆柱表面的分布函数需要覆盖。

3.3 半步反弹边界与宏观量提取

半步反弹的核心是:对每个固体节点,把它的邻居流体节点上指向它的分布函数反向赋回去。

def bounce_back(f, solid): """半步反弹:固体节点反向赋值给流体邻居""" for i in range(Nx): for j in range(Ny): if solid[i, j]: for k in range(9): ni, nj = i + c[k,0], j + c[k,1] if 0 <= ni < Nx and 0 <= nj < Ny and not solid[ni, nj]: # 反向速度索引 k_inv = (k + 4) % 8 if k != 0 else 0 f[k_inv, ni, nj] = f[k, i, j] return f

这里k_inv的计算是 D2Q9 的反向速度映射:0→0,1↔3,2↔4,5↔7,6↔8。用(k+4)%8对 k=1..8 成立,k=0 单独处理。

宏观量提取:

def macro(f): rho = np.sum(f, axis=0) ux = np.sum(f * c[:,0,None,None], axis=0) / rho uy = np.sum(f * c[:,1,None,None], axis=0) / rho return rho, ux, uy

密度就是九个分布函数之和,速度是动量除以密度。注意在固体节点上这些量没有物理意义,后处理时要屏蔽掉。

3.4 入口出口边界与主循环组装

入口用 Zou-He 速度边界。已知 ux=U, uy=0,反推未知的三个分布函数(对应速度指向域内的方向)。

def zou_he_inlet(f, U): """入口 Zou-He 速度边界,ux=U, uy=0""" rho_in = (f[0,0,:] + f[2,0,:] + f[4,0,:] + 2*(f[3,0,:] + f[6,0,:] + f[7,0,:])) / (1 - U) f[1,0,:] = f[3,0,:] + (2/3)*rho_in*U f[5,0,:] = f[7,0,:] - 0.5*(f[2,0,:] - f[4,0,:]) + (1/6)*rho_in*U f[8,0,:] = f[6,0,:] + 0.5*(f[2,0,:] - f[4,0,:]) + (1/6)*rho_in*U return f

出口用零梯度:把倒数第二列的分布函数直接复制到最后一列。

def outlet_zero_grad(f): f[:, -1, :] = f[:, -2, :] return f

主循环:

# 初始化 rho0 = np.ones((Nx, Ny)) ux0 = np.full((Nx, Ny), U) uy0 = np.zeros((Nx, Ny)) f = equilibrium(rho0, ux0, uy0) # 时间步进 for step in range(20000): rho, ux, uy = macro(f) f = collide(f, rho, ux, uy, tau) f = stream(f) f = bounce_back(f, solid) f = zou_he_inlet(f, U) f = outlet_zero_grad(f) if step % 1000 == 0: print(f"step {step}, max ux = {ux.max():.4f}")

20000 步大约对应 10 个涡脱落周期。判断收敛的方法是监测圆柱后方某点的 uy 信号,等它进入周期性振荡后,取 5 个以上周期做 FFT,主频就是脱落频率 f,St = f*D/U。

4. 圆柱绕流 LBM 的避坑与排查清单

4.1 涡街不脱落,尾迹对称得像镜子

现象:跑了几万步,圆柱后面始终是稳定的对称尾迹,没有任何振荡。

原因:雷诺数没到临界值,或者数值耗散太大把扰动吃掉了。圆柱绕流的临界 Re 大约在 47 左右,低于这个值就是稳态。但如果你设的 Re=100 还不脱落,大概率是 τ 太接近 0.5 导致黏性被高估,或者格子分辨率太低(D < 10)让数值耗散压过了物理失稳。

解决:先确认 Re 计算无误,再检查 D 是否至少 20。如果还不行,在圆柱附近加一个小的初始扰动,比如给圆柱正后方一个格子的 uy 一个 1e-4 的脉冲,帮它触发失稳。

4.2 计算几分钟后密度爆到 NaN

现象:前几千步正常,突然 rho 出现 NaN 并迅速扩散到全场。

原因:τ 太接近 0.5,碰撞算子放大高频振荡。或者入口 Zou-He 的密度反推在角点处除了负密度。

解决:把 Ma 从 0.05 降到 0.03,τ 会相应增大,稳定性改善。入口角点单独处理,或者把入口速度改成渐进式,前 1000 步从 0 线性升到 U。

4.3 斯特劳哈尔数比文献值高 10% 以上

现象:算出来的 St 在 0.18 附近,而 Re=100 的文献值约 0.164。

原因:阻塞比太大。侧壁离圆柱太近,把涡街挤快了。另一个可能是出口太近,涡还没充分发展就被边界截断。

解决:把侧向尺寸从 10D 加到 20D,出口从 15D 加到 30D。代价是格子数翻倍,但 St 会明显回落。如果不想加格子,把侧壁改成自由滑移边界,等效于消除侧壁边界层。

4.4 圆柱表面出现非物理的密度层

现象:圆柱表面一圈格子的密度明显偏离 1,形成一层「壳」。

原因:半步反弹对曲面边界的锯齿处理在低分辨率下会产生阶梯效应,密度在固体节点附近堆积。

解决:把 D 从 20 加到 40,或者改用插值反弹格式。插值反弹的代码量大约是半步反弹的两倍,但能把曲面精度提到二阶。如果只是做基准验证,D=40 的半步反弹已经够用。

4.5 并行后结果和单核不一致

现象:用 MPI 拆成 4 块跑,涡街形态和单核不一样。

原因:迁移步骤在子域交界处需要通信,如果通信顺序和碰撞顺序没对齐,交界处会出现一个格子的错位。

解决:把碰撞和迁移分开,迁移后统一做一次 halo 交换,再执行边界条件。不要在迁移中间插入通信。另外,圆柱如果跨子域,反弹格式的索引要特别小心,建议把圆柱完整放在一个子域内,或者用全局索引做反弹。

5. 把圆柱绕流 LBM 用起来:从验证算例到工程参数的进阶技巧

跑通 Re=100 只是起点。真正让这套代码产生价值的,是把它变成参数扫描工具。我一般会固定 D=40、Ma=0.05,然后扫 Re = 100, 200, 400, 800, 1000,每个工况跑 20 个脱落周期,提取 St 和圆柱受力。

受力计算用动量交换法:统计单位时间内所有反弹边界上交换的动量,除以时间步长就是阻力。升力则取 y 方向分量。Re=100 时阻力系数 Cd 约 1.35,Re=1000 时降到 1.0 左右,这个趋势可以用来验证你的受力代码是否正确。

ReSt(文献)Cd(文献)建议格子数建议步数
1000.1641.35800×40020000
2000.1961.341000×50030000
4000.2151.251200×60040000
10000.2101.051600×80060000

一个容易被忽略的技巧是:用涡量场而不是速度场来判断脱落周期。速度信号在圆柱近尾迹区信噪比不高,而涡量在剪切层里非常清晰。提取圆柱后方 2D 处一条线上的涡量,做 FFT,主频比速度信号稳定得多。

另一个进阶方向是把二维推到三维。三维圆柱绕流的涡街会出现展向失稳,Re>190 时涡街变成 Mode A,Re>260 变成 Mode B。三维 LBM 的代码结构和二维几乎一样,只是速度集从 D2Q9 换成 D3Q19,内存涨 4 倍左右。如果二维已经跑顺,三维的坑主要在边界条件的展向处理:用周期性边界时展向至少 4D,否则展向模态会被压制。

最后说一个我踩过的坑:不要用np.roll做圆柱附近的迁移。np.roll是全周期性假设,圆柱固体节点上的分布函数会被错误地搬到对面。正确做法是迁移前把固体节点的分布函数清零,迁移后再用反弹填充。这个 bug 不会让代码崩溃,但会让涡街的脱落频率偏移 5% 左右,而且极难通过看流场发现。希望帮到你。

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

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

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

立即咨询