Skip to content
cfd-lab:~/en/posts/2026-06-10-entropic-latt…● online

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

NOTE #068DAY WED CFD기법DATE 2026.06.10UPDATED 2026.10.19READ 6 min read#CFD#LBM#Entropic-LBM#H-Theorem#Numerical-Stability

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

β carries the viscosity; the non-trivial root α is what stops discrete H from growing in the collision.

Calling entropy "lost information" does not explain why this run stays up. In the lattice Boltzmann method (LBM), lowering viscosity means pushing τ\tau onto 1/21/2, and BGK walks past equilibrium. Populations go negative. Karlin-style entropic LBM (ELBM) pins the discrete H to its pre-collision value right there. The price is one nonlinear root per node.

Drag the non-equilibrium perturbation slider to the right and the yellow dot leaves the dashed line at α=2\alpha=2, so the second root of G(α)=0G(\alpha)=0 shifts left.

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.

With a small perturbation the dot sits on 2. The gap between that dashed line and the yellow dot is the overshoot BGK refuses to drop.

When τ approaches 0.5, BGK falls apart#

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

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

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 ν=cs2(τ−1/2)\nu = c_s^2(\tau - 1/2). cs2=1/3c_s^2 = 1/3 is the squared lattice sound speed. High-Reynolds flow, meaning low viscosity, forces τ→1/2\tau \to 1/2, so ω→2\omega \to 2.

As ω\omega nears 2, the collision lands on the far side of equilibrium. A population that goes negative swings harder on the next step. The lattice blows up. BGK has nothing that stops this over-relaxation.

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

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

H=∫fln⁡f dcH = \int f \ln f \, d\mathbf{c}

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

H is negative entropy. A monotone drop in H is the same statement as a monotone rise in entropy. Irreversibility. The arrow of time. The inequality is a stability certificate. A scheme in which H cannot grow cannot let the amplitude run away, because an unbounded amplitude would have to drag H up with it.

The discrete H-function and entropic equilibrium#

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

H(f)=∑ifiln⁡fiwiH(f) = \sum_i f_i \ln\frac{f_i}{w_i}

wiw_i are the lattice weights: center 4/94/9, faces 1/91/9, diagonals 1/361/36 on D2Q9. The log of the weight-normalized ratio.

Entropic equilibrium is the population that minimizes this HH under the conserved moments. Mass ∑ifi=ρ\sum_i f_i = \rho and momentum ∑icifi=ρu\sum_i \mathbf{c}_i f_i = \rho\mathbf{u} stay fixed while H is driven down. A Lagrange-multiplier problem. D2Q9 has a closed product form.

fieq=ρ wi∏α=x,y(2−1+3uα2)(2uα+1+3uα21−uα)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). The second-order truncation in standard LBM is a polynomial equilibrium cut from a Hermite expansion, and that polynomial is not the minimizer of H. The product above is.

The entropic stabilizer — solving for α each step#

ELBM's collision has the same shape as BGK. The rate is split in two.

fi∗=fi+αβ (fieq−fi)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 inviscid. α\alpha sets the stability.

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

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

The update travels toward equilibrium only far enough to leave H identical to its pre-collision value. There is no room for H to grow. α=0\alpha=0 is the trivial root. The root we need sits past equilibrium. Near equilibrium α≈2\alpha \approx 2, which is BGK again. Only a large departure pulls α\alpha off 2 and shaves the overshoot.

Numbers make the viscosity formula less abstract. With cs2=1/3c_s^2 = 1/3 and ν=cs2(τ−1/2)\nu = c_s^2(\tau - 1/2), τ=0.51\tau = 0.51 leaves a gap of 0.010.01, so ν=1/300≈0.00333\nu = 1/300 \approx 0.00333. The rate is ω=1/0.51≈1.961\omega = 1/0.51 \approx 1.961. A departure of length 1 is carried 1.961 of the way and lands 0.961 past equilibrium. At τ=0.501\tau = 0.501, ω≈1.996\omega \approx 1.996. The leftover gap to the mirror value ω=2\omega = 2 is 0.004.

The listing's β=0.95\beta = 0.95 is the same identity. 12β−12=11.9−12=138\tfrac{1}{2\beta}-\tfrac{1}{2} = \tfrac{1}{1.9}-\tfrac{1}{2} = \tfrac{1}{38}, so ν=1/114≈0.00877\nu = 1/114 \approx 0.00877. BGK at that viscosity has τ=1/(2β)≈0.526\tau = 1/(2\beta) \approx 0.526. Near equilibrium, α≈2\alpha \approx 2 gives an effective rate αβ=1.9\alpha\beta = 1.9, and the viscosity matches that BGK step. Once a large departure pulls α\alpha below 2, αβ\alpha\beta drops under 1.9 and the overshoot αβ−1\alpha\beta-1 shrinks below 0.9. Viscosity stays where β\beta put it. Only the overshoot is cut.

Python — solving the entropic α by bisection#

Product-form equilibrium, the H-function, and one bisection for α\alpha.

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 printed α\alpha is close to 2 and not exactly 2. That gap is the correction that holds H fixed.

The two perturbation lines are a moment shift. Adding 0.030.03 to f1f_1 and subtracting 0.030.03 from f3f_3 leaves the mass sum unchanged. Lattice speeds on those links are +1+1 and −1-1, so xx-momentum rises by 0.03−(−0.03)=0.060.03-(-0.03)=0.06. From ρ=1\rho=1, ux=0.05u_x=0.05, the state just after the kick has ux=0.11u_x=0.11. The product factor is (2u+1+3u2)/(1−u)(2u+\sqrt{1+3u^2})/(1-u): about 1.162 at u=0.05u=0.05, about 1.391 at u=0.11u=0.11, and about 2.467 at nondimensional speed 0.3, where 1−u1-u is only 0.7. The band ∣u∣≲0.1|u| \lesssim 0.1 is where that factor is still mild.

The bisection cap is a sign constraint. A component with negative did_i hits zero at α=−fi/di\alpha=-f_i/d_i, and the smallest of those values is amax⁡a_{\max}. The search stops at min⁡(0.999 amax⁡, 4)\min(0.999\,a_{\max},\,4). A root past 4, or one that flips a population's sign, is not a candidate.

Interactive — the second root of G(α)#

The curve above is this GG. The yellow dot is the non-trivial root α∗\alpha^*. A larger perturbation peels it off 2. BGK nails that slot to 2. Stack the difference for enough steps and the lattice diverges.

Interactive — blocking the moment H grows#

Feed the same non-equilibrium state to both collision rules and only the populations that went negative turn into red bars.

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 stays on Hent=HpreH_{ent} = H_{pre}. That is the construction. On the BGK side, once the departure grows, HBGKH_{BGK} crosses the pre-collision H. The red warning lights there. That rise in H is where a low-viscosity lattice starts to rupture.

Three things I hit while implementing#

A nonlinear solve at every node costs more than the viscosity term is worth. Near equilibrium, when ∥f−feq∥\|f - f^{eq}\| is small, set α≈2\alpha \approx 2 and turn bisection on only where the departure crosses a threshold. The cost falls to about a tenth.

The product equilibrium is defined only for ∣u∣<1|u| < 1. The denominator carries 1−u1 - u. Past a nondimensional speed of 0.3 the pressure tensor starts to skew by direction. Even ELBM is safer inside ∣u∣≲0.1|u| \lesssim 0.1.

Keep the second-order polynomial as the equilibrium and the H-minimizer guarantee is gone. The stabilizer pays off fully only when the equilibrium itself is the entropic product. Bolting α\alpha onto a polynomial gets half of that.

Locking α at 2 while pushing β undoes the H bound#

Skipping to α≈2\alpha \approx 2 near equilibrium is a cost cut for small ∥f−feq∥\|f - f^{eq}\|. Push β\beta toward 1 and freeze that shortcut on every node, and the effective rate αβ\alpha\beta sits on 2 again. The overshoot out to the mirror comes back, and discrete H can exceed its pre-collision value. Viscosity dropped. The root was never solved.

A collision can hold H flat and still be late if streaming imports a population that is already negative from a neighbor. Whether a sign broken in advection still needs a positivity-preserving flux limiter, or whether the entropic root is enough on its own, is the next cut.

Share if you found it helpful.