Skip to content
cfd-lab:~/en/posts/2026-08-14-lbm-convectio…● online

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

NOTE #130DAY FRI CFD기법DATE 2026.08.14UPDATED 2026.10.19READ 7 min read#LBM#Chapman-Enskog#Convection-Diffusion#Advection#Diffuse-Interface

Raise the Velocity, Lose 27% of Your Diffusion — The Extra Flux in LBM Convection-Diffusion

The diffusivity you tuned through tau is only correct at u = 0. The moment flow appears, u²/cs² of it quietly disappears.

In lattice Boltzmann, one relaxation time sets the diffusivity. D=cs2(τ−1/2)δtD = c_s^2(\tau - 1/2)\delta t. Short line. No velocity in it. Does the same DD survive once the flow is on? Under a uniform advection speed uu it drops to D(1−u2/cs2)D(1 - u^2/c_s^2). At u=0.3u = 0.3 that is 27% gone. The Chapman–Enskog expansion is where the extra term leaks out, and one source term is what puts it back.

Phase-field multiphase work is where the miss hurts. Cahn–Hilliard or Allen–Cahn on a lattice Boltzmann kernel ties interface thickness to the mobility. A mobility 27% off gives a thickness 27% off, and the surface-tension coefficient follows it.

The diffusivity is set, yet the interface keeps thinning#

Start by looking at it. Below is the one-dimensional convection-diffusion equation

∂tϕ+∂x(uϕ)=D ∂xxϕ\partial_t \phi + \partial_x(u\phi) = D\,\partial_{xx}\phi

solved with a D1Q3 lattice Boltzmann scheme. The initial condition is a single Gaussian; the exact solution is also a Gaussian, widening as σ2(t)=σ02+2Dt\sigma^2(t) = \sigma_0^2 + 2Dt. Push u from 0 to 0.30 and the hump narrows against the dashed curve; tau does not move that ratio.

D = cs²(tau−½) =0.1667
sigma² sim 0 / exact 0
Push u to 0.30 with the source term off: the amber packet climbs above the dashed exact curve and its sigma² line falls below the dashed target — it is diffusing at 0.73 D, exactly 1 − u²/cs². Now drag tau. The deficit ratio does not move, because the missing flux scales with D itself. Switch the source term on and the two curves merge at every u.

At u = 0.30 the solid curve (computed) stands sharper than the dashed one (exact). The σ2\sigma^2 trace below misses the dashed slope 2D2D. Park tau at 0.6 or at 1.5 and the deficit ratio stays in the same slot.

D1Q3 has only three moments to honor#

Unlike a Navier–Stokes lattice Boltzmann scheme, a distribution gig_i built for scalar transport has fewer moments to satisfy. The equilibrium Guo used in his 2009 nonlinear convection-diffusion model is this:

gieq=wi ϕ(1+ci⋅ucs2)g_i^{eq} = w_i\,\phi\left(1 + \frac{\mathbf{c}_i\cdot\mathbf{u}}{c_s^2}\right)

Here wiw_i are the lattice weights, ci\mathbf{c}_i the lattice velocities, and cs2=1/3c_s^2 = 1/3 the squared lattice sound speed. It satisfies three moment constraints.

∑igieq=ϕ,∑icigieq=ϕu,∑icicigieq=cs2ϕ I\sum_i g_i^{eq} = \phi, \qquad \sum_i \mathbf{c}_i g_i^{eq} = \phi\mathbf{u}, \qquad \sum_i \mathbf{c}_i\mathbf{c}_i g_i^{eq} = c_s^2\phi\,\mathbf{I}

The third one is where this parts ways with a flow equilibrium: there is no ϕuu\phi\mathbf{u}\mathbf{u} term. The second-order term in velocity was never put in. A scalar equation has no stress tensor, so an isotropic second moment is enough, and D2Q5 can replace D2Q9. The half step inside τ−1/2\tau - 1/2 in front of the diffusivity is the same leftover the second-order expansion of the discrete LBE leaves in the viscosity.

That looks sufficient, and at u=0u = 0 it is exact. The catch is that the truncation is not free.

The fourth term Chapman-Enskog leaves behind#

Write the BGK lattice Boltzmann equation with a source SiS_i attached:

gi(x+ciδt, t+δt)−gi(x,t)=−1τ(gi−gieq)+δt Sig_i(\mathbf{x}+\mathbf{c}_i\delta t,\, t+\delta t) - g_i(\mathbf{x},t) = -\frac{1}{\tau}\left(g_i - g_i^{eq}\right) + \delta t\, S_i

Expand gi=gi(0)+εgi(1)+ε2gi(2)g_i = g_i^{(0)} + \varepsilon g_i^{(1)} + \varepsilon^2 g_i^{(2)} and split the time derivative as ∂t=ε∂t1+ε2∂t2\partial_t = \varepsilon\partial_{t_1} + \varepsilon^2\partial_{t_2}. The zeroth moment at order ε\varepsilon hands back the advective part of the target equation unchanged.

∂t1ϕ+∇⋅(ϕu)=0\partial_{t_1}\phi + \nabla\cdot(\phi\mathbf{u}) = 0

The first moment of that same order-ε\varepsilon equation is where everything is decided, because it fixes the flux carried by g(1)g^{(1)}.

∑icigi(1)=−τδt[∂t1(ϕu)+cs2∇ϕ−∑iciSi]\sum_i \mathbf{c}_i g_i^{(1)} = -\tau\delta t\left[\partial_{t_1}(\phi\mathbf{u}) + c_s^2\nabla\phi - \sum_i \mathbf{c}_i S_i\right]

The second term in the bracket, cs2∇ϕc_s^2\nabla\phi, is the diffusive flux we asked for. The first one is riding along with it. In a flow scheme the ρuu\rho\mathbf{u}\mathbf{u} term in the equilibrium second moment cancels most of it; the scalar model has no such term. So it survives.

Collecting the zeroth moment at order ε2\varepsilon^2 gives the final form.

∂tϕ+∇⋅(ϕu)=∇⋅(D∇ϕ)+∇⋅[(τ−12)δt ∂t(ϕu)]−∇⋅[τδt∑iciSi]\partial_t \phi + \nabla\cdot(\phi\mathbf{u}) = \nabla\cdot(D\nabla\phi) + \nabla\cdot\left[\left(\tau-\tfrac{1}{2}\right)\delta t\,\partial_t(\phi\mathbf{u})\right] - \nabla\cdot\left[\tau\delta t\sum_i \mathbf{c}_i S_i\right]

D=cs2(τ−1/2)δtD = c_s^2(\tau - 1/2)\delta t appears as expected. The second term on the right is a flux nobody ordered. It vanishes in a genuine steady state, where uu is constant in time and ϕ\phi has stopped changing. But as long as ϕ\phi is still moving — as long as the computation is running — ∂t(ϕu)\partial_t(\phi\mathbf{u}) is not zero.

Under uniform uu that term is negative diffusion#

Take the simplest case, uu constant in space and time. Then ∂t(ϕu)=u ∂tϕ\partial_t(\phi u) = u\,\partial_t\phi, and to leading order ∂tϕ≃−u ∂xϕ\partial_t\phi \simeq -u\,\partial_x\phi. Substituting,

∂x[(τ−12)δt ∂t(ϕu)]=−(τ−12)δt u2 ∂xxϕ\partial_x\left[\left(\tau-\tfrac{1}{2}\right)\delta t\,\partial_t(\phi u)\right] = -\left(\tau-\tfrac{1}{2}\right)\delta t\, u^2\,\partial_{xx}\phi

A diffusion term with the sign reversed. Adding it to the physical one,

(τ−12)δt(cs2−u2)∂xxϕ=D(1−u2cs2)∂xxϕ\left(\tau-\tfrac{1}{2}\right)\delta t\left(c_s^2 - u^2\right)\partial_{xx}\phi = D\left(1 - \frac{u^2}{c_s^2}\right)\partial_{xx}\phi Deff=D(1−u2cs2)D_{\text{eff}} = D\left(1 - \frac{u^2}{c_s^2}\right)

The deficit is one square. Lattice sound speed squared is one third, so a speed of 0.3 squares to 0.09 and triples to 0.27. At relaxation time 1 the dialed diffusivity is one third times one half, 0.166667. What remains is 0.166667 times 0.73, which is 0.121667. The gap, 0.045, is the dialed value times 0.27. The u=0.30u = 0.30 row of the printout is that subtraction. At low speed the square shrinks. A speed of 0.05 squares to 0.0025, triples to 0.0075, and the deficit is 0.75%. The hump sits next to the exact curve. Easy to miss by eye.

The ratio refuses to track relaxation time for the same reason. The factor of relaxation time minus one half multiplies both the physical diffusion and the extra term. Divide the two and the factor drops out. The ratio does not depend on τ\tau. Raising τ\tau from 0.6 to 1.5 takes DD from 0.033333 to 0.333333, a factor of ten. At speed 0.25, three times 0.0625 is 0.1875, and one minus that is 0.8125, fixed on all four rows. Only the absolute gap grows, from about 0.006 to about 0.062. There is no room to bury the error under a larger diffusivity.

When speed reaches the lattice sound speed, the reciprocal square root of one third, near 0.577, the parenthesis hits zero. Past that point DeffD_{\text{eff}} is negative. The hump sharpens itself instead of spreading, and the distribution blows up.

1−1/(2τ)1 - 1/(2\tau) shows up again#

Now fix the coefficient of SiS_i. For the spurious term and the source term to cancel in the equation above,

τδt∑iciSi=(τ−12)δt ∂t(ϕu)\tau\delta t\sum_i \mathbf{c}_i S_i = \left(\tau-\tfrac{1}{2}\right)\delta t\,\partial_t(\phi\mathbf{u}) ∑iciSi=(1−12τ)∂t(ϕu)\sum_i \mathbf{c}_i S_i = \left(1 - \frac{1}{2\tau}\right)\partial_t(\phi\mathbf{u})

Only one simple form satisfies this while also keeping ∑iSi=0\sum_i S_i = 0.

Si=wi(1−12τ)ci⋅∂t(ϕu)cs2S_i = w_i\left(1 - \frac{1}{2\tau}\right)\frac{\mathbf{c}_i\cdot\partial_t(\phi\mathbf{u})}{c_s^2}

There is 1−1/(2τ)1 - 1/(2\tau) again. This factor sits on the same line as the third-moment limit a forcing term has to respect. On a discrete-time lattice a source acts twice — once through g(1)g^{(1)}, once through the second-order term of the Taylor expansion — and the factor of 1/21/2 is the residue of that double counting.

What happens if you drop the coefficient and set ∑iciSi=∂t(ϕu)\sum_i \mathbf{c}_i S_i = \partial_t(\phi\mathbf{u})? At τ=1\tau = 1 you apply exactly twice the correction, and a 27% deficit becomes a 27% excess. The magnitude of the error is unchanged, so a log-log convergence plot will not catch it.

Computing ∂t(ϕu)\partial_t(\phi\mathbf{u}) in code means storing ϕu\phi u from the previous step and taking a backward difference. One extra array is the entire cost.

Measuring DeffD_{\text{eff}} in 60 lines of Python#

Rather than argue, measure. Advect a Gaussian, fit the growth slope of its second moment σ2\sigma^2 by least squares, and that slope is 2Deff2D_{\text{eff}}.

import numpy as np
 
CS2 = 1.0 / 3.0
C = np.array([0, 1, -1])
W = np.array([2 / 3, 1 / 6, 1 / 6])
 
 
def d1q3_equilibrium(phi, u):
    """g_i^eq = w_i phi (1 + c_i u / cs^2) — an equilibrium fixing only three moments"""
    return np.stack([W[i] * phi * (1.0 + C[i] * u / CS2) for i in range(3)])
 
 
def gaussian_moments(x, phi):
    m0 = phi.sum()
    mean = (x * phi).sum() / m0
    return mean, (((x - mean) ** 2) * phi).sum() / m0
 
 
def run_cde_lbm(L, steps, tau, u, sigma0, x0, corrected):
    x = np.arange(L, dtype=float)
    phi = np.exp(-((x - x0) ** 2) / (2 * sigma0**2))
    g = d1q3_equilibrium(phi, u)
    phi_old = phi.copy()
    hist = []
    for n in range(steps + 1):
        if n % 100 == 0:
            hist.append((n, gaussian_moments(x, phi)[1]))
        src = np.zeros_like(g)
        if corrected and n > 0:
            # S_i = w_i (1 - 1/(2 tau)) c_i d_t(phi u) / cs^2,  dt = 1
            dt_phiu = (1.0 - 1.0 / (2 * tau)) * u * (phi - phi_old)
            for i in range(3):
                src[i] = W[i] * C[i] * dt_phiu / CS2
        geq = d1q3_equilibrium(phi, u)
        g = g - (g - geq) / tau + src          # collision
        for i in range(3):
            g[i] = np.roll(g[i], C[i])         # streaming
        phi_old = phi
        phi = g.sum(axis=0)
    return np.array(hist)
 
 
def fit_diffusivity(hist):
    """read D_eff off the slope of sigma^2 = sigma0^2 + 2 D_eff t"""
    return np.polyfit(hist[:, 0], hist[:, 1], 1)[0] / 2.0
 
 
L, STEPS, SIG0, X0 = 800, 1600, 10.0, 80.0
 
tau = 1.0
D = CS2 * (tau - 0.5)
print(f"tau = {tau},  D = cs^2 (tau-1/2) = {D:.6f},  cs^2 = {CS2:.6f}")
print(f"{'u':>6} {'u^2/cs^2':>9} | {'D_eff (no src)':>14} {'ratio':>7} {'1-u^2/cs^2':>11} |"
      f" {'D_eff (src)':>12} {'ratio':>7}")
for u in [0.05, 0.10, 0.20, 0.30]:
    d_raw = fit_diffusivity(run_cde_lbm(L, STEPS, tau, u, SIG0, X0, False))
    d_fix = fit_diffusivity(run_cde_lbm(L, STEPS, tau, u, SIG0, X0, True))
    print(f"{u:>6.2f} {u * u / CS2:>9.4f} | {d_raw:>14.6f} {d_raw / D:>7.4f} {1 - u * u / CS2:>11.4f} |"
          f" {d_fix:>12.6f} {d_fix / D:>7.4f}")
 
u = 0.25
print(f"\nu = {u} fixed, tau sweep   (theory: ratio = 1 - u^2/cs^2 = {1 - u * u / CS2:.4f}, tau-independent)")
print(f"{'tau':>6} {'D':>10} | {'D_eff (no src)':>14} {'ratio':>7} | {'D_eff (src)':>12} {'ratio':>7}")
for tau in [0.6, 0.8, 1.0, 1.5]:
    D = CS2 * (tau - 0.5)
    d_raw = fit_diffusivity(run_cde_lbm(L, STEPS, tau, u, SIG0, X0, False))
    d_fix = fit_diffusivity(run_cde_lbm(L, STEPS, tau, u, SIG0, X0, True))
    print(f"{tau:>6.1f} {D:>10.6f} | {d_raw:>14.6f} {d_raw / D:>7.4f} | {d_fix:>12.6f} {d_fix / D:>7.4f}")

The output:

tau = 1.0,  D = cs^2 (tau-1/2) = 0.166667,  cs^2 = 0.333333
     u  u^2/cs^2 | D_eff (no src)   ratio  1-u^2/cs^2 |  D_eff (src)   ratio
  0.05    0.0075 |       0.165417  0.9925      0.9925 |     0.166666  1.0000
  0.10    0.0300 |       0.161667  0.9700      0.9700 |     0.166666  1.0000
  0.20    0.1200 |       0.146667  0.8800      0.8800 |     0.166663  1.0000
  0.30    0.2700 |       0.121667  0.7300      0.7300 |     0.166658  0.9999
 
u = 0.25 fixed, tau sweep   (theory: ratio = 1 - u^2/cs^2 = 0.8125, tau-independent)
   tau          D | D_eff (no src)   ratio |  D_eff (src)   ratio
   0.6   0.033333 |       0.027096  0.8129 |     0.033345  1.0004
   0.8   0.100000 |       0.081258  0.8126 |     0.100006  1.0001
   1.0   0.166667 |       0.135417  0.8125 |     0.166661  1.0000
   1.5   0.333333 |       0.270794  0.8124 |     0.333275  0.9998

Raise u and only the rose ghost flux grows as u2u^2. Drag alpha from 0 to 1 and the source term cancels that width.

D_eff / D =0.693
At alpha = 0 the rose lobe sits mirror-imaged under the blue one: the ghost flux points the wrong way everywhere, so the amber net curve is visibly shorter than the blue physical flux. Raise u and the rose lobe grows as u² while blue stays put. Then drag alpha to 1 — green fills in exactly on top of rose, and amber lands back on blue at every x.

At alpha = 0 the rose lobe mirrors the blue physical flux upside down. At alpha = 1 green lands on rose and the amber sum returns to the blue curve. That is the column where the source is on and ratio snaps to 1.0000.

In the first table the ratio column matches 1-u^2/cs^2 to four decimal places. The prediction is not an estimate; it is the exact leading order. Switch the source term on and all four velocities return to 1.0000.

The second table bites harder. Sweeping τ\tau from 0.6 to 1.5 changes DD by a factor of ten, and the deficit ratio moves only from 0.8129 to 0.8124. Trying to bury the error under a larger diffusivity does not work: raise DD tenfold and the amount that disappears rises tenfold too.

The lattice velocity budget was already spent#

u2/cs2u^2/c_s^2 is the squared lattice Mach number. At u=0.1u = 0.1, 0.01×3=0.030.01 \times 3 = 0.03, and 3% of the diffusion is gone. At u=0.2u = 0.2, 0.04×3=0.120.04 \times 3 = 0.12, which is 12%. At or below 0.05 the loss is 0.75% and it hides in the grid error. The habit of pinning uu near 0.1 because of compressibility has not seen the scalar bill yet. If an interface thickness or a Sherwood number is going out as a number, the source term goes on the moment uu passes 0.1. Below that, skipping one array still leaves the printed 0.9925 standing.

Phase-field work cannot trust u2/cs2u^2/c_s^2 alone. The extra term is the whole ∂t(ϕu)\partial_t(\phi\mathbf{u}), and the square holds only when uu is uniform and steady. Near an interface uu jumps and ∂tu\partial_t u is not zero, so the ϕ ∂tu\phi\,\partial_t u piece survives too. ∂t(ϕu)\partial_t(\phi\mathbf{u}) goes in as a backward difference of ϕu\phi u from the previous step. The τ\tau inside 1−1/(2τ)1 - 1/(2\tau) is the relaxation time of the first moment. How viscosity drops when the velocity frame is shifted is the same kind of bill as the missing third moment of the equilibrium. If the mobility arithmetic checks out and the interface still thins, τ\tau was not the miss. The flow took the diffusion budget first.

Share if you found it helpful.