Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
130cc9b
Add RANS CFD integration via Flow360
aeronauty Mar 21, 2026
e11ca2b
chore: update package-lock.json after npm install
aeronauty Mar 21, 2026
7b870bb
Improve mesh quality: hybrid O-grid with multi-body support
aeronauty Mar 21, 2026
ebfcfb1
Rewrite mesh generator: C-grid topology with wake cut
aeronauty Mar 21, 2026
856e764
Add CSM-based meshing via Flow360 automated mesher
aeronauty Mar 21, 2026
e5b4172
Use Project.run_case for automated mesh + solve pipeline
aeronauty Mar 21, 2026
6a64146
Fix: use AerospaceCondition.from_mach_reynolds (correct API name)
aeronauty Mar 21, 2026
2b9c73e
Fix face grouping: use geometry['airfoil'] not geometry['*']
aeronauty Mar 21, 2026
8dafa76
Add parallel polar_rans via run_rans_batch
aeronauty Mar 21, 2026
f708f76
Fix parallel polar: serialize submissions, parallelize waits
aeronauty Mar 21, 2026
84bf06f
Fix batch wait: replace case.wait() with manual polling
aeronauty Mar 21, 2026
5d10ff0
Fix crossflow: default to single-cell hex mesh, not auto-mesher
aeronauty Mar 22, 2026
b06c028
Fix parallel polar: serialize submissions, parallelize waits
aeronauty Mar 22, 2026
0d56c81
Fix face grouping: use geometry['airfoil'] not geometry['*']
aeronauty Mar 22, 2026
83e5809
Add parallel polar_rans via run_rans_batch
aeronauty Mar 22, 2026
db09727
Add gmsh-based mesh generation for proper pseudo-2D RANS
aeronauty Mar 22, 2026
0cfb38a
Improve mesh and config: boundary naming consistency, CSM face tagging
aeronauty Mar 22, 2026
c5b3433
Fix TE cell crossing: replace normal-offset with TFI blending
aeronauty Mar 22, 2026
32bbbb4
C-grid with proper wake: TFI outer boundary preserves wake opening
aeronauty Mar 22, 2026
18eb65f
Rewrite gmsh mesher: C-block transfinite structured mesh
aeronauty Mar 22, 2026
efd37af
Rewrite gmsh mesher: 5-block C-grid with LE split (wuFoil topology)
aeronauty Mar 22, 2026
aa53267
Fix boundary edge extraction for new 5-block topology
aeronauty Mar 22, 2026
aaa6d8e
Fix open-TE airfoil: separate upper/lower TE points
aeronauty Mar 22, 2026
1e1df77
Fix open-TE airfoil: separate upper/lower TE points
aeronauty Mar 22, 2026
bdd7890
Rewrite gmsh mesher: 5-block C-grid with LE split (wuFoil topology)
aeronauty Mar 22, 2026
0487409
Fix open-TE airfoil: separate upper/lower TE points
aeronauty Mar 22, 2026
7d91db7
Fork gmshairfoil2d: preserve open trailing edges
aeronauty Mar 22, 2026
35db896
Fix open-TE closure: use tiny downstream offset instead of midpoint
aeronauty Mar 22, 2026
83211ef
Integrate gmsh C-grid pipeline with proper coordinate handling
aeronauty Mar 22, 2026
898a4f0
Add volume output (primitiveVars) for spanwise velocity validation
aeronauty Mar 22, 2026
22ef7f5
Add temp file patterns to gitignore
aeronauty Mar 22, 2026
1b96e83
Add 6-block blunt TE topology to forked gmshairfoil2d
aeronauty Mar 23, 2026
25ca5ac
Add mesh quality checks: volume, aspect ratio, skewness
aeronauty Mar 23, 2026
a38907d
Fix farfield corner: spline arc + degenerate cell removal
aeronauty Mar 23, 2026
592e3e3
Add FlexFoil-guided mesh refinement for CSM auto-mesh path
aeronauty Mar 23, 2026
4f1737b
Fix CL extraction: try CL first, then CFy fallback
aeronauty Mar 23, 2026
1f161a2
Remove RANS code from open source flexfoil
aeronauty Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,7 @@ packages/flexfoil-python/src/flexfoil/_static/
packages/flexfoil-python/dist/
packages/flexfoil-python/*.png
packages/flexfoil-python/*.csv
flow360_case.user.log
surfaces.tar.gz
*.pvtu
*.vtu
4 changes: 2 additions & 2 deletions flexfoil-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

230 changes: 200 additions & 30 deletions flexfoil-ui/src/components/panels/SolvePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { runSweep, type SweepConfig, type SweepRunData } from '../../lib/sweepEn
import type { PolarPoint, SweepAxis, SweepParam } from '../../types';
import type { RunInsert } from '../../lib/storageBackend';
import { parseSweepValues, formatSweepValues } from '../../lib/parseSweepValues';
import { isLocalMode } from '../../lib/storageBackend';

type SolveOrCacheResult = {
result: AnalysisResult | null;
Expand Down Expand Up @@ -164,6 +165,16 @@ export function SolvePanel() {
const polar = useMemo(() => lastSeries?.points ?? [], [lastSeries]);

const isViscous = solverMode === 'viscous';
const isRans = solverMode === 'rans';

// RANS state
const [ransResult, setRansResult] = useState<{
cl: number; cd: number; cm: number; alpha: number;
converged: boolean; success: boolean; error?: string | null;
case_id?: string; cd_pressure?: number; cd_friction?: number;
} | null>(null);
const [ransStatus, setRansStatus] = useState<string | null>(null);
const [ransJobId, setRansJobId] = useState<string | null>(null);

const serializePoints = useCallback((points: { x: number; y: number; s?: number; surface?: 'upper' | 'lower' }[]) => {
return JSON.stringify(points.map((point) => ({
Expand Down Expand Up @@ -831,6 +842,81 @@ export function SolvePanel() {
maxIterations, solverMode, geometryDesign.flaps, name, isViscous, upsertPolar, addRun, addRunBatch,
jobDispatch, jobComplete, jobUpdate]);

// --------------- RANS analysis ---------------

const runRansAnalysis = useCallback(async () => {
if (panels.length < 3) return;
setIsRunning(true);
setError(null);
setRansResult(null);
setRansStatus('Submitting...');

const coordsJson = JSON.stringify(panels.map((p) => ({ x: p.x, y: p.y })));

try {
const resp = await fetch('/api/rans/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
coordinates_json: coordsJson,
alpha: targetAlpha,
reynolds: reynolds,
mach: mach || 0.2,
airfoil_name: name,
}),
});
const data = await resp.json();
if (!resp.ok) {
setError(data.error || 'Failed to submit RANS case');
setIsRunning(false);
setRansStatus(null);
return;
}
const jobId = data.job_id;
setRansJobId(jobId);
setRansStatus('Submitted — generating mesh...');

// Poll for completion
const pollInterval = setInterval(async () => {
try {
const statusResp = await fetch(`/api/rans/status/${jobId}`);
const statusData = await statusResp.json();
const status = statusData.status;

if (status === 'Generating mesh') setRansStatus('Generating mesh...');
else if (status === 'Uploading mesh') setRansStatus('Uploading mesh to Flow360...');
else if (status === 'Submitting case') setRansStatus('Submitting case...');
else if (status === 'Running RANS solver' || status === 'running') setRansStatus('Solving (this may take a few minutes)...');
else if (status === 'Fetching results') setRansStatus('Fetching results...');
else if (status === 'complete' || status === 'failed') {
clearInterval(pollInterval);
setIsRunning(false);
if (statusData.result) {
setRansResult(statusData.result);
if (statusData.result.success) {
setRansStatus('Complete');
} else {
setRansStatus(null);
setError(statusData.result.error || 'RANS case failed');
}
} else {
setRansStatus(null);
setError('RANS case failed');
}
} else {
setRansStatus(`${status}...`);
}
} catch {
// Silently retry
}
}, 3000);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Failed to submit RANS case');
setIsRunning(false);
setRansStatus(null);
}
}, [panels, targetAlpha, reynolds, mach, name]);

// --------------- derived ---------------

const clAlpha = useMemo(() => {
Expand Down Expand Up @@ -868,15 +954,27 @@ export function SolvePanel() {
>
Viscous
</button>
{isLocalMode() && (
<button
onClick={() => setSolverMode('rans')}
className={solverMode === 'rans' ? 'active' : ''}
style={{ flex: 1 }}
title="RANS CFD via Flow360 (cloud)"
>
RANS
</button>
)}
</div>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginTop: '2px' }}>
{isViscous
? 'XFOIL viscous solver with boundary layer coupling'
: 'Linear-vorticity panel method (CD = 0)'}
{isRans
? 'RANS CFD via Flow360 (cloud compute)'
: isViscous
? 'XFOIL viscous solver with boundary layer coupling'
: 'Linear-vorticity panel method (CD = 0)'}
</div>
</div>

{isViscous && (
{(isViscous || isRans) && (
<div className="form-group">
<div className="form-label">Reynolds Number</div>
<input
Expand All @@ -892,6 +990,23 @@ export function SolvePanel() {
</div>
)}

{isRans && (
<div className="form-group">
<div className="form-label">Mach Number</div>
<input
type="number"
value={mach || 0.2}
onChange={(e) => setMach(Math.max(0.01, Math.min(0.9, parseFloat(e.target.value) || 0.2)))}
step={0.01}
min={0.01}
max={0.9}
/>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginTop: '2px' }}>
Required for compressible RANS (SA turbulence model)
</div>
</div>
)}

{isViscous && (
<div className="form-group">
<button
Expand Down Expand Up @@ -959,51 +1074,73 @@ export function SolvePanel() {
<div className="form-group" data-tour="solve-alpha">
<div className="form-label">Single Point</div>

<div style={{ display: 'flex', gap: '4px', marginBottom: '8px' }}>
<button
onClick={() => setRunMode('alpha')}
className={runMode === 'alpha' ? 'active' : ''}
style={{ flex: 1 }}
>
Run to α
</button>
<button
onClick={() => setRunMode('cl')}
className={runMode === 'cl' ? 'active' : ''}
style={{ flex: 1 }}
>
Run to CL
</button>
</div>
{!isRans && (
<div style={{ display: 'flex', gap: '4px', marginBottom: '8px' }}>
<button
onClick={() => setRunMode('alpha')}
className={runMode === 'alpha' ? 'active' : ''}
style={{ flex: 1 }}
>
Run to α
</button>
<button
onClick={() => setRunMode('cl')}
className={runMode === 'cl' ? 'active' : ''}
style={{ flex: 1 }}
>
Run to CL
</button>
</div>
)}

<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<label style={{ fontSize: '12px', minWidth: '60px' }}>
{runMode === 'alpha' ? 'Alpha (°):' : 'Target CL:'}
{isRans ? 'Alpha (°):' : runMode === 'alpha' ? 'Alpha (°):' : 'Target CL:'}
</label>
<input
type="number"
value={runMode === 'alpha' ? targetAlpha : targetCl}
value={isRans ? targetAlpha : (runMode === 'alpha' ? targetAlpha : targetCl)}
onChange={(e) => {
const val = parseFloat(e.target.value);
if (runMode === 'alpha') setTargetAlpha(val);
if (isRans || runMode === 'alpha') setTargetAlpha(val);
else setTargetCl(val);
}}
step={runMode === 'alpha' ? 0.5 : 0.1}
step={isRans || runMode === 'alpha' ? 0.5 : 0.1}
style={{ flex: 1 }}
/>
<button
onClick={runAnalysis}
disabled={isRunning || !isWasmReady()}
style={{ minWidth: '60px' }}
onClick={isRans ? runRansAnalysis : runAnalysis}
disabled={isRunning || (!isRans && !isWasmReady())}
style={{ minWidth: isRans ? '100px' : '60px' }}
data-tour="solve-run"
>
{isRunning ? '...' : 'Run'}
{isRunning ? '...' : isRans ? 'Run RANS ☁️' : 'Run'}
</button>
</div>

{/* RANS progress indicator */}
{isRans && ransStatus && (
<div style={{
fontSize: '11px',
color: 'var(--accent-primary)',
marginTop: '8px',
padding: '6px 8px',
background: 'var(--bg-secondary)',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
{isRunning && (
<span style={{ animation: 'spin 1s linear infinite', display: 'inline-block' }}>⟳</span>
)}
{ransStatus}
</div>
)}
</div>

{/* Single Point Results */}
{result && result.success && (
{/* Single Point Results — XFOIL */}
{!isRans && result && result.success && (
<div className="form-group">
<div className="form-label">Results</div>
<div style={{
Expand All @@ -1028,6 +1165,39 @@ export function SolvePanel() {
</div>
)}

{/* Single Point Results — RANS */}
{isRans && ransResult && ransResult.success && (
<div className="form-group">
<div className="form-label">RANS Results</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '8px',
}}>
<ResultCard label="CL" value={ransResult.cl.toFixed(4)} />
<ResultCard label="CD" value={ransResult.cd.toFixed(5)} />
<ResultCard label="CM" value={ransResult.cm.toFixed(4)} />
</div>
{ransResult.cd_pressure != null && ransResult.cd_friction != null && (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '8px',
marginTop: '8px',
}}>
<ResultCard label="CD_p" value={ransResult.cd_pressure.toFixed(5)} />
<ResultCard label="CD_f" value={ransResult.cd_friction.toFixed(5)} />
</div>
)}
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginTop: '8px' }}>
Flow360 RANS (SA) at α={ransResult.alpha.toFixed(1)}°, Re={ransResult.reynolds.toExponential(1)}, M={ransResult.mach.toFixed(2)}
{ransResult.case_id && (
<span> · Case: {ransResult.case_id.slice(0, 8)}</span>
)}
</div>
</div>
)}

{/* Parameter Sweep */}
<div className="form-group" style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border-color)' }} data-tour="solve-polar">
<div className="form-label">Parameter Sweep</div>
Expand Down
2 changes: 1 addition & 1 deletion flexfoil-ui/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface AirfoilPoint extends Point {

/** Control modes for airfoil manipulation */
export type ControlMode = 'parameters' | 'camber-spline' | 'thickness-spline' | 'inverse-design' | 'geometry-design';
export type SolverMode = 'viscous' | 'inviscid';
export type SolverMode = 'viscous' | 'inviscid' | 'rans';
export type RunMode = 'alpha' | 'cl';
export type AxisVariable = 'alpha' | 'cl' | 'cd' | 'cm' | 'ld'
| 'reynolds' | 'mach' | 'ncrit' | 'flapDeflection' | 'flapHingeX';
Expand Down
1 change: 1 addition & 0 deletions packages/flexfoil-python/src/flexfoil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from flexfoil.database import RunDatabase, get_database
from flexfoil.polar import PolarResult


__all__ = [
"Airfoil",
"BLResult",
Expand Down
Loading
Loading