この記事は著者が韓国語で書いた原文の翻訳です。 原文(한국어)を読む
τ=0.51で粘性を51倍も大きく読んでいた — 離散LBEの2次展開が残す半ステップ
格子LBEの粘性はτではなくτ−1/2に応じます。その半ステップは離散展開の2次項が残した値です。
連続式が与える粘性と格子が返す粘性が51倍違った#
連続BGK方程式だけを見れば、緩和時間 と動粘性係数 の関係は です。 を0.51にすれば は0.17になるはずです。ところが同じ をD2Q9コードに入れ、Taylor–Green渦の減衰率を測ると0.00333が出ます。51倍の違いです。
これはバグではありません。符号の取り違えでもありません。緩和時間からちょうど が抜けた値です。半ステップは二通りに出ます。特性曲線上の2次Taylor展開と、衝突積分の中点則です。二つの道は同じ場所で合流します。 のスライダーを0.51側へ寄せると、橙の破線 が格子の振幅から離れます。
The blue curve is the vortex actually living on the lattice at the left. Green is cs2(tau-1/2) = 3.333e-2, orange is cs2*tau = 2.000e-1 — 1.0x apart. Fitted from the blue curve: nu = 0.000e+0. Push tau toward 0.51 and the orange line stops resembling anything on screen.
青い曲線は32×32格子の上で生きている渦の振幅です。緑の破線は です。 で緑は0.00333、橙は0.17に分かれます。
格子上の一行は微分方程式ではない
実際に回しているのはこの一行です。
は離散速度 方向の分布関数、 は非平衡部分(平衡からのずれ)、 は1ステップです。
左辺は微分ではありません。有限の間隔 を実際に飛び越えた差です。これを微分に直すには展開が必要で、展開には2次項があります。CFDでよくあるのは1次項だけ残して残りを「打ち切り誤差」と呼ぶことです。ここではその2次項が誤差ではなく物理係数を書き換えます。
無視できない理由は次数を数えれば見えます。2次項の大きさは です。右辺の衝突項は で割られています。格子単位では は0.5から2の間の の数で、 です。つまり2次項と1次項の比は小さくなりません。無限小になるはずの を1に固定したことが格子気体法の出発点であり、その代償がこの項です。
特性曲線に沿って2次まで展開すると何が残るか#
と置くと左辺は の指数関数になります。2次まで残して で割ります。
問題の項が です。これを捨てると になり、残すと別の値になります。
Chapman–Enskog展開をかけてみましょう。、、、そして です。 の次数では
が出ます。この式を の次数でそのまま再利用するのが要点です。 なので
となります。 の前に括弧が一つ付きました。粘性応力は の2次モーメントに を掛けて出るので、その が に変わります。
格子単位 では です。 なので なら です。上のシミュレーションの緑の破線がまさにこの値です。
残った項を分布関数の中へ押し込む
展開を使わずに同じ結果へ至る道もあります。連続BGKを特性曲線上で積分し、台形則を使うのです。緩和時間を と書きます。
右辺に未来時刻が入っているので陰解法です。このままでは回せません。そこで変数を変えます。
で書き直すと未来時刻の項が打ち消えて消えます。 を代入すると残るのはこれです。
完全に陽解法で、私たちが使っているあの一行と文字まで同じです。ただし のときだけです。つまりコードの中の は物理的な緩和時間ではありません。物理的なほうは です。丸めの問題ではありません。変数変換が請求した値です。台形積分がこの半ステップを残す場所は緩和時間に付いた に続きます。
同じ一行を中点則として読み直す
台形則は 重み則の の場合です。 を動かせば、なぜちょうど半分なのかが見えます。1ステップの増幅率は で、正解は です。ここで です。
を1/2から外すと、1ステップ増幅率の誤差は として傾き2に乗ります。 でだけ傾き3に落ちます。半分は調整値ではなく、 の2次係数 と噛み合う唯一の値です。
Taylor–Green渦をτ6通りで減衰させた#
主張はここまでです。あとは格子に直接聞きます。2次元Taylor–Green渦は を測る定規になります。振幅が で落ちるからです。
import numpy as np
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]*4 + [1/36]*4)
CS2 = 1/3
def equilibrium_d2q9(rho, ux, uy):
cu = C[:,0,None,None]*ux + C[:,1,None,None]*uy
u2 = ux*ux + uy*uy
return W[:,None,None]*rho*(1 + cu/CS2 + cu*cu/(2*CS2**2) - u2/(2*CS2))
def taylor_green_init(n, u0):
k = 2*np.pi/n
x, y = np.meshgrid(np.arange(n), np.arange(n), indexing='ij')
ux = -u0*np.cos(k*x)*np.sin(k*y)
uy = u0*np.sin(k*x)*np.cos(k*y)
return np.ones((n,n)), ux, uy, k
def advance_lattice(f, tau):
rho = f.sum(axis=0)
ux = (C[:,0,None,None]*f).sum(axis=0)/rho
uy = (C[:,1,None,None]*f).sum(axis=0)/rho
f = f - (f - equilibrium_d2q9(rho, ux, uy))/tau # 衝突
for i in range(9): # 移流
f[i] = np.roll(f[i], (C[i,0], C[i,1]), axis=(0,1))
return f, ux, uy
def measure_decay_rate(n, tau, u0=0.01, nstep=400, skip=100):
rho, ux, uy, k = taylor_green_init(n, u0)
f = equilibrium_d2q9(rho, ux, uy)
amp = []
for s in range(nstep):
f, ux, uy = advance_lattice(f, tau)
amp.append(np.sqrt((ux**2 + uy**2).mean()))
t = np.arange(skip, nstep, dtype=float)
a = np.log(np.array(amp[skip:])) # 初期の過渡区間は捨てる
slope = np.polyfit(t, a, 1)[0]
return -slope/(2*k*k), k
def viscosity_scan(n=32):
print(f"{'tau':>6} {'nu_measured':>12} {'cs2*(tau-1/2)':>14} {'cs2*tau':>10} "
f"{'err_half[%]':>11} {'err_naive[%]':>12}")
for tau in (0.51, 0.55, 0.6, 0.8, 1.0, 1.5):
nu, k = measure_decay_rate(n, tau)
half = CS2*(tau - 0.5)
naive = CS2*tau
print(f"{tau:6.2f} {nu:12.6f} {half:14.6f} {naive:10.6f} "
f"{100*abs(nu-half)/half:11.2f} {100*abs(nu-naive)/naive:12.2f}")
viscosity_scan() tau nu_measured cs2*(tau-1/2) cs2*tau err_half[%] err_naive[%]
0.51 0.003333 0.003333 0.170000 0.01 98.04
0.55 0.016667 0.016667 0.183333 0.00 90.91
0.60 0.033337 0.033333 0.200000 0.01 83.33
0.80 0.100115 0.100000 0.266667 0.12 62.46
1.00 0.167205 0.166667 0.333333 0.32 49.84
1.50 0.337688 0.333333 0.500000 1.31 32.46theta を1/2から外すと右の傾きは2に付き、1/2でだけ3に落ちます。
Only theta = 1/2 puts the pink line where the blue area demands, so its error rides slope 3 instead of slope 2 — that single half is the whole difference. Realised relaxation: tau_eff/tau = 0.9442. The lattice Boltzmann update you write is theta = 1/2 in f-bar, which is why the viscosity answers to tau - 1/2 and not tau. Cursor sits at n = 0.
左の青い領域が の面積で、桃色の破線が 則の代わりに使う高さです。 だけが の2次係数 と噛み合います。表の半ステップ誤差0.01%は、その中点が合っているときの残りです。
側の誤差は0.01〜1.31%です。格子の離散化が残す高次誤差の水準です。 側は32〜98%です。誤差と呼べる大きさではありません。
初期振幅を0.01と低くしたのは圧縮性の影響を消すためです。マッハ数が0.02程度なら、 の打ち切られた3次項が減衰率に混ざりません。最初の100ステップを捨てるのも同じ理由です。初期条件を平衡分布で与えると がまだ0なので、非平衡成分が落ち着くまで数十ステップかかります。その区間を入れて傾きを測ると粘性が実際より小さく出ます。
が大きくなるほど 側の誤差が少しずつ育つのも読みどころです。 で1.31%になります。展開で捨てた の項が とともに大きくなるからです。半ステップの補正は完結ではなく、2次までの話です。
表を で読み直すと、51倍は一行です。 なら です。 も51.0です。 は で、 も11です。 は6、 は2、 は1.5です。ナイーブ誤差の98.04%、90.91%、83.33%、62.46%、49.84%、32.46%は、この比が1へ近づきながら縮んだ跡です。
半ステップ側はもっと静かです。 では測定も予測も0.003333で、誤差は0.01%です。 は測定0.100115、予測0.100000、差0.000115で0.12%です。 は0.337688対0.333333、差0.004355で1.31%です。捨てた が とともに大きくなる、という話がこの三つのマスに入っています。初期振幅0.01、先頭100ステップ、格子32は、その残りを圧縮性から切り離すために選んだ値です。マッハ0.02なら、切られた3次項は減衰率に混ざりません。 のナイーブ誤差90.91%は と同じです。その比は1/11なので、連続式 で読むと十一倍の粘性をコードに書くことになります。 でも比は1.5で、ナイーブ誤差32.46%を小さい誤差とは呼べません。
境界条件と外力項が同じ半ステップを再度請求する
この半ステップは一度しか現れないわけではありません。
bounce-back境界が壁をノード上ではなく二つのノードの中間に置く理由も同じです。分布関数が反射する時点がステップの中間だからです。 が0.5から離れるとこの実効的な壁位置が に依存して動き、それが有名な2次精度の崩れです。
Guo形式の外力項に が付く理由も同じです。外力も同じ特性曲線上で積分され、同じ台形則を受けます。 変換が にしか作用しないので係数が残ります。この括弧を落とすと、静止しているべき流体がゆっくり流れたり、重力下の静水圧分布がわずかに傾いたりします。
格子幅を半分にするとき を単純に2倍してはいけない理由もここにあります。 を保つには を解く必要があります。細分化ループの中でこの一行をどこに置くかが、誤差を分けます。
をノードごとに変える場合、下限はまさにこの半ステップです。 で なので、粘性が局所的に0へ近づく問題は格子そのものの壁に当たります。
0.5は下限ではなく座標原点である#
を安定条件として暗記すると、原因を落とします。物理的な緩和時間は です。 では 、 なので比は51です。負の緩和時間は物理ではありません。
コードに書くべきは ではなく です。 を入力の一箇所に置きます。外力項に付く も、同じ半ステップの請求書です。その括弧を力の3次モーメントと並べて置く限界は力項が守るべき線に続きます。減衰が51倍遅くなれば、どこかで を として読んでいるということです。
関連記事
役に立ったらシェアしてください。