把参考系推到0.2,粘性少了5.97% — LBM平衡分布缺失的三阶矩
LBM的速度上限不是由稳定性决定的,而是由平衡分布能匹配的矩阶数先定下来的。
静止的水和流动的水,粘性应该一样吗
把同一种流体量两次。一次在静止的盒子里,一次让盒子以匀速平移。两次得到的粘性系数应该完全相同。伽利略不变性,也就是在两个匀速相对运动的坐标系中物理定律形式相同这一性质,要求如此。
在D2Q9格子玻尔兹曼方法(LBM)上照做这个实验,第二个数值会小5.97%。加密网格没有用。改变松弛时间 也几乎不改变这个比例。本文把这5.97%一路追到平衡分布的一行矩条件上。先说结论:这不是稳定性问题,而是代数问题。格子速度集合根本造不出那个缺失的项。
把格子上的那一行做Taylor展开#
LBM求解的方程只有一行。
其中 是沿格子速度 运动的分布函数, 是当地的平衡分布, 是松弛时间。
把左边按 展开,除一阶项外还留下二阶项。
正是这个二阶项,在Chapman–Enskog展开(把分布函数按克努森数展成幂级数的多尺度方法)中产生了著名的 。同一展开的下一步会给出一个条件:要让得到的连续介质方程成为Navier–Stokes方程,平衡分布必须精确满足四个矩。
前三个决定质量、动量和压力。第四个,也就是三阶矩,用来替换粘性应力的时间导数。这里一旦错位,连续方程和Euler层面都完好无损,唯独粘性项被污染。
在下面的模拟里可以亲手检查三阶矩是否吻合。
拖动速度滑块,黄色曲线(Maxwell分布要求的值)与蓝色直线(D2Q9实际给出的值)会分开。点击 sweep u 让它自动往复,就能看到间距随 的奇数次幂变化。打开 speeds ±2,柱子变成五根,间距闭合为零。
— 格子造不出来的那一项#
D2Q9的 分量只有 三个值。这三个数的立方都等于它们自己。于是对任意分布 ,
恒成立。平衡分布怎么设计都一样。哪怕往九个格子里填随机数,等式依然成立。三阶矩早已被一阶矩锁死。
而Maxwell分布要求的值是 ,在D2Q9上 ,所以 ,目标值折叠成 。两者之差恰好是 这一项。不是"大致是这个量级",而是不多不少就这一项。
缺失项在动量方程里留下的样子
缺掉的三阶矩会直接加到粘性应力上。把Chapman–Enskog展开推到底,应力里会多出一项。
设平均流 沿 方向流动,其上叠加一个小扰动 。只保留对 的一阶部分,该项变为 ,形式与粘性项完全一致。也就是说, 方向的粘性系数被整体改写。
相对误差是 。注意 在分子和分母中同时消去:无论怎样调节松弛,这个百分比都不变。而误差正比于速度的平方,用马赫数写就是 。
用Python在两个参考系里测同一个涡#
把Taylor–Green涡放在64×64的周期格子上,再叠加一个匀速 。用振幅衰减的对数斜率反算粘性系数。只改变 ,其余全部固定。
import numpy as np
CX = np.array([0, 1, 0, -1, 0, 1, -1, -1, 1], dtype=float)
CY = np.array([0, 0, 1, 0, -1, 1, 1, -1, -1], dtype=float)
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
def f_equilibrium(rho, ux, uy):
# 展开到二阶的标准平衡分布
cu = CX[:, None, None] * ux + CY[:, None, None] * uy
usq = ux * ux + uy * uy
return W[:, None, None] * rho * (1 + cu / CS2 + cu * cu / (2 * CS2**2) - usq / (2 * CS2))
print("[1] third moment audit: sum f_i c_ix^3 vs Maxwell rho*(u^3 + 3*cs2*u)")
print(" u lattice Maxwell gap gap/u^3")
for u in (0.05, 0.10, 0.20, 0.30):
feq = f_equilibrium(np.ones((1, 1)), np.full((1, 1), u), np.zeros((1, 1)))
lat = float((feq * (CX**3)[:, None, None]).sum())
exact = u**3 + 3 * CS2 * u
print(f" {u:4.2f} {lat:11.8f} {exact:11.8f} {exact-lat:11.8f} {(exact-lat)/u**3:8.5f}")
rng = np.random.default_rng(7)
frand = rng.random((9, 1, 1))
d = float((frand * (CX**3)[:, None, None]).sum() - (frand * CX[:, None, None]).sum())
print(f" any random f: sum f c_x^3 - sum f c_x = {d:.3e} (c_x^3 = c_x, so always 0)")
def taylor_green_run(U0, tau=0.8, N=64, steps=900, amp=0.04, warmup=300, sample=25):
# 给同一个涡叠加匀速U0,再从衰减率把粘性测回来
k = 2 * np.pi / N
x = np.arange(N)[:, None] * np.ones(N)[None, :]
y = np.ones(N)[:, None] * np.arange(N)[None, :]
ux = U0 - amp * np.cos(k * x) * np.sin(k * y)
uy = amp * np.sin(k * x) * np.cos(k * y)
rho = np.ones((N, N))
f = f_equilibrium(rho, ux, uy)
ts, logs = [], []
for n in range(steps + 1):
rho = f.sum(axis=0)
ux = (f * CX[:, None, None]).sum(axis=0) / rho
uy = (f * CY[:, None, None]).sum(axis=0) / rho
if n % sample == 0:
up, vp = ux - ux.mean(), uy - uy.mean() # 扣掉平均流之后的脉动分量
ts.append(n)
logs.append(0.5 * np.log(2 * np.mean(up * up + vp * vp)))
f += (f_equilibrium(rho, ux, uy) - f) / tau # BGK碰撞
for i in range(9): # 迁移
f[i] = np.roll(np.roll(f[i], int(CX[i]), axis=0), int(CY[i]), axis=1)
ts, logs = np.array(ts, dtype=float), np.array(logs)
m = ts >= warmup
slope = np.polyfit(ts[m], logs[m], 1)[0]
return -slope / (2 * k * k)
print("\n[2] same vortex measured in a uniformly moving frame (tau=0.8, nu_theory=0.100000)")
nu_th = CS2 * (0.8 - 0.5)
print(" U0 nu_eff rel.err rel.err/U0^2")
for U0 in (0.00, 0.05, 0.10, 0.15, 0.20):
nu = taylor_green_run(U0)
e = nu / nu_th - 1
tail = f"{e/U0**2:10.4f}" if U0 > 0 else " -"
print(f" {U0:4.2f} {nu:10.7f} {e:+9.4%} {tail}")
print("\n[3] does the error depend on tau? (U0=0.15 fixed)")
print(" tau nu_theory nu_eff rel.err")
for tau in (0.6, 0.8, 1.0):
nu = taylor_green_run(0.15, tau=tau)
th = CS2 * (tau - 0.5)
print(f" {tau:4.2f} {th:10.7f} {nu:10.7f} {nu/th-1:+9.4%}")[1] third moment audit: sum f_i c_ix^3 vs Maxwell rho*(u^3 + 3*cs2*u)
u lattice Maxwell gap gap/u^3
0.05 0.05000000 0.05012500 0.00012500 1.00000
0.10 0.10000000 0.10100000 0.00100000 1.00000
0.20 0.20000000 0.20800000 0.00800000 1.00000
0.30 0.30000000 0.32700000 0.02700000 1.00000
any random f: sum f c_x^3 - sum f c_x = 0.000e+00 (c_x^3 = c_x, so always 0)
[2] same vortex measured in a uniformly moving frame (tau=0.8, nu_theory=0.100000)
U0 nu_eff rel.err rel.err/U0^2
0.00 0.1000175 +0.0175% -
0.05 0.0996433 -0.3567% -1.4268
0.10 0.0985207 -1.4793% -1.4793
0.15 0.0966495 -3.3505% -1.4891
0.20 0.0940296 -5.9704% -1.4926
[3] does the error depend on tau? (U0=0.15 fixed)
tau nu_theory nu_eff rel.err
0.60 0.0333333 0.0321975 -3.4076%
0.80 0.1000000 0.0966495 -3.3505%
1.00 0.1666667 0.1611502 -3.3099%误差挂在 上,而不是 上#
[1]最后一列全部是1.00000。这说明"缺失项恰好是 "这一断言精确到小数点后五位。
[2]中静止参考系的误差是 ,这是测量方法本身的噪声基准线。把 提高,误差依次走到 、、、。最后一列是误差除以 , 越小,它越贴近 。
这个 正是上一节的预测。相对误差 只作用在 分量上,而Taylor–Green模式是 的对角模式,衰减率取两个方向的平均。所以是一半,即 。在 处,预测是 ,实测是 。
[3]更值得注意。把 从0.6提到1.0,粘性变成五倍,相对误差却只从 移动到 。也就是说,靠加大粘性并不能把这个误差埋掉。
提高 frame velocity U,右边的涡一边平移一边比左边更慢地变淡。同样的流体,右边偏偏消不掉。晃动 滑块还能看到下方的error数值几乎不动。
三种修法,以及各自的账单
降低速度。 误差按 走,格子速度减半,误差就降到四分之一。代价是复现同样物理时间所需的步数增加。LBM里"马赫数保持在0.1以下"的经验法则通常被当作稳定性规则介绍,但真正先起作用的是这个缺失项。
加补偿项。 用差分计算 ,以相反符号注入碰撞步。额外成本是一次梯度计算,同时格子玻尔兹曼完全局部碰撞这一优点会被削弱一点。即便如此,对旋转坐标系这类平均流很大的问题,它仍是最便宜的选项。
扩大速度集合。 包含 的多速度格子满足 ,因此可以精确匹配三阶矩。第一个viz里的 speeds ±2 按钮做的就是这个求解。代价是模板变宽、内存增加、边界处理更复杂,而且部分速度的权重可能变负,稳定性需要重新评估。
这个缺失项在真实计算中露面的地方
凡是平均流很大的场合都是候选。旋转机械的旋转坐标系、滑移网格、叠加在高速均匀来流上的湍流、贴在运动物体上的参考系。一份在静止流基准算例上表现完美的代码,偏偏在这些问题上把有效雷诺数算偏,就是这个症状。
LBM网格加密中的非平衡重标度讲过按层级重新取 的做法,而这个缺失项不会因此消失,因为它的相对误差与 无关。它和热LBM里藏着的两个常数是同一种结构:没有作为物性输入的量,被格子结构自动定死。
诊断很便宜。给同一个算例叠加一个平均流再跑一次,比较衰减率或阻力系数。如果差距随 增大,就是这一项。不必像STL体素化的奇偶判定那样去怀疑边界处理。
下次粘性随参考系改变时
在格子玻尔兹曼里,决定速度上限的不只是稳定性。平衡分布能匹配的矩阶数先定下上限。D2Q9在三阶丢掉 ,账单以 的粘性误差形式寄来。
数值不对劲时,第一反应往往是加密网格或调 。这个误差对两者都无动于衷。它只在降低 、把缺失项手动补回去、或者扩大速度集合时才会缩小。
相关文章
如果对您有帮助,请分享。