How it works

The model treats the flow domain as a point cloud rather than a fixed grid or mesh, which is what lets it generalize to airfoil shapes it has never seen.

The problem

A RANS (Reynolds-Averaged Navier–Stokes) solve computes the steady-state flow field around a shape by iterating a discretized PDE to convergence — accurate, but slow (minutes to hours per case) and tied to a specific mesh. AirfRANS ships 1,000 such solves (OpenFOAM, k-ω SST turbulence closure) over NACA 4/5-digit airfoils, Reynolds 2–6M, angle of attack −5° to 15°. The goal here is a learned function that maps (geometry, Re, AoA) → (velocity, pressure, turbulent viscosity) directly, skipping the solve entirely once trained.

Point-cloud representation

Every mesh node becomes an independent training example with 7 input features and 4 target fields:

Inputs (7)

positionx, y
inlet velocityvx, vy
distance to surfacesdf
surface normalnx, ny

Outputs (4)

velocityvx, vy
pressurep / ρ
turbulent viscosityνₜ

Normals are zero everywhere except exactly on the airfoil surface — and point inward, into the solid, matching a real quirk of how AirfRANS stores them (verified against training data; getting this backwards silently corrupts every surface-adjacent prediction — see the Files page, PROGRESS.md, 2026-07-07 entry, for how that bug was caught).

Two architectures

Per-point MLP

Every point runs through the same feedforward network independently — 5 hidden layers of 256 units, ReLU, ~266k parameters. Simple and fast, but each point has zero awareness of its neighbors. That turns out to matter a lot: lift is dominated by the coarse pressure distribution (which the MLP predicts well), while drag depends on fine near-wall velocity gradients — exactly what a context-free per-point model can't resolve. This is the model that runs in your browser on the Tool page.

GraphSAGE

Same 7→4 shape, but each layer is a message-passing step over a k-nearest-neighbor graph built from the point positions (k=10). With 4 message-passing layers, a point's prediction is informed by neighbors up to 4 hops away — real local geometric context the MLP lacks. This wins decisively on the official benchmark (see Results), but its accuracy is tightly coupled to matching its training-time point density, so it's not used for this page's live in-browser predictions — see the note on the Tool page for why.

Training at scale

The project started on a 16GB Mac mini and was later re-run on an M4 Pro with 64GB unified memory, which changed what was tractable — a scope call Advieek and Claude worked through together as the memory constraints became clear in practice:

Scaling up wasn't free: a full-resolution training run means every batch has a never-before-seen shape (a different combination of variably-sized simulations each step), which turned out to defeat PyTorch's MPS memory allocator — a slow leak that silently killed a 400-epoch run about a day in. The fix ended up being three layers deep: periodic cache-clearing, then a full resumable-checkpoint system (model + optimizer + LR-schedule state, not just weights), then an auto-restart supervisor that resumes from the last checkpoint within seconds of any crash. All of it is browsable on the Files page, with the full incident writeup in PROGRESS.md.

Evaluation

Lift and drag coefficients aren't predicted directly — they're integrated from the predicted surface pressure and velocity gradient, the same way a real RANS post-processor computes them. For the withheld AirfRANS test simulations (which have a real OpenFOAM mesh), the evaluation reuses airfrans's own force-integration code directly, monkey-patching in the model's predictions. For arbitrary airfoils with no mesh (this Tool page), it falls back to a simplified panel method over the parametrized surface curve, ported to JavaScript and verified bit-for-bit against the Python implementation on synthetic test cases.