Skip to content
cfd-lab:~/ja/posts/2026-09-23-multicomponen…● online

この記事は著者が韓国語で書いた原文の翻訳です。 原文(한국어)を読む

NOTE #163DAY WED CFD기법DATE 2026.09.23UPDATED 2026.10.19READ 7 min read#Species-Transport#LBM#Chapman-Enskog#Diffusion#Combustion

化学種ごとに τ を変えたら全質量が 16.8% ずれた — 多成分 LBM の補正速度

拡散係数が互いに違った瞬間に連続の式は壊れます。補正速度の一行が 0.168 を 0.0005 に変えます。

化学種を三つに増やした日、密度の帳尻が合わなくなった

燃焼計算に化学種を一つ足すたびに、分布関数も一組ずつ増えます。化学種ごとに拡散係数が違うので、緩和時間 τk\tau_k も別々に与えます。ここまでは教科書どおりです。ところが数百ステップ回すと ∑kYk\sum_k Y_k が 1 から外れます。質量分率(mass fraction、全質量のうちその化学種が占める割合)の和は定義上 1 でなければなりません。

この記事では、そのずれがどこから来るのかを Chapman–Enskog 展開で突き止め、一行の補正で戻します。3成分混合を 2000 ステップ回し、最大欠損が 0.168 から 0.0005 へ下がる過程を数字で示します。先に結論を書くと、これはバグではなくモデルです。τk\tau_k を互いに違う値にした瞬間、連続の式が壊れるようにできています。

分布関数を三組に分けて使う構造

圧縮性多相多成分 LBM は、分布関数を用途別に分けます。fif_i が流れ場、gig_i がエネルギー、hk,ih_{k,i} が kk 番目の化学種を担当します。三つとも同じ格子速度 ci\mathbf{c}_i の上で、同じ形の離散 Boltzmann 方程式を解きます。

hk,i(x+ciδt,  t+δt)−hk,i(x,t)=−1τk[hk,i(x,t)−hk,ieq(x,t)]h_{k,i}(\mathbf{x}+\mathbf{c}_i\delta t,\;t+\delta t)-h_{k,i}(\mathbf{x},t) =-\frac{1}{\tau_k}\Big[h_{k,i}(\mathbf{x},t)-h_{k,i}^{\mathrm{eq}}(\mathbf{x},t)\Big]

hk,ih_{k,i} は方向 ii に載った化学種 kk の分布関数、τk\tau_k はその化学種の緩和時間、δt\delta t は時間ステップです。

平衡分布は化学種密度 ρk\rho_k と流れ速度 u\mathbf{u} だけを使います。流れ場の fif_i と違って 2 次項は要りません。化学種の式は移流と拡散まで復元すれば足りるからです。

hk,ieq=ρkwi(1+ci⋅ucs2)h_{k,i}^{\mathrm{eq}}=\rho_k w_i\left(1+\frac{\mathbf{c}_i\cdot\mathbf{u}}{c_s^2}\right)

wiw_i は格子重み、csc_s は格子音速です。化学種密度は ρk=∑ihk,i\rho_k=\sum_i h_{k,i}、質量分率は Yk=ρk/ρY_k=\rho_k/\rho で読み取ります。

DA,DB,DCD_A,D_B,D_C のスライダーを離すと三つの拡散係数が分かれ、下のパネルの質量和が 1 からずれます。

step 0 — current |sum Y_k - 1| = 0.00e+0, peak = 0.00e+0. Drag the three sliders to the same value and the lower panel flattens to round-off. Spread them apart and the error grows every step: each species relaxes with its own tau, so the three Fickian fluxes no longer cancel. Tick the checkbox to inject the correction velocity and the growth stops on the spot.

三つのスライダーを同じ値に揃えると、下のパネルが丸め誤差まで平らになります。離したままにすると毎ステップ誤差が積み上がります。チェックボックスは補正速度です。入れると和が再び 1 側へ畳まれます。

Chapman–Enskog が hkh_k から取り出す式#

上の離散式を δt\delta t で Taylor 展開し、分布関数を hk,i=hk,ieq+ϵhk,i(1)+⋯h_{k,i}=h_{k,i}^{\mathrm{eq}}+\epsilon h_{k,i}^{(1)}+\cdots と並べます。0 次モーメントを次数ごとに集めると、化学種一つ分の移流拡散方程式が出ます。

∂tρk+∇⋅(ρku)=∇⋅(Dk∇ρk),Dk=cs2(τk−12)δt\partial_t \rho_k+\nabla\cdot(\rho_k\mathbf{u})=\nabla\cdot\big(D_k\nabla\rho_k\big), \qquad D_k=c_s^2\Big(\tau_k-\tfrac{1}{2}\Big)\delta t

DkD_k がその化学種の拡散係数です。−1/2-1/2 は離散化が生む項であって物理ではありません。同じ構造の −1/2-1/2 が、流れ場では粘性、エネルギー場では熱拡散として現れます。その二つの帳簿を束ねると Prandtl 数が定数に固定される話は熱 LBM に隠れている二つの定数に書きました。

ここまでは美しく見えます。τk\tau_k を一つ回せば拡散係数を好きな値に合わせられます。本当にそうなるかは後で測ります。

τk\tau_k が互いに違うと何が残るか#

問題は化学種の式一つではなく、全部を足すときに生じます。kk について上の式を足し、∑kρk=ρ\sum_k\rho_k=\rho を使うとこうなります。

∂tρ+∇⋅(ρu)=∇⋅(∑kDk∇ρk)\partial_t \rho+\nabla\cdot(\rho\mathbf{u})=\nabla\cdot\left(\sum_k D_k\nabla\rho_k\right)

左辺は連続の式です。右辺はゼロでなければなりません。ところが右辺は一般にゼロになりません。

∑kDk∇ρk=ρ∑kDk∇Yk≠0\sum_k D_k\nabla\rho_k=\rho\sum_k D_k\nabla Y_k \neq 0

すべての DkD_k が同じときだけ ∑kDk∇Yk=D∇∑kYk=D∇(1)=0\sum_k D_k\nabla Y_k=D\nabla\sum_k Y_k=D\nabla(1)=0 になります。化学種ごとに τk\tau_k を変えるとは、この相殺を自分で壊すことです。流束で書くともっとはっきりします。Fick 流束 Jk=−ρDk∇Yk\mathbf{J}_k=-\rho D_k\nabla Y_k の和がゼロになりません。

物理としては知られた事実です。Fick 則は 2 成分混合の近似で、3 成分以上では Stefan–Maxwell 関係が正しい閉包です。LBM はこの近似を化学種ごとの独立な τk\tau_k へ書き写す際に、欠損もそのまま引き継ぎます。

3成分混合を 2000 ステップ回して帳尻を合わせてみた#

D1Q3 格子に化学種を三組載せました。ρ=1\rho=1、u=0\mathbf{u}=0 の静止混合です。拡散以外は何も起きないので、∑kρk\sum_k\rho_k は永遠に 1 のはずです。まず減衰する正弦モードで DkD_k が本当に cs2(τk−1/2)c_s^2(\tau_k-1/2) かを測り、次に DkD_k の組み合わせを変えながら欠損を測りました。

import numpy as np
 
N, CS2 = 256, 1.0/3.0
W = np.array([2.0/3.0, 1.0/6.0, 1.0/6.0])   # D1Q3 の重み: c = 0, +1, -1
C = np.array([0, 1, -1])
 
def feq_species(rho_k, u):
    return rho_k[None, :] * W[:, None] * (1.0 + C[:, None] * u[None, :] / CS2)
 
def correction_velocity(rho_k, D):
    dY = np.array([(np.roll(r, -1) - np.roll(r, 1)) * 0.5 for r in rho_k])
    return np.sum(D[:, None] * dY, axis=0)           # V_c = sum_k D_k grad Y_k
 
def lbm_species_step(h, D, use_correction):
    rho_k = h.sum(axis=1)                            # (K, N)
    u = correction_velocity(rho_k, D) if use_correction else np.zeros(N)
    tau = D / CS2 + 0.5
    for k in range(len(D)):
        h[k] += (feq_species(rho_k[k], u) - h[k]) / tau[k]
        for i in range(3):
            h[k, i] = np.roll(h[k, i], C[i])
    return h
 
def diffusion_leak_run(D, steps, use_correction):
    x = np.arange(N)
    r1 = 0.30 + 0.20 * np.sin(2 * np.pi * x / N)
    r2 = 0.45 + 0.15 * np.cos(4 * np.pi * x / N)
    r3 = 1.0 - r1 - r2                               # 開始時の総和はちょうど 1
    h = np.array([feq_species(r, np.zeros(N)) for r in [r1, r2, r3]])
    worst = 0.0
    for _ in range(steps):
        h = lbm_species_step(h, D, use_correction)
        worst = max(worst, np.abs(h.sum(axis=(0, 1)) - 1.0).max())
    return worst
 
def measure_D_eff(tau, steps=400):
    x = np.arange(N)
    amp0 = 0.01
    rho = 0.5 + amp0 * np.sin(2 * np.pi * x / N)     # 正弦モードを一つ入れて減衰率を測る
    h = feq_species(rho, np.zeros(N))
    for _ in range(steps):
        r = h.sum(axis=0)
        h += (feq_species(r, np.zeros(N)) - h) / tau
        for i in range(3):
            h[i] = np.roll(h[i], C[i])
    amp = np.abs(np.fft.rfft(h.sum(axis=0))[1]) * 2.0 / N
    k2 = (2 * np.pi / N) ** 2
    return -np.log(amp / amp0) / (k2 * steps)
 
print("tau    D_theory=cs^2(tau-1/2)   D_measured   error[%]")
for tau in (0.55, 0.80, 1.20, 2.00):
    dth, dm = CS2 * (tau - 0.5), measure_D_eff(tau)
    print(f"{tau:.2f}   {dth:.6f}                {dm:.6f}     {100*(dm-dth)/dth:+.3f}")
 
print()
print("D1,D2,D3            correction   max|sum(rho_k)-1|")
for D in (np.array([0.05, 0.05, 0.05]),
          np.array([0.05, 0.10, 0.02]),
          np.array([0.01, 0.10, 0.30]),
          np.array([0.005, 0.10, 0.50])):
    for corr in (False, True):
        print(f"{D[0]:.3f},{D[1]:.3f},{D[2]:.3f}     {str(corr):5s}        "
              f"{diffusion_leak_run(D, 2000, corr):.3e}")
tau    D_theory=cs^2(tau-1/2)   D_measured   error[%]
0.55   0.016667                0.016874     +1.243
0.80   0.100000                0.100137     +0.137
1.20   0.233333                0.233122     -0.091
2.00   0.500000                0.498135     -0.373
 
D1,D2,D3            correction   max|sum(rho_k)-1|
0.050,0.050,0.050     False        1.865e-13
0.050,0.050,0.050     True         2.091e-13
0.050,0.100,0.020     False        5.050e-02
0.050,0.100,0.020     True         5.201e-05
0.010,0.100,0.300     False        1.155e-01
0.010,0.100,0.300     True         1.354e-04
0.005,0.100,0.500     False        1.683e-01
0.005,0.100,0.500     True         5.215e-04

DkD_k のスライダーを離すと、赤い合計矢印の長さが変わります。

sum_k J_k = 0.00e+0, V_c = 0.0000. With three different D_k the red resultant almost never lands on zero, whatever the gradients — that residue is exactly the mass the solver invents each step. Switch the correction on and the green segments rebalance the three arrows so the resultant collapses to zero for every slider setting.

DkD_k と勾配をどう置いても、赤い合計の矢印はほとんどゼロに立ちません。チェックボックスを入れると緑の区間が付き、合計がきっちりゼロへ畳まれます。

上の表から読みます。Dk=cs2(τk−1/2)D_k=c_s^2(\tau_k-1/2) は 0.1% の水準で合います。τ=0.55\tau=0.55 で 1.24% に跳ねるのは、τ\tau が 1/21/2 に近づくほど大きくなる格子誤差です。関係式そのものは信頼してよいということです。

下の表が本題です。三つの拡散係数をすべて 0.05 に揃えると欠損は 1.9×10−131.9\times10^{-13}、倍精度の丸め誤差です。2.5 倍離すだけで 5×10−25\times10^{-2} になります。100 倍まで離すと 0.168 に達します。ρ=1\rho=1 ですから、これは全質量の 16.8% です。

cs2=1/3c_s^2=1/3 を Dk=cs2(τk−1/2)D_k=c_s^2(\tau_k-1/2) に入れると、上の表の理論値がそのまま出ます。τ=0.80\tau=0.80 なら (0.30)/3=0.100000(0.30)/3=0.100000 です。測定は 0.100137、誤差 +0.137%+0.137\% です。τ=2.00\tau=2.00 なら (1.50)/3=0.500000(1.50)/3=0.500000、測定 0.498135、誤差 −0.373%-0.373\% です。符号が変わります。τ=0.55\tau=0.55 は (0.05)/3=0.016667(0.05)/3=0.016667 なのに測定 0.016874、+1.243%+1.243\% です。τ=1.20\tau=1.20 は理論 0.233333、測定 0.233122、−0.091%-0.091\% です。1/21/2 から離れるほどパーセント誤差は小さくなります。

欠損の比も下の表からそのまま割れます。D=(0.050, 0.100, 0.020)D=(0.050,\,0.100,\,0.020) で 5.050×10−25.050\times10^{-2} を 5.201×10−55.201\times10^{-5} で割ると 971 です。D=(0.005, 0.100, 0.500)D=(0.005,\,0.100,\,0.500) で 1.683×10−11.683\times10^{-1} を 5.215×10−45.215\times10^{-4} で割ると 323 です。拡散係数の比が 2.5 倍のときより、100 倍のときの方が補正倍率はむしろ小さい。三つとも 0.05 なら欠損は 1.865×10−131.865\times10^{-13}、補正を入れても 2.091×10−132.091\times10^{-13} です。相殺がすでに成り立っているので、補正速度の仕事がありません。2000 ステップ、ρ=1\rho=1 で 0.168 は全質量の 16.8% であり、補正後の 5.215×10−45.215\times10^{-4} はその場所の残差です。

0.168 を 0.0005 に変えた一行#

直し方は古くから知られています。すべての化学種に同じ補正速度 Vc\mathbf{V}_c を載せ、流束の和を強制的にゼロにします。

Jk=−ρDk∇Yk+ρYkVc,Vc=∑kDk∇Yk\mathbf{J}_k=-\rho D_k\nabla Y_k+\rho Y_k\mathbf{V}_c, \qquad \mathbf{V}_c=\sum_k D_k\nabla Y_k

この Vc\mathbf{V}_c を入れると ∑kJk=−ρ∑kDk∇Yk+ρVc∑kYk=0\sum_k\mathbf{J}_k=-\rho\sum_k D_k\nabla Y_k+\rho\mathbf{V}_c\sum_k Y_k=0 になります。∑kYk=1\sum_k Y_k=1 という拘束をそのまま使ったものです。

LBM ではこれをソース項として付けません。平衡分布の速度を u\mathbf{u} から u+Vc\mathbf{u}+\mathbf{V}_c に変えるだけです。上のコードの correction_velocity 一行と use_correction の分岐がすべてです。速度を上げたときに拡散が減る余分な流束は、移流拡散 LBM が残す項と同じ種類です。

表の数字でも同じです。欠損 5.05×10−25.05\times10^{-2} が 5.20×10−55.20\times10^{-5} へ、1.68×10−11.68\times10^{-1} が 5.22×10−45.22\times10^{-4} へ下がります。それぞれ 971 倍と 323 倍です。

補正を入れてもゼロにならない残差

一つ目に留まるものがあります。拡散係数がすべて同じときは 1.9×10−131.9\times10^{-13} なのに、補正を入れた場合は 5×10−45\times10^{-4} で止まります。機械精度ではありません。

理由は離散演算子が二つあるからです。correction_velocity は中心差分で ∇Yk\nabla Y_k を計算します。一方で LBM が実際に作る拡散流束は、非平衡部分 hk,i(1)h_{k,i}^{(1)} の 1 次モーメント、つまりストリーミングが定める別の離散勾配です。二つの勾配が O(δx2)O(\delta x^2) だけずれます。その差が相殺されずに残ります。

消すには補正速度も同じ非平衡モーメントから取ればよいわけです。Vc=1ρ∑k(1−12τk)∑icihk,i(1)\mathbf{V}_c=\frac{1}{\rho}\sum_k\big(1-\frac{1}{2\tau_k}\big)\sum_i\mathbf{c}_i h_{k,i}^{(1)} のように書く方式です。中心差分が一ステップ遅れた値を使うことも、同じ理由で残差を作ります。フレームをずらすと粘性そのものが変わる場所は、平衡分布の3次モーメント欠損です。

ただし実務では 5×10−45\times10^{-4} と 1.7×10−11.7\times10^{-1} の差は大きいです。前者は蓄積せず、格子を細かくすれば減ります。後者は時間とともに育ちます。

燃焼コードでこの項が顔を出す場所

メタン空気燃焼の Lewis 数は化学種ごとに 0.3 から 1.8 まで散らばります。H と H2_2 は極端に速く、大きな炭化水素は遅いです。拡散係数で見ると 20 倍以上開きます。上の表の三行目がちょうどその範囲です。

欠損は密度誤差だけでは終わりません。化学反応率は YkY_k の指数に入ります。和が 1 を超えると反応率が過大評価され、その熱が再び密度を押して発散へ向かいます。多くのコードは毎ステップ YkY_k を和で割って正規化しますが、それは欠損を消すのではなく化学種の間に恣意的に配り直すだけです。どの化学種が損をするかは格子番号に依存します。

最もよくある実務の処方は、最も重い化学種を余剰種(bath species)に指定し、YN2=1−∑k≠N2YkY_{N_2}=1-\sum_{k\neq N_2}Y_k と直接計算することです。こうすると和はきっちり 1 になりますが、欠損は全部 N2_2 に積もります。補正速度はその負担を YkY_k の比で分けます。

欠損を N2_2 に寄せると帳簿は合っても種が違う#

τk\tau_k を化学種ごとに変える瞬間、道は三つです。欠損を放置する道、和で割って一種に寄せる道、補正速度で YkY_k の比に分ける道。放置は道になりません。寄せる道は余剰種が多いときだけ持ちます。希釈燃焼の YN2≈0.7Y_{N_2}\approx 0.7 がその条件です。既定は補正速度です。費用はステップごとの勾配一組と、平衡分布の速度一行です。

τ\tau の範囲も同時にロックされます。表では τ=0.55\tau=0.55 の拡散係数誤差が +1.243%+1.243\%、τ=2.00\tau=2.00 では −0.373%-0.373\% で符号が変わります。その床は τ=0.51\tau=0.51 で粘性を 51 倍読み違える半ステップと同じ場所です。化学種の軸でその床がもう一度敷かれます。ログに ∑kYk=1.0003\sum_k Y_k=1.0003 と出たら、まず DkD_k を並べます。値が違えば原因はその行にあります。

役に立ったらシェアしてください。