Skip to content
cfd-lab:~/en/posts/2026-10-23-shan-chen-vel…● online

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

NOTE #192DAY FRI CFD기법DATE 2026.10.23READ 6 min read#Shan-Chen#Forcing-Term#LBM#Chapman-Enskog#Multiphase

Changing only τ changed the density of a gas at rest — the (τ−½)² stress of Shan–Chen velocity-shift forcing

A forcing that shifts the equilibrium velocity by τF/ρ adds (τ−½)²F²/ρ to the momentum flux, so the density profile at rest depends on the relaxation time.

Viscosity changed, and so did a fluid that doesn't move#

A gas at rest has no idea what its viscosity is. With zero velocity, the viscous stress is zero. So a resting state where force and pressure balance should not depend on viscosity. In the lattice Boltzmann (LB) method, viscosity is set by the relaxation time τ\tau. Changing τ\tau should therefore leave the resting density profile untouched.

One family of forcing schemes breaks this. It is the velocity-shift forcing used by Shan–Chen multiphase models. Same force, same lattice, change only τ\tau from 0.8 to 3, and the density profile moves. This post puts three forcing schemes side by side and tracks down where the difference comes from. You get one line per scheme: the coefficient of the stress it quietly adds to the momentum flux. Guo gives 0, Shan–Chen gives (τ−12)2(\tau-\tfrac12)^2, and EDM gives 14\tfrac14.

Criterion 1: where does the equilibrium velocity go?#

All three schemes add momentum F δtF\,\delta t per step. They differ in which distribution carries it. Below, u′=∑ifici/ρu'=\sum_i f_i c_i/\rho is the velocity read straight from the populations. For all three, the physical velocity is u=u′+F/(2ρ)u = u' + F/(2\rho) (the half-step correction).

SchemeVelocity in the equilibriumExtra source
Guouu(1−12τ)Si(u,F)(1-\tfrac{1}{2\tau})S_i(u,F)
Shan–Chenu′+τF/ρu' + \tau F/\rhonone
EDM (Kupershtokh)u′u'fieq(u′+F/ρ)−fieq(u′)f_i^{eq}(u'+F/\rho)-f_i^{eq}(u')

At rest u=0u=0, so u′=−F/(2ρ)u'=-F/(2\rho). The velocity Shan–Chen feeds into its equilibrium becomes

ueq=u′+τFρ=(τ−12)Fρu_{eq} = u' + \frac{\tau F}{\rho} = \left(\tau - \tfrac12\right)\frac{F}{\rho}

Here uequ_{eq} is the velocity used to build the equilibrium, τ\tau the dimensionless relaxation time, and FF the force per unit volume. The gas is still, yet its equilibrium moves at a speed proportional to τ\tau.

The second moment of the equilibrium (the momentum flux) is a parabola in that velocity:

Πeq(ueq)=∑ifieqcici=cs2ρ+ρ ueq2\Pi_{eq}(u_{eq}) = \sum_i f_i^{eq} c_i c_i = c_s^2\rho + \rho\, u_{eq}^2

cs2=1/3c_s^2=1/3 is the squared lattice sound speed. Any nonzero uequ_{eq} lifts the flux by ρueq2\rho u_{eq}^2. Try it in the simulation below. A cursor scans the column, and the local force F(y)F(y) places each scheme's equilibrium point on the parabola.

y = 0.0 · F = 0.0000 · SC u_eq = (τ−½)F/ρ = 0.0000 · SC extra 0.00e+0 · EDM extra 0.00e+0 · Guo 0

Drag τ\tau from near 0.5 up to 3. The orange dot (Shan–Chen) climbs the parabola while the green dot (Guo) stays at the bottom. The purple dots (EDM) sit at ±F/(2ρ)\pm F/(2\rho) and keep the same height F2/(4ρ)F^2/(4\rho) regardless of τ\tau.

Criterion 2: does the lift survive into the steady state?#

A lifted equilibrium doesn't automatically mean a lifted macroscopic stress. What actually transports momentum is the second moment of the post-collision populations, Π∗\Pi^*. Setting the pre- and post-collision second moments equal in the steady state gives

Π∗=Πeq+τ SΠ\Pi^* = \Pi_{eq} + \tau\, S_\Pi

where SΠS_\Pi is the second moment of the extra source. Guo's source has zero second moment at u=0u=0. EDM's source has ρ(u′+F/ρ)2−ρu′2=0\rho(u'+F/\rho)^2-\rho u'^2 = 0. So the three schemes differ only through the velocity placed in the equilibrium. The force balance in the resting column becomes

ddy(cs2ρ+b F2ρ)=F,bGuo=0,bSC=(τ−12)2,bEDM=14\frac{d}{dy}\left(c_s^2\rho + b\,\frac{F^2}{\rho}\right) = F, \qquad b_{Guo}=0,\quad b_{SC}=\left(\tau-\tfrac12\right)^2,\quad b_{EDM}=\tfrac14

bb is the coefficient of the fake stress. Continuum hydrostatics has b=0b=0.

Why doesn't this term show up in textbook derivations? The standard Chapman–Enskog expansion (a multiscale expansion that sorts the populations by powers of a small parameter ϵ\epsilon) treats FF as first order. Then F2/ρF^2/\rho is an ϵ2\epsilon^2 term and gets dropped at the Navier–Stokes level. Once its coefficient grows like τ2\tau^2, nothing guarantees it is small. The paper cited in my source notes, Revised Chapman–Enskog analysis for a class of forcing schemes in the lattice Boltzmann method, revisits exactly this order assignment of the force term. Whether to feed the force in as a pressure gradient or as a body force was the subject of the two forms of non-ideal LBM forcing. This post asks how the same force gets loaded onto the populations.

Code: a periodic column, three forcings, five values of τ#

A periodic D1Q3 column (velocities 0, ±1) of 32 cells is driven by F=Gsin⁡(ky)F = G\sin(ky). It starts at uniform density. Once the sound waves die out through viscosity, a resting state remains. On the converged density, a least-squares fit of cs2ρ=λP(y)−bF2/ρ+cc_s^2\rho = \lambda P(y) - bF^2/\rho + c measures bb. P(y)=−Gkcos⁡(ky)P(y)=-\tfrac{G}{k}\cos(ky) is the hydrostatic potential with P′=FP'=F. λ\lambda absorbs the amplitude factor introduced by lattice discretization.

import math
 
# D1Q3 lattice: velocities 0, +1, -1 ; cs^2 = 1/3
C = (0.0, 1.0, -1.0)
W = (2/3, 1/6, 1/6)
CS2 = 1/3
 
def d1q3_eq(rho, u):
    return [W[i]*rho*(1 + C[i]*u/CS2 + 0.5*(C[i]*u)**2/CS2**2 - 0.5*u*u/CS2)
            for i in range(3)]
 
def collide_node(fn, F, tau, scheme):
    rho = sum(fn)
    mom = fn[1] - fn[2]
    if scheme == "guo":                 # Guo: u = (sum f c + F/2)/rho, source with (1 - 1/2tau)
        u = (mom + 0.5*F)/rho
        feq = d1q3_eq(rho, u)
        out = []
        for i in range(3):
            S = W[i]*((C[i] - u)/CS2 + C[i]*u*C[i]/CS2**2)*F
            out.append(fn[i] - (fn[i] - feq[i])/tau + (1 - 0.5/tau)*S)
        return out
    if scheme == "sc":                  # Shan-Chen: shift equilibrium velocity by tau F / rho
        feq = d1q3_eq(rho, mom/rho + tau*F/rho)
        return [fn[i] - (fn[i] - feq[i])/tau for i in range(3)]
    up = mom/rho                        # EDM: add feq(u'+F/rho) - feq(u')
    feq = d1q3_eq(rho, up)
    fsh = d1q3_eq(rho, up + F/rho)
    return [fn[i] - (fn[i] - feq[i])/tau + fsh[i] - feq[i] for i in range(3)]
 
def run_column(scheme, tau, G, N=32, tol=1e-12):
    k = 2*math.pi/N
    F = [G*math.sin(k*(j + 0.5)) for j in range(N)]
    f = [d1q3_eq(1.0, 0.0) for _ in range(N)]
    for it in range(1, 100001):
        post = [collide_node(f[j], F[j], tau, scheme) for j in range(N)]
        new = [[post[j][0], post[j-1][1], post[(j+1) % N][2]] for j in range(N)]
        if it % 100 == 0 and max(abs(sum(new[j]) - sum(f[j])) for j in range(N)) < tol:
            return F, [sum(fn) for fn in new], it
        f = new
    return F, [sum(fn) for fn in f], it
 
def stress_fit(F, rho, G):
    # fit  cs^2 rho = lam*P(y) - b*F^2/rho + c,  with P' = F  (hydrostatic: b = 0)
    N = len(F); k = 2*math.pi/N
    X = [[F[j]**2/rho[j], -G/k*math.cos(k*(j + 0.5)), 1.0] for j in range(N)]
    r = [CS2*x for x in rho]
    A = [[sum(X[n][a]*X[n][b] for n in range(N)) for b in range(3)] for a in range(3)]
    v = [sum(X[n][a]*r[n] for n in range(N)) for a in range(3)]
    for i in range(3):
        for j in range(i + 1, 3):
            m = A[j][i]/A[i][i]
            A[j] = [A[j][c] - m*A[i][c] for c in range(3)]
            v[j] -= m*v[i]
    x = [0.0]*3
    for i in (2, 1, 0):
        x[i] = (v[i] - sum(A[i][c]*x[c] for c in range(i + 1, 3)))/A[i][i]
    return -x[0], x[1]
 
G = 0.02
print(f"periodic column N=32, F = {G} sin(ky), rho0 = 1")
print(" tau | b_Guo  b_SC   (tau-1/2)^2  b_EDM | lam   | max|rho_SC-rho_Guo|  iters")
for tau in [0.8, 1.0, 1.5, 2.0, 3.0]:
    out = {s: run_column(s, tau, G) for s in ("guo", "sc", "edm")}
    b = {s: stress_fit(out[s][0], out[s][1], G) for s in out}
    drho = max(abs(p - q) for p, q in zip(out["sc"][1], out["guo"][1]))
    bg = round(b["guo"][0], 3) + 0.0
    print(f"{tau:4.1f} | {bg:5.3f} {b['sc'][0]:6.3f}   {(tau-0.5)**2:6.3f}    {b['edm'][0]:5.3f} |"
          f" {b['guo'][1]:.4f} |  {drho:.2e}            {out['sc'][2]}")
periodic column N=32, F = 0.02 sin(ky), rho0 = 1
 tau | b_Guo  b_SC   (tau-1/2)^2  b_EDM | lam   | max|rho_SC-rho_Guo|  iters
 0.8 | 0.000  0.090    0.090    0.250 | 0.9968 |  5.49e-05            6100
 1.0 | 0.000  0.250    0.250    0.250 | 0.9968 |  1.53e-04            3600
 1.5 | 0.000  1.000    1.000    0.250 | 0.9968 |  6.11e-04            1900
 2.0 | 0.000  2.250    2.250    0.250 | 0.9968 |  1.38e-03            1300
 3.0 | 0.000  6.250    6.250    0.250 | 0.9968 |  3.84e-03            800

What 0, (τ−½)², and 1/4 are telling us#

Now run the code above in your browser. The three columns advance together under the same force, and bb is refitted every frame.

step 0 · b: Guo 0.000 · SC 0.000 (theory 1.000) · EDM 0.000 · max|ρ_SC−ρ_Guo| 0.00e+0 · Δρ/step 1.0e+0

At the defaults τ=1.5\tau=1.5, G=0.02G=0.02, the orange curve settles at 1.000, purple at 0.250, and green at 0.000 — the third row of the output block. Raise τ\tau to 3 and hit Reset: only orange climbs, to 6.25. Changing GG rescales the dots by G2G^2, but bb stays put.

The first thing to check in the output is the bSCb_{SC} column against the (τ−12)2(\tau-\tfrac12)^2 column. All five rows agree to three decimals. Nothing was lost in the derivation. Guo's 0.000 matters too: hydrostatic balance holds all the way down to the F2F^2 level.

At τ=1\tau=1, Shan–Chen and EDM both read 0.250. That is no accident. With τ=1\tau=1, the post-collision populations are the equilibrium itself. Shan–Chen becomes feq(u′+F/ρ)f^{eq}(u'+F/\rho), and EDM becomes feq(u′)+feq(u′+F/ρ)−feq(u′)f^{eq}(u') + f^{eq}(u'+F/\rho) - f^{eq}(u'), the same thing. The two schemes meet at a single point.

λ=0.9968\lambda=0.9968 has nothing to do with forcing. It is the discretization factor a 32-cell lattice applies to the sine amplitude, and it matches 1−k2/121-k^2/12. It goes to 1 as the grid is refined. The fit is built so that bb is separated from this factor.

The density difference itself looks small: 3.84×10−33.84\times10^{-3} at τ=3\tau=3, or 1.3% of the 0.30 density amplitude. But that ratio grows in proportion to GG. The main effect scales with FF, while the fake stress scales with F2F^2.

The three schemes on one page#

GuoShan–ChenEDM
Equilibrium velocity (at rest)00(τ−12)F/ρ(\tau-\tfrac12)F/\rho−F/(2ρ)-F/(2\rho)
Fake-stress coefficient bb00(τ−12)2(\tau-\tfrac12)^214\tfrac14
τ\tau dependencenonequadraticnone
Rest state independent of viscosityyesnoyes
Matches hydrostatic balanceyesonly near τ=12\tau=\tfrac12no (14\tfrac14 fixed)

EDM is independent of τ\tau but off by a constant 14\tfrac14, which makes it easy to correct. Shan–Chen is small only near τ=0.5\tau=0.5, which is exactly where LB is least stable.

At a multiphase interface, F is large#

The force in this experiment was smooth and weak. Pseudopotential multiphase models are different. The interaction force F=−Gψ∇ψF=-\mathcal{G}\psi\nabla\psi (with G\mathcal{G} the interaction strength and ψ\psi the effective density) is large and concentrated within a few cells of the interface. There, bF2/ρbF^2/\rho can grow to the same size as the pressure from the equation of state.

That is why a flat interface built with Shan–Chen forcing has coexistence densities (the density of each phase at equilibrium) that drift with τ\tau. You touch τ\tau to change viscosity, and the liquid density changes. The column experiment isolates that effect without any interface. The fake stress enters the mechanical equilibrium condition of the interface unchanged.

Some methods use this term on purpose. Treat bb as a tunable parameter, and it becomes a knob for matching coexistence densities to thermodynamic consistency: add as much as needed on top of EDM's 14\tfrac14 or Guo's 0. Either way, you first need to know what bb your own code has. For another hidden term found with the same Chapman–Enskog machinery, see the quasi-equilibrium g* of two-population LB.

The 32-cell test to run before trusting a new forcing#

When adding a new forcing scheme to a code, don't start with a multiphase case. At an interface, causes get tangled. Run the periodic column above first. Measure bb at two values of τ\tau. If bb moves with τ\tau, a velocity shift is hiding inside the equilibrium. If bb is a nonzero constant, that constant is what will be added to your interface pressure. It takes 32 cells and a few thousand steps.

Share if you found it helpful.