Skip to content

Commit ff24bf7

Browse files
aeronautyclaude
andcommitted
feat(ui): add Mode 1/2/3 Reynolds type selector to Solve panel
Expose XFOIL's ReType (constant Re, fixed Re√CL, fixed Re·CL) in the UI as a three-button toggle under the Reynolds Number input. Mode 1 is the default. Threads re_type through WASM bindings, sweep engine, and airfoil store. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f49b17a commit ff24bf7

6 files changed

Lines changed: 68 additions & 17 deletions

File tree

crates/rustfoil-wasm/src/lib.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -925,13 +925,22 @@ fn bl_error(message: impl Into<String>) -> BLDistribution {
925925
}
926926
}
927927

928+
fn re_type_from_int(v: u8) -> rustfoil_xfoil::config::ReType {
929+
match v {
930+
2 => rustfoil_xfoil::config::ReType::Type2,
931+
3 => rustfoil_xfoil::config::ReType::Type3,
932+
_ => rustfoil_xfoil::config::ReType::Type1,
933+
}
934+
}
935+
928936
fn faithful_snapshot(
929937
coords: &[f64],
930938
alpha_deg: f64,
931939
reynolds: f64,
932940
mach: f64,
933941
ncrit: f64,
934942
max_iterations: usize,
943+
re_type: u8,
935944
) -> Result<FaithfulSnapshot, String> {
936945
if coords.len() < 6 || coords.len() % 2 != 0 {
937946
return Err("Invalid coordinates: need at least 3 points (6 values)".to_string());
@@ -996,6 +1005,7 @@ fn faithful_snapshot(
9961005
mach,
9971006
ncrit,
9981007
max_iterations,
1008+
re_type: re_type_from_int(re_type),
9991009
..Default::default()
10001010
};
10011011
let oper_result = solve_operating_point_from_state(&mut state, &factorized, &options)
@@ -1141,8 +1151,9 @@ pub fn analyze_airfoil_faithful(
11411151
mach: f64,
11421152
ncrit: f64,
11431153
max_iterations: usize,
1154+
re_type: u8,
11441155
) -> JsValue {
1145-
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) {
1156+
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, re_type) {
11461157
Ok(snapshot) => snapshot.result,
11471158
Err(message) => analysis_error(message),
11481159
};
@@ -1158,7 +1169,7 @@ pub fn get_bl_distribution_faithful(
11581169
ncrit: f64,
11591170
max_iterations: usize,
11601171
) -> JsValue {
1161-
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) {
1172+
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) {
11621173
Ok(snapshot) => rows_to_bl_distribution(
11631174
&snapshot.upper_rows,
11641175
&snapshot.lower_rows,
@@ -1264,7 +1275,7 @@ pub fn get_bl_visualization_faithful(
12641275
ncrit: f64,
12651276
max_iterations: usize,
12661277
) -> JsValue {
1267-
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) {
1278+
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) {
12681279
Ok(snapshot) => build_bl_visualization(&snapshot),
12691280
Err(message) => bl_vis_error(message),
12701281
};
@@ -1442,7 +1453,7 @@ pub fn compute_streamlines_faithful(
14421453
seed_count: u32,
14431454
bounds: &[f64],
14441455
) -> JsValue {
1445-
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) {
1456+
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) {
14461457
Ok(snapshot) => {
14471458
if bounds.len() != 4 {
14481459
StreamlineResult {
@@ -1585,7 +1596,7 @@ pub fn compute_dividing_streamline_faithful(
15851596
max_iterations: usize,
15861597
bounds: &[f64],
15871598
) -> JsValue {
1588-
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) {
1599+
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) {
15891600
Ok(snapshot) => {
15901601
if bounds.len() != 4 {
15911602
DividingStreamlineResult {
@@ -1809,7 +1820,7 @@ pub fn compute_psi_grid_faithful(
18091820
bounds: &[f64],
18101821
resolution: &[u32],
18111822
) -> JsValue {
1812-
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) {
1823+
let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) {
18131824
Ok(snapshot) => {
18141825
if bounds.len() != 4 {
18151826
PsiGridResult {
@@ -1952,7 +1963,7 @@ impl WasmSmokeSystem {
19521963
ncrit: f64,
19531964
max_iterations: usize,
19541965
) {
1955-
if let Ok(snapshot) = faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) {
1966+
if let Ok(snapshot) = faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) {
19561967
let field = FaithfulFlowField::from_snapshot(&snapshot, alpha_deg);
19571968
self.coords = field.nodes.clone();
19581969
self.gamma = field.gamma.clone();
@@ -3503,7 +3514,7 @@ mod tests {
35033514
let paneled_flat = repanel_xfoil(&buffer_flat, 160);
35043515
let alpha_deg = 15.0;
35053516

3506-
let snapshot = faithful_snapshot(&paneled_flat, alpha_deg, 1.0e6, 0.0, 9.0, 100)
3517+
let snapshot = faithful_snapshot(&paneled_flat, alpha_deg, 1.0e6, 0.0, 9.0, 100, 1)
35073518
.expect("faithful snapshot should solve");
35083519
let field = FaithfulFlowField::from_snapshot(&snapshot, alpha_deg);
35093520
let options = StreamlineOptions {

flexfoil-ui/src/components/panels/SolvePanel.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useCaseLogStore } from '../../stores/caseLogStore';
1414
import { analyzeAirfoil, analyzeAirfoilInviscid, isWasmReady, type AnalysisResult } from '../../lib/wasm';
1515
import { useSolverJobStore } from '../../stores/solverJobStore';
1616
import { runSweep, type SweepConfig, type SweepRunData } from '../../lib/sweepEngine';
17-
import type { PolarPoint, SweepAxis, SweepParam } from '../../types';
17+
import type { PolarPoint, SweepAxis, SweepParam, ReType } from '../../types';
1818
import type { RunInsert } from '../../lib/storageBackend';
1919
import { parseSweepValues, formatSweepValues } from '../../lib/parseSweepValues';
2020

@@ -61,6 +61,8 @@ export function SolvePanel() {
6161
setNcrit,
6262
setMaxIterations,
6363
setSolverMode,
64+
reType,
65+
setReType,
6466
upsertPolar,
6567
clearAllPolars,
6668
} = useAirfoilStore();
@@ -178,10 +180,10 @@ export function SolvePanel() {
178180

179181
const runSolver = useCallback((coords: { x: number; y: number }[], alpha: number) => {
180182
if (isViscous) {
181-
return analyzeAirfoil(coords, alpha, reynolds, mach, ncrit, maxIterations);
183+
return analyzeAirfoil(coords, alpha, reynolds, mach, ncrit, maxIterations, reType);
182184
}
183185
return analyzeAirfoilInviscid(coords, alpha);
184-
}, [isViscous, reynolds, mach, ncrit, maxIterations]);
186+
}, [isViscous, reynolds, mach, ncrit, maxIterations, reType]);
185187

186188
/** Cache key uses Re=0/Mach=0/Ncrit=0/maxIter=0 for inviscid to separate caches. */
187189
const cacheRe = isViscous ? reynolds : 0;
@@ -761,6 +763,7 @@ export function SolvePanel() {
761763
ncrit,
762764
maxIterations,
763765
solverMode,
766+
reType,
764767
baseCoordinates: base,
765768
panels,
766769
flaps: geometryDesign.flaps,
@@ -828,7 +831,7 @@ export function SolvePanel() {
828831
sweepAbortRef.current = null;
829832
}
830833
}, [panels, sweepPrimary, sweepSecondary, displayAlpha, reynolds, mach, ncrit,
831-
maxIterations, solverMode, geometryDesign.flaps, name, isViscous, upsertPolar, addRun, addRunBatch,
834+
maxIterations, solverMode, reType, geometryDesign.flaps, name, isViscous, upsertPolar, addRun, addRunBatch,
832835
jobDispatch, jobComplete, jobUpdate]);
833836

834837
// --------------- derived ---------------
@@ -892,6 +895,31 @@ export function SolvePanel() {
892895
</div>
893896
)}
894897

898+
{isViscous && (
899+
<div className="form-group">
900+
<div className="form-label">Mode</div>
901+
<div style={{ display: 'flex', gap: '4px', marginBottom: '4px' }}>
902+
{([1, 2, 3] as ReType[]).map((m) => (
903+
<button
904+
key={m}
905+
onClick={() => setReType(m)}
906+
className={reType === m ? 'active' : ''}
907+
style={{ flex: 1 }}
908+
>
909+
{m}
910+
</button>
911+
))}
912+
</div>
913+
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginTop: '2px' }}>
914+
{reType === 1
915+
? 'Constant Re'
916+
: reType === 2
917+
? 'Fixed Re·√CL (variable speed)'
918+
: 'Fixed Re·CL (propeller/turbo)'}
919+
</div>
920+
</div>
921+
)}
922+
895923
{isViscous && (
896924
<div className="form-group">
897925
<button

flexfoil-ui/src/lib/sweepEngine.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
FlapDefinition,
1212
PolarPoint,
1313
SolverMode,
14+
ReType,
1415
SweepAxis,
1516
SweepParam,
1617
} from '../types';
@@ -27,6 +28,7 @@ export interface SweepConfig {
2728
ncrit: number;
2829
maxIterations: number;
2930
solverMode: SolverMode;
31+
reType: ReType;
3032
baseCoordinates: AirfoilPoint[];
3133
panels: AirfoilPoint[];
3234
flaps: FlapDefinition[];
@@ -234,7 +236,7 @@ export async function runSweep(
234236
// Solve
235237
try {
236238
const res = isViscous
237-
? analyzeAirfoil(panels, alpha, reynolds, mach, ncrit, config.maxIterations)
239+
? analyzeAirfoil(panels, alpha, reynolds, mach, ncrit, config.maxIterations, config.reType)
238240
: analyzeAirfoilInviscid(panels, alpha);
239241

240242
if (res.success) {

flexfoil-ui/src/lib/wasm.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,20 +320,22 @@ export function analyzeAirfoil(
320320
reynolds: number = 1e6,
321321
mach: number = 0,
322322
ncrit: number = 9,
323-
maxIterations: number = 100
323+
maxIterations: number = 100,
324+
reType: number = 1,
324325
): AnalysisResult {
325326
if (!initialized) {
326327
throw new Error('WASM not initialized. Call initWasm() first.');
327328
}
328-
329+
329330
const coordsFlat = pointsToFlat(coordinates);
330331
return analyze_airfoil_faithful(
331332
coordsFlat,
332333
alphaDeg,
333334
reynolds,
334335
mach,
335336
ncrit,
336-
maxIterations
337+
maxIterations,
338+
reType,
337339
) as AnalysisResult;
338340
}
339341

flexfoil-ui/src/stores/airfoilStore.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
AirfoilPoint,
1212
ControlMode,
1313
SolverMode,
14+
ReType,
1415
BezierHandle,
1516
BSplineControlPoint,
1617
SpacingKnot,
@@ -143,7 +144,8 @@ interface AirfoilStore extends AirfoilState {
143144
setNcrit: (ncrit: number) => void;
144145
setMaxIterations: (maxIterations: number) => void;
145146
setSolverMode: (mode: SolverMode) => void;
146-
147+
setReType: (reType: ReType) => void;
148+
147149
// Point manipulation (legacy, kept for compatibility)
148150
updatePoint: (index: number, point: AirfoilPoint) => void;
149151
addPoint: (index: number, point: AirfoilPoint) => void;
@@ -288,6 +290,7 @@ export const useAirfoilStore = create<AirfoilStore>()(
288290
ncrit: 9,
289291
maxIterations: 100,
290292
solverMode: 'viscous',
293+
reType: 1 as ReType,
291294
polarData: [],
292295
spacingPanelMode: 'simple', // Default to simple curvature-based
293296
sspInterpolation: 'linear', // Default to linear (Mark Drela's original)
@@ -316,6 +319,7 @@ export const useAirfoilStore = create<AirfoilStore>()(
316319
setNcrit: (ncrit) => set({ ncrit: Math.max(1, Math.min(14, ncrit)) }),
317320
setMaxIterations: (maxIterations) => set({ maxIterations: Math.max(10, Math.min(500, maxIterations)) }),
318321
setSolverMode: (solverMode) => set({ solverMode }),
322+
setReType: (reType) => set({ reType }),
319323

320324
updatePoint: (index, point) => set((state) => {
321325
const newCoords = [...state.coordinates];

flexfoil-ui/src/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface AirfoilPoint extends Point {
1919
/** Control modes for airfoil manipulation */
2020
export type ControlMode = 'parameters' | 'camber-spline' | 'thickness-spline' | 'inverse-design' | 'geometry-design';
2121
export type SolverMode = 'viscous' | 'inviscid';
22+
/** XFOIL Reynolds number constraint type (1=constant Re, 2=fixed Re√CL, 3=fixed Re·CL) */
23+
export type ReType = 1 | 2 | 3;
2224
export type RunMode = 'alpha' | 'cl';
2325
export type AxisVariable = 'alpha' | 'cl' | 'cd' | 'cm' | 'ld'
2426
| 'reynolds' | 'mach' | 'ncrit' | 'flapDeflection' | 'flapHingeX';
@@ -157,6 +159,8 @@ export interface AirfoilState {
157159
maxIterations: number;
158160
/** Active solver mode */
159161
solverMode: SolverMode;
162+
/** Reynolds number constraint type (Mode 1/2/3) */
163+
reType: ReType;
160164
/** Polar sweep data — multiple series keyed by solver config */
161165
polarData: PolarSeries[];
162166
/** Spacing panel mode: simple (curvature-based) or advanced (SSP) */

0 commit comments

Comments
 (0)