Cluster 3 Potential Flow PINN
A Physics-Informed Neural Network predicting velocity potential (φ) and stream function (ψ) for 2D potential flow around a rotating cylinder — the pre-image, under the exact Joukowski conformal map, of a Joukowski airfoil — at any angle of attack. Trained as part of the 9-cluster Scientific AI Cluster Orchestration Framework, which pairs this network with an exact symbolic ("Symetria") Joukowski solver and two physics-grounded safety audits under LangGraph supervision.
Architecture
| Input | (x, y, angle_of_attack_deg) — 3 features |
| Output | (φ, ψ) — velocity potential, stream function |
| Hidden layers | 5 × 128 neurons, Tanh activation |
| Parameters | ~66,000 |
| Circulation term | Computed analytically (exact, via atan2), not learned |
The network predicts only the smooth, single-valued doublet+uniform-flow
part of φ (plus ψ). The multi-valued vortex/circulation contribution to φ —
(Γ(α)/2π)·θ, a genuine topological feature of point-vortex potentials that
no smooth feedforward network can represent without corrupting its local
gradient near the branch cut — is added analytically. This decomposition was
a real architecture fix made during training (see Training History below),
not a design chosen upfront.
Quickstart
import torch
from huggingface_hub import hf_hub_download
from modeling import PotentialFlowPINN
ckpt_path = hf_hub_download("dave1368/cluster-03-potential-flow-pinn", "potential_flow_pinn.pt")
# weights_only=False: the checkpoint is a dict with metadata (model_state_dict
# plus training info), not a bare tensor, so torch's default-safe loader can't
# be used as-is. Only do this for checkpoints you trust the source of.
checkpoint = torch.load(ckpt_path, map_location="cpu", weights_only=False)
model = PotentialFlowPINN()
model.load_state_dict(checkpoint["model_state_dict"]) # checkpoint also carries training-time loss history, see training_metrics.json
model.eval()
# coords: (x, y, angle_of_attack_degrees)
coords = torch.tensor([[1.5, 0.5, 5.0]])
phi_psi = model(coords)
print(phi_psi) # tensor([[phi, psi]])
Training data
Exact closed-form labels — no synthetic correlation needed. For each sampled
(r, θ, α), the exact potential-flow-around-a-rotating-cylinder solution
(uniform flow + doublet + circulation, with circulation set by the Kutta
condition at the trailing edge) provides the ground-truth (φ, ψ):
- 60,000 training points, 10,000 validation points
- Domain:
r ∈ [1, 5·R],θ ∈ [-π, π],α ∈ [-10°, 25°] - Final train loss: 3.80e-05 · Final val loss: 3.76e-05 (MSE, 3000 epochs)
Training history — a real architecture bug, not just hyperparameter tuning
The network originally took only (x, y), so angle of attack had nowhere to
go as an input — the AoA slider in the app changed nothing about the
computed flow, a genuine dead-input bug. Fixed by extending the network to
(x, y, α) → (φ, ψ).
That alone wasn't enough: training on the raw (φ, ψ) labels directly still
produced large surface-velocity errors, traced to φ's multi-valued
circulation term — moving its branch cut from θ=0 to θ=π just relocated a
band of large error from one side of the cylinder to the other, rather than
removing it, confirming the issue was structural (a smooth network cannot
represent a jump discontinuity's local gradient) rather than a training
artifact. Fixed via the exact-analytic + learned-smooth-residual
decomposition described above.
Validated against classical sources (post-deployment finding)
Cross-checked against Euler (1757), d'Alembert (1752), and Joukowski (1910) — the papers cited in this cluster's Master Specification. Full data tables in the Space README. Headline finding: independently re-deriving d'Alembert's zero-drag theorem exposed a genuine bug in how drag itself was computed downstream of this model (a force-rotation-into-flow-aligned-frame omission in the orchestrator, not a defect in this checkpoint) — this network's own true drag error, once correctly measured, is small and roughly constant across the full AoA range (0.13–0.35) rather than growing with angle as originally appeared.
| Check | Result |
|---|---|
| Joukowski (1910) exact mapping | Exact solver matches independent recomputation to floating-point precision |
| Kutta condition self-consistency | v_θ ≈ 0 (numerical precision) at trailing edge, α ∈ [−10°, 25°] |
| d'Alembert (1752) drag, exact solution (flow-aligned) | ≈0 at every tested angle, as required |
| d'Alembert (1752) drag, this network (flow-aligned) | 0.13–0.35 across α ∈ [−10°, 25°] |
| Pointwise φ, ψ vs. exact (32-pt grid, α=0°/15°) | Mean |err|: φ=1.06, ψ=1.23 (field magnitudes ~20–115) |
| Euler/Laplace ∇²φ (r ≥ 1.5, away from body) | Small relative to local velocity-gradient scale |
Limitations
- Pointwise accuracy degrades near the airfoil surface (r ≈ R), especially right at the stagnation point — the region of steepest field curvature.
- Inviscid potential flow cannot represent boundary layers, separation, or stall — by construction (this is the content of d'Alembert's Paradox).
- The d'Alembert audit doesn't really grade how well this model learned the physics — a barely-trained version of this network, whose output changes very little from point to point, would also pass, since it too produces near-zero drag. What the audit is actually good at is catching a badly broken model, not measuring how accurate a working one is.