Skip to content
cfd-lab:~/ko/posts/2026-05-20-positivity-pr…● online
NOTE #047DAY WED CFD기법DATE 2026.05.20UPDATED 2026.10.19READ 5 min read#CFD#Positivity-Preserving#Flux-Limiter#Shock-Capturing#Compressible-Euler

음수 밀도가 새어나오는 자리 — 폭발파를 위한 Positivity-Preserving 플럭스 한정자

다음 스텝 밀도가 바닥 위에 남는 한에서만, 고차 플럭스와 LF 사이의 간격을 θ로 연다.

오전 2시 27분. Le Blanc 충격관이 0.003초 만에 NaN을 토했다. CFL을 0.1까지 낮춰도, 격자를 두 배로 해도, 곡선 두 개가 같이 무너졌다. 원인은 단순했다. 강한 팽창파(rarefaction, 압력이 빠르게 떨어지는 영역) 머리에서 한 셀의 밀도가 음수로 떨어졌다. 두 스텝 뒤 음속이 NaN이 되고, 그다음 스텝에 도메인 전체가 죽는다. Hu·Adams·Shu(2013)의 θ-블렌드는 그 자리에서 고차 플럭스를 한 칸만 뒤로 물린다.

오른쪽 밀도 ρ_R을 10⁻³ 아래로 내리면 허용 곡선이 빨간 구역으로 들어가고, 그 면의 θ는 0.2 밑으로 떨어진다.

Per-face θ search · single rarefaction face · curve ρin+1(θ) at fixed left face

The cyan curve is ρn+1 after one Euler step as a function of how much central-flux we mix into the right face. The red zone is where ρ has crossed the floor 10⁻³. The yellow dot is the largest θ that still lands above the floor — that is the θ the per-face limiter picks. Drop ρR below 10⁻³ or push uR past 6 and θ collapses toward zero: the limiter falls back to first-order Lax–Friedrichs.

빨간 구역은 다음 스텝 밀도가 바닥을 뚫는 쪽이다. θ가 그 경계에서 멈추면 그 면은 사실상 1차 Lax–Friedrichs다.

음수 밀도는 어디에서 새어나오는가#

고차 스킴은 안정성과 정확도 사이에서 흔들린다. 1차 업윈드는 충격을 다섯 칸쯤 흐려 놓는다. 2차 중심차분(central difference)은 충격 양쪽에 톱니를 심는다. 진동이 가벼우면 압력만 살짝 흔들리고 끝난다. 이웃 셀 밀도비가 100:1을 넘으면 — Le Blanc, Sedov 폭발, 초음속 노즐 후방 — 이야기가 달라진다.

밀도 보존식을 셀 ii에 대해 적으면 이렇다.

ρin+1=ρin−ΔtΔx(Fi+1/2ρ−Fi−1/2ρ)\rho_i^{n+1} = \rho_i^n - \frac{\Delta t}{\Delta x}\bigl( F_{i+1/2}^\rho - F_{i-1/2}^\rho \bigr)

Fρ=ρuF^\rho = \rho u는 밀도 플럭스다. Fi+1/2ρ≫Fi−1/2ρF_{i+1/2}^\rho \gg F_{i-1/2}^\rho인 셀, 즉 한쪽으로 질량이 많이 빠지는 셀에서 ρin+1\rho_i^{n+1}은 음수가 될 수 있다. 1D Euler에서 강한 충격 직후 팽창의 첫 셀이 그 자리다.

음수가 되면 음속부터 죽는다. a=γp/ρa = \sqrt{\gamma p / \rho}에서 ρ→0−\rho \to 0^-이면 a2<0a^2 < 0이다. 다음 플럭스의 ⋅\sqrt{\cdot}가 NaN을 돌려준다. 한 셀의 NaN이 이웃 둘로 번지고, 두 스텝 뒤에는 도메인이 끝난다.

Lax–Friedrichs — 흐리지만 무너지지 않는다#

1954년 Lax의 LF 플럭스는 이 함정에서 빠져 있다.

Fi+1/2LF=12(FL+FR)−12α(UR−UL)F_{i+1/2}^{LF} = \tfrac{1}{2}\bigl(F_L + F_R\bigr) - \tfrac{1}{2}\alpha\bigl(U_R - U_L\bigr)

α\alpha는 좌우 셀의 최대 신호 속도, U=(ρ,ρu,E)TU = (\rho, \rho u, E)^T는 보존 변수다. 빼는 항이 인공점성(artificial viscosity)이다. 이웃을 평균해서 톱니를 죽인다. 발상은 충격에 일부러 점성을 바른 1950년의 처리와 같은 자리다. Lax는 CFL≤\leq1 아래에서 ρn+1≥0\rho^{n+1} \geq 0임을 보였다. Hu et al. 정리 1을 단순화한 형태다. 충격이 세든 약하든 LF는 안 무너진다.

대가는 정확도다. 두 칸짜리 충격이 다섯 칸이 되고, 접촉 불연속(contact discontinuity)은 스텝마다 한 셀씩 더 흐려진다. 실무에서 LF만 쓰는 사람은 없다. 그래도 그 안전 여유가 한정자의 출발점이다.

θ-블렌드 — 한 칸씩만 물러난다#

고차 플럭스 Fi+1/2HOF_{i+1/2}^{HO}(WENO, MUSCL, 중심차분)는 LF보다 날카롭지만 음수 밀도를 만들 수 있다. Hu·Adams·Shu는 두 플럭스를 한 줄로 묶는다.

Fi+1/2=Fi+1/2LF+θi+1/2(Fi+1/2HO−Fi+1/2LF)F_{i+1/2} = F_{i+1/2}^{LF} + \theta_{i+1/2}\bigl( F_{i+1/2}^{HO} - F_{i+1/2}^{LF} \bigr)

θ∈[0,1]\theta \in [0, 1]은 면(face)마다 정하는 가중치다. θ=1\theta=1이면 순수 고차, θ=0\theta=0이면 순수 LF. 한정자가 하는 일은 하나다. 모든 셀의 다음 밀도가 양수로 남는 한에서 가장 큰 θ\theta를 고른다. 멀리 물러나지 않는다. 한 칸이다.

LF 결과가 ρiLF≥ρmin⁡\rho^{LF}_i \geq \rho_{\min}이므로, 보정 ΔF=FHO−FLF\Delta F = F^{HO} - F^{LF}를 더해도 양수가 유지되는 θ\theta의 상한을 면마다 푼다. 한 면은 양쪽 셀에 닿는다. 둘 다 바닥 위여야 하니 더 작은 쪽이 한계다.

앞에서 ρ_R을 내렸을 때 θ가 0.2 아래로 주저앉던 것이 이 상한이다. 그 면은 사실상 1차 LF로 풀린다.

두 단계 한정 — 밀도 먼저, 압력은 그 다음#

밀도가 양수여도 압력이 음수면 솔버는 똑같이 죽는다. 음속이 허수가 된다. Hu et al.은 그래서 두 패스로 나눈다.

밀도 패스. 위 식으로 θρ\theta^\rho를 면마다 정한다. 모든 셀의 다음 밀도가 ρmin⁡\rho_{\min} 이상이다.

압력 패스. θρ\theta^\rho로 밀도 플럭스를 고정한 뒤, 운동량·에너지 플럭스에 θp≤θρ\theta^p \leq \theta^\rho를 구한다. 압력 조건

p=(γ−1)(E−12ρu2)≥pmin⁡p = (\gamma - 1)\bigl(E - \tfrac{1}{2}\rho u^2\bigr) \geq p_{\min}

은 비선형이다. 일반적으로 이차식이다. θp\theta^p를 작은 쪽으로 잡는 보수적 근사면 한 줄로 끝난다.

합친 가중치. θ=min⁡(θρ,θp)\theta = \min(\theta^\rho, \theta^p)로 모든 보존 플럭스를 같은 무게로 섞는다. 한 면, 한 θ\theta. 보존량 사이의 일관성이 유지된다.

50줄 NumPy — Le Blanc 충격관에서 θ 자동 결정#

Le Blanc은 ρL=1\rho_L=1, pL=100p_L=100 대 ρR=0.02\rho_R=0.02, pR=0.05p_R=0.05인 강한 충격관이다. 강한 팽창이 왼쪽으로 가면서 첫 셀 밀도가 음수로 떨어지는 시험이다.

적힌 좌우를 나누면 밀도비는 1/0.02=501/0.02 = 50, 압력비는 100/0.05=2000100/0.05 = 2000이다. 앞에서 100:1을 위험선의 예로 들었지만, 이 쌍의 밀도 점프는 그 선 아래다. 센 쪽은 압력이다. γ=1.4\gamma = 1.4이면 음속 a=γp/ρa = \sqrt{\gamma p / \rho}라 왼쪽은 140≈11.83\sqrt{140} \approx 11.83, 오른쪽은 3.5≈1.871\sqrt{3.5} \approx 1.871이다. 둘의 비는 40≈6.32\sqrt{40} \approx 6.32로 딱 떨어진다.

격자도 식에 넣으면 시간 간격 탓이 아님이 보인다. N=200N = 200이라 Δx=1/200=0.005\Delta x = 1/200 = 0.005이고, Δt=9.0×10−5\Delta t = 9.0 \times 10^{-5}이다. Δt/Δx=0.018\Delta t/\Delta x = 0.018이다. 왼쪽 음속만으로 잰 쿠란트 수는 aLΔt/Δx≈0.213a_L \Delta t/\Delta x \approx 0.213이다. 새벽에 CFL을 0.1로 내려도 죽었던 것과 맞다. ρn+1=ρn−(Δt/Δx)ΔF\rho^{n+1} = \rho^n - (\Delta t/\Delta x)\Delta F에서 플럭스 차가 밀도 여유를 넘으면, 스텝을 줄여도 바닥을 뚫는다.

오른쪽 초기 밀도 0.020.02에서 바닥 10−310^{-3}까지 여유는 0.0190.019다. θ\theta가 1에 머물려면 밀도 보정 ∣d∣|d|가 0.019/0.018≈1.060.019/0.018 \approx 1.06을 넘으면 안 된다. ∣d∣=2|d| = 2이면 상한은 0.019/(0.018×2)≈0.530.019/(0.018 \times 2) \approx 0.53이다. 여유가 5×10−45 \times 10^{-4}까지 줄면, 같은 ∣d∣=1|d| = 1에서 θ\theta 상한은 0.0005/0.018≈0.0280.0005/0.018 \approx 0.028이다. 그 면은 거의 LF다.

블렌드를 가중치로 다시 쓰면 F=(1−θ)FLF+θFHOF = (1-\theta)F^{LF} + \theta F^{HO}다. θ=0.53\theta = 0.53이면 고차 몫은 53%다. θ=0.028\theta = 0.028이면 고차 몫은 2.8%뿐이고 나머지는 LF다. 코드의 상한은 업데이트와 같은 부등식이다. 보정이 밀도를 움직이는 양은 (Δt/Δx) θ ∣d∣(\Delta t/\Delta x)\,\theta\,|d|이고, 오른쪽 여유 0.0190.019를 넘으면 안 된다. 그래서 θ≤0.019/(0.018 ∣d∣)\theta \le 0.019 / (0.018\,|d|)다. ∣d∣≈1.06|d| \approx 1.06에서 등호가 θ=1\theta = 1에 닿는다.

압력 식도 같은 방식으로 읽힌다. p=(γ−1)(E−12ρu2)p = (\gamma - 1)(E - \tfrac{1}{2}\rho u^2)이고 γ−1=0.4\gamma - 1 = 0.4다. EE와 ρ\rho를 고정하면 운동에너지 1이 압력을 0.4만큼 깎는다. 오른쪽 p=0.05p = 0.05는 운동에너지 0.05/0.4=0.1250.05/0.4 = 0.125에서 0에 닿는다. 왼쪽 p=100p = 100은 같은 계산으로 250250까지 버틴다. 밀도 바닥만 지키고 이 몫을 빼먹으면, 밀도는 양수인데 음속이 허수가 되는 쪽으로 샌다.

import numpy as np
 
GAMMA = 1.4
RHO_FLOOR = 1e-3
N, NSTEP = 200, 220
dx = 1.0 / N
dt = 9.0e-5
 
def cons_to_prim(U):
    rho, mu, E = U[0], U[1], U[2]   # 보존 → 원시 변수
    u = mu / rho
    p = (GAMMA - 1.0) * (E - 0.5 * rho * u * u)
    return u, p
 
def euler_flux(U):
    u, p = cons_to_prim(U)
    return np.array([U[1], U[1] * u + p, u * (U[2] + p)])
 
def alpha_face(UL, UR):
    uL, pL = cons_to_prim(UL); uR, pR = cons_to_prim(UR)
    aL = np.sqrt(GAMMA * max(pL, 0.0) / UL[0])
    aR = np.sqrt(GAMMA * max(pR, 0.0) / UR[0])
    return max(abs(uL) + aL, abs(uR) + aR)
 
def density_theta(U, dF, dt_dx):
    """면별 θ 결정 — ρ_LF 위에서 양수 유지 최대값."""
    theta = np.ones(N + 1)
    for f in range(1, N):
        d = dF[0, f]                          # 밀도 anti-diffusion
        if abs(d) < 1e-14: continue
        rL_room = U[0, f - 1] - RHO_FLOOR     # 좌 셀 여유
        rR_room = U[0, f]     - RHO_FLOOR     # 우 셀 여유
        cap = (rL_room if d > 0 else rR_room) / (dt_dx * abs(d))
        theta[f] = max(0.0, min(theta[f], cap))
    return theta

전체 드라이버는 1차 LF 한 패스, 중심차분 anti-diffusion 한 패스, θ 결정 한 패스다. 220스텝, 200셀. 한정자를 끄면 30스텝 부근에서 NaN이고, 켜면 끝까지 남는다.

한정자 스위치를 끄면 빨간 곡선이 30스텝 근처에서 밀도를 0 아래로 끌고 간다.

1D strong shock tube · ρ_L=1, p_L=100 / ρ_R=0.02, p_R=0.05 · central + θ·anti-diffusion · 200 cells

Red is the raw centered scheme — its rarefaction tail dives below zero around t ≈ 0.006 and the solver blows up. Cyan is the same scheme blended with Lax–Friedrichs by a per-face θ ≤ θ_max so that ρ stays above the floor 10⁻³. Drag the time slider to watch the two solutions separate.

그 순간이 붕괴다. 청록은 같은 시각에 ρ≈0.02\rho \approx 0.02 근처에서 평평해진다. 정확도 차이는 5% 미만인데, 한쪽은 살고 한쪽은 죽는다.

한정자가 가져가는 것#

θ\theta가 0으로 내려가는 면은 1차 LF로 풀린다. 충격 앞면과 강한 팽창의 머리는 거기서 무뎌진다. 그 구역은 도메인의 5% 미만이라 전역 정확도는 거의 그대로다. 평균 θ\theta는 0.95에서 1.0 사이를 오간다.

ρmin⁡\rho_{\min}을 10−1210^{-12}처럼 너무 낮추면 한정자가 늦게 깨어나서 같은 NaN이 다시 난다. 실무에서는 평균 밀도의 10−410^{-4} 근처를 바닥으로 둔다. RK 다단계에서는 단계마다 한정자를 다시 돌려야 한다. 첫 단계만 통과시키면 다음 단계에서 그대로 발산한다.

바닥을 1e-12로 내리면 한정자는 NaN 다음에 깨어난다#

ρmin⁡=10−12\rho_{\min} = 10^{-12}는 바닥이 아니라 장식이다. 오른쪽 밀도 0.02에서 보면 여유의 거의 전부가 아직 남아 있고, 위의 식으로 θ\theta 상한은 1 근처에 머문다. 밀도가 이미 10−810^{-8} 아래로 꺼진 뒤에야 상한이 조여진다. 음속 제곱 γp/ρ\gamma p / \rho는 그 전에 부호가 뒤집힌다. 한정자는 죽은 셀을 보고 나서야 LF로 물러난다.

면의 θ\theta는 셀 평균만 바닥 위에 둔다. 평균은 양수인데 셀 안 재구성이 한 점에서 음수로 꺾이면 이 한 줄은 그 점을 보지 못한다. 그 구멍을 평균은 지키는데 점이 음수로 꺾이는 재구성이 따로 막는지가 다음 선택이다.

참고: X. Y. Hu, N. A. Adams, C.-W. Shu. Positivity-preserving method for high-order conservative schemes solving compressible Euler equations. J. Comput. Phys. 242 (2013) 169–180.

도움이 됐다면 공유해주세요.