☰
D2Q9格子玻尔兹曼法求解二维对流扩散:原理、代码与调参
2026/10/1 22:28:26 网站建设 项目流程

简介:这是一份基于D2Q9模型的格子玻尔兹曼方法(LBM)二维对流扩散模拟MATLAB源码,面向计算流体力学初学者、研究生及需要完成相关课程设计的工科学生,可用于理解温度场在矩形区域内的对流与扩散耦合演化过程。程序采用二元九点速度模型,通过设定左边界恒温1.0、右边界及上下边界恒温0,构造出一个两侧温差驱动的热传输场景,同时兼顾对流传热与分子扩散两种机制,能够清晰展示流速与扩散系数对温度分布的影响。压缩包内仅包含1个m文件,整体大小约1KB,代码量精简、无冗余依赖,便于逐行阅读、调试和修改参数,适合作为LBM入门练习或课程作业的基础模板。目前已有256人学习下载,说明其具有一定的参考价值。通过运行该脚本,读者可以直观观察二维扩散场随迭代步数的变化,掌握D2Q9模型的网格划分、边界条件处理、碰撞与迁移步骤的时间步进实现思路,并可将代码扩展至含源项或更复杂几何结构的对流扩散问题。

1. 对流扩散问题的格子玻尔兹曼解法:一个D2Q9同时处理流动和标量输运

做热处理温度场、污染物浓度扩散或相场粗化这类工程仿真时,最棘手的问题往往不是流场本身,而是浓度或温度这个标量场既要跟着流体走,又要在同一套网格上扩散。许多做CFD的工程师第一反应是“流场用CFD解,标量再用传输方程耦合”,两套网格两套求解器,插值误差和守恒修正能把人折腾到怀疑人生。格子玻尔兹曼里的D2Q9模型可以在一套正方形网格上同时放“流场分布函数”和“标量分布函数”两套量,速度集相同、对流速度相同,只有松弛时间不同,这种天然耦合是它在中低雷诺数传热传质问题里受欢迎的根本原因。

标题里的convection_d2q9_二维扩散_对流扩散_格子玻尔兹曼指向的正是这类实现,而L1_S0_R0_X0这类后缀在工程里通常是工况标签,用来区分网格层级、源项强度、雷诺数和扩展开关。这篇文章会把 D2Q9 的对流扩散原理、可复现的最小代码、标签参数怎么映射、以及常见翻车点一次讲透。新手按第 3 章的骨架就能跑出第一张浓度云图,熟手可以直接跳到第 4 章和第 5 章对参数和边界做校准。

2. 从D2Q9速度集到对流扩散方程:平衡态函数和两套分布函数的配合

2.1 为什么标量场不能直接用f_i的零阶矩:两套分布函数的由来

标准D2Q9的分布函数 (f_i) 通过 Chapman-Enskog 展开恢复的是 Navier-Stokes 方程,它的零阶矩是密度 (\rho),而密度在等温LBM里通过状态方程 (p = c_s^2 \rho) 直接决定压力。如果你试图把温度或浓度直接塞进 (\rho) 里,马上就会遇到一个问题:局部浓度涨落会被当成压力涨落,造成伪速度和非物理的密度脉动。所以常规做法是再引入一套独立的标量分布函数 (g_i),它的演化方程和 (f_i) 同构,但只承载“被流体携带的标量”这一个物理量。

这第二个分布函数的宏观量定义为 (\phi = \sum_i g_i),对应温度、浓度或其他标量浓度。演化方程写成:

[ g_i(\mathbf{x} + \mathbf{e}_i \Delta t, t+\Delta t) = g_i(\mathbf{x}, t) - \omega_k \left[ g_i(\mathbf{x}, t) - g_i^{eq}(\mathbf{x}, t) \right] + S_i ]

这里的 (\omega_k) 是标量分布函数的松弛频率,它和扩散系数直接相关。流场 (u) 由 (f_i) 的矩计算出来,然后带入 (g_i^{eq}),这样对流项就通过平衡态函数传递给了标量场。整个流程里 (f_i) 独立演化,(g_i) 依赖 (f_i) 给出的速度场,但 (g_i) 不反向影响 (f_i),这是“被动标量”假设,适用于温度变化不大、浓度对密度影响可忽略的传热传质问题。

如果想进一步耦合浮升力,可以在 (f_i) 的碰撞项中加一个外力项,把标量场反馈到流场,那属于 Boussinesq 近似 LBM,不在本标题这个基础版本范围内。先把这个被动标量骨架跑稳,再谈扩展。

2.2 D2Q9的速度集、权重与宏观量恢复:九个方向怎么分配

D2Q9 里的“9”不是随便给的,它由 1 个静止方向、4 个轴向方向、4 个对角方向组成。标准速度集和权重如下表:

方向 i速度 e_i权重 w_i
0(0, 0)4/9
1(1, 0)1/9
2(0, 1)1/9
3(-1, 0)1/9
4(0, -1)1/9
5(1, 1)1/36
6(-1, 1)1/36
7(-1, -1)1/36
8(1, -1)1/36

这套权重满足各向同性要求,是格子玻尔兹曼方法能够正确恢复宏观方程的前提。在格子单位下 (c_s^2 = 1/3),所以平衡态函数写成:

[ f_i^{eq} = w_i \rho \left[ 1 + \frac{\mathbf{e}_i \cdot \mathbf{u}}{c_s^2} + \frac{(\mathbf{e}_i \cdot \mathbf{u})^2}{2 c_s^4} - \frac{\mathbf{u}^2}{2 c_s^2} \right] ]

宏观量恢复很简单:密度 (\rho = \sum_i f_i),动量密度 (\rho \mathbf{u} = \sum_i \mathbf{e}_i f_i)。对于标量分布函数,平衡态同样依赖速度集,但宏观量只剩下 (\phi = \sum_i g_i)。

2.3 对流项怎样进入标量平衡态:单松弛BGK的完整更新式

标量分布函数的平衡态在不同实现里有两种写法。最简版本只保留一阶项:

[ g_i^{eq} = w_i \phi \left[ 1 + \frac{\mathbf{e}_i \cdot \mathbf{u}}{c_s^2} \right] ]

它能恢复对流扩散方程,但在高 Peclet 数下数值扩散偏大。常见实现会补上二阶项,写成与 (f_i^{eq}) 同构的形式:

[ g_i^{eq} = w_i \phi \left[ 1 + \frac{\mathbf{e}_i \cdot \mathbf{u}}{c_s^2} + \frac{(\mathbf{e}_i \cdot \mathbf{u})^2}{2 c_s^4} - \frac{\mathbf{u}^2}{2 c_s^2} \right] ]

补上二阶项后,对流主导工况下的稳定性会明显改善。扩散系数由标量松弛频率 (\omega_k) 控制:

[ D = c_s^2 \left( \frac{1}{\omega_k} - \frac{1}{2} \right) \Delta t ]

这里 (\tau_k = 1/\omega_k),必须大于 0.5,否则扩散系数为负,数值上立刻发散。二维扩散工况下如果 (R=0),也就是速度为零,那么对流项消失,方程退化为纯扩散;一旦 (R) 非零,速度项通过 (\mathbf{e}_i \cdot \mathbf{u}) 进入 (g_i^{eq}),这正是“对流扩散”四个字的来源。

源项的处理同样在碰撞步完成,常用做法是在碰撞后加 (w_i Q),同时宏观量修正为 (\phi = \sum_i g_i + 0.5 Q),这个 0.5 是半隐式时间积分带来的。标题里的S0一般就对应 (Q=0)。

3. 把convection_d2q9从RAR变成能跑的程序:解压、最小Python实现与第一个算例

3.1 RAR解压后先找什么:参数文件、主循环、边界函数

拿到D2Q9_L1_S0_R0_X0.rar这类压缩包,我一般不会先去看代码主体,而是先看参数文件和 README。因为标题里的下划线已经暴露了这是一个按工况归档的项目,解压后大概率长这样:一个参数文件用来读入网格尺寸、松弛时间、源项强度、边界类型;一个主循环文件负责碰撞和迁移;一个边界处理文件单独处理出入口和固壁;剩下的是后处理和批处理脚本。

如果压缩包里的代码不是我熟悉的语言,我也不会急着重写。先确认三件事:标量分布函数g的迁移方向数组是否和f共用;碰撞模型是 BGK 还是 MRT;出入口边界用的是反弹还是平衡态强制。这三点直接决定代码能不能直接复用。换成我自己写的话,会用 Python 配合 NumPy 先做一版最小实现,因为周期边界可以用np.roll一行完成,物理逻辑最容易暴露出来。

3.2 最小可运行参考:初始化、碰撞、迁移一个循环

下面这份代码是我在实际项目里反复用的骨架,它把流场固定成一个均匀背景流,标量场初始化为一个方形斑块,然后用 D2Q9 同时对流和扩散。直接复制成.py文件就能跑。

import numpy as np def d2q9_velocity_set(): ex = np.array([0, 1, 0, -1, 0, 1, -1, -1, 1]) ey = np.array([0, 0, 1, 0, -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], dtype=np.float64) return ex, ey, w def init_fields(nx, ny, ux0=0.1): rho = np.ones((nx, ny)) ux = np.full((nx, ny), ux0, dtype=np.float64) uy = np.zeros((nx, ny)) phi = np.zeros((nx, ny)) phi[nx//4:3*nx//4, ny//4:3*ny//4] = 1.0 ex, ey, w = d2q9_velocity_set() f = np.zeros((9, nx, ny)) g = np.zeros((9, nx, ny)) for i in range(9): cu = 3.0 * (ex[i] * ux + ey[i] * uy) f[i] = rho * w[i] * (1.0 + cu + 0.5*cu*cu - 1.5*(ux*ux + uy*uy)) g[i] = phi * w[i] * (1.0 + cu) return f, g, rho, ux, uy, phi, ex, ey, w def convection_diffusion_step(f, g, rho, ux, uy, phi, ex, ey, w, omega_f, omega_k, source=0.0): f_new = np.zeros_like(f) g_new = np.zeros_like(g) for i in range(9): cu = 3.0 * (ex[i] * ux + ey[i] * uy) feq = rho * w[i] * (1.0 + cu + 0.5*cu*cu - 1.5*(ux*ux + uy*uy)) geq = phi * w[i] * (1.0 + cu) shift = (ex[i], ey[i]) f_new[i] = np.roll(f[i] - omega_f * (f[i] - feq), shift, axis=(0, 1)) g_new[i] = np.roll(g[i] - omega_k * (g[i] - geq) + w[i] * source, shift, axis=(0, 1)) f = f_new g = g_new rho = f.sum(axis=0) ux = np.tensordot(ex, f, axes=(0, 0)) / rho uy = np.tensordot(ey, f, axes=(0, 0)) / rho phi = g.sum(axis=0) return f, g, rho, ux, uy, phi

先说代码逻辑:碰撞步把分布函数向平衡态松弛,松弛率由omega_f和omega_k控制;迁移步用np.roll把分布函数按速度方向搬移到相邻格点,np.roll的环绕特性天然实现周期边界。宏观量的重算放在迁移之后,这是必须的顺序,因为迁移后的分布函数才代表当前时刻的物理状态。

参数设置上,omega_k直接决定扩散系数。例如omega_k = 1.5,则 (D = (1/3) \times (1/1.5 - 0.5) = 0.0556),在格子单位下对应中等扩散强度。想要更弱的扩散,把omega_k降到 1.2,但不要低于 1.0,否则数值稳定性变差。source参数对应源项 (Q),S0工况就传 0.0。ux0控制对流速度,R0工况传 0 即可退化为纯二维扩散。

3.3 跑通后怎么看结果:云图与守恒性检查

用下面这段后处理代码跑 400 步,每 50 步打印一次总量,就能直观看到方形斑块向右移动并展宽:

import matplotlib.pyplot as plt nx, ny = 128, 128 f, g, rho, ux, uy, phi, ex, ey, w = init_fields(nx, ny, ux0=0.1) total_before = phi.sum() for step in range(400): f, g, rho, ux, uy, phi = convection_diffusion_step( f, g, rho, ux, uy, phi, ex, ey, w, omega_f=1.0, omega_k=1.5, source=0.0 ) if step % 50 == 0: print(step, phi.sum(), (phi.sum() - total_before) / total_before) plt.imshow(phi.T, origin='lower', cmap='jet') plt.colorbar() plt.savefig('convection_d2q9.png')

守恒性检查是这步最重要的验收标准。周期边界下没有出入口,全场标量总量应该恒定,打印结果显示的偏差应该小于 (10^{-12}) 量级。如果偏差明显偏大,说明碰撞或迁移索引有重复更新,这通常是np.roll在不同方向组合下覆盖了数组造成的,先检查维度轴顺序。云图方面,方形斑块应保持大致形态,沿流动方向略微拉长,边缘平滑而非锯齿状。

4. L1_S0_R0_X0标签拆解:工况编号、物理量映射与批量调参思路

4.1 先看命名:L/S/R/X的常见约定

这类下划线命名不是数学公式,它的含义高度依赖项目自身的 README,不存在全世界统一的标准。但根据我做过的多个 LBM 项目经验,L、S、R、X一般有非常固定的分工:

标签常见含义典型取值对应物理量或开关
L1网格层级或分辨率等级0/1/2L1 通常代表第一层细化网格
S0标量源项强度0/0.01/0.1(Q),即单位时间产生的浓度
R0流动雷诺数或速度档位0/100/1000R0 代表纯扩散或静止流场
X0扩展功能开关0/1边界类型、外力项、是否输出诊断量

拿 (R0) 来说,它和流场松弛频率 (\omega_f)、特征速度 (u_0)、网格数 (N) 一起决定雷诺数:

[ Re = \frac{u_0 N}{\nu}, \quad \nu = c_s^2 \left(\frac{1}{\omega_f} - \frac{1}{2}\right) ]

如果 (\omega_f = 1.0),那么 (\nu = 1/6),128 格网格上 (u_0 = 0.1) 时 (Re \approx 0.1 \times 128 / 0.1667 \approx 76),算得上一个低雷诺数对流工况。标题里的L1_S0_R0_X0大概率就是一组“最基础、无源、静流、无扩展”的基准算例,用来和解析解对校。

4.2 从标签到代码:每个编号改的是哪一行

拿到L1_S0_R0_X0这类编号,落到第 3 章的代码上,改动位置非常明确。L1决定nx和ny,例如 (L0) 对应 (64 \times 64),(L1) 对应 (128 \times 128);S0对应source=0.0;R0对应ux0=0.0和uy0=0.0,让流场静止,方程退化为纯扩散;X0对应默认周期边界和默认输出频率。

实际调参时我习惯于把 L 当作第一优先项,因为网格数决定了可分辨的最小涡尺度和浓度梯度尺度。网格加密一倍,计算量涨四倍,但误差通常只降一半,盲目加密性价比极低。网格数确认后,再调 (R) 和 (S)。(R) 非零时,注意对流速度 (u_0) 不要超过 0.1 太多,否则逃逸 D2Q9 的稳定范围,出现明显的伪振荡。(S) 非零时,注意宏观量修正,否则总量增长曲率和理论上差半个时间步。

4.3 用标签组织批处理,别让脚本覆盖掉有效数据

标签不只是给人看的,它在批处理里能当作文件名前缀直接参与归档。我通常会写一个 bash 循环把几十组工况串起来:

for L in 1 2 3; do for R in 0 100; do for S in 0 0.01; do ./run_lbm.py --grid "$L" --rey "$R" --source "$S" --extra 0 \ --outdir "output/D2Q9_L${L}_S${S}_R${R}_X0" done done done

这个脚本把参数直接拼进目录名,跑完就是一组自带完整工况描述的归档目录。需要特别注意的是浮点参数别用精确字符串匹配去判断if source == 0,0.0 在配置里的精度可能因为解析方式变成0.00000001,导致走了带源项分支。用 (10^{-12}) 阈值判断开关状态是更稳妥的做法。

5. 二维对流扩散LBM的5个翻车现场:现象、原因与排查顺序

5.1 固定边界附近出现负浓度或负温度

现象:在入口浓度恒定的边界附近,云图里出现负值,尤其在时间步推进几十步之后,负值区域沿着边界向内扩散。

原因:标量平衡态函数只保留一阶项时,边界强制浓度会发生数值过冲,本质是二阶精度格式在陡梯度处的振荡,常见于驰豫外推边界与纯反弹边界混用的工况。

解决:边界单元不用分布函数直接赋值,改用反反弹格式,让浓度边界通过未知分布函数的镜像关系确定;同时给 (g_i^{eq}) 补上二阶项,增加格式本身的稳定性。如果负值仍然存在,就降低局部 Peclet 数,也就是将对流速度减半或增大一点的扩散系数。

5.2 周期边界下全场总标量不守恒

现象:第 3 章那个守恒性检验打印出的全场总量随时间明显下降,比如 400 步掉了 5%。

原因:最直接的原因是迁移步索引写错,比如分布函数在(1,0)方向迁移时,右边界格点应该接到左侧格点的值,但代码里用了np.roll(..., axis=1)和axis=0弄反,导致标量在边界处漏掉。其次是源项半隐式修正缺失,(Q \neq 0) 时没加那半个时间步的修正量。

解决:把主循环里四组方向的shift顺序统一成(ex[i], ey[i]),再打印phi.sum()的前 20 步变化。如果前 10 步就开始掉,迁移索引问题实锤;如果前 50 步守恒、之后才飘,那就是浮点累积误差或边界处理不稳定,改用双精度并检查出口流量修正。

5.3 云团速度和设定的对流速度不一致

现象:初始化时设了 (u_0 = 0.1),结果斑块每 100 步实际移动距离折算成格子速度只有 0.086。

原因:流场的宏观速度是从f的矩重算出来的,重算之前在入口或出口受到反弹边界影响,边界层内的速度被拉低,云团经过该区域时整体速度被拖慢。这属于典型的速度滑移误差。

解决:在入口处同时强制 (f_i^{eq}) 和 (g_i^{eq}),且两个平衡态用同一个宏观速度场,不要分别多次计算。然后检查云团中心位置随时间变化,尽量取远离边界的中心区域速度来对比,避免边界层污染统计。

5.4 松弛时间逼近0.5时高频振荡

现象:为了让扩散系数足够小,把 (\tau_k = 1/\omega_k) 调成 0.5001,结果前 20 步还算平稳,之后云图边缘出现棋盘格状高频噪声。

原因:(\tau_k) 接近 0.5 时,扩散系数趋近于零,对流扩散方程实际上退化成纯对流方程,而纯对流在BGK-D2Q9框架里需要额外的迎风效应来稳定。当数值耗散不足以抑制锯齿波时,高频模式就会被激发。

解决:把 (\tau_k) 放回到 0.6 至 0.8 区间,这是二维对流扩散 LBM 最稳的松弛范围。如果确实需要小扩散系数,优先降低对流速度而不是继续压缩 (\tau_k),或者改用带二阶平衡态项的形式,把 Peclet 数控制住。

5.5 斜向对流产生“假扩散”,云团沿流动方向被拉长

现象:在 (45^\circ) 方向对流时,解析解本应是等轴高斯斑,LBM 结果却沿流向明显拉长,横向扩散反而偏小。

原因:D2Q9 速度集在对角方向上的离散误差更高,一阶平衡态进一步放大了这种各向异性,表现在宏观方程里就是多余的数值扩散,俗称假扩散。

解决:把 (g_i^{eq}) 从一阶项升级到完整的二阶形式,这是最直接的修正;再不行就把对流速度降低到 0.05 以下,并对标量分布使用多松弛碰撞算子。用网格对齐主流方向的做法也能缓解,但会牺牲程序的通用性。

6. 进阶:用解析解给D2Q9标量模块验算,再往D3Q19和GPU方向扩展

6.1 高斯烟团验算:一个公式验证整条链路

二维无限域中,点源在均匀流场中的浓度分布有解析解:

[ \phi(x, y, t) = \frac{M}{4\pi D t} \exp \left[ -\frac{(x - x_0 - u_x t)^2 + (y - y_0 - u_y t)^2}{4Dt} \right] ]

用这个公式验证程序时,初始条件不用方形斑块,而是直接取 (t_0) 时刻的高斯分布作为初场,然后让程序推进到 (t_1),再用解析解对比。网格从 (64 \times 64) 加密到 (128 \times 128),时间步数一致,(L_2) 相对误差通常能下降三分之一到四分之一。如果误差不降,重点检查迁移方向和宏观速度重算是否与解析解在同一参考系。误差量级参考:在 (\tau_k=0.65)、对流速度 0.03、Peclet 数小于 5 时,(64 \times 64) 网格跑 200 步的 (L_2) 误差一般在 (10^{-2}) 到 (10^{-3}) 之间。

6.2 从D2Q9到D3Q19:别自己编速度表和权重

把二维方案扩展到三维,最稳妥的路径是换用 D3Q19 模型,它包含 1 个静止方向、6 个面心方向和 12 个边中点方向,权重分别是 (1/3)、(1/18)、(1/36)。三维版本里容易出现两个问题:一是速度表顺序和权重的对应关系手抄错误,二是迁移步的np.roll要同时处理三个空间轴,方向组合写错会让标量沿着奇怪的路径泄漏。

我的习惯是从公开参考表原样拷贝 D3Q19 的速度数组,不手工推导,然后先用零速度纯扩散工况跑 1000 步,验证解为正且总量守恒,再开对流。二维代码里所有向量操作从长度为 9 改成 19,宏观量恢复公式不变,但tensordot的轴要同步调整。

6.3 输出档案比代码本身更值钱

所有参数调通以后,我最后做的一件事是把每轮工况的输出文件名改成和标题一致的格式,比如phi_L1S0R100X0_t2000.npy。这个习惯源自一次教训:曾经连续跑 20 组工况,脚本忘了写输出标签,最后回看数据时完全分不清哪组是哪个参数,只能重跑。从那以后,所有 LBM 算例的输出文件都强制带L/S/R/X后缀。标题里那串D2Q9_L1_S0_R0_X0其实就是别人已经帮你演示过的命名习惯。希望你从这篇文章里拿到的不只是代码,还有这套能让工况可追溯、让翻车可复盘的工作流。希望帮到你。

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

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

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

立即咨询