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)
| position | x, y |
| inlet velocity | vx, vy |
| distance to surface | sdf |
| surface normal | nx, ny |
Outputs (4)
| velocity | vx, vy |
| pressure | p / ρ |
| 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:
- Full task — 800 training simulations instead of the 200-sim
scarcesubset. - Full resolution — every mesh point per simulation per epoch (~180k), not a 16–32k subsample.
- Multi-simulation batching and larger model capacity.
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.