Skip to content
cfd-lab:~/en/posts/2026-07-27-lbm-forcing-t…● online

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

NOTE #114DAY MON CFD기법DATE 2026.07.27UPDATED 2026.10.19READ 10 min read#LBM#Forcing-Term#Guo-Forcing#Shan-Chen#Poiseuille

Where Half the Force Goes in LBM — Four Forcing Schemes and 1−1/(2τ)

Keep δt F/2ρ in the velocity and drop (1−1/(2τ)) on the source, and the momentum injected at τ=0.6 is 1.83 times too large.

In 1993 Shan and Chen pulled water and oil apart on a lattice. The prescription was modest. All they did was add τF/ρ\tau\mathbf{F}/\rho to the velocity fed to the equilibrium during collision. In 1998 He attached the force directly to the equilibrium distribution. In 2002 Guo argued that the earlier prescriptions leave an error in the stress, and put (1−12τ)(1-\frac{1}{2\tau}) in front of the force term. In 2004 Kupershtokh did the same job with nothing but the difference of two equilibrium distributions.

Four answers to one question. So which one should you use? Today I put that question straight to a D2Q9 channel. The answer was not what I expected. In steady flow, every scheme gave the same error out to the fourth decimal place. The real trap is not where you pick a scheme. The derivation locates it. The numbers size it.

The moment dial changes scheme, τ\tau, ∣u∣|u|, and the prefactor together, and the M0, M1, and M2 bars move with them.

Spin the dial and M0 stays at zero. Raise ∣u∣|u| and only plain's M2 misses the target. Kill the prefactor and M1 and Δj\Delta j break in the same step.

Cutting the force term out of the continuous equation#

The Boltzmann equation with an external force gains one term.

∂tf+ξ⋅∇xf+Fρ⋅∇ξf=−1λ(f−feq)\partial_t f + \boldsymbol{\xi}\cdot\nabla_{\mathbf{x}} f + \frac{\mathbf{F}}{\rho}\cdot\nabla_{\boldsymbol{\xi}} f = -\frac{1}{\lambda}\left(f - f^{\rm eq}\right)

ff is the distribution function, ξ\boldsymbol{\xi} the particle velocity, F\mathbf{F} the body force per unit volume, and λ\lambda the relaxation time.

The trouble is the third term. The velocity-space derivative ∇ξf\nabla_{\boldsymbol{\xi}} f does not exist on a lattice. All we have is nine discrete velocities. So we swap ff for feqf^{\rm eq} and handle the derivative analytically.

Fρ⋅∇ξf≃−(ξ−u)⋅Fρcs2 feq\frac{\mathbf{F}}{\rho}\cdot\nabla_{\boldsymbol{\xi}} f \simeq -\frac{(\boldsymbol{\xi}-\mathbf{u})\cdot\mathbf{F}}{\rho c_s^2}\,f^{\rm eq}

csc_s is the lattice sound speed (cs2=1/3c_s^2=1/3 in D2Q9) and u\mathbf{u} is the fluid velocity.

Is it legal to swap ff for feqf^{\rm eq}? At low Mach number, yes. The non-equilibrium part f(1)f^{(1)} is first order in the Knudsen number (mean free path over characteristic length), so once it is multiplied by the force it drops to a negligible order. What the swap does instead is leave a residual error in the stress. That is exactly where the schemes part ways.

Project this expression onto the discrete velocities ci\mathbf{c}_i and truncate the Hermite series at second order. Out comes the force term on the lattice.

Fi=wi[ci−ucs2+(ci⋅u)cs4ci]⋅FF_i = w_i\left[\frac{\mathbf{c}_i-\mathbf{u}}{c_s^2} + \frac{(\mathbf{c}_i\cdot\mathbf{u})}{c_s^4}\mathbf{c}_i\right]\cdot\mathbf{F}

wiw_i is the lattice weight. The reason for cutting at second order is plain. The moments needed to recover Navier–Stokes go up to second order.

Three moments are worth pinning down.

∑iFi=0,∑iciFi=F,∑iciciFi=uF+Fu\sum_i F_i = 0,\qquad \sum_i \mathbf{c}_i F_i = \mathbf{F},\qquad \sum_i \mathbf{c}_i\mathbf{c}_i F_i = \mathbf{u}\mathbf{F}+\mathbf{F}\mathbf{u}

Zeroth order is mass. A force creates no mass, so it has to vanish. First order is momentum, second order is stress.

The half left behind by the trapezoidal rule#

Now integrate the discrete-velocity Boltzmann equation along a characteristic over δt\delta t. Integrating the collision and force terms with the trapezoidal rule buys second-order accuracy. The price is that t+δtt+\delta t slips onto the right-hand side.

fi(x+ciδt, t+δt)−fi(x,t)=−δt2λ[(fi−fieq)n+(fi−fieq)n+1]+δt2[Fi n+Fi n+1]f_i(\mathbf{x}+\mathbf{c}_i\delta t,\ t+\delta t) - f_i(\mathbf{x},t) = -\frac{\delta t}{2\lambda}\left[(f_i-f_i^{\rm eq})^{n} + (f_i-f_i^{\rm eq})^{n+1}\right] + \frac{\delta t}{2}\left[F_i^{\,n} + F_i^{\,n+1}\right]

The superscripts nn and n+1n+1 mean tt and t+δtt+\delta t. As written this is implicit. It would take a linear system solved every step.

LBM erases that implicitness with a change of variables.

fˉi=fi+δt2λ(fi−fieq)−δt2Fi\bar f_i = f_i + \frac{\delta t}{2\lambda}\left(f_i - f_i^{\rm eq}\right) - \frac{\delta t}{2}F_i

Rewrite in terms of fˉi\bar f_i and set τ=λ/δt+1/2\tau = \lambda/\delta t + 1/2, and you get the familiar one-liner.

fˉi(x+ciδt, t+δt)=fˉi−1τ(fˉi−fieq)+δt(1−12τ)Fi\bar f_i(\mathbf{x}+\mathbf{c}_i\delta t,\ t+\delta t) = \bar f_i - \frac{1}{\tau}\left(\bar f_i - f_i^{\rm eq}\right) + \delta t\left(1-\frac{1}{2\tau}\right)F_i

The transformation left two traces. One is the (1−12τ)(1-\frac{1}{2\tau}) in front of the force. The other is easier to miss. What we store in the array is fˉi\bar f_i, not fif_i. So the momentum is off by that same half.

ρu=∑ifˉici+δt2F\rho\mathbf{u} = \sum_i \bar f_i \mathbf{c}_i + \frac{\delta t}{2}\mathbf{F}

These two are one body. They fell out of the same transformation together. The half step the trapezoidal rule leaves in the viscosity comes from that same root. Keep one and drop the other and the books do not balance. How badly they fail to balance gets measured below.

A comparison table for four schemes#

Call j=∑ifˉici\mathbf{j}=\sum_i \bar f_i\mathbf{c}_i the raw momentum of the stored distributions, and δu=δt F/ρ\delta\mathbf{u}=\delta t\,\mathbf{F}/\rho the velocity increment the force delivers in one step.

SchemeWhere the force entersVelocity used in the equilibriumSecond moment ∑ciciFi\sum\mathbf{c}_i\mathbf{c}_i F_i
plainFi=wi(ci⋅F)/cs2F_i = w_i(\mathbf{c}_i\cdot\mathbf{F})/c_s^2j/ρ\mathbf{j}/\rho0 — the uF\mathbf{u}\mathbf{F} term is missing entirely
Shan–Chen (1993)no explicit termj/ρ+τF/ρ\mathbf{j}/\rho + \tau\mathbf{F}/\rhouF+Fu+τFF/ρ\mathbf{u}\mathbf{F}+\mathbf{F}\mathbf{u} + \tau\mathbf{F}\mathbf{F}/\rho
He (1998)(ci−u)⋅F fieq/(ρcs2)(\mathbf{c}_i-\mathbf{u})\cdot\mathbf{F}\,f_i^{\rm eq}/(\rho c_s^2)(j+δtF/2)/ρ(\mathbf{j}+\delta t\mathbf{F}/2)/\rhouF+Fu\mathbf{u}\mathbf{F}+\mathbf{F}\mathbf{u} (within the equilibrium truncation error)
EDM (2004)fieq(ρ,u+δu)−fieq(ρ,u)f_i^{\rm eq}(\rho,\mathbf{u}+\delta\mathbf{u}) - f_i^{\rm eq}(\rho,\mathbf{u})j/ρ\mathbf{j}/\rhouF+Fu+ρ δuδu\mathbf{u}\mathbf{F}+\mathbf{F}\mathbf{u} + \rho\,\delta\mathbf{u}\delta\mathbf{u}
Guo (2002)the FiF_i above times (1−12τ)(1-\frac{1}{2\tau})(j+δtF/2)/ρ(\mathbf{j}+\delta t\mathbf{F}/2)/\rho(1−12τ)(uF+Fu)(1-\frac{1}{2\tau})(\mathbf{u}\mathbf{F}+\mathbf{F}\mathbf{u})

Read the five rows down the columns and the common ground shows. Before the gain multiplies it, every source term's first moment is exactly F\mathbf{F}. No surprise — they were designed that way. What differs is the second moment, which is the stress.

But the target the change of variables demands is not uF+Fu\mathbf{u}\mathbf{F}+\mathbf{F}\mathbf{u}. It is (1−12τ)(uF+Fu)(1-\frac{1}{2\tau})(\mathbf{u}\mathbf{F}+\mathbf{F}\mathbf{u}). Only the bottom row carries that coefficient explicitly. The rest go along carrying a stress deviation of size 12τ\frac{1}{2\tau}.

The deviation scales as force times velocity, order uFu F. At low Mach, uu is small, so the term itself is small. The FF\mathbf{F}\mathbf{F} term that Shan–Chen and EDM leave behind is smaller still, order F2F^2. That is why it barely matters which one you use when the driving is weak gravity. The differences surface where the force is strong (when τF/ρ\tau\mathbf{F}/\rho rivals u\mathbf{u}), or where the force changes sharply across an interface, as in multiphase flow.

The M1 target is not F\mathbf{F} but (1−12τ)F(1-\frac{1}{2\tau})\mathbf{F}, because the bar measures the source after the gain. The missing F/(2τ)\mathbf{F}/(2\tau) comes back from the half-shifted equilibrium, and Δj\Delta j lands on F\mathbf{F}. Only M2 splits by scheme. Plain is the wrong shape, so no gain fixes it. Push τ\tau toward 2 and the Shan–Chen deviation grows.

Steady Poiseuille cannot tell them apart#

Let's go to the most common validation problem. A channel pushed by a body force between an upper and a lower wall. The steady solution is a known parabola.

u(y)=Fx2ν y (H−y),ν=cs2(τ−12)u(y) = \frac{F_x}{2\nu}\,y\,(H-y),\qquad \nu = c_s^2\left(\tau-\tfrac{1}{2}\right)

HH is the channel height and ν\nu the kinematic viscosity. With half-way bounce-back the wall sits half a cell outside the lattice nodes, so HH equals the number of fluid nodes and node jj sits at y=j+0.5y=j+0.5.

Swapping the scheme leaves the parabola's amplitude alone. Maximum relative error after 40,000 steps, 33 fluid nodes, Fx=10−5F_x = 10^{-5}.

τ\tauplainShan–ChenHeEDMGuo
0.600.0875%0.0875%0.0875%0.0875%0.0875%
1.000.0306%0.0306%0.0306%0.0306%0.0306%
1.800.7358%0.7358%0.7358%0.7358%0.7358%

There is nothing to compare across the columns. The values are identical. The error that remains comes from the discretization of bounce-back, not from the scheme.

The reason lies in where the second moment enters. This flow is steady and unidirectional. The velocity has a single component ux(y)u_x(y) and the force has only an xx component. The term where the schemes part ways is uF\mathbf{u}\mathbf{F}, the xxxx component. But what actually governs the momentum balance of this flow is the xyxy shear stress. The xxxx deviation the force creates has no route into the balance.

That ∂tu=0\partial_t\mathbf{u}=0 works in the same direction. There is no room for the force-term error to accumulate in time. To see the difference between schemes you have to go to unsteady flow, or to a spatially varying force. With this test, at least, the five cannot be told apart.

This is not bad news. It is something to know when choosing a validation problem. It means a passing Poiseuille does not let you say your forcing implementation is right.

Count the half twice and the force grows#

So what does actually go wrong? The two things called one body earlier — the half-step velocity correction and the coefficient in front of the force.

Count the momentum actually injected in one step. Add δtF/2ρ\delta t\mathbf{F}/2\rho to the equilibrium velocity and collision carries 1/τ1/\tau of it across each step. On top of that comes the share the force term delivers directly. Write s∈{0,1}s\in\{0,1\} for whether the half correction is on, and gg for the coefficient in front of the force, and

Δ(ρu)=1τ⋅s δtF2+g δtF\Delta(\rho u) = \frac{1}{\tau}\cdot\frac{s\,\delta t F}{2} + g\,\delta t F

This has to equal δtF\delta t F. The condition is a single one, g=1−s/(2τ)g = 1 - s/(2\tau). The two switches are not independent.

Mismatch them and the amplitude misses by a predictable ratio. Here are the amplitude ratios measured with the Guo force term held fixed and only the two switches changed.

τ\tauboth onshift onlypredicted 1+12τ1+\frac{1}{2\tau}gain onlypredicted 1−12τ1-\frac{1}{2\tau}
0.600.99911.83161.83330.16660.1667
0.800.99951.62401.62500.37510.3750
1.001.00031.50021.50000.50050.5000
1.401.00301.36091.35710.64520.6429
1.801.00741.28671.27780.72800.7222

Measurement and prediction agree to the third decimal place. At τ=0.6\tau=0.6, turning on only the half correction makes the force 83% larger. Turning on only the coefficient shrinks it to a sixth.

This is not a subtle accuracy issue. It is exactly what happens when you copy Guo's FiF_i straight out of a paper and leave the velocity as the old code's j/ρ\mathbf{j}/\rho. Start touching τ\tau because the viscosity looks off and you go in deeper. Every change to τ\tau moves the error ratio along with it.

There is a way to spot it from the symptoms. If doubling the grid does not shrink the error ratio, it is not a discretization error. If the ratio converges toward 1.5 as τ\tau is brought near 1 and grows as if diverging as τ\tau drops toward 0.5, that is the fingerprint of 1+12τ1+\frac{1}{2\tau}. And if raising the force leaves the ratio unchanged, it is a bookkeeping problem, not a nonlinear one. When all three line up, do not suspect the scheme — open up the velocity definition first.

At τ=0.6\tau=0.6, 2τ=1.22\tau=1.2, so 1/(2τ)=5/6≈0.83331/(2\tau)=5/6\approx 0.8333. Both switches on means s=1s=1 and g=1−1/(2τ)=1/6g=1-1/(2\tau)=1/6. Substituting into (1/τ)⋅(s/2)+g(1/\tau)\cdot(s/2)+g gives (1/0.6)⋅0.5+1/6=5/6+1/6=1(1/0.6)\cdot 0.5 + 1/6 = 5/6 + 1/6 = 1. The both column, 0.9991, is that 1 with the lattice shaved off.

Shift only freezes gg at 1, so the slot becomes 5/6+1=1.83335/6+1=1.8333. The table prints 1.8316. The gap of 0.0017 is about 0.09% of the prediction. Gain only sets s=0s=0 and g=1/6g=1/6, so the injection is 0.1667. Measured 0.1666 meets it at the fourth decimal.

At τ=1\tau=1, 1/(2τ)=1/21/(2\tau)=1/2. Shift only predicts 1.5 and gain only 0.5. The table says 1.5002 and 0.5005. Both on gives 1.0003, 0.03% off a target of 1. At τ=1.8\tau=1.8, 1/(2τ)=1/3.6=0.27781/(2\tau)=1/3.6=0.2778. Predictions 1.2778 and 0.7222 sit next to 1.2867 and 0.7280. (1.2867−1.2778)/1.2778(1.2867-1.2778)/1.2778 is about 0.007, or 0.7%. The scheme error at that τ\tau is 0.7358%. Same size.

The relative-error columns read 0.0875% at τ=0.6\tau=0.6, 0.0306% at τ=1.0\tau=1.0, and 0.7358% at τ=1.8\tau=1.8. With ν=cs2(τ−1/2)\nu=c_s^2(\tau-1/2) and cs2=1/3c_s^2=1/3, the viscosities are 1/301/30, 1/61/6, and 13/3013/30. The (τ−1/2)(\tau-1/2) ratios run 1 to 5 to 13. The peak is u(H/2)=FxH2/(8ν)u(H/2)=F_x H^2/(8\nu), so at Fx=10−5F_x=10^{-5} and H=33H=33 the τ=1.8\tau=1.8 peak is 1/131/13 of the τ=0.6\tau=0.6 peak. The percent grows there. The five cells still share one number.

Python — mismatching the two switches#

A D2Q9 channel solver with the two switches pulled out as arguments. The flow is homogeneous in xx, so only one column is kept. The five schemes in the table above only need source_terms and the velocity fed to the equilibrium swapped out, so here we fix on the Guo force term alone.

import numpy as np
 
EX = np.array([0, 1, 0, -1, 0, 1, -1, -1, 1])
EY = np.array([0, 0, 1, 0, -1, 1, 1, -1, -1])
W = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36])
OPP = np.array([0, 3, 4, 1, 2, 7, 8, 5, 6])
CS2 = 1.0 / 3.0
 
 
def lattice_equilibrium(rho, ux, uy):
    feq = np.empty((9, rho.size))
    usq = ux**2 + uy**2
    for i in range(9):
        eu = EX[i] * ux + EY[i] * uy
        feq[i] = W[i] * rho * (1 + eu/CS2 + eu**2/(2*CS2**2) - usq/(2*CS2))
    return feq
 
 
def source_terms(rho, ux, uy, fx):
    """Guo forcing term, before the (1 - 1/2tau) gain."""
    src = np.empty((9, rho.size))
    for i in range(9):
        eu = EX[i]*ux + EY[i]*uy
        src[i] = W[i] * ((EX[i] - ux)/CS2 + eu*EX[i]/CS2**2) * fx
    return src
 
 
def run_forced_channel(tau, half_shift, prefactor, ny=33, fx=1.0e-5, steps=40000):
    rho = np.ones(ny)
    f = lattice_equilibrium(rho, np.zeros(ny), np.zeros(ny))
    gain = (1.0 - 1.0/(2*tau)) if prefactor else 1.0
    for _ in range(steps):
        rho = f.sum(axis=0)
        jx = (f*EX[:, None]).sum(axis=0)
        jy = (f*EY[:, None]).sum(axis=0)
        ux = (jx + (0.5*fx if half_shift else 0.0)) / rho   # switch 1
        uy = jy / rho
        fpost = f - (f - lattice_equilibrium(rho, ux, uy))/tau \
                + gain*source_terms(rho, ux, uy, fx)        # switch 2
        for i in range(9):                                   # streaming in y
            f[i] = fpost[i] if EY[i] == 0 else np.roll(fpost[i], EY[i])
        for i in range(9):                                   # half-way bounce-back
            if EY[i] > 0:
                f[i, 0] = fpost[OPP[i], 0]
            elif EY[i] < 0:
                f[i, -1] = fpost[OPP[i], -1]
    jx = (f*EX[:, None]).sum(axis=0)
    return (jx + 0.5*fx) / f.sum(axis=0)      # physical velocity, always shifted
 
 
NY, FX = 33, 1.0e-5
y = np.arange(NY) + 0.5
for tau in (0.6, 1.0, 1.8):
    exact = FX / (2*CS2*(tau - 0.5)) * y * (NY - y)
    ok = run_forced_channel(tau, True, True).max() / exact.max()
    m1 = run_forced_channel(tau, True, False).max() / exact.max()
    m2 = run_forced_channel(tau, False, True).max() / exact.max()
    print(f"tau={tau:4.2f}  both={ok:.4f}  shift_only={m1:.4f} (pred {1+1/(2*tau):.4f})"
          f"  gain_only={m2:.4f} (pred {1-1/(2*tau):.4f})")

The output looks like this.

tau=0.60  both=0.9991  shift_only=1.8316 (pred 1.8333)  gain_only=0.1666 (pred 0.1667)
tau=1.00  both=1.0003  shift_only=1.5002 (pred 1.5000)  gain_only=0.5005 (pred 0.5000)
tau=1.80  both=1.0074  shift_only=1.2867 (pred 1.2778)  gain_only=0.7280 (pred 0.7222)

Match the channel lab's scheme buttons, τ\tau, half-shift, and prefactor to the both, shift_only, and gain_only columns printed above.

All four scheme buttons leave the teal curve on the dashed parabola. Kill one switch and, at τ=0.6\tau=0.6, the amplitude jumps toward 1.83 or 0.17.

At τ=1.8\tau=1.8 the measurement misses the prediction by 0.7%, and under the same conditions the scheme's own discretization error is 0.74%. Same size. Which means the mismatch comes from the lattice, not from the bookkeeping.

Leave only the half-shift on and the force grows to 1.83#

At τ=0.6\tau=0.6, shift_only reads 1.8316. The prediction 1+12τ=1.83331+\frac{1}{2\tau}=1.8333 agrees to the third decimal. Copy Guo's source and keep the velocity as j/ρ\mathbf{j}/\rho, and the run lands in exactly that cell: shift on, prefactor off. Doubling the grid does not shrink the ratio. Bring τ\tau to 1 and the ratio sits near 1.5. Drop τ\tau toward 0.5 and the ratio grows. That is the fingerprint of 1+12τ1+\frac{1}{2\tau}.

Every scheme's first moment is F\mathbf{F}. The split is the second moment, and the target carries (1−12τ)(1-\frac{1}{2\tau}). A steady channel has no path for that xxxx error into the shear balance, so the five columns share 0.0875%, 0.0306%, and 0.7358%. Check M2 when the flow is unsteady or the force jumps across an interface. Filling the third moment on top of the second is a different bill, told in the third-moment limit of the force term.

Share if you found it helpful.