A wind tunnel in the browser. Aeolus solves 2D incompressible flow around bodies in real time with the lattice Boltzmann method, running entirely in WebGPU compute shaders, and measures the forces on them: drag, lift, shedding frequency, and full parameter sweeps.
Open the tunnel (needs WebGPU)
Scenes: cylinder, NACA 4-digit airfoil, stalled wing, spinning cylinder (Magnus effect), flat plate, tandem cylinders, tube bank, Ahmed body, backward-facing step, empty sandbox.
| Readout | How |
|---|---|
| CD, CL | Momentum exchange on every solid cell, every frame, averaged over the last 8 convective times |
| CL′ rms | Fluctuating lift, i.e. how hard the wake is shedding |
| St | Shedding frequency from mean crossings of the lift signal, St = fL/U |
| xr/h | Reattachment length behind the step, from the sign of the near-wall velocity |
| Theory | Thin airfoil theory, potential-flow Magnus lift, or the Williamson & Brown St(Re) fit, for the current setting |
Sweeps step a parameter through a range and plot the averaged coefficients against it, with the theory curve dashed:
- Airfoil and stalled wing: CL and CD against angle of attack, with thin airfoil theory 2π(α − α₀) and α₀ computed from the camber line
- Cylinder: Strouhal number and CD against Reynolds number
- Magnus effect: CL against spin ratio ΩR/U, against Kutta–Joukowski 2π·ΩR/U
- Flat plate: against angle to the flow
- Ahmed body: against rear slant angle
Force histories and sweep results export as CSV.
Some numbers it reproduces at the default 1024 × 384 grid:
| Case | Aeolus | Reference |
|---|---|---|
| Cylinder, Re 160: St | 0.199 | 0.186 (Williamson & Brown fit, unconfined; walls here add ~12% blockage) |
| Cylinder, Re 160: CD | 1.50 | ≈ 1.3 unconfined, higher with blockage |
| Spinning cylinder, Re 100, ΩR/U = 1: CL | 2.7 | ≈ 2.5 (published 2D laminar simulations) |
| Spinning cylinder, Re 100, ΩR/U = 2 | shedding stops, CL 6.4 | shedding suppressed above ΩR/U ≈ 1.9 |
| Input | Action |
|---|---|
| Drag (Move) | Move the body |
| Drag (Draw / Erase) | Paint or erase walls; right-drag always erases |
| Space | Pause / play |
| R | Reset the flow |
| 1 – 5 | Smoke, vorticity, speed, pressure, streamlines |
| M / D / E | Move, draw, erase |
| T | Tracer particles |
| ↑ / ↓ | Step the angle of attack, spin ratio or slant (Shift: ×4) |
| C | Clear drawing |
| S | Save a screenshot |
The URL keeps the scene and its settings (?scene=airfoil&naca=4412&alpha=8&re=2000), so a setup can be shared as a link. ?res=512|768|1024|1536|2048 sets the grid width; the height is 3/8 of it.
Every cell carries nine populations fi, one for each velocity ci of the D2Q9 lattice. A time step streams each population to its neighbour and relaxes it towards the local equilibrium (BGK):
Density and velocity are moments, ρ = Σfi and ρu = Σcifi. At low Mach number this recovers the Navier–Stokes equations with kinematic viscosity ν = (τ − ½)/3, so for a body of length L in a stream U the relaxation time follows from the Reynolds number: τ = 3UL/Re + ½.
Turbulence model. As Re grows τ approaches ½ and BGK becomes unstable. A Smagorinsky large-eddy model adds an eddy viscosity νt = (CsΔ)²|S|. The strain rate comes from the non-equilibrium stress Πneq = Σcici(fi − fieq), which is local to the cell, so the effective relaxation time has a closed form:
Boundaries.
- Bodies use halfway bounce-back. A population that would stream out of a solid is replaced by the cell's own outgoing population in the opposite direction. For the spinning cylinder that population gains 6wiρ(ci·uwall).
- The inlet holds the free-stream equilibrium.
- The outlet is zero-gradient, behind a sponge whose extra viscosity damps vortices before they reach it.
- Thin absorbing layers at both ends relax towards the free stream, so the pressure waves from the impulsive start leave instead of ringing.
- Tunnel walls are free-slip (specular reflection) for the open test section, or no-slip for channels.
Forces. Every population about to bounce off a solid transfers momentum ci(fi + fī) to it. One reduction kernel sums this over the whole grid each frame, and only the per-workgroup partial sums are read back.
Visualisation.
- Smoke is a passive scalar advected through the velocity field with a MacCormack scheme. The scheme is clamped to stay monotone, and smoke is injected at an inlet rake and at every body surface.
- Tracers are advected with RK2.
- Streamlines are line integral convolution: noise smeared along the local velocity in the fragment shader.
All quantities are in lattice units. Coefficients are per unit span and t* = tU/L counts convective times. The tunnel is two-dimensional and its walls confine the flow, so expect the right trends and magnitudes rather than free-air 3D values.
| Path | Contents |
|---|---|
src/core/lattice-core.ts |
The GPU lattice: distribution and obstacle buffers, parameters, stepping, force readback |
src/gpu/sim.ts |
The interactive tunnel on top of the core: fields, smoke, tracers, row readback |
src/shaders/lbm.wgsl |
Streaming, boundaries, collision, LES, sponge |
src/shaders/force.wgsl |
Momentum-exchange force reduction |
src/shaders/fields.wgsl |
Macroscopic fields |
src/shaders/transport.wgsl |
Smoke (MacCormack) and tracer advection |
src/shaders/render.wgsl |
Field colouring, LIC, tracer dots |
src/physics/lattice.ts |
D2Q9 constants and a CPU reference solver that mirrors the shader |
src/physics/geometry.ts |
NACA 4-digit outlines, thin airfoil theory, mask rasterization |
src/physics/analysis.ts, sweep.ts |
Coefficients, Strouhal estimation, reattachment, sweeps |
src/scenes.ts |
Scene catalogue |
npm test runs the physics checks against the CPU reference solver with Node's built-in test runner:
- Equilibrium moments
- A uniform stream staying uniform
- Mass conservation
- A channel developing the Poiseuille profile (within 2%)
- Drag and Magnus lift with the right signs
- Absorbing layers damping a pressure pulse
- NACA thickness and camber
- Zero-lift angles against thin airfoil theory (NACA 2412: −2.08°)
- Strouhal estimation from a noisy signal
- Reattachment detection
- The sweep state machine
Requirements:
- Node.js 22.18 or newer (includes npm)
- A browser with WebGPU to run it: a recent Chrome or Edge, Safari 26+, or a recent Firefox
Clone and install dependencies:
git clone https://github.com/ylfeid/aeolus.git
cd aeolus
npm installStart the dev server, then open the URL it prints (usually http://localhost:5173):
npm run devRun the tests:
npm testMake a production build. This type-checks the code and writes the site to dist/:
npm run buildServe that build locally to check it (usually at http://localhost:4173):
npm run previewdist/ is plain static files with relative paths, so it can be uploaded to any static host, including under a subpath.
.github/workflows/deploy.yml runs the tests, builds and publishes to GitHub Pages on pushes to main. Enable it under Settings → Pages → Source: GitHub Actions.
MIT

