Translated from the author's Korean original. Read the original (한국어)
Giving Conduction Its Own τ Left Viscous Heating Stuck at Pr = 1 — The Quasi-Equilibrium g* in Two-Population LB
The viscous-work part of the energy flux must relax at the same rate as the stress, so pulling g only toward equilibrium keeps viscous heating at its Pr = 1 value no matter which Pr you set.
Pr changed, so why didn't the temperature?#
Picture a Couette channel. The bottom wall is at rest and the top wall slides at speed . Both walls have the same temperature. The middle still heats up, because viscosity turns kinetic energy into heat (viscous heating). The peak temperature rise is exactly proportional to the Prandtl number (Pr, the ratio of momentum diffusion to heat diffusion).
This post is about changing Pr in a lattice Boltzmann (LB) thermal model. Split the distribution into two sets, give each its own relaxation time, and Pr looks easy to tune. Run it, though, and conduction changes while viscous heating stays frozen at its Pr = 1 value. You'll see why, and you'll get the one-line fix: the quasi-equilibrium .
Move the Pr slider and the gap between the stress clock and the conduction clock changes, along with which curve the viscous-work flux sticks to.
At Pr = 0.71 the orange curve (stress) and the dashed green curve (viscous-work flux in the g* model) stay on top of each other. The red curve (plain two-τ model) follows the purple conduction clock and drifts away from the stress. Slide Pr to 1 and the four curves collapse into two.
One set of populations only knows Pr = 1#
The BGK collision operator pulls every non-equilibrium moment toward equilibrium at one rate . Stress and heat flux relax on the same clock, so viscosity and conductivity are tied to the same . That is why the continuous BGK equation gives Pr = 1.
Air has Pr = 0.71 and water about 7. A model locked to Pr = 1 can't even get the ratio of boundary-layer thicknesses right. There are two remedies. One relaxes only the heat-flux moment at a different rate inside the collision operator (MRT, Shakhov-type). The other hands energy to a separate distribution. This post follows the second route.
Two sets: f carries mass and momentum, g carries total energy#
In a two-population model, carries density and momentum, and carries the total energy (). The kinetic equations are:
sets viscosity (kinematic viscosity ), and sets conduction (thermal diffusivity ). is the local equilibrium and is the quasi-equilibrium.
The two rates map to the diffusivities as:
is the squared lattice sound speed. With and , .
The code locks . Then , so . The four table entries fall out at once. Pr = 0.5 gives . Pr = 0.71 adds , so . Pr = 1 gives . Pr = 2 gives . Since , heat at Pr = 2 relaxes at while viscosity sits at . Viscous work belongs on the slow clock. Plain two-τ puts that work on the fast . Only the Pr = 1 row has both rates equal, and both columns read 1.000. Pass only that row and the bug stays quiet.
Viscous work hides inside the energy flux#
In this toy, fixes only three moments. Zeroth: . First (energy flux): . Second: . The non-equilibrium part of the first moment, , is the actual heat flux.
At first order in the Chapman–Enskog expansion (solving for non-equilibrium at small Knudsen number), . Here splits into two pieces:
The first term is Fourier conduction. The second is the raw material for the viscous work . It has exactly the shape of the non-equilibrium stress in , . The problem is the in front.
If is pulled only toward equilibrium at , both pieces come out carrying . The viscous-work coefficient scales with instead of . Conduction is right, but viscous heating stays at its Pr = 1 value. That's the red curve in the first viz.
Quasi-equilibrium g*: tie only the flux to the stress clock#
The fix is one line in the definition of . It shares every moment with except the energy flux:
is the actual momentum flux measured from , and is its equilibrium value.
Taking the first moment of the kinetic equation gives a one-step update for the two non-equilibrium moments:
itself relaxes at , so conduction follows . At the same time, if , then holds at the next step. The viscous-work piece rides the stress clock . A Chapman–Enskog calculation says the same thing: the viscous work carrying cancels against the term, leaving only .
With the term vanishes. The model reduces to single relaxation, and Pr = 1. is a correction that only works when Pr . A thermal lattice with one population kept Pr stuck at 1 even when τ was raised. That is the same wall as the two constants locked in place. Splitting the populations does not leave that wall unless the viscous-work clock moves with them. The question is still which moment relaxes at which rate.
Code: one Couette channel, two kinds of g#
The analytical solution is simple. The steady energy equation is . With a linear velocity profile, temperature is a parabola:
The peak in the middle is . The code below runs D2Q9 with a single periodic cell in and solves only along . Walls use Guo's non-equilibrium extrapolation. For simplicity, uses an isothermal equilibrium and energy is coupled one way. In low-Mach () Couette flow the pressure is uniform, so this simplification doesn't change the result.
import numpy as np
# D2Q9, one periodic cell in x → 1D arrays along y
cx = np.array([0, 1, 0, -1, 0, 1, -1, -1, 1])
cy = np.array([0, 0, 1, 0, -1, 1, 1, -1, -1])
w = np.array([4/9] + [1/9]*4 + [1/36]*4)
CV, T0 = 1.0, 1.0
def feq_lattice(rho, ux):
cu = cx[:, None]*ux
return w[:, None]*rho*(1 + 3*cu + 4.5*cu**2 - 1.5*ux**2)
def geq_energy(E, rho, ux):
p = rho*(E/rho - 0.5*ux**2)/CV # p = rho*T
qx = (E + p)*ux
cu = cx[:, None]*ux
return w[:, None]*(E + 3*cx[:, None]*qx + 4.5*E*cu**2 - 1.5*E*ux**2)
def couette_heating(Pr, quasi, N=21, U=0.1, tau_mu=1.0, steps=8000):
om_mu = 1/tau_mu
om_ka = 1/(0.5 + (tau_mu - 0.5)/Pr)
rho = np.ones(N); ux = np.zeros(N)
E = rho*CV*T0*np.ones(N)
f = feq_lattice(rho, ux); g = geq_energy(E, rho, ux)
uw = np.zeros(N); uw[-1] = U
for _ in range(steps):
rho = f.sum(0); ux = (cx[:, None]*f).sum(0)/rho; E = g.sum(0)
fe = feq_lattice(rho, ux); ge = geq_energy(E, rho, ux)
Pxy = ((cx*cy)[:, None]*(f - fe)).sum(0)
f = f + om_mu*(fe - f)
if quasi: # g*: quasi-equilibrium with u·(P - P_eq) added to the energy flux
gs = ge + w[:, None]*3*cy[:, None]*(ux*Pxy)
g = g + om_mu*(ge - g) + (om_ka - om_mu)*(gs - g)
else: # plain two-τ: g relaxes only to equilibrium at ω_κ
g = g + om_ka*(ge - g)
for k in range(9):
f[k] = np.roll(f[k], cy[k]); g[k] = np.roll(g[k], cy[k])
for wall, nb in ((0, 1), (N-1, N-2)): # non-equilibrium extrapolation walls
r = f[:, nb].sum(); un = (cx*f[:, nb]).sum()/r; En = g[:, nb].sum()
fn = feq_lattice(np.array([r]), np.array([un]))[:, 0]
gn = geq_energy(np.array([En]), np.array([r]), np.array([un]))[:, 0]
Ew = r*(CV*T0 + 0.5*uw[wall]**2)
f[:, wall] = feq_lattice(np.array([r]), np.array([uw[wall]]))[:, 0] + f[:, nb] - fn
g[:, wall] = geq_energy(np.array([Ew]), np.array([r]), np.array([uw[wall]]))[:, 0] + g[:, nb] - gn
rho = f.sum(0); ux = (cx[:, None]*f).sum(0)/rho
T = (g.sum(0)/rho - 0.5*ux**2)/CV
theta = (T - T0)/(U**2/(8*CV))
return theta.max(), ux
print(" Pr exact 2-tau(g_eq) g* quasi-eq")
for Pr in (0.5, 0.71, 1.0, 2.0):
a, _ = couette_heating(Pr, False)
b, u = couette_heating(Pr, True)
print(f"{Pr:4.2f} {Pr:6.3f} {a:9.3f} {b:9.3f}")
print(f"u(mid)/U = {u[10]/0.1:.4f}") Pr exact 2-tau(g_eq) g* quasi-eq
0.50 0.500 1.000 0.498
0.71 0.710 1.000 0.710
1.00 1.000 1.000 1.000
2.00 2.000 1.000 1.997
u(mid)/U = 0.5000Now run that code in your browser. Both models march forward side by side in the same channel.
Move the Pr slider: the green peak follows the dashed analytical value Pr, while the red one sticks to 1.000. Changing or doesn't move the red off 1 either. Try Pr = 3, which isn't in the output block.
What the 1.000 is telling you#
The plain two-τ column reads 1.000 regardless of Pr. That isn't noise, it's structure. The viscous-work coefficient scales with , and so does the conductivity. In their ratio, , cancels exactly. For air (Pr = 0.71) that overpredicts the temperature rise between the walls by 41%. For a Pr = 7 fluid like water it underpredicts by a factor of 7.
With and , the denominator is . Air's true is . Plain two-τ gives . On the lattice that is 1.0009 versus 1.0013. Two decimal places print both as 1.00. At Pr = 7 the true rise is and the model gives . 1.009 versus 1.001. Only splits 1 from 7.
The column reads 0.498, 0.710, 1.000, 1.997. 0.498 is 0.4% under 0.500, and 1.997 is 0.15% under 2. 0.710 matches exact. On 21 nodes the rest is the wall and the grid. The midline velocity matches because both models share . The temperature error never shows up in the velocity field.
Go compressible and f gets a quasi-equilibrium too#
The in this post used an isothermal equilibrium. In compressible two-population models, also gets a quasi-equilibrium . D2Q9's third moments have a defect: . When temperature varies or Mach number grows, that defect puts an error term into the stress. carries a pressure tensor that corrects it. repairs the momentum flux; attaches viscous work to the energy flux.
The principle is the same. Equilibrium and quasi-equilibrium share the conserved quantities (density, momentum, total energy). The only difference is the one moment that needs its own relaxation rate. Where to put the pressure term for a non-ideal fluid is the same choice of moment.
The one line to check when your thermal LB code sets τκ separately#
Open the collision step for the energy distribution and look at where multiplies. If all of is pulled toward equilibrium at , viscous heating is stuck at its Pr = 1 value. One Couette run is enough to check. If doesn't track Pr, the quasi-equilibrium term is missing.
Validate only at and the bug stays quiet. A thermal LB check needs one viscous-heating case with Pr . Temperature diffusing in a fluid at rest never turns this term on. Reynolds' analogy between friction and heat starts from the premise that those two stay in a ratio tied to Pr. If does not follow Pr, that premise is already broken on the lattice.
Related
Share if you found it helpful.