Skip to content

Add RANS CFD integration via Flow360#10

Open
aeronauty-flexcompute wants to merge 37 commits intomainfrom
claude/recursing-rhodes
Open

Add RANS CFD integration via Flow360#10
aeronauty-flexcompute wants to merge 37 commits intomainfrom
claude/recursing-rhodes

Conversation

@aeronauty-flexcompute
Copy link
Copy Markdown
Collaborator

Summary

  • FlexFoil → Flow360 RANS pipeline: foil.solve_rans(alpha=5, Re=6e6, mach=0.15) generates a pseudo-3D hex mesh, uploads to Flow360, runs SA-RANS, and returns CL/CD/CM
  • Pure numpy mesh generation: hybrid O-grid with normal-offset BL + TFI outer layers, no external meshing dependencies
  • Multi-body ready: mesh generator accepts single or multi-element airfoil coordinates (slat/main/flap)
  • Full stack: Python API (solve_rans, polar_rans), REST endpoints with SSE progress, web UI "RANS" button

Motivation

From #cfd_dev_application discussion: FlexFoil users doing pip install flexfoil want to click "run RANS" without understanding pseudo-3D extrusion, boundary conditions, or Flow360 setup. This PR makes it one function call.

Verified on Flow360

NACA 0012, α=5°, Re=6M, M=0.15 (240 panels, 100 BL layers, gr=1.08):

Result Expected
CL 0.567 ~0.55
CDf 0.006 ~0.006 ✅
CDp 0.029 ~0.001 ⚠️

CL and skin friction are accurate. Pressure drag is elevated due to the O-grid topology at the TE — a C-grid or Flow360's automated meshing would fix this in a follow-up.

New files

  • rans/__init__.pyRANSResult, RANSPolarResult dataclasses
  • rans/mesh.py — structured O-grid mesh gen + UGRID writer (multi-body extensible)
  • rans/config.py — Flow360 case JSON builder
  • rans/flow360.py — mesh upload, case submission, polling, result fetch

Test plan

  • Mesh generation: zero negative volumes verified for single and multi-body
  • TypeScript compiles clean (tsc --noEmit)
  • End-to-end Flow360 run: NACA 0012 α=0° (CL≈0) and α=5° (CL=0.567)
  • Web UI RANS button (manual test in server mode)
  • Multi-element RANS case (flap configuration)

🤖 Generated with Claude Code

aeronauty and others added 30 commits March 21, 2026 16:32
FlexFoil users can now run RANS simulations on their airfoils via Flow360's
cloud solver with a single function call:

    foil.solve_rans(alpha=5.0, Re=6e6, mach=0.15)

The pipeline generates a pseudo-3D hex mesh (structured O-grid with hybrid
normal-offset/TFI), writes it as UGRID binary, uploads to Flow360, runs
steady SA-RANS with symmetry BCs on the spanwise faces, and returns
CL/CD/CM. No external meshing dependencies — pure numpy.

New files:
- rans/__init__.py: RANSResult, RANSPolarResult dataclasses
- rans/mesh.py: structured mesh generation + UGRID writer
- rans/config.py: Flow360 case JSON config builder
- rans/flow360.py: mesh upload, case submission, polling, result fetch

Modified:
- airfoil.py: solve_rans() and polar_rans() methods on Airfoil
- server.py: /api/rans/submit, /status, /result endpoints with SSE
- SolvePanel.tsx: RANS button (server mode), progress indicator, results
- types/index.ts: SolverMode extended with 'rans'
- pyproject.toml: [rans] optional dependency group (flow360client)

Verified end-to-end: NACA 0012 at α=5°, Re=6M, M=0.15 → CL=0.709

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrote the mesh generator to use a refined hybrid O-grid approach:
- Normal-offset for inner BL layers (proper y+ resolution)
- Smooth TFI blend for outer layers (guaranteed positive volumes)
- Transition at 10% chord (up from 5%) for better near-field resolution
- Multi-body support: coords can be a list of body coordinate lists
  for slat/main/flap configurations
- Updated defaults: 100 layers, gr=1.08, farfield=50c

Results improved significantly on NACA 0012 at α=5°, Re=6M:
  CL:  0.709 → 0.567 (expected ~0.55)
  CDf: 0.008 → 0.006 (expected ~0.006) ← on target
  CDp: 0.079 → 0.029 (still high, needs C-grid for wake)
  L/D: 8.2 → 16.4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace O-grid with proper C-grid:
- Split airfoil at TE, extend wake cut downstream
- No more degenerate TE cells (upper/lower mesh lines don't converge)
- Full domain coverage (geometric growth fills entire farfield, not just BL)
- Wake boundary with Freestream BC for clean outflow

Also:
- Upgrade to modern flow360 SDK (v25+) for Project view visibility
- Fall back to flow360client if modern SDK unavailable
- Config uses boundary names (not integer tags) for modern SDK compat
- Add wake BC to case config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Primary path now generates a CSM geometry file from airfoil coordinates
and lets Flow360's mesher handle surface + volume meshing. This produces
proper BL meshes with wake refinement — fixing the TE cell quality and
domain coverage issues from the hand-rolled O-grid/C-grid.

The C-grid UGRID approach is kept as a fallback (use_auto_mesh=False).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the manual CSM → surface mesh → volume mesh → case pipeline
with the modern Project API: Project.from_geometry() + run_case().
Flow360 handles farfield generation, BL meshing, wake refinement, and
solving in a single call.

Based on the official 2D CRM tutorial pattern:
- AutomatedFarfield(method="quasi-3d") for quasi-2D setup
- SimulationParams with meshing, models, operating_condition
- project.run_case() handles everything

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The AutomatedFarfield quasi-3d method deletes the symmetry plane faces
(end caps of the extrusion) and replaces them with its own. Using
geometry['*'] included those faces, causing validation errors.

Now:
- CSM adds faceName attribute to all faces
- group_faces_by_tag('faceName') groups them
- geometry['airfoil'] selects only the airfoil skin faces
- farfield.symmetry_planes provides the symmetry BCs

Also bump solver version to release-25.8 (SDK v25.8.7 requires it).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
polar_rans() now submits all alpha cases concurrently to Flow360
(up to max_workers=4 at a time) instead of sequentially. A 9-point
polar takes ~8 min instead of ~45 min.

New function run_rans_batch() handles:
- Phase 1: Submit all CSM geometries in parallel via ThreadPoolExecutor
- Phase 2: Wait for all cases to complete in parallel
- Results returned in original alpha order

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Flow360 SDK uses Rich live displays for upload progress bars,
which aren't thread-safe ("Only one live display may be active").
Fix: submit cases sequentially (each starts solving immediately on
the cloud), then wait for all in parallel via ThreadPoolExecutor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
case.wait() uses Rich progress bars internally, which crash in
ThreadPoolExecutor ("Only one live display may be active"). Replace
with a simple polling loop using case.get_info() that checks all
pending cases every 15s. All cases still solve concurrently on
Flow360's cloud.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flow360's automated mesher (quasi-3d mode) generates multiple spanwise
cells with triangular prisms, which allows spurious crossflow — exactly
the issue Mike Park warned about. Switch default to use_auto_mesh=False:
generate a single-cell-deep hex mesh locally for true pseudo-2D.

Also fix batch polar:
- Generate mesh ONCE, upload ONCE, submit N cases against same mesh
- Use legacy SDK for polling + force fetch (thread-safe, no Rich)
- Add _fetch_results_legacy() to avoid Rich progress bar conflicts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use legacy SDK (flow360client) for both mesh upload and case submission
in run_rans_batch. The modern SDK's VolumeMesh.from_file creates mesh
IDs in vm-* format that aren't compatible with the V1 Case.create API,
causing "Failed to get input mesh directory path" errors.

Legacy SDK mesh IDs work with NewCase correctly. Cases still solve in
parallel on Flow360's cloud — only the submission is serialized.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add wake boundary tag (5) to the legacy SDK boundary mapping.
Without this, the solver rejects the case with "Boundary 5 in
the mesh file is not defined in the Flow360 case JSON."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch batch polar from local UGRID mesh (which has degenerate cells
near the TE causing divergence) to the CSM + automated meshing path.
Each alpha gets its own Project with auto-meshed geometry. Sequential
submission (Rich isn't thread-safe), parallel solve + polling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gmsh generates high-quality BL meshes with structured quad layers
near the airfoil and unstructured quads in the farfield. The 2D
mesh is then extruded one cell deep in y with volume-orientation
checks to ensure all hexes have positive volume.

Falls back to the algebraic C-grid if gmsh is not installed.

New dependency: gmsh>=4.0 (optional, in [rans] extra).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Config: use Temperature (capital T) for Flow360 compatibility
- Mesh: fix CSM face attribute tagging for automated mesher
- Mesh: improve algebraic C-grid layer blending exponent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The algebraic C-grid used pure normal-offset to grow layers from the
airfoil surface. Near the TE, upper and lower normals point in opposite
directions, causing cell inversion as layers grow.

Replace with Transfinite Interpolation (TFI): blend between the inner
boundary (airfoil + wake) and an outer boundary (circle at farfield).
Since TFI is a convex combination, cells can NEVER cross.

Verified: 0 negative volumes out of 31,800 hex cells (240 panels,
100 BL layers, gr=1.08).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous TFI mapped all points to a circle, collapsing the wake
cut. Now the outer boundary is C-shaped: semicircle around the front
with straight segments extending the wake downstream. Wake gap opens
from 0.0014 at surface to 100.0 at farfield (proper C-grid topology).

Zero negative volumes, BL clustering preserved via TFI s-parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the unstructured BoundaryLayer field approach with a proper
5-block C-grid topology using gmsh's transfinite meshing:

  1. Upper airfoil block (TE→LE along upper surface, radially out)
  2. Lower airfoil block (LE→TE along lower surface, radially out)
  3. Upper wake block (TE upper → downstream exit)
  4. Lower wake block (TE lower → downstream exit)

All blocks use setTransfiniteCurve() with Progression spacing for
BL clustering, and setRecombine() for all-quad elements → all-hex
after extrusion. The wake extends 25 chord lengths downstream with
proper cell clustering near the TE.

Methodology follows the standard C-block approach used in airfoil
CFD (e.g. wuFoil, NASA CFL3D validation grids).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major rewrite following the standard C-block approach:
- Split airfoil into 3 segments: upper aft, LE region, lower aft
- LE gets its own block with "Bump" distribution for good resolution
- Semicircle centered at the LE split point (x=0.05c)
- Wake extends from TE with single centerline, split into top/bottom
- Farfield top/bottom directly above/below TE for orthogonal cells
- All progression directions carefully matched to wuFoil conventions

5 blocks: inlet (LE), top (upper surface), bottom (lower surface),
wake top, wake bottom. All transfinite + recombined = all-hex.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update variable names in wall/farfield edge extraction to match the
new curve names (c_inlet, c_top_line, etc.) and include the LE curve
in wall edges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NACA 0012 has an open trailing edge (y=±0.00126). The previous code
forced both surfaces to share one TE point, pulling the lower surface
spline to the upper TE position and distorting the entire airfoil shape.

Now upper and lower TE are separate gmsh points. The wake splits into
two lines (upper wake from TE_upper, lower wake from TE_lower) that
meet at the wake center exit point. This preserves the correct airfoil
geometry for any TE gap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 6th block (TE closure strip) to close the gap between upper and
lower trailing edge points. NACA 0012 has an open TE with y=±0.00126
at x=1.0 — the mesh now has a thin quad strip connecting these points
to the farfield, making the volume watertight.

Also add c_te_base as a wall boundary so the TE surface is properly
resolved for Cp extraction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adopt the wuFoil mesh topology: close the TE to a single shared point,
use 5 transfinite blocks (inlet, top, bottom, wake_top, wake_bottom).
This eliminates the TE strip and wake gap blocks that caused watertight
failures.

The TE closure moves the trailing edge by ~0.13% chord for open-TE
airfoils (NACA 0012) — negligible for RANS accuracy.

Added automatic watertight check: verifies every interior face is shared
by exactly 2 hexes and every boundary face by exactly 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For open-TE airfoils (NACA 0012), the BSplines now pass through the
true upper and lower TE coordinates, then converge to a shared midpoint
for clean C-grid topology. This preserves the airfoil shape everywhere
except the last ~0.1% chord at the TE tip.

Closed-TE airfoils use the standard wuFoil single-point topology.

Also fixed boundary edge extraction to use the topology-specific
wall_curves/ff_curves lists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upstream gmshairfoil2d always closes open TEs by extending
upper/lower curves to a sharp intersection point. This distorts
the airfoil shape (NACA 0012 loses its finite-thickness TE).

Our fork instead:
- Detects open TE (two distinct points near x_max)
- Keeps both TE points unchanged
- Inserts midpoint as the reference TE for the C-grid topology
- Stores te_upper/te_lower for downstream use (TE block meshing)

Based on gmshairfoil2d v0.2.33 (Apache 2.0 license).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Place the TE closure point at x = te.x + 0.1*gap_height (just 0.025%
chord downstream for NACA 0012). This avoids degenerate geometry from
three coincident x=1.0 points while preserving the airfoil shape.

The previous midpoint approach caused index issues in gen_skin_struct
and zero-division in CType normal computation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hand-rolled C-grid mesh generator with the forked
gmshairfoil2d pipeline, which produces verified watertight meshes.

Key changes:
- mesh.py: generate_and_write_mesh_gmsh() now uses the forked
  gmshairfoil2d CLI → .geo_unrolled → Extrude{Surface{:}} approach.
  Boundary faces extracted from hex connectivity (guaranteed watertight).
- flow360.py: _submit_modern() uses Project API with SimulationParams.
  betaAngle used for AoA (airfoil in x-y plane). CFy read as lift.
- geometry_def.py: refined open-TE closure with tiny downstream offset.

Verified: NACA 0012, α=5°, Re=6M, M=0.15:
  CL=0.5502, CD=0.01062, CDp=0.00387, CDf=0.00674 (2 min total)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verified across 5 cases:
- Spanwise velocity |w/V|_max = 1.5e-05 (0.0015% of freestream)
- CFz/CFy = 10⁻¹¹ to 10⁻¹⁴ (machine zero crossflow)
- CL within 2.3% of Ladson experiment at α=10°, Re=6M, M=0.3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
aeronauty and others added 7 commits March 22, 2026 22:39
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For open-TE airfoils (like NACA 0012 with TE gap = 0.00252c),
preserve the blunt trailing edge instead of pinching it to a point.
The sharp-TE pinch causes CDp to be 2.5× too high vs experiment.

Changes:
- AirfoilSpline: detect open TE, store te_upper/te_lower separately
- gen_skin_struct: create separate upper/lower splines for blunt TE
- CType: 6-block topology with TE gap block (F) between C and D
- mesh.py: call gmshairfoil2d classes directly (bypass broken
  read_airfoil_from_file), write geo_unrolled for clean extrusion
- Front spline: proper k1/k2 computation for Selig-ordered points

Status: 6-block mesh generates correctly (44K hexes, 0 negative volumes).
Flow360 solver diverges at farfield corner — investigating cell quality
at arc-to-rectangle junction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Checks every hex for negative volume, aspect ratio, and equiangle
skewness before writing UGRID. Logs warnings for:
- Any negative volume cells
- Aspect ratio > 100,000
- Skewness > 0.95

Identified root cause of Flow360 divergence: farfield corner cells
at the arc-to-rectangle junction have infinite aspect ratio and
0.998 skewness (nearly degenerate).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace circle arc with spline for smoother C-shape transition
- Offset p1/p7 from boundary to reduce corner cell skewness
- Remove degenerate hexes (zero-length edges) before UGRID write
- Quality checks now report after degenerate cell cleanup

Root cause: gmsh transfinite meshing creates collapsed cells at the
arc-to-line junction (p7) where two curves share an endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Before submitting to Flow360, run FlexFoil's XFOIL solver to extract:
- Wake thickness at TE (δ* upper + δ* lower)
- Transition locations (x_tr_upper, x_tr_lower)

These guide Flow360's volume mesher with:
- Wake refinement box: 3c downstream, width = 4× wake thickness
- Transition refinement box: 0.2c centered at x_tr_upper

Also add clean C-grid generator (cgrid.py) as alternative to gmshairfoil2d.
Uses pure numpy TFI with normal extrusion + farfield blending.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CSM path has airfoil in x-z plane (CL is correct). gmsh path has
airfoil in x-y plane (CFy is the lift). Previously tried CFy first
which gave 0 for CSM cases.

Also fix Box creation: units must be inside SI_unit_system context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RANS CFD integration has been moved to the private flexfoil-rans
package (flexcompute/flex). The open source flexfoil package now
contains only the XFOIL-faithful solver and web UI.

Removed:
- rans/ subpackage (mesh gen, flow360 integration, config)
- solve_rans() and polar_rans() methods from Airfoil
- [rans] optional dependency group from pyproject.toml

The RANS functionality is now accessed via:
    from flexfoil_rans import solve_rans
    result = solve_rans(foil, alpha=5, Re=6e6)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants