Skip to content
cfd-lab:~/en/posts/2026-06-10-entropic-latt…online
NOTE #070DAY WED CFD기법DATE 2026.06.10READ 5 min readWORDS 916#CFD#LBM#Entropic-LBM#H-Theorem#Numerical-Stability

Entropy Stops the Blow-Up — Karlin's Entropic Lattice Boltzmann

Stabilizing LBM with an H-theorem-enforcing entropic collision

Entropy usually arrives labeled as "lost information" — the direction disorder grows, a measure of irreversible loss. Yet in the Lattice Boltzmann Method (LBM), that very quantity is what catches a runaway simulation and holds it still. This post is about how Karlin-style Entropic LBM (ELBM) enforces the H-theorem at the discretization level to kill blow-up in the low-viscosity regime. By the end you'll see why a single line of the collision operator is worth solving a nonlinear equation at every step.

When τ approaches 0.5, BGK falls apart#

Standard LBM uses the BGK collision (a single-relaxation-time approximation). It pulls the distribution fif_i toward equilibrium fieqf_i^{eq} by a factor ω\omega.

fi=fi+ω(fieqfi)f_i^* = f_i + \omega\,(f_i^{eq} - f_i)

Here fif_i is the population in lattice direction ii, fieqf_i^{eq} is the equilibrium, and ω=1/τ\omega = 1/\tau is the relaxation rate.

Viscosity is set by ν=cs2(τ1/2)\nu = c_s^2(\tau - 1/2), where cs2=1/3c_s^2 = 1/3 is the squared lattice sound speed. To resolve high-Reynolds (low-viscosity) flow, you push τ1/2\tau \to 1/2, i.e. ω2\omega \to 2.

The trouble is that as ω\omega nears 2, the collision overshoots equilibrium and lands on the far side. Populations dip negative, and once an fif_i goes negative it oscillates harder on the next step. The lattice blows up. BGK has nothing to stop this over-relaxation.

Boltzmann's H-theorem — the arrow of time#

In 1872 Boltzmann proved that the following quantity never grows under collisions alone.

H=flnfdcH = \int f \ln f \, d\mathbf{c}

ff is the velocity distribution and c\mathbf{c} is the molecular velocity. We have dH/dt0dH/dt \le 0, with equality only at equilibrium (the Maxwell-Boltzmann distribution).

H is negative entropy. A monotone decrease of H is the same statement as a monotone increase of entropy. That is irreversibility — the arrow of time. The key point is that this inequality is a stability certificate. A numerical scheme in which H never grows cannot diverge, because unbounded amplitude growth would require H to increase.

The discrete H-function and entropic equilibrium#

LBM slices the continuous velocity into 9 directions (D2Q9). H is discretized to match.

H(f)=ifilnfiwiH(f) = \sum_i f_i \ln\frac{f_i}{w_i}

wiw_i are the lattice weights (in D2Q9: center 4/94/9, faces 1/91/9, diagonals 1/361/36). It is the log of the weight-normalized ratio.

The entropic equilibrium is defined as the distribution that minimizes this HH under the conserved-moment constraints. Holding mass ifi=ρ\sum_i f_i = \rho and momentum icifi=ρu\sum_i \mathbf{c}_i f_i = \rho\mathbf{u} fixed, you minimize H (a Lagrange-multiplier problem). In D2Q9 a closed product form exists.

fieq=ρwiα=x,y(21+3uα2)(2uα+1+3uα21uα)ciαf_i^{eq} = \rho\, w_i \prod_{\alpha=x,y}\left(2 - \sqrt{1+3u_\alpha^2}\right)\left(\frac{2u_\alpha + \sqrt{1+3u_\alpha^2}}{1 - u_\alpha}\right)^{c_{i\alpha}}

uαu_\alpha is a velocity component and ciαc_{i\alpha} is a lattice-velocity component (−1, 0, 1). Unlike the second-order truncated polynomial equilibrium of standard LBM, this one is the true minimizer of H.

The entropic stabilizer — solving for α each step#

ELBM's collision has the same shape as BGK but splits the relaxation rate in two.

fi=fi+αβ(fieqfi)f_i^* = f_i + \alpha\beta\,(f_i^{eq} - f_i)

β(0,1)\beta \in (0,1) sets the viscosity (ν=cs2(12β12)\nu = c_s^2(\tfrac{1}{2\beta} - \tfrac{1}{2}), with β1\beta\to1 being inviscid), and α\alpha sets the stability.

α\alpha is not a constant. At every step, at every lattice node, it is solved as the non-trivial root of

H(f+α(feqf))=H(f)H\big(f + \alpha(f^{eq} - f)\big) = H(f)

The meaning is plain: relax toward equilibrium by exactly the amount that leaves H identical to its pre-collision value. Any increase of H is structurally forbidden. α=0\alpha=0 is the trivial root; what we want is the second root beyond equilibrium. Near equilibrium α2\alpha \approx 2, which is exactly BGK. Only when the non-equilibrium part is large does α\alpha depart from 2 and shave off the over-relaxation.

Python — solving the entropic α by bisection#

Here is the product-form equilibrium, the H-function, and one step that solves α\alpha by bisection.

import numpy as np
 
# D2Q9 lattice weights & velocities
W  = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36])
CX = np.array([0, 1, 0, -1,  0, 1, -1, -1,  1])
CY = np.array([0, 0, 1,  0, -1, 1,  1, -1, -1])
 
def h_function(f):
    # discrete H-function: H = Σ f_i ln(f_i / w_i)  (lattice version of Boltzmann's H)
    return np.sum(f * np.log(f / W))
 
def entropic_equilibrium(rho, ux, uy):
    # exact Ansumali–Karlin product-form entropic equilibrium
    def factor(u):
        s = np.sqrt(1 + 3 * u * u)
        return (2 - s), (2 * u + s) / (1 - u)
    bx, rx = factor(ux)
    by, ry = factor(uy)
    return rho * W * bx * by * (rx ** CX) * (ry ** CY)
 
def entropic_alpha(f, feq):
    # non-trivial root α* of H(f + α(feq − f)) = H(f), via bisection
    d = feq - f
    H0 = h_function(f)
    G = lambda a: h_function(f + a * d) - H0
    amax = np.min(np.where(d < 0, -f / d, np.inf))   # positivity bound
    hi = min(0.999 * amax, 4.0) if np.isfinite(amax) else 4.0
    if G(hi) <= 0:
        return 2.0                                   # near-equilibrium safe value
    lo = 1.0
    while G(lo) > 0 and lo > 1e-3:
        lo *= 0.5
    for _ in range(60):
        mid = 0.5 * (lo + hi)
        if G(mid) > 0: hi = mid
        else:          lo = mid
    return 0.5 * (lo + hi)
 
# --- one node, one step ---
feq = entropic_equilibrium(1.0, 0.05, 0.0)
f = feq.copy()
f[1] += 0.03; f[3] -= 0.03            # non-equilibrium carried in by streaming
rho = f.sum(); ux = (CX * f).sum() / rho; uy = (CY * f).sum() / rho
feq = entropic_equilibrium(rho, ux, uy)
 
alpha = entropic_alpha(f, feq)
beta  = 0.95                          # ν = c_s²(1/(2β) − 1/2), β→1 is low viscosity
f_post = f + alpha * beta * (feq - f)
 
print(f"entropic α  = {alpha:.4f}   (BGK fixed value 2.0)")
print(f"H before    = {h_function(f):.8f}")
print(f"H after     = {h_function(f_post):.8f}")
print(f"min pop     = {f_post.min():.3e}  (> 0)")

The output shows α\alpha close to but not exactly 2. That tiny gap is the correction that conserves H.

Interactive — the second root of G(α)#

Play with the simulation below. Increase the non-equilibrium perturbation and watch the curve bend.

The yellow dot is the non-trivial root α* where the discrete H-function returns to its pre-collision value. Near equilibrium α* ≈ 2, which is exactly standard BGK. Push the perturbation up and α* drifts away from 2 — that gap is the entropic correction BGK ignores.

The yellow dot is the α\alpha^* we want. For small perturbations it sits right on the dashed line (α=2\alpha=2). Push the slider right and α\alpha^* peels away from 2. BGK ignores this shift and always uses 2. When the gap accumulates, the lattice diverges.

Interactive — blocking the moment H grows#

The same non-equilibrium state, processed by two collision rules side by side. Red bars are populations that went negative.

Both panels show post-collision populations. The entropic collision lands exactly on the equal-entropy point, so H_ent = H_pre. Fixed-α BGK misses it; once the non-equilibrium part grows, BGK over-shoots and H actually increases — a violated discrete H-theorem, which is the seed of low-viscosity blow-up.

The entropic side always has Hent=HpreH_{ent} = H_{pre} — that is how it is built. On the BGK side, once the non-equilibrium part grows, a moment arrives where HBGKH_{BGK} exceeds the pre-collision value. That is when the red warning lights up. That increase in H is exactly the seed that ruptures the lattice at low viscosity.

Three things I hit while implementing#

First, a nonlinear solve at every node is expensive. So in practice you set α2\alpha \approx 2 near equilibrium (when ffeq\|f - f^{eq}\| is small) and turn on bisection only at nodes where the non-equilibrium part crosses a threshold. The cost drops to roughly a tenth.

Second, the product-form equilibrium is only defined for u<1|u| < 1 — there is a 1u1 - u in the denominator. Beyond a non-dimensional speed of 0.3 the pressure tensor begins to skew anisotropically, so even ELBM is safest in the u0.1|u| \lesssim 0.1 regime.

Third, if you keep the second-order truncated polynomial equilibrium, the H-minimization guarantee breaks. To get the full benefit of the entropic stabilizer the equilibrium itself must be the entropic product form. Bolting α\alpha onto a polynomial equilibrium gets you only half the effect.

Three-line takeaway#

  • BGK blows up at low viscosity because over-relaxation increases the discrete H. ELBM prevents this by solving, each step, the α\alpha that makes H after collision equal to H before.
  • α\alpha is the non-trivial root of H(f+α(feqf))=H(f)H(f + \alpha(f^{eq}-f)) = H(f), and near equilibrium α2\alpha \approx 2 automatically recovers BGK.
  • Viscosity is carried by β\beta, stability by α\alpha. The cost is a per-step nonlinear solve, but the near-equilibrium shortcut keeps it affordable in practice.

Share if you found it helpful.