Skip to content
cfd-lab:~/en/posts/2026-06-22-lu-sgs-implic…● online

Translated from the author's Korean original. Read the original (한국어)

NOTE #080DAY MON CFD기법DATE 2026.06.22UPDATED 2026.10.19READ 6 min read#LU-SGS#Implicit#Time-Integration#Compressible#Linear-Solver

When Explicit Blows Up, Solve Implicit Without Inverting the Matrix — LU-SGS

The left half-plane stays stable at any time step, but a diffusion number of 40 flattens the inner sweep that drops ten orders near d=1.

The run died at 3 a.m. Last log line: pressure residual = NaN. CFL went from 0.9 to 0.5, then 0.3. Each step was so short that steady state meant days. Familiar wall in compressible steady work. Implicit time-stepping breaks that wall on the step-size side. The version that never inverts the full matrix is LU-SGS. A Burgers shock still exists at CFL 5.

Toggle the scheme between forward Euler and backward Euler, and drag Re(λΔt)\mathrm{Re}(\lambda\Delta t) to −2.5-2.5. The stable set changes from a small disk to the left half-plane.

Cyan = stable region (|G| ≤ 1). Forward Euler and RK4 are bounded blobs — push λΔt past their edge and the mode blows up. Backward Euler and Trapezoidal cover the entire left half plane, so any decaying physical mode stays stable no matter how large Δt grows.

On forward Euler that point leaves the cyan disk and flips to UNSTABLE. Backward Euler fills the whole left half-plane with cyan, so the same coordinate is stable again.

The stability region opens to infinity#

Time-stepping stability is decided by a single model equation.

dudt=λu\frac{du}{dt} = \lambda u

Here λ\lambda is the eigenvalue produced by the spatial discretization (it comes from wave speeds and viscosity), and uu is the amplitude of one mode. After one step the amplitude is multiplied by the amplification factor GG. We need ∣G∣≤1|G|\le 1 to avoid blow-up.

Explicit stepping (forward Euler) gives

G=1+λΔtG = 1 + \lambda\Delta t

so ∣1+λΔt∣≤1|1+\lambda\Delta t|\le 1 — the point λΔt\lambda\Delta t must sit inside a small disk centered at −1-1 with radius 11. A viscous grid or a thin cell makes λ\lambda large, and it leaves that disk fast.

Implicit stepping (backward Euler) is the opposite.

G=11−λΔtG = \frac{1}{1 - \lambda\Delta t}

For every physical decaying mode with Re(λ)<0\mathrm{Re}(\lambda)<0, ∣G∣<1|G|<1. It stays stable no matter how large Δt\Delta t grows. The stability constraint dissolves, and now the time step only has to be as small as accuracy demands.

The explicit disk ends at 0 and −2-2 on the real axis. Center −1-1, radius 1. Re(λΔt)=−2.5\mathrm{Re}(\lambda\Delta t)=-2.5 sits 1.5 from the center, outside the disk, which is why the marker reads UNSTABLE. Backward Euler uses G=1/(1−λΔt)G=1/(1-\lambda\Delta t). When Re(λ)<0\mathrm{Re}(\lambda)<0 the denominator exceeds 1, so ∣G∣|G| stays under 1 as Δt\Delta t grows. The night log cut CFL through 0.9, 0.5, and 0.3 to trap λΔt\lambda\Delta t inside that disk. On a viscous grid λ\lambda grows first. The disk breaks before accuracy asks for a shorter step.

Inverting the full matrix is a cost you can't pay#

Nothing is free. Implicit stepping asks for the residual at the future state.

Uin+1−UinΔt=−Ri(Un+1)\frac{U_i^{n+1} - U_i^n}{\Delta t} = -R_i(U^{n+1})

RiR_i is the flux divergence (residual) of cell ii, and UU is the conserved variable. Linearize R(Un+1)R(U^{n+1}) about the current state,

Ri(Un+1)≈Rin+∂Ri∂U ΔUR_i(U^{n+1}) \approx R_i^n + \frac{\partial R_i}{\partial U}\,\Delta U

and a linear system for ΔU=Un+1−Un\Delta U = U^{n+1}-U^n remains.

(IΔt+∂R∂U)ΔU=−Rn\left( \frac{I}{\Delta t} + \frac{\partial R}{\partial U} \right) \Delta U = -R^n

The trouble is the matrix AA on the left. With fifty million cells, AA has fifty million dimensions. A direct inverse is impossible in both memory and time. A Krylov solver like GMRES works too, but it has to store the matrix explicitly and needs a preconditioner. In the 1990s compressible analysts found a cheaper path: approximate the matrix with no storage, no multiplications, and two sweeps.

LU-SGS — split into D, L, U and sweep twice#

Decompose AA into a block lower triangle LL, a block diagonal DD, and a block upper triangle UU.

A=D+L+UA = D + L + U

LU-SGS (Lower-Upper Symmetric Gauss-Seidel) approximates this AA by the product

(D+L) D−1 (D+U) ΔU=−Rn(D + L)\,D^{-1}\,(D + U)\,\Delta U = -R^n

which discards the LD−1UL D^{-1} U term created by the LULU product. When ΔU\Delta U is small it's negligible. The solve is now two triangular back-substitutions in sequence.

Forward sweep — lower triangle, front to back:

ΔUi∗=Di−1 ⁣(−Rin−Li ΔUi−1∗)\Delta U_i^{*} = D_i^{-1}\!\left( -R_i^n - L_i\,\Delta U_{i-1}^{*} \right)

Backward sweep — upper triangle, back to front:

ΔUi=ΔUi∗−Di−1 Ui ΔUi+1\Delta U_i = \Delta U_i^{*} - D_i^{-1}\,U_i\,\Delta U_{i+1}

What fills DD, LL, and UU decides how good the approximation is. Yoon and Jameson (1988) approximated the nonlinear flux Jacobian with its spectral radius ρ\rho. Splitting first-order upwind as A±=12(a±∣a∣)A^{\pm} = \tfrac{1}{2}(a \pm |a|) leaves the diagonal Di=I/Δt+∣ai∣/ΔxD_i = I/\Delta t + |a_i|/\Delta x and off-diagonals that carry only the split face Jacobians. You never build the whole matrix — a few scalars (or a small block) per cell are enough.

The approximate product is (D+L) D−1 (D+U)(D+L)\,D^{-1}\,(D+U). Expand it and LD−1UL D^{-1} U sits next to AA; that term is dropped. Because Di=I/Δt+∣ai∣/ΔxD_i = I/\Delta t + |a_i|/\Delta x, a small Δt\Delta t inflates I/ΔtI/\Delta t and the dropped term shrinks. Raise CFL to 5 and Δt\Delta t grows, so I/ΔtI/\Delta t shrinks and the approximation error grows with it. Explicit stepping still hits NaN in two or three steps on that interval. The printed implicit run crosses it in 49 steps with uu closed in [0.012,0.988][0.012, 0.988], on n=200n=200 cells out to t=0.22t=0.22 with Δx=1/200\Delta x=1/200. A stored-matrix path such as the Krylov convergence comparison keeps the matrix. Here a diagonal division and two sweeps take that seat.

Python — advancing implicit Burgers with one factorization#

The toy problem is shock formation in the 1D Burgers equation. A smooth sine wave steepens into a shock over time. Explicit stepping would pin the CFL below 1. The LU-SGS implicit step survives at CFL 5.

import numpy as np
 
def upwind_flux_residual(u, dx):
    """First-order upwind flux for the residual R_i of d(u^2/2)/dx (periodic)."""
    f = 0.5 * u * u
    a = u                       # local wave speed df/du
    f_left  = np.where(a >= 0, np.roll(f, 1), f)
    a_right = np.roll(a, -1)
    f_right = np.where(a_right >= 0, f, np.roll(f, -1))
    return (f_right - f_left) / dx
 
def lu_sgs_burgers_step(u, dt, dx):
    """One time step: advance the implicit system with a single LU-SGS pass."""
    n = u.size
    a  = u
    ap = 0.5 * (a + np.abs(a))         # A+  (lower-triangle contribution)
    am = 0.5 * (a - np.abs(a))         # A-  (upper-triangle contribution)
    D  = 1.0 / dt + np.abs(a) / dx     # diagonal block
    R  = upwind_flux_residual(u, dx)
 
    dus = np.zeros(n)                  # forward sweep: (D+L) dU* = -R
    for i in range(n):
        lower = ap[i - 1] / dx * dus[i - 1]
        dus[i] = (-R[i] + lower) / D[i]
 
    du = dus.copy()                   # backward sweep: (D+U) dU = D dU*
    for i in range(n - 1, -1, -1):
        upper = am[(i + 1) % n] / dx * du[(i + 1) % n]
        du[i] = dus[i] - upper / D[i]
    return u + du
 
def drive_burgers_shock(n=200, cfl=5.0, tmax=0.22):
    dx = 1.0 / n
    x  = (np.arange(n) + 0.5) * dx
    u  = 0.5 + 0.5 * np.sin(2 * np.pi * x)   # smooth initial condition
    t, steps = 0.0, 0
    while t < tmax:
        dt = cfl * dx / np.max(np.abs(u))    # CFL>1 allowed (implicit)
        dt = min(dt, tmax - t)
        u = lu_sgs_burgers_step(u, dt, dx)
        t += dt; steps += 1
    return x, u, steps
 
x, u, steps = drive_burgers_shock(cfl=5.0)
print(f"CFL=5 -> {steps} steps,  u in [{u.min():.3f}, {u.max():.3f}]")
# CFL=5 -> 49 steps,  u in [0.012, 0.988]

Run the same problem with explicit forward Euler at CFL 5 and it produces NaN within two or three steps. The implicit version reaches the shock in 49 stable steps. No heavy Riemann solver per face — one diagonal division and two sweeps did the whole job.

Sweep after sweep, the residual collapses#

A single LU-SGS application gives an approximate solution. To solve more accurately, you repeat the sweep on the same system. The convergence rate then depends on the diagonal dominance of the matrix. In the viscous-dominated case — where the viscous Jacobian becomes a 1D Laplacian — the diagonal is 1+2d1+2d and the off-diagonals are −d-d. Here d=νΔt/Δx2d=\nu\Delta t/\Delta x^2 is the diffusion number (the nondimensional time step of viscous diffusion).

At d=νΔt/Δx2=1d=\nu\Delta t/\Delta x^2=1 the diagonal is 1+2d=31+2d=3 and each off-diagonal is −d=−1-d=-1. The diagonal beats the sum of the two neighbors, and a few sweeps drop the residual ten orders. At d=40d=40 the diagonal is 81 and the off-diagonals are −40-40 twice, so the row sum is 81−80=181-80=1 and nearly cancels. That is why the curve lies down. The stability bought with a large Δt\Delta t and the sweep count the inner relaxation needs are written on the same ledger.

Slide dd from near 1 up to 40 and the slope of the residual curve changes at the same sweep count.

Each sweep is one forward + one backward Gauss-Seidel pass. At d ≈ 1 the residual drops ten orders in a handful of sweeps. Crank d up to 40 (a huge implicit Δt) and the same curve flattens — the matrix is stiffer, so LU-SGS needs more sweeps or a stronger preconditioner to keep up.

Keep dd near 1 and the residual drops ten orders in a handful of sweeps. Crank dd up to 40 (a huge implicit Δt\Delta t) and the same curve flattens. You bought stability by enlarging the step, but the matrix got stiffer, so the inner relaxation needs more sweeps. Stability and inner convergence do not arrive free together.

Drop the viscous radius from the diagonal and the sweep dies#

Leave ν/Δx2\nu/\Delta x^2 out of the diagonal DD and DD shrinks in a viscous cell. The spectral radius is then only the convective ∣a∣/Δx|a|/\Delta x. The sweep diverges at once. If the viscous Jacobian is a 1D Laplacian, diagonal dominance is the balance of 1+2d1+2d against −d-d, and stripping the viscous term empties the left side of that balance. When the nonlinearity is strong, do not add the full ΔU\Delta U at once. Use Un+1=Un+ω ΔUU^{n+1}=U^n+\omega\,\Delta U with ω∈[0.5,0.8]\omega\in[0.5,0.8]. Oscillations near shocks shrink.

Direction is a decision too. Repeating only the forward sweep sends information toward increasing indices. Alternating forward and backward keeps LL and UU symmetric. Across an MPI partition, one communication has to sit between the two sweeps or the neighbor ΔU\Delta U never arrives. How the cells are numbered changes that sweep order. The record of LU-SGS slowing after an RCM renumbering is the other side of the same choice.

Share if you found it helpful.