本文译自作者的韩语原文。 阅读原文(한국어)
老实补上三阶矩后误差反而涨了2.8倍 — 力项真正需要守住的几行
力项的三阶矩只在Burnett阶才到达动量方程。格子只允许一半的张量修正,补了比不补更糟。
力项要匹配到几阶矩
在LBM代码里加入重力或表面张力时,我们匹配的是三行。 零阶矩为0,一阶矩为 ,二阶矩为 。 那么三阶呢?连续玻尔兹曼方程的力项同样有一个明确的三阶矩。 把这个值在格子上也补齐,看起来能白拿一份精度。
把数字打出来,结果是反的。 把三阶矩补成动理论要求的值之后,误差从0.053%涨到了0.150%。 速度滑块 、 往上推,原本对齐的矩行会裂开。复选框只能关掉黄行。
Drag ux, uy toward zero and every row lines up. Push them out and the two yellow rows open a gap that the checkbox can close, while the red row cannot be touched at all: on D2Q9 cx^3 is the same vector as cx. Largest remaining gap, relative to the biggest moment on screen: 0.00%.
把速度拉回0,行又对齐。红行不管滑块推多远都关不上。
连续玻尔兹曼给出的力的三阶矩
在有体积力 的玻尔兹曼方程中,力项是速度空间的导数。
是分子速度, 是密度。三阶矩只需一次分部积分就闭合。
其中 。 于是被要求的三阶矩可以写成下式。
项和 项并列其中。前者与速度无关,后者是速度的二次量。
Guo forcing实际提供的值#
放到格子上的力项通常写成Guo的形式。去掉半步因子,只把括号里叫做 。
是格子权重, 是离散速度。真正的 还要再乘 。 这个因子为什么必需,在离散LBE留下的半步里单独讲过。
用D2Q9的四阶各向同性计算这个 的三阶矩,剩下的只有下式。
项完全正确。 项一个都没有。第二项构成五阶矩,在D2Q9上为0。
前面把速度推上去时,张开的只有两条黄行,红行一直锁着。
— 格子锁死的两个分量#
红行的真身就是格子本身。D2Q9的速度分量只有 。 于是每个方向上都有 。两个向量其实是同一个向量。
不是可以挑的值。一阶矩定成 的那一刻它就一起定了。 要求值是 ,所以 是永久亏空。
相比之下 和 是独立的。九个自由度里零到二阶用掉六个,还剩三个。 所以四个分量里两个自由、两个被锁。也就是说张量只能修一半。
既不搬质量也不搬动量的项却搬走了动量
可以构造一个只把 改变 的修正向量 。 它的零阶、一阶、二阶矩全为0,所以在原地既不动密度,也不动动量和应力。 在D2Q9上这个向量长这样。
可是只要迁移一步,情况就变了。各个分量飞向不同的邻点。 只把 方向的动量收集起来,剩下的是下式。
这是二阶中心差分。 均匀时严格为0, 振荡时按波数的平方增长。 波形均匀时,格子之间的变化消失。正弦下把波长缩短,变化就按波数的平方长起来。格子的总和保持为0。
固定力的图样并通上横流的通道
验证需要有精确解的问题。在周期区域里固定一个 方向的正弦体积力, 再沿 方向通入均匀横流 。相当于多孔壁一侧吹入、另一侧抽出。
是运动粘度, 是波数, 是力的幅值。单一模态给出闭式解。
横流做两件事。削掉幅值,并把相位推迟 。 同时 一直存在,于是 在空间上振荡。这正是上一节那个泄漏的开关。
把四种组合放在同一个格子上
16节点周期格子,,,跑了四种。 标准Guo、补力的三阶、补平衡分布的三阶、两个都补。
import numpy as np
CX = np.array([0, 1, 0,-1, 0, 1,-1,-1, 1], float)
CY = np.array([0, 0, 1, 0,-1, 1, 1,-1,-1], float)
WT = 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 hermite3_vectors():
"""0~2阶矩为0,仅让3阶 xxy / xyy 为1的修正向量。"""
rows = np.vstack([np.ones(9), CX, CY, CX*CX, CX*CY, CY*CY, CX*CX*CY, CX*CY*CY])
pinv = np.linalg.pinv(rows)
return pinv @ np.eye(8)[6], pinv @ np.eye(8)[7]
VXXY, VXYY = hermite3_vectors()
def feq_second_order(rho, ux, uy):
cu = CX[:, None, None]*ux + CY[:, None, None]*uy
return WT[:, None, None]*rho*(1 + cu/CS2 + cu*cu/(2*CS2**2)
- (ux*ux + uy*uy)/(2*CS2))
def force_populations(ux, uy, fx, fy, tau):
"""Guo forcing:一阶矩为 F,二阶矩为 uF+Fu。"""
cF = CX[:, None, None]*fx + CY[:, None, None]*fy
gxx, gyy, gxy = ux*fx, uy*fy, 0.5*(ux*fy + uy*fx)
quad = (gxx*(CX[:, None, None]**2 - CS2) + gyy*(CY[:, None, None]**2 - CS2)
+ 2*gxy*(CX*CY)[:, None, None])
return WT[:, None, None]*(1 - 1/(2*tau))*(cF/CS2 + quad/CS2**2)
def analytic_wave(ny, nu, V, a0):
"""nu u'' - V u' + a0 sin(ky) = 0 的周期定常解。"""
k = 2*np.pi/ny
y = np.arange(ny, dtype=float)
c1 = a0/(nu*k*k + V*V/nu)
return c1*np.sin(k*y) - (V*c1/(nu*k))*np.cos(k*y)
def run_cross_flow(ny, tau, V, a0, cubic_eq, cubic_force, nstep=6000, tol=1e-12):
nx = 3
k = 2*np.pi/ny
fx = np.tile(a0*np.sin(k*np.arange(ny)), (nx, 1))
fy = np.zeros((nx, ny))
rho = np.ones((nx, ny)); ux = np.zeros((nx, ny)); uy = np.full((nx, ny), V)
f = feq_second_order(rho, ux, uy)
half, prev = 1 - 1/(2*tau), ux.copy()
for s in range(nstep):
rho = f.sum(0)
ux = ((CX[:, None, None]*f).sum(0) + 0.5*fx)/rho
uy = ((CY[:, None, None]*f).sum(0) + 0.5*fy)/rho
feq = feq_second_order(rho, ux, uy)
if cubic_eq: # 补上平衡分布的三阶缺失 rho*u*u*u
feq = feq + VXXY[:, None, None]*(rho*ux*ux*uy) \
+ VXYY[:, None, None]*(rho*ux*uy*uy)
src = force_populations(ux, uy, fx, fy, tau)
if cubic_force: # 补上力项的三阶缺失 F*u*u
src = src + half*(VXXY[:, None, None]*(2*fx*ux*uy + fy*ux*ux)
+ VXYY[:, None, None]*(fx*uy*uy + 2*fy*ux*uy))
f = f - (f - feq)/tau + src
for i in range(9):
f[i] = np.roll(f[i], (int(CX[i]), int(CY[i])), axis=(0, 1))
if s % 1000 == 999:
if np.max(np.abs(ux - prev)) < tol:
break
prev = ux.copy()
return ux[0]
print("D2Q9 lock check: max|cx^3 - cx| =", float(np.abs(CX**3 - CX).max()))
for tag, mom in (("xxy", CX*CX*CY), ("xyy", CX*CY*CY), ("xxx", CX**3), ("xy", CX*CY)):
print(f" correction vector v_xxy : sum v_i c_{tag:3s} = {float(mom @ VXXY): .6f}")
NY, TAU = 16, 1.0
NU, A0 = CS2*(TAU - 0.5), 0.004
print(f"\nny={NY} tau={TAU} nu={NU:.5f} k={2*np.pi/NY:.4f} a0={A0}")
print("\n V |u|max Guo +force +feq3 +both")
for V in (0.05, 0.10, 0.20):
ref = analytic_wave(NY, NU, V, A0)
errs = []
for ce, cf in ((0, 0), (0, 1), (1, 0), (1, 1)):
u = run_cross_flow(NY, TAU, V, A0, bool(ce), bool(cf))
errs.append(100*np.linalg.norm(u - ref)/np.linalg.norm(ref))
print(f"{V:5.2f} {np.abs(ref).max():.5f} " + " ".join(f"{e:7.4f}%" for e in errs))D2Q9 lock check: max|cx^3 - cx| = 0.0
correction vector v_xxy : sum v_i c_xxy = 1.000000
correction vector v_xxy : sum v_i c_xyy = -0.000000
correction vector v_xxy : sum v_i c_xxx = 0.000000
correction vector v_xxy : sum v_i c_xy = -0.000000
ny=16 tau=1.0 nu=0.16667 k=0.3927 a0=0.004
V |u|max Guo +force +feq3 +both
0.05 0.12258 0.5993% 0.5915% 0.0181% 0.0143%
0.10 0.08374 1.6422% 1.6207% 0.0295% 0.0375%
0.20 0.04826 3.7307% 3.6840% 0.0531% 0.1503%表里 +feq3 和 +both 的差距会随波长缩短而变大。波形开关和波长滑块就是这道泄漏的柱子。
Switch d(y) to uniform: the dots still fly, the green bars all collapse to zero. Switch back to sine and shorten the wavelength: the bars grow like the second difference. Peak delivered momentum 0.00e+0, total over the column 0 (exactly), predicted peak 0.5*|d''| = 0.00e+0.
切到 uniform,绿色柱子归零。在 sine 下缩短波长,柱子按波数的平方长起来,格子的总和仍是0。
把0.053%变成0.150%的修正#
一行一行读表格,顺序就出来了。数字取 。
| 组合 | 误差 | 与标准相比 |
|---|---|---|
| 标准Guo | 3.7307% | — |
| 补力的三阶 | 3.6840% | 改善1.3% |
| 补平衡分布的三阶 | 0.0531% | 改善70倍 |
| 两个都补 | 0.1503% | 比只补平衡分布差2.8倍 |
补力的三阶矩顶多换来1.3%。补平衡分布的三阶却把误差压到七十分之一。 两个一起放进去又变差。三个数字讲的是同一件事。
原因在Chapman-Enskog展开的阶数上。力的二阶矩直接进入 的应力方程。 力的三阶矩进入 的三阶矩方程,再从那里下降一级到应力, 又从应力下降一级到动量。它到达动量方程时已经是 ,也就是Burnett阶。 对以Navier-Stokes为目标的等温LBM来说,这一项不是要匹配的对象。
平衡分布的三阶矩则直接进入 的应力。这就是 平衡分布的三阶缺失占主导的原因。
修正制造误差的路径
那么补力的三阶为什么反而有害?上一节的泄漏公式给出答案。 因为 ,迁移会制造出如下大小的伪体积力。
代入 、、,它是 的 。 代码里量到的两个解的最大差是 的 。两个数字吻合。 动量方程根本没要的项,被格子老老实实送到了。
的依赖关系也照样可见。把 从 提到 再到 , 两个解的差是 。每次四倍。
三阶矩真正开始重要的地方
这并不意味着 的推导没有意义。确实有需要它的地方。
在同时求解能量方程的热与可压缩LBM里,三阶矩就是热流。 那里力的三阶矩直接进入 的能量方程,不必绕到Burnett阶。 用非理想状态方程生成界面的多相LBM也类似。力很大,而且在空间上很陡。 一旦 接近1, 就不再小了。
时标准Guo是3.7307%。只补力的三阶是3.6840%。相差0.0467个百分点,除以3.7307大约是1.3%。只补平衡分布的三阶是0.0531%。3.7307除以0.0531大约是70。两个都补是0.1503%。0.1503除以0.0531大约是2.83,和表上的2.8倍一致。 时两个都补是0.0143%,比只补平衡的0.0181%还小。 变成0.0375%对0.0295%,次序反过来。 的间隔是0.0972个百分点。
把 、、 代进泄漏式, 是 。再乘前面的 得到 ,。 是 的平方,约 。 乘 乘 等于 ,也就是 的 。代码量到的差是 的 ,落在 个百分点以内。 从 到 再到 ,每次翻倍, 就是四倍。间隔从 到 再到 ,也是每次四倍。 依次是 、、。 的分母是 ,,所以 越大振幅被削得越多。
力的那一半放在哪里,把Guo、Shan–Chen、EDM放在一起的比较正是拿同一个 来分的。先数目标方程是什么,以及该项在第几阶到达。
为什么不能只补一半格子没有的项
这次实验真正的教训在阶数之前。 是张量。 D2Q9只允许其中的 、,把 、 锁住。 只补张量的一半,等于放进了一个各向同性被破坏的修正。 原本互相抵消的误差可能因此露头,这次就是如此。
发现格子搬不了的项时,选择只有两个。 扩大速度集合直到拿到那个矩(D2Q17、D2Q37),或者干脆不碰。 没有中间选项。给力项加一行之前,先数一数这一行剩下的分量在格子上是否存在。
相关文章
如果对您有帮助,请分享。