Skip to content
cfd-lab:~/en/posts/2026-07-27-lbm-forcing-t…online
NOTE #116DAY MON CFD기법DATE 2026.07.27READ 9 min readWORDS 1,775#LBM#Forcing-Term#Guo-Forcing#Shan-Chen#Poiseuille

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

Comparing lattice Boltzmann body-force schemes and pinning down the half-force correction

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 (112τ)(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. This post uses the derivation to locate that trap, and numbers to measure its size.

Cutting the force term out of the continuous equation#

The Boltzmann equation with an external force gains one term.

tf+ξxf+Fρξf=1λ(ffeq)\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ρcs2feq\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[ciucs2+(ciu)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λ[(fifieq)n+(fifieq)n+1]+δt2[Fin+Fin+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λ(fifieq)δ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ˉi1τ(fˉifieq)+δt(112τ)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 (112τ)(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. 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=δtF/ρ\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(ciF)/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)(ciu)Ffieq/(ρ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 (112τ)(1-\frac{1}{2\tau})(j+δtF/2)/ρ(\mathbf{j}+\delta t\mathbf{F}/2)/\rho(112τ)(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 (112τ)(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.

Let's work the figure below directly.

Spin the force-direction dial as far as you like and the M0 (mass) bar stays glued to zero. A force makes no mass, so that one is free. The M1 (momentum) target is not F\mathbf{F} but (112τ)F(1-\frac{1}{2\tau})\mathbf{F}, because the bar measures the source term after the gain has multiplied it. The missing F/(2τ)\mathbf{F}/(2\tau) is handed back by the half-shifted equilibrium, and the bottom row Δj\Delta j landing exactly on F\mathbf{F} is the proof.

M2 (stress) is the only row where the schemes part ways. Raise the u|u| slider and plain's M2 sits at zero and misses the target — the shape is wrong, not the size, so no gain will fix it. Push τ\tau toward 2 and Shan–Chen's deviation grows visibly. Flip the prefactor switch off and M1, M2 and Δj\Delta j all break at once.

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(Hy),ν=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.

Let's work the simulation below directly.

Press all four scheme buttons. The teal curve does not budge off the dashed parabola. Push τ\tau from 0.6 up to 2.0 and it is the same story. Then switch off just one of the two toggles below. The amplitude of the parabola jumps wholesale.

The numbers say the same thing. Maximum relative error measured after 40,000 steps with 33 fluid nodes and Fx=105F_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=1s/(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 112τ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.

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)

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.

What to remember#

  • The (112τ)(1-\frac{1}{2\tau}) in front of the force term and the +δtF/2ρ+\delta t\mathbf{F}/2\rho on the velocity are a pair that came out of the same change of variables in the trapezoidal integration. Use only one of the two and the force is off by a factor of 1±12τ1\pm\frac{1}{2\tau}. At τ=0.6\tau=0.6 that is 83% too much.
  • Steady unidirectional Poiseuille cannot tell forcing schemes apart. All five produce the same error to the fourth decimal place. Scheme comparison has to be done in unsteady flow or with an inhomogeneous force.
  • Whichever scheme you take, the source term's own first moment is matched to F\mathbf{F}. The difference sits in the second moment, the stress, and its target value carries a (112τ)(1-\frac{1}{2\tau}). When reviewing a new scheme, checking M2 first is the fast route.

Share if you found it helpful.