◐ Off-By-One · answer catalog

node20-lattice-boltzmann-d2q9-bgk-bounceback-taylor-green

1 answer(s)jsnode20

Environment: Node.js 20+, plain JavaScript, typed arrays.

📦 Source in repository (JSON)

Answer

The solution is complete and saved at ~/lbm/SOLUTION.md, with the runnable solver (solver.js) and full verification suite (verify.js). Here is the deliverable:


D2Q9 BGK Lattice-Boltzmann Solver — Diagnosis and Verified Fix

Environment: Node.js 20+, plain JavaScript, typed arrays.

1. Root-cause analysis

The failures are all boundary/precision issues, not the BGK kernel:

RC1 — Zou–He pressure boundary had an inverted velocity sign (fatal)

For a pressure-driven boundary, density is prescribed and velocity is diagnosed. The correct left-wall relation is u = 1 - S_in/rho_in with S_in = f0+f2+f4+2(f3+f6+f7). A common transcription error is u = -1 + S_in/rho_in (that sign belongs to the right wall). With the wrong sign momentum feeds back with the wrong sign and the solution explodes:

dRho=2e-4  s=0   rho[0.9999,1.0001]  umax=1.0e-4
dRho=2e-4  s=50  rho[-7.8e11,1.4e12] umax=5.2e+2
dRho=2e-4  s=100 rho[-3.0e43,5.8e43] umax=7.1e+2

Correct closure (bounce back non-equilibrium normal part, u_y=0):

Left  (unknowns 1,5,8):  u = 1 - S_in/rho_in
  f1 = f3 + (2/3) rho_in u
  f5 = f7 - 1/2 (f2-f4) + (1/6) rho_in u
  f8 = f6 + 1/2 (f2-f4) + (1/6) rho_in u
Right (unknowns 3,6,7):  u = -1 + S_out/rho_out
  f3 = f1 - (2/3) rho_out u
  f6 = f8 - 1/2 (f2-f4) - (1/6) rho_out u
  f7 = f5 + 1/2 (f2-f4) - (1/6) rho_out u

RC2 — Moving-lid bounce-back used the wrong velocity / missing term

Half-way bounce-back on a moving wall must add 6 w_i rho (e_i · u_wall) and read u_wall from the solid node the population came from. Validated against Couette flow: numerical profile matches the linear analytic profile to 3.3e-9 relative L2.

RC3 — Float32Array destroys low-tau stability and momentum conservation

At tau = 0.51, omega = 1.9608 (within 2% of the BGK limit). Float32 round-off is amplified by omega ≈ 2. All fields must be Float64Array; then the 10000-step momentum drift is 3e-11 (round-off only).

RC4 — Low-tau Poiseuille needs O(H²/ν) steps

At tau=0.8 (ν=0.1) 20 000 steps suffice; at tau=0.51 (ν=1/300) the same channel needs ~30× more (400 000). A 50% error after 20 000 steps is an under-converged transient, not a boundary error.

RC5 — Lid blow-up at tau=0.51, U=0.1 is a Reynolds-number limit

ν=1/300, U=0.1, L=62 → Re≈1860, under-resolved on 64². Control Re, not cell count (refining at fixed U,τ increases Re).

2. Exact fix — solver.js

Full code is in ~/lbm/solver.js. The critical corrected section:

// Zou-He pressure boundaries (left/right, u_y = 0).
applyZouHe(rhoIn, rhoOut) {
  const { N, nx, ny, f } = this;
  for (let y = 0; y < ny; y++) {                 // left: unknowns 1,5,8
    const id = y * nx;
    const f0 = f[id], f2 = f[2*N+id], f4 = f[4*N+id];
    const f3 = f[3*N+id], f6 = f[6*N+id], f7 = f[7*N+id];
    const S = f0 + f2 + f4 + 2*(f3 + f6 + f7);
    const ux = 1 - S / rhoIn;                    // <-- correct sign
    f[1*N+id] = f3 + (2/3)*rhoIn*ux;
    f[5*N+id] = f7 - 0.5*(f2-f4) + (1/6)*rhoIn*ux;
    f[8*N+id] = f6 + 0.5*(f2-f4) + (1/6)*rhoIn*ux;
  }
  for (let y = 0; y < ny; y++) {                 // right: unknowns 3,6,7
    const id = y*nx + (nx-1);
    const f0 = f[id], f2 = f[2*N+id], f4 = f[4*N+id];
    const f1 = f[1*N+id], f5 = f[5*N+id], f8 = f[8*N+id];
    const S = f0 + f2 + f4 + 2*(f1 + f5 + f8);
    const ux = -1 + S / rhoOut;
    f[3*N+id] = f1 - (2/3)*rhoOut*ux;
    f[6*N+id] = f8 - 0.5*(f2-f4) - (1/6)*rhoOut*ux;
    f[7*N+id] = f5 + 0.5*(f2-f4) - (1/6)*rhoOut*ux;
  }
}

and the moving-wall bounce-back in stream():

ftmp[o] = f[OPP[i]*N + id]
        + 6 * W[i] * rho[id] * (EX[i]*wx + EY[i]*wy);   // wx,wy from solid node

All state uses Float64Array; omega = 1/tau < 2; Guo forcing is added consistently to collision and to rho u = Σ f_i e_i + F/2.

3. Verification

node verify.js (≈64 s). Results:

Taylor–Green vortex decay (analytic rate 2νk²)

τ N measured λ theory 2νk² rel. error
0.80 64 1.928203e-3 1.927657e-3 0.028 %
0.51 64 6.427676e-5 6.425524e-5 0.033 %

Plane Poiseuille (analytic u = g/(2ν) Y(H-Y))

driver τ Ma rel. L2
body force (Guo) 0.80 1.7e-2 0.070 %
body force (Guo) 0.51 1.7e-2 0.134 %
Zou–He 0.80 1.7e-2 0.087 %
Zou–He 0.51 1.7e-2 0.145 %

Stability at τ = 0.51 (moving-lid cavity, 6000 steps)

n U Re NaN Ma
64 0.02 372 no 3.2e-2
64 0.05 930 no 7.6e-2
128 0.02 756 no 3.2e-2

Momentum conservation (periodic box, non-zero net momentum)

|ΔP| / |P0| after 10000 steps = 3.2e-11   (Ma = 1.9e-2)

Grid convergence (diffusive scaling, TGV)

N rel. L2
16 2.4306e-2
32 6.0826e-3
64 1.5346e-3
128 3.8171e-4
order 16->32: 1.999 ; 32->64: 1.987 ; 64->128: 2.007

Observed order ≈ 2.0. All spec targets are met: Poiseuille ≤ 2 % L2 (best 0.07 %), TGV rate within 0.03 %, stable at τ = 0.51, momentum drift ~10⁻¹¹ over 10 000 steps, convergence order 2.

Files: ~/lbm/solver.js, ~/lbm/verify.js, ~/lbm/SOLUTION.md.

Evidence & signatures

# Evidence
- Problem class: node20-lattice-boltzmann-d2q9-bgk-bounceback-taylor-green
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-03T22:41:30.114Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a 2D lattice Boltzmann D2Q9 BGK solver in JavaScript using typed arrays for the distribution functions, with half-way bounce-back walls, Zou-He inlet/outlet boundaries, and a moving lid. Validate against the analytical Taylor-Green vortex decay rate and a plane Poiseuille profile within 2% relative L2 error while remaining stable at relaxation times down to 0.51. Report Mach number, momentum conservation drift over 10000 steps, and the grid convergence order across two resolutions.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "node20-lattice-boltzmann-d2q9-bgk-bounceback-taylor-green", "provider": "openrouter", "solved_at": "2026-10-03T22:41:30.114Z", "version": "20"}
Generated from the verified corpus · MIT licensedBack to the catalog