Skip to content
cfd-lab:~/ja/posts/2026-05-08-advection-ftc…● online

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

NOTE #035DAY FRI CFD기법DATE 2026.05.08UPDATED 2026.10.19READ 4 min read#CFD#Advection#Upwind#CFL#Numerical-Diffusion

FTCSが震えるとき、風上は鈍る — 1次移流スキーム、人工粘性、CFLの等価交換

N=200、c=0.8なら一周は250ステップです。コードの400ステップは1.6周で、初期パルスとは位置がずれます。

1960年代初頭、von Neumannの近くにいた人たちが1次元線形移流へ中心差分を入れました。矩形パルスは静かに流れるはずでした。後ろの縁で振動が膨らみ、数ステップで格子の外へ出ました。

原因は次数ではありません。下流の値を同じ重みで引くと因果が壊れます。一格子の風上差分はその暴走を鎮める代わりに角を殺します。その境界がCFLです。

CFLの目盛りを0.05から1.5まで動かすと、同じ矩形パルスでFTCSは震え続け、風上は1の近くでだけ破線に重なります。

centered (FTCS)1st-order upwindLax–Wendroffanalytic shifttry CFL > 1 to break the safe zone

目盛りを0.5に置くと風上がいちばん広がります。1.05を超えると風上もLax–Wendroffも一緒に飛びます。ステンシルが特性線より短くなった場所です。

最も自然な差分が真っ先に崩れる

1次元の線形移流は一行です。

∂tq+u ∂xq=0\partial_t q + u\,\partial_x q = 0

qq は流れる量、u>0u>0 は一定の速度です。解は初期形状が速度 uu で平行移動したものです。

時間に前進オイラー、空間に中心差分(2次)を使うと更新は次です。

qin+1=qin−c2 (qi+1n−qi−1n)q_i^{n+1} = q_i^{n} - \tfrac{c}{2}\,\big(q_{i+1}^{n} - q_{i-1}^{n}\big)

c≡u Δt/Δxc \equiv u\,\Delta t / \Delta x はクーラン数です。1ステップで波が横切る格子数です。空間2次、時間1次なのでLTE(局所打切り誤差)は O(Δt,Δx2)\mathcal{O}(\Delta t,\Delta x^2) です。cc をどれだけ小さくしても、このスキームは暴走します。

qin=gneikxiq^n_i = g^n e^{i k x_i} を代入すると増幅率は

g=1−i c sin⁡(kΔx)g = 1 - i\,c\,\sin(k\Delta x)

で、∣g∣2=1+c2sin⁡2(kΔx)>1|g|^2 = 1 + c^2 \sin^2(k\Delta x) > 1 です。すべてのモードが毎ステップ増えます。∣g∣|g| が1を超えるモードが一つあれば場は壊れます。

コードの c=0.8c=0.8 で sin⁡=1\sin=1 のモードの大きさは 1+c2=1.64≈1.28\sqrt{1+c^2}=\sqrt{1.64}\approx 1.28 です。1ステップに1.28倍、400ステップはその累乗です。下に書いた目分量の 1.784001.78^{400} はこのモードの率ではありません。式が返すのは1.28です。

情報は片側からだけ流れてくる

u>0u>0 なら時刻 tn+1t_{n+1} の qiq_i は上流、つまり x<xix<x_i だけを見るべきです。中心差分は下流の値 qi+1nq_{i+1}^n を同じ重みで取ります。流れがまだ知らない側を先に読んでいます。

風上差分はその向きを固定します。

qin+1=qin−c (qin−qi−1n),u>0q_i^{n+1} = q_i^{n} - c\,\big(q_i^{n} - q_{i-1}^{n}\big), \quad u>0

増幅率は

g=1−c+c e−ikΔxg = 1 - c + c\,e^{-i k \Delta x}

で、∣g∣2=1−c(1−c)⋅4sin⁡2(kΔx/2)|g|^2 = 1 - c(1-c)\cdot 4\sin^2(k\Delta x/2) です。0≤c≤10 \le c \le 1 なら ∣g∣≤1|g|\le 1 です。

減衰項の係数 c(1−c)c(1-c) は c=0.5c=0.5 で0.25と最大です。c=0.8c=0.8 では0.16です。最短波長では ∣g∣2=1−4⋅0.16=0.36|g|^2 = 1 - 4\cdot 0.16 = 0.36、つまり ∣g∣=0.6|g|=0.6 です。c=0.5c=0.5 ではそのモードの ∣g∣|g| が0まで落ちます。c=1c=1 では引く項が消えて、すべてのモードが ∣g∣=1|g|=1 です。鈍りの頂点は0.5であり、安定域の端ではありません。

風上 = 中心差分 + 隠れた粘性

1次風上の傾きを分けると恒等式です。

qi−qi−1Δx=qi+1−qi−12Δx−Δx2 qi+1−2qi+qi−1Δx2\frac{q_i - q_{i-1}}{\Delta x} = \frac{q_{i+1} - q_{i-1}}{2\Delta x} - \frac{\Delta x}{2}\,\frac{q_{i+1} - 2q_i + q_{i-1}}{\Delta x^2}

前の項は中心差分です。後ろの項は2次微分の離散形です。だから風上が実際に積分している式は

∂tq+u ∂xq=u Δx2 ∂x2q\partial_t q + u\,\partial_x q = \frac{u\,\Delta x}{2}\,\partial_x^2 q

です。移流を解こうとして粘性 νnum=uΔx/2\nu_{\text{num}} = u\Delta x/2 を差し込んだ修正方程式です。人工粘性(artificial viscosity)、あるいは numerical diffusion です。

この粘性が暴走を殺します。代償として矩形パルスの角が厚さ 4νnumt\sqrt{4\nu_{\text{num}} t} に広がります。Δx\Delta x を小さくすれば粘性も減りますが、同じ時刻までステップが増えて、ぼやけは残ります。

あとで出てくる係数 uΔx(1−c)/2u\Delta x(1-c)/2 がこの式と違う理由は cc です。上の uΔx/2u\Delta x/2 は c→0c\to 0 側です。c=0.8c=0.8 なら (1−c)=0.2(1-c)=0.2 なので、実際に残る係数はその5分の1、つまり uΔx/10u\Delta x/10 です。c=1c=1 で0になるのは (1−c)(1-c) が付いた側だけです。

CFL — 格子が追いつけなくなる瞬間#

時刻 tn+1t_{n+1} の qiq_i は、時刻 tnt_n に位置 xi−uΔtx_i - u\Delta t にあった値です。その足が xi−1x_{i-1} と xix_i の間にあれば、二つの格子値で補間できます。uΔt>Δxu\Delta t > \Delta x になると足は xi−2x_{i-2} の向こうです。ステンシルはそこまで見ていません。

Courant–Friedrichs–Lewy 条件はその幾何です。

Δt≤Δx∣u∣\Delta t \le \frac{\Delta x}{|u|}

数値的な依存領域(ステンシル)が物理的な依存領域(特性線)を含む必要があります。CFLは陽解法の必要条件であって十分条件ではありません。FTCSはCFLの内側でも爆発します。爆発はステンシルの長さではなく、因果が壊れた問題です。

NumPy 50行で追う3スキーム#

同じ格子、同じ矩形パルス、同じCFLです。

import numpy as np
 
N = 200
dx = 1.0 / N
u = 1.0
cfl = 0.8
dt = cfl * dx / u
n_steps = 400
 
x = (np.arange(N) + 0.5) * dx
q0 = ((x > 0.2) & (x < 0.4)).astype(float)  # 矩形パルス
 
def ftcs_advect(q, c):
    return q - 0.5 * c * (np.roll(q, -1) - np.roll(q, 1))
 
def upwind_advect(q, c):
    return q - c * (q - np.roll(q, 1))  # u > 0
 
def lax_wendroff_advect(q, c):
    flux_diff = 0.5 * c * (np.roll(q, -1) - np.roll(q, 1))
    visc = 0.5 * c * c * (np.roll(q, -1) - 2 * q + np.roll(q, 1))
    return q - flux_diff + visc
 
q_ftcs, q_up, q_lw = q0.copy(), q0.copy(), q0.copy()
for _ in range(n_steps):
    q_ftcs = ftcs_advect(q_ftcs, cfl)
    q_up = upwind_advect(q_up, cfl)
    q_lw = lax_wendroff_advect(q_lw, cfl)
 
# 厳密解: 1周して元の位置
q_exact = q0
print("FTCS  L1 err:", np.abs(q_ftcs - q_exact).sum() * dx)
print("Upwind L1 err:", np.abs(q_up - q_exact).sum() * dx)
print("LW    L1 err:", np.abs(q_lw - q_exact).sum() * dx)

cfl=0.8、400ステップについて本文が残している目分量は次です。

  • FTCS: 振幅が約 1.784001.78^{400} 倍に増えて NaN または inf になります。
  • Upwind: 矩形パルスがガウス型に広がり、L1誤差は0.04付近に留まります。
  • Lax–Wendroff: 振幅は保たれますが、角の後ろにさざ波(Gibbs)が残ります。

c=0.5が風上をいちばん広げる#

c(1−c)c(1-c) が0.5で最大だという事実が、画面のぼやけと一致します。c=1c=1 では (1−c)(1-c) が0なので、1次風上はその矩形を一格子ずつそのまま動かします。c=0.8c=0.8 はその間の0.16です。安定だからといって鈍らないわけではありません。0.8は0.5より粘性が小さいだけです。

1次では足りないが、2次にはまた別の頭痛#

風上の粘性は衝撃の近くの偽の振動を押さえてくれます。滑らかな領域でも同じ項がオンのままなので、誤差は2次微分の大きさに縛られます。実務のコードは次の二つへ進みます。

どちらも「粘性をどこで受け、どこで断るか」を決める装置です。1次風上はすべてのセルで受けます。単純で、安全で、いちばん広がります。

400ステップを一周と書くと失敗する#

N=200N=200、u=1u=1、c=0.8c=0.8 なら Δt=c/N=0.004\Delta t = c/N = 0.004 です。長さ1の領域を一周するステップ数は 1/Δt=2501/\Delta t = 250、つまり N/cN/c です。400ステップの移動距離は 400×0.004=1.6400\times 0.004 = 1.6 です。幅0.2のパルスが0.6だけずれると [0.2,0.4][0.2,0.4] と [0.8,1.0][0.8,1.0] は重なりません。鋭い二つの矩形のL1は0.4です。

本文の風上L1 0.04は、その比較にはなれません。0.04を一周分のぼやけとして読むなら、ステップは250である必要があります。1次元の風上が c=1c=1 まで生きるこの上限は、運ぶものが界面になったとき、なぜずっと小さいCFLで止まるのでしょうか。その界面移流で0.05が残った場所を開くと、ステンシルの長さと界面の拘束がどこで分かれるかが見えます。

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