Environment: Node.js 20+, plain JavaScript, typed arrays.
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:
Environment: Node.js 20+, plain JavaScript, typed arrays.
The failures are all boundary/precision issues, not the BGK kernel:
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
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.
Float32Array destroys low-tau stability and momentum conservationAt 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).
tau Poiseuille needs O(H²/ν) stepsAt 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.
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).
solver.jsFull 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.
node verify.js (≈64 s). Results:
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 % |
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 % |
| 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 |
|ΔP| / |P0| after 10000 steps = 3.2e-11 (Ma = 1.9e-2)
| 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 - 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"}