ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LBM两相流模拟核心:Shan-Chen伪势模型原理与代码实战

LBM两相流模拟核心:Shan-Chen伪势模型原理与代码实战

简介:这是一份基于陈山(Shan Chen)伪势模型实现两相流模拟的 LBM(格子玻尔兹曼方法)MATLAB 代码资源,面向计算流体动力学入门者及需要开展气液两相流仿真的研究人员。格子玻尔兹曼方法通过离散化微粒运动规则来模拟流体宏观行为,伪势模型则引入随密度变化的势函数刻画相间相互作用力,从而自然呈现表面张力效应,可有效模拟水滴在空气中的运动、气泡生成等典型两相流现象。资源包内仅含 1 个 m 文件,压缩包整体约 2KB,代码量精炼但功能完整。代码涵盖初始条件设定、势函数定义、LBM 碰撞与迁移迭代、结果输出等关键步骤,适合初学者逐行研读,弄清 Shan-Chen 模型从理论到代码的映射关系。目前已有 1675 人学习此资源,下载后既可对照描述理解多相流模拟的核心逻辑,也可作为起点进一步扩展,迁移应用到喷雾冷却、沸腾换热、泡沫动力学等工程场景。

1. 用 LBM 做两相流,为什么绕不开 Shan-Chen 伪势模型

最近帮一个做微流控的朋友调液滴生成器的模拟,他上来就问我有没有不用追踪界面、不用做网格重构的两相方案。我直接甩给他这套 Shan-Chen 伪势模型资源包——它用 LBM 框架,把“界面在哪”完全交给伪势自己去演化,表面张力不是显式加的,而是从粒子间作用力里长出来的。对做液滴、气泡、润湿性研究的人来说,这是 LBM 两相流最友好的入口,代码量小、物理直观,比 VOF 和 Level Set 少掉一大半几何处理的麻烦。这篇就把这套资源从原理到复现讲透,哪些参数动不得、哪些位置有坑,都是我自己跑程序撞出来的经验。

2. Shan-Chen 模型:伪势是怎么把“相”分开的

2.1 从“追踪界面”到“界面自己长出来”

传统两相方法的核心是找一个方式描述界面:VOF 用体积分数,Level Set 用距离函数,都需要显示地构造界面、处理拓扑变化。SC 模型换了一条路——它不让任何变量去标记界面,而是给每个格子定义一个伪势 ψ(ρ),让相邻格子的伪势之间产生一个微观作用力。当这个力满足一定强度时,密度场会自发失稳分离成高密度区和低密度区,中间自然形成一定厚度的界面。

关键在于:这个力不需要求解压力 Poisson 方程,不需要表面张力模型,密度场演化到稳态后,界面曲率与内外压差自然满足 Laplace 定律。这意味着你做液滴合并、分裂、接触角这类界面拓扑剧烈变化的问题,不需要任何“界面重构”逻辑,伪势模型天然能处理。

选 SC 模型还有一个现实理由:它在 LBM 框架里实现成本极低。你只需要在标准 BGK 碰撞循环里额外算一次邻近格点伪势的加权和,代码量增加不到 50 行。对比一下,Allen-Cahn 或 Cahn-Hilliard 相场模型要额外解一个四阶 PDE,每步迭代要处理梯度算子和高阶项,新手上手调试周期至少多一到两周。

2.2 伪势形式与状态方程:决定你的密度比上限

伪势模型的核心公式是作用在 x 处流体上的微观力:

F(x) = -G ψ(x) Σᵢ wᵢ ψ(x+eᵢ) eᵢ

其中 G 是相互作用强度,wᵢ 是 D2Q9 离散速度方向的权重。G 为负时表现为吸引,界面处力指向高密度相内部,从而维持液滴形状。但 ψ 怎么取,直接决定你能稳定跑到多大密度比。

我在资源里见过三种常见取法,按稳定性排序如下:

伪势形式表达式稳定性适用场景
线性型ψ = ρ差教学演示,密度比 10 以内
指数型ψ = 1 - exp(-ρ)较好中等密度比,约 100:1
状态方程型ψ = √(2(p_EOS - ρ cₛ²) / (G cₛ²))好配合 Carnahan-Starling EOS,可破 1000:1

概念上要区分“伪势”和“真实压力”:伪势只是一个用于产生力的数学构造,不必对应真实物理量;但当你把 ψ 代入上述状态方程形式时,p_EOS 可以是你选择的任何状态方程,这相当于把真实流体热力学性质嵌进了伪势模型,界面处的密度差由 EOS 在临界温度附近的共存密度决定。

2.3 力项处理:选错离散格式会直接翻车

SC 模型给人最大的困惑点是“同样一个 F 公式,不同论文的实现长得不一样”。这里主要区别在力项的离散化处理方式:

  • 原始 Shan-Chen 格式:直接用 F = -G ψ(x) Σ wᵢ ψ(x+eᵢ) eᵢ,不对邻近伪势做差分,优点是实现简单,缺点是动量守恒不严格。
  • EDM(Explicit Density Method)格式:F = -G ψ(x) Σ wᵢ [ψ(x+eᵢ) - ψ(x)] eᵢ。做了一次邻近差分,动量守恒明显改善。
  • Guo 格式:在碰撞算子中直接加入力项贡献((1 - 1/(2τ)) wᵢ [...]),并要求宏观速度用 u + τF/ρ 修正,稳定性最好。

我一般直接用 Guo 格式。它对速度场的修正能显著抑制伪势模型最常见的“棋盘振荡”问题,代价只是碰撞循环里多写一个力项表达式。后面第 3 章会给出可直接照抄的完整实现。

注意:无论选哪种格式,碰撞、力项、迁移这三大步骤的执行顺序不能随意改。同一个模型,先算力再碰撞和先碰撞再算力,结果可能在 1 万步之后出现完全不同的相态分布。

3. 从头搭一个二维 SC 模型:核心代码逐段拆解

3.1 初始化:密度场和速度场这样设置才不容易炸

一个可复现的 SC 模型,错误几乎都出在初始化阶段。我先给最小骨架:128×128 网格、D2Q9、单位松弛时间 τ = 0.8。初始化时不要用硬阶跃密度场,那样界面处伪势梯度极大,第一步就会激发数值振荡。常见做法是给液滴区域一个光滑过渡的密度剖面。

import numpy as np nx, ny = 128, 128 tau = 0.8 rho0 = 0.1 # 气相密度 rho1 = 0.7 # 液相密度(EOS 共存密度下) x, y = np.meshgrid(np.arange(nx), np.arange(ny), indexing='ij') cx, cy = 64.0, 64.0 R0 = 30.0 # 液滴初始半径,格子单位 thick = 4.0 # 界面初始厚度,格子单位 # 双曲正切过渡,避免硬界面 dist = np.sqrt((x - cx)**2 + (y - cy)**2) rho = rho0 + 0.5 * (rho1 - rho0) * (1.0 - np.tanh((dist - R0) / thick)) ux = np.zeros((nx, ny)) uy = np.zeros((nx, ny)) # 平衡态初始化 f = np.zeros((nx, ny, 9)) e = 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]) cs2 = 1.0 / 3.0 for i in range(9): cu = e[i,0]*ux + e[i,1]*uy u2 = ux**2 + uy**2 f[..., i] = w[i] * rho * (1.0 + cu/cs2 + 0.5*cu**2/cs2**2 - 0.5*u2/cs2)

这段代码里最值得留意的是thick参数。很多新手为了省事直接dist > R0赋值两段常数,跑出来液滴是能形成,但界面厚度只有 1 个格子,伪势梯度被压缩,之后每步都在界面处产生大密度振荡。用 tanh 函数给 4 格宽的过渡带,界面处的力场平滑,稳定性明显改善。

另外rho1 = 0.7不是拍脑袋取的,它来自 EOS 在给定过冷度下的液相共存密度。如果随意取一个与 EOS 不匹配的值,系统会在前几千步强制“重新平衡”,导致界面处出现长时间的负密度修正,甚至直接发散。

3.2 平衡态分布与宏观量:注意力修正速度

碰撞步的关键是先用当前宏观量构造平衡态 f_eq,再用 BGK 松弛更新分布函数。但 SC 模型多了力项,宏观速度的还原不能只取 f 的一阶矩,要加上半时间步的力修正:

def macroscopic(f): rho = np.sum(f, axis=-1) ux = np.sum(f * e[:,0][None,None,:], axis=-1) / rho uy = np.sum(f * e[:,1][None,None,:], axis=-1) / rho return rho, ux, uy def equilibrium(rho, ux, uy): feq = np.zeros_like(f) for i in range(9): cu = e[i,0]*ux + e[i,1]*uy u2 = ux**2 + uy**2 feq[..., i] = w[i] * rho * (1.0 + cu/cs2 + 0.5*cu**2/cs2**2 - 0.5*u2/cs2) return feq

当计算完碰撞后,速度修正发生在宏观量重建时:

rho = np.sum(f, axis=-1) ux = (np.sum(f * e[:,0][None,None,:], axis=-1) + 0.5*Fx) / rho uy = (np.sum(f * e[:,1][None,None,:], axis=-1) + 0.5*Fy) / rho

注意0.5*Fx/rho就是 Guo 格式的半步力速度贡献。这个修正本质上相当于压力梯度在速度场里引起的“加速度”,漏掉它会出现两类诡异现象:静止液滴整体缓慢漂移,或者两相界面处速度场出现周期性“呼吸”扰动。调试时如果发现这些症状,第一反应就检查宏观量重建时是否带0.5 * F / rho。

3.3 伪势与力计算:EOS 型伪势的实现

这一部分是整套资源里最值得抄的代码。我以 Carnahan-Starling 状态方程为例,给出 EOS 型伪势和力计算的最小实现:

G = -1.0 a = 1.0 b = 4.0 R_gas = 1.0 T = 0.7 def psi_from_eos(rho): eta = b * rho / 4.0 p_eos = rho * R_gas * T * (1.0 + eta + eta**2 - eta**3) / (1.0 - eta)**3 - a * rho**2 # 开方前必须保证非负 return np.sqrt(np.maximum(2.0 * (p_eos - rho * cs2) / (G * cs2), 1e-12)) def compute_force(psi): Fx = np.zeros((nx, ny)) Fy = np.zeros((nx, ny)) for i in range(1, 9): # 取出 x + e_i 处的伪势 psi_nbr = np.roll(np.roll(psi, -e[i,0], axis=0), -e[i,1], axis=1) force_mag = -G * w[i] * psi * psi_nbr Fx += force_mag * e[i,0] Fy += force_mag * e[i,1] return Fx, Fy

关键参数说明:

  • G = -1.0是吸引强度,负号保证同相聚集。G 绝对值越大表面张力越大,但超过约 -1.2 后界面处密度振荡急剧增强,属于“后悔药都救不回来”的参数区间。
  • a和b是 EOS 参数。Carnahan-Starling 的a控制分子吸引力项,b控制分子体积修正。实际调参时要保证 p_eos - ρ cₛ² 在共存密度范围内非负,否则开根号直接失败。
  • T = 0.7是约化温度,相对临界温度 Tc 的比值。T 越接近 1,密度差越小;T 越低,密度比越大,但界面厚度也越小,数值稳定性越差。通常从 T = 0.8 起步,稳定后再逐步往低调。

np.roll的做法是周期性边界条件下最省事的邻近取值方式。如果你是敞开边界或反弹边界,需要换成带边界条件处理的索引访问,否则边界格子会把对面边界的值卷进来,伪势计算区域变形。

4. 静态液滴验证:一个 20 万步的 Laplace 定律测试

4.1 算例设计与参数表

初始化完成后,你需要的第一个“模型体检”不是让液滴流动,而是测静态力学平衡。具体做法是放一个圆形液滴在周期性盒子里,跑足够多步,看界面两侧压差是否满足 Laplace 定律 ΔP = σ/R。这一步不通过,后面所有动态模拟都是建在沙子上。

推荐参数如下:

参数取值说明
网格128×128至少保证液滴半径 20 格以上
液相密度0.7来自 EOS 共存密度
气相密度0.1同上
液滴半径R = 12, 16, 20, 24至少做 4 组
松弛时间0.8低于 0.55 易触发负密度
相互作用强度-0.8 ~ -1.1从小到大扫描
迭代步数200000让界面充分弛豫
数据记录间隔2000 步后处理时做时间平均

每组半径都需要单独跑一次模拟。注意一点:R 不要取太大。如果液滴半径占满半个盒子,周期性镜像之间的相互作用会干扰压力测量,得到 ΔP 偏大。边界效应在 R > 40 格时非常明显,等效于一个液滴阵列而非孤立液滴。

4.2 运行过程与监测指标

主循环按“伪势 → 力 → 碰撞 → 迁移”顺序推进。每 2000 步记录一次全局密度最大值和最小值,这是最直观的稳定指标。如果 rho_min 跌破 1e-6 甚至变成负数,说明界面处已经被数值振荡击穿,立即停止并回退参数。

实际跑的时候,你要观察的密度剖面大概是这样的演变过程:

  1. 前 1000 步,界面由初始的 tanh 剖面开始自发调整,厚度稍微收缩。
  2. 5000 步内,液滴整体可能沿某个方向移动零点几个格子,属于初场对称性破缺后的“整体漂移”,不是系统错误。
  3. 5 万步之后,密度场基本稳定,压力场的内外平台开始显现。
  4. 20 万步时,界面处会有微小的持续振荡,幅度通常在 0.001 量级,这是 SC 模型的常态性波动,不是发散信号。

4.3 结果提取与有效性判断

从稳态密度场中提取界面中心位置,做法是沿着一条穿过圆心的直线读出密度剖面,取液相密度与气相密度平均值对应的位置作为界面半径 R。然后用同一剖面提取压力 p = ρcₛ² + G cₛ² ψ²/2(这是 SC 模型压力张量的近似计算式),在界面两侧分别取平台区平均值,得到 ΔP。

x_axis = np.arange(nx) profile = rho[x_axis, cy] interface_pos = np.where((profile > rho0 + 0.4*(rho1-rho0)))[0] # 界面半径近似 R_left = interface_pos[0] R_right = interface_pos[-1] R = (R_right - R_left) / 2.0 # 压力平台区取均值(避开界面) p_bulk = rho * cs2 + G * cs2 * psi**2 / 2.0 P_out = np.mean(p_bulk[0:interface_pos[0]-2, cy]) P_in = np.mean(p_bulk[interface_pos[-1]+2:, cy]) dP = P_in - P_out print(f"R={R:.2f}, dP={dP:.6f}, sigma={dP*R:.6f}")

这里最关键的一个坑:如果只用单条直线剖面,界面位置定位误差就能带来 10% 以上的表面张力误差。我一般会沿 0°、30°、60°、90° 四条方向各提取一次,取平均。另外P_in和P_out的平台区必须离界面至少 3 个格子,否则界面处的高梯度伪势会把压力污染进去。

多个半径 R 的结果算完后,用线性回归拟合 dP 对 1/R。斜率的物理单位就是格子单位的表面张力 σ,相关系数应达到 0.99 以上。如果相关系数只有 0.9 左右,排在前面的原因是界面厚度过大(液滴的等效半径要减去界面厚度的一半)或边界效应;如果回归线截距明显偏离 0,说明还存在一个额外的背景压力梯度,需要检查初始化时整个密度场是否已经处于力学平衡。

5. 常见问题与排查:五条写了硬代码的踩坑记录

5.1 界面附近负密度和棋盘振荡

现象:运行 2000 步左右,密度场出现黑白交替的棋盘形斑块,某些格子密度降到 -0.01 量级。

原因:伪势梯度在界面跨度过大时被离散格式放大,配合低粘性(τ 接近 0.5)产生标准的高波数振荡。

解决:先看 τ,低于 0.55 直接调回 0.8;再看 G,从 -0.7 起步而非直接上 -1.2。如果两者都正常,把伪势改成指数型 ψ = 1 - exp(-ρ),这是代价最小、见效最快的改法。

5.2 静止液滴整体漂移

现象:初始化后没加任何外力,液滴整体以恒定速度朝一个方向移动,每秒几百个格子的速度都有。

原因:在 Guo 格式中漏掉0.5 * F / rho的宏观速度修正,等效于每步都对流体额外施加了半个时间步的冲量。不是随机误差,是系统性的均匀加速。

解决:检查宏观量重建的代码,速度必须带 0.5F/rho 修正。如果修正已经存在仍然漂移,则检查初始化时整个盒子的密度平均值是否与 EOS 的气相密度匹配——不匹配时系统会通过整体移动来重新分布质量。

5.3 密度比做不高,卡在 30:1 就崩

现象:T 降到 0.6 以下想提高密度比,结果界面处梯度爆炸,模拟直接中断。

原因:线性伪势和指数伪势在低过冷度下无法产生足够的化学势差,界面处数值稳定性急剧下降。

解决:换 EOS 型伪势(第 3.3 节代码),并将 T 每次只降低 0.02,每降一次重新跑 Laplace 测试。配合界面厚度不低于 3 格,密度比 100:1 是稳定可复现的。想破 1000:1 需要额外引入第 6 章的两个修正手段。

5.4 D2Q9 速度集方向顺序不统一

现象:单独跑每个方向没问题,但合并代码后液滴变成六边形,界面各向异性严重。

原因:不同参考代码里 e 数组对角方向的排列顺序不一致(有的是 [1,1],[-1,1],[-1,-1],[1,-1],有的是 [1,1],[1,-1],[-1,-1],[-1,1]),而同一套代码里平衡态分布和迁移步混用了两套顺序。

解决:强制规定e = [[0,0],[1,0],[0,1],[-1,0],[0,-1],[1,1],[-1,1],[-1,-1],[1,-1]],并全代码只写这一个数组。这个坑极其隐蔽,排查一次至少消耗一整天。

5.5 表面张力测量结果不随 G 单调变化

现象:G 从 -0.6 调到 -1.0,σ 先升后降,不符合物理直觉。

原因:界面厚度也在随 G 改变,而采用单条剖面压力曲线求 ΔP 时,界面内压力梯度被横跨的格子数稀释。G 绝对值增大后界面变薄,压力截取区间反而污染更重。

解决:测量 σ 时对界面厚度做一次修正——实际有效半径 R_eff = R - δ/2,其中 δ 由密度剖面的 10%-90% 宽度获得。修正后再拟合 dP 对 1/R_eff,单调性就恢复了。

6. 把密度比推到 1000:1:三个可落地的进阶改进

如果你已经通过了第 4 章的 Laplace 测试,接下来真正有挑战的是把密度比从 100:1 推到 1000:1 以上。这个量级在微流控中很常见,比如水-空气体系的物理密度比大约是 1000:1。SC 原始模型在这里已经到极限,必须配合额外手段。

第一个改进是伪势高阶逼近。原始 SC 力只用了最近邻格点的伪势,在界面陡峭时对梯度的捕捉误差很大。常见做法是把力计算扩展到大邻域:

# 扩展权重:近邻 1/9,对角 1/36,二阶近邻 1/144 e_ext = np.array([[2,0],[0,2],[-2,0],[0,-2],[2,2],[-2,2],[-2,-2],[2,-2]]) w_ext = np.ones(8) / 144.0 # 力计算时将 psi_nbr 的取点扩展到二阶邻域 for i in range(8): psi_nbr = np.roll(np.roll(psi, -e_ext[i,0], axis=0), -e_ext[i,1], axis=1) Fx += -G * w_ext[i] * psi * psi_nbr * e_ext[i,0]

注意这个改法会让表面张力数值明显偏大,用第 4.3 节的 Laplace 测试重新标定一次 σ 是必须的流程,不能沿用原来的换算关系。

第二个改进是把界面厚度控制在 5~6 个格子。密度比高了以后,界面变薄是数值发散的直接原因。常见做法是用一个小系数安全阀:当密度低于 1e-10 时,用局部高阶插值替代伪势开方操作,防止负密度把整个流场“毒化”。

第三个改进是初始化直接使用 EOS 的共存密度,而不是手动猜一个 rho1。做法是先跑一个平面分层的 1D 周期盒,取稳定后的高低密度值作为 2D 算例的初始化输入,这比任何滤噪手段都有效。

从那以后我每次调 SC 模型,都强制走一遍“EOS 共存密度 → 平面分层验证 → Laplace 标定 → 动态算例”这四步流程,几乎没再遇到发散的半夜。这套资源里对应的代码与参数文件,下载后对照本文第 3、4 章跑一遍静态液滴测试,半小时内就能确认模型是否工作正常。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进