diff --git a/crates/rustfoil-python/src/lib.rs b/crates/rustfoil-python/src/lib.rs index e0903f41..6dad2e12 100644 --- a/crates/rustfoil-python/src/lib.rs +++ b/crates/rustfoil-python/src/lib.rs @@ -3,7 +3,7 @@ use pyo3::types::PyDict; use rayon::prelude::*; use rustfoil_core::{naca, point, Body, CubicSpline, PanelingParams, Point}; use rustfoil_xfoil::oper::{solve_body_oper_point, AlphaSpec}; -use rustfoil_xfoil::XfoilOptions; +use rustfoil_xfoil::{ReType, XfoilOptions}; struct FaithfulResult { alpha_deg: f64, @@ -17,10 +17,19 @@ struct FaithfulResult { x_tr_lower: f64, cd_friction: f64, cd_pressure: f64, + reynolds_eff: f64, success: bool, error: Option, } +fn re_type_from_int(v: u8) -> ReType { + match v { + 2 => ReType::Type2, + 3 => ReType::Type3, + _ => ReType::Type1, + } +} + fn points_from_flat(coords: &[f64]) -> Vec { coords.chunks(2).map(|c| point(c[0], c[1])).collect() } @@ -34,7 +43,7 @@ fn flat_from_points(pts: &[Point]) -> Vec<(f64, f64)> { /// Returns a dict with keys: cl, cd, cm, converged, iterations, residual, /// x_tr_upper, x_tr_lower, cd_friction, cd_pressure, alpha_deg, success, error. #[pyfunction] -#[pyo3(signature = (coords, alpha_deg, reynolds=1.0e6, mach=0.0, ncrit=9.0, max_iterations=100))] +#[pyo3(signature = (coords, alpha_deg, reynolds=1.0e6, mach=0.0, ncrit=9.0, max_iterations=100, re_type=1))] fn analyze_faithful( py: Python<'_>, coords: Vec, @@ -43,6 +52,7 @@ fn analyze_faithful( mach: f64, ncrit: f64, max_iterations: usize, + re_type: u8, ) -> PyResult> { if coords.len() < 6 || coords.len() % 2 != 0 { let d = PyDict::new(py); @@ -67,6 +77,7 @@ fn analyze_faithful( mach, ncrit, max_iterations, + re_type: re_type_from_int(re_type), ..Default::default() }; @@ -84,6 +95,7 @@ fn analyze_faithful( d.set_item("cd_friction", r.cd_friction)?; d.set_item("cd_pressure", r.cd_pressure)?; d.set_item("alpha_deg", r.alpha_deg)?; + d.set_item("reynolds_eff", r.reynolds_eff)?; d.set_item("success", true)?; d.set_item("error", py.None())?; } @@ -242,6 +254,7 @@ fn solve_one_faithful(body: &Body, alpha_deg: f64, options: &XfoilOptions) -> Fa converged: r.converged, iterations: r.iterations, residual: r.residual, x_tr_upper: r.x_tr_upper, x_tr_lower: r.x_tr_lower, cd_friction: r.cd_friction, cd_pressure: r.cd_pressure, + reynolds_eff: r.reynolds_eff, success: true, error: None, }, Err(e) => FaithfulResult { @@ -249,6 +262,7 @@ fn solve_one_faithful(body: &Body, alpha_deg: f64, options: &XfoilOptions) -> Fa converged: false, iterations: 0, residual: 0.0, x_tr_upper: 1.0, x_tr_lower: 1.0, cd_friction: 0.0, cd_pressure: 0.0, + reynolds_eff: options.reynolds, success: false, error: Some(format!("{e}")), }, } @@ -267,6 +281,7 @@ fn faithful_result_to_pydict(py: Python<'_>, r: &FaithfulResult) -> PyResult, r: &FaithfulResult) -> PyResult, coords: Vec, @@ -285,6 +300,7 @@ fn analyze_faithful_batch( mach: f64, ncrit: f64, max_iterations: usize, + re_type: u8, ) -> PyResult>> { let err_msg = if coords.len() < 6 || coords.len() % 2 != 0 { Some("Invalid coordinates".to_string()) @@ -298,6 +314,7 @@ fn analyze_faithful_batch( converged: false, iterations: 0, residual: 0.0, x_tr_upper: 1.0, x_tr_lower: 1.0, cd_friction: 0.0, cd_pressure: 0.0, + reynolds_eff: reynolds, success: false, error: Some(msg.clone()), }; faithful_result_to_pydict(py, &r) @@ -315,6 +332,7 @@ fn analyze_faithful_batch( converged: false, iterations: 0, residual: 0.0, x_tr_upper: 1.0, x_tr_lower: 1.0, cd_friction: 0.0, cd_pressure: 0.0, + reynolds_eff: reynolds, success: false, error: Some(msg.clone()), }; faithful_result_to_pydict(py, &r) @@ -324,6 +342,7 @@ fn analyze_faithful_batch( let options = XfoilOptions { reynolds, mach, ncrit, max_iterations, + re_type: re_type_from_int(re_type), ..Default::default() }; @@ -421,7 +440,7 @@ fn analyze_inviscid_batch( /// ue_upper, ue_lower, x_tr_upper, x_tr_lower, converged, iterations, /// residual, success, error. #[pyfunction] -#[pyo3(signature = (coords, alpha_deg, reynolds=1.0e6, mach=0.0, ncrit=9.0, max_iterations=100))] +#[pyo3(signature = (coords, alpha_deg, reynolds=1.0e6, mach=0.0, ncrit=9.0, max_iterations=100, re_type=1))] fn get_bl_distribution( py: Python<'_>, coords: Vec, @@ -430,6 +449,7 @@ fn get_bl_distribution( mach: f64, ncrit: f64, max_iterations: usize, + re_type: u8, ) -> PyResult> { use rustfoil_xfoil::oper::{build_state_from_coords, solve_operating_point_from_state, coords_from_body, AlphaSpec}; use rustfoil_xfoil::XfoilOptions; @@ -455,6 +475,7 @@ fn get_bl_distribution( let body_coords = coords_from_body(&body); let options = XfoilOptions { reynolds, mach, ncrit, max_iterations, + re_type: re_type_from_int(re_type), ..Default::default() }; diff --git a/crates/rustfoil-wasm/src/lib.rs b/crates/rustfoil-wasm/src/lib.rs index d1dc97e7..0729f00a 100644 --- a/crates/rustfoil-wasm/src/lib.rs +++ b/crates/rustfoil-wasm/src/lib.rs @@ -925,6 +925,14 @@ fn bl_error(message: impl Into) -> BLDistribution { } } +fn re_type_from_int(v: u8) -> rustfoil_xfoil::config::ReType { + match v { + 2 => rustfoil_xfoil::config::ReType::Type2, + 3 => rustfoil_xfoil::config::ReType::Type3, + _ => rustfoil_xfoil::config::ReType::Type1, + } +} + fn faithful_snapshot( coords: &[f64], alpha_deg: f64, @@ -932,6 +940,7 @@ fn faithful_snapshot( mach: f64, ncrit: f64, max_iterations: usize, + re_type: u8, ) -> Result { if coords.len() < 6 || coords.len() % 2 != 0 { return Err("Invalid coordinates: need at least 3 points (6 values)".to_string()); @@ -996,6 +1005,7 @@ fn faithful_snapshot( mach, ncrit, max_iterations, + re_type: re_type_from_int(re_type), ..Default::default() }; let oper_result = solve_operating_point_from_state(&mut state, &factorized, &options) @@ -1141,8 +1151,9 @@ pub fn analyze_airfoil_faithful( mach: f64, ncrit: f64, max_iterations: usize, + re_type: u8, ) -> JsValue { - let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) { + let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, re_type) { Ok(snapshot) => snapshot.result, Err(message) => analysis_error(message), }; @@ -1158,7 +1169,7 @@ pub fn get_bl_distribution_faithful( ncrit: f64, max_iterations: usize, ) -> JsValue { - let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) { + let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) { Ok(snapshot) => rows_to_bl_distribution( &snapshot.upper_rows, &snapshot.lower_rows, @@ -1264,7 +1275,7 @@ pub fn get_bl_visualization_faithful( ncrit: f64, max_iterations: usize, ) -> JsValue { - let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) { + let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) { Ok(snapshot) => build_bl_visualization(&snapshot), Err(message) => bl_vis_error(message), }; @@ -1442,7 +1453,7 @@ pub fn compute_streamlines_faithful( seed_count: u32, bounds: &[f64], ) -> JsValue { - let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) { + let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) { Ok(snapshot) => { if bounds.len() != 4 { StreamlineResult { @@ -1585,7 +1596,7 @@ pub fn compute_dividing_streamline_faithful( max_iterations: usize, bounds: &[f64], ) -> JsValue { - let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) { + let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) { Ok(snapshot) => { if bounds.len() != 4 { DividingStreamlineResult { @@ -1809,7 +1820,7 @@ pub fn compute_psi_grid_faithful( bounds: &[f64], resolution: &[u32], ) -> JsValue { - let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) { + let result = match faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) { Ok(snapshot) => { if bounds.len() != 4 { PsiGridResult { @@ -1952,7 +1963,7 @@ impl WasmSmokeSystem { ncrit: f64, max_iterations: usize, ) { - if let Ok(snapshot) = faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations) { + if let Ok(snapshot) = faithful_snapshot(coords, alpha_deg, reynolds, mach, ncrit, max_iterations, 1) { let field = FaithfulFlowField::from_snapshot(&snapshot, alpha_deg); self.coords = field.nodes.clone(); self.gamma = field.gamma.clone(); @@ -3503,7 +3514,7 @@ mod tests { let paneled_flat = repanel_xfoil(&buffer_flat, 160); let alpha_deg = 15.0; - let snapshot = faithful_snapshot(&paneled_flat, alpha_deg, 1.0e6, 0.0, 9.0, 100) + let snapshot = faithful_snapshot(&paneled_flat, alpha_deg, 1.0e6, 0.0, 9.0, 100, 1) .expect("faithful snapshot should solve"); let field = FaithfulFlowField::from_snapshot(&snapshot, alpha_deg); let options = StreamlineOptions { diff --git a/crates/rustfoil-xfoil/src/config.rs b/crates/rustfoil-xfoil/src/config.rs index 85d55451..ca8d6ac8 100644 --- a/crates/rustfoil-xfoil/src/config.rs +++ b/crates/rustfoil-xfoil/src/config.rs @@ -1,3 +1,19 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReType { + /// Type 1: constant Re (default). `Re_eff = Re`. + Type1, + /// Type 2: fixed `Re * sqrt(CL)`. `Re_eff = Re / sqrt(|CL|)`. + Type2, + /// Type 3: fixed `Re * CL`. `Re_eff = Re / |CL|`. + Type3, +} + +impl Default for ReType { + fn default() -> Self { + Self::Type1 + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum OperatingMode { PrescribedAlpha, @@ -19,6 +35,7 @@ pub struct XfoilOptions { pub tolerance: f64, pub wake_length_chords: f64, pub operating_mode: OperatingMode, + pub re_type: ReType, } impl Default for XfoilOptions { @@ -31,6 +48,7 @@ impl Default for XfoilOptions { tolerance: 1.0e-4, wake_length_chords: 1.0, operating_mode: OperatingMode::PrescribedAlpha, + re_type: ReType::Type1, } } } @@ -46,6 +64,17 @@ impl XfoilOptions { self } + pub fn effective_reynolds(&self, cl: f64) -> f64 { + if cl.abs() < 1e-6 { + return self.reynolds; + } + match self.re_type { + ReType::Type1 => self.reynolds, + ReType::Type2 => self.reynolds / cl.abs().sqrt(), + ReType::Type3 => self.reynolds / cl.abs(), + } + } + pub fn validate(&self) -> Result<(), &'static str> { if self.reynolds <= 0.0 { return Err("Reynolds number must be positive"); @@ -65,3 +94,64 @@ impl XfoilOptions { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn opts(re_type: ReType, reynolds: f64) -> XfoilOptions { + XfoilOptions { + reynolds, + re_type, + ..Default::default() + } + } + + #[test] + fn type1_returns_nominal() { + let o = opts(ReType::Type1, 1e6); + assert_eq!(o.effective_reynolds(0.5), 1e6); + assert_eq!(o.effective_reynolds(-1.2), 1e6); + } + + #[test] + fn type2_scales_by_sqrt_cl() { + let o = opts(ReType::Type2, 1e6); + let re = o.effective_reynolds(0.25); + // Re / sqrt(0.25) = 1e6 / 0.5 = 2e6 + assert!((re - 2e6).abs() < 1.0); + } + + #[test] + fn type3_scales_by_cl() { + let o = opts(ReType::Type3, 1e6); + let re = o.effective_reynolds(0.5); + // Re / 0.5 = 2e6 + assert!((re - 2e6).abs() < 1.0); + } + + #[test] + fn negative_cl_uses_abs() { + let o = opts(ReType::Type2, 1e6); + let re_pos = o.effective_reynolds(0.5); + let re_neg = o.effective_reynolds(-0.5); + assert!((re_pos - re_neg).abs() < 1.0); + } + + #[test] + fn near_zero_cl_returns_nominal() { + let o = opts(ReType::Type2, 1e6); + assert_eq!(o.effective_reynolds(0.0), 1e6); + assert_eq!(o.effective_reynolds(1e-8), 1e6); + + let o3 = opts(ReType::Type3, 1e6); + assert_eq!(o3.effective_reynolds(0.0), 1e6); + } + + #[test] + fn default_is_type1() { + assert_eq!(ReType::default(), ReType::Type1); + let o = XfoilOptions::default(); + assert_eq!(o.re_type, ReType::Type1); + } +} diff --git a/crates/rustfoil-xfoil/src/lib.rs b/crates/rustfoil-xfoil/src/lib.rs index a5933ed7..8337fcca 100644 --- a/crates/rustfoil-xfoil/src/lib.rs +++ b/crates/rustfoil-xfoil/src/lib.rs @@ -20,7 +20,7 @@ pub mod state_ops; pub mod update; pub mod wake_panel; -pub use config::{OperatingMode, XfoilOptions}; +pub use config::{OperatingMode, ReType, XfoilOptions}; pub use error::{Result, XfoilError}; pub use oper::{ build_state_from_coords, coords_from_body, solve_body_oper_point, solve_coords_oper_point, diff --git a/crates/rustfoil-xfoil/src/oper.rs b/crates/rustfoil-xfoil/src/oper.rs index b54b6ce6..b7978714 100644 --- a/crates/rustfoil-xfoil/src/oper.rs +++ b/crates/rustfoil-xfoil/src/oper.rs @@ -140,6 +140,16 @@ pub fn solve_operating_point_from_state( cl_inv, cm_inv ); } + // Seed state.cl from inviscid solution so Type 2/3 have an initial CL estimate. + { + let (cl_inv, _cm_inv) = compute_panel_forces_from_gamma( + &state.panel_x, + &state.panel_y, + &state.qinv, + state.alpha_rad, + ); + state.cl = cl_inv; + } stfind(state); iblpan(state); xicalc(state); @@ -163,14 +173,15 @@ pub fn solve_operating_point_from_state( } for iter in 1..=options.max_iterations { - let mut assembly = setbl(state, options.reynolds, options.ncrit, options.mach, iter); + let re_eff = options.effective_reynolds(state.cl); + let mut assembly = setbl(state, re_eff, options.ncrit, options.mach, iter); let solve = blsolv(state, &mut assembly, iter); update( state, &assembly, &solve, options.mach, - options.reynolds, + re_eff, iter, ); if let OperatingMode::PrescribedCl { .. } = state.operating_mode { @@ -179,7 +190,7 @@ pub fn solve_operating_point_from_state( } if is_debug_active() { // Match XFOIL: dump BL state after UPDATE, before QVFUE/GAMQV/STMOVE. - emit_full_state(state, iter, options.mach, options.reynolds); + emit_full_state(state, iter, options.mach, re_eff); } qvfue(state); gamqv(state); @@ -190,7 +201,7 @@ pub fn solve_operating_point_from_state( if std::env::var("RUSTFOIL_DISABLE_STMOVE").is_err() { stmove(state); } - update_force_state(state, options.mach, options.reynolds); + update_force_state(state, options.mach, re_eff); state.iterations = iter; state.residual = solve.rms; state.converged = solve.rms <= options.tolerance; @@ -211,6 +222,7 @@ pub fn solve_operating_point_from_state( } } + let final_re_eff = options.effective_reynolds(state.cl); Ok(XfoilViscousResult { alpha_deg: state.alpha_rad.to_degrees(), cl: state.cl, @@ -223,8 +235,9 @@ pub fn solve_operating_point_from_state( residual: state.residual, cd_friction: state.cdf, cd_pressure: state.cdp, - x_separation: separation_x(state, crate::state::XfoilSurface::Upper, options.mach, options.reynolds) - .or_else(|| separation_x(state, crate::state::XfoilSurface::Lower, options.mach, options.reynolds)), + x_separation: separation_x(state, crate::state::XfoilSurface::Upper, options.mach, final_re_eff) + .or_else(|| separation_x(state, crate::state::XfoilSurface::Lower, options.mach, final_re_eff)), + reynolds_eff: final_re_eff, }) } diff --git a/crates/rustfoil-xfoil/src/result.rs b/crates/rustfoil-xfoil/src/result.rs index 5f27f82e..edc3ff9e 100644 --- a/crates/rustfoil-xfoil/src/result.rs +++ b/crates/rustfoil-xfoil/src/result.rs @@ -14,6 +14,7 @@ pub struct XfoilViscousResult { pub cd_friction: f64, pub cd_pressure: f64, pub x_separation: Option, + pub reynolds_eff: f64, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/rustfoil-xfoil/tests/support.rs b/crates/rustfoil-xfoil/tests/support.rs index 58d12c81..076743ad 100644 --- a/crates/rustfoil-xfoil/tests/support.rs +++ b/crates/rustfoil-xfoil/tests/support.rs @@ -549,6 +549,7 @@ pub fn run_workflow_case(alpha_deg: f64) -> rustfoil_xfoil::result::XfoilViscous tolerance: 1.0e-4, wake_length_chords: 1.0, operating_mode: OperatingMode::PrescribedAlpha, + ..Default::default() }, ) .expect("run workflow case") diff --git a/docs-site/docs/python-api.mdx b/docs-site/docs/python-api.mdx index fbc09118..1c167ee6 100644 --- a/docs-site/docs/python-api.mdx +++ b/docs-site/docs/python-api.mdx @@ -137,6 +137,26 @@ for defl in [0, 5, 10, 15]: polar = f.polar(alpha=(-4, 14, 1.0), Re=1e6) ``` +### Variable-Reynolds modes (Type 2 & 3) + +XFOIL supports three Reynolds number constraint types for polar sweeps. +Pass `re_type=` to `solve()` or `polar()`: + +| Mode | Constraint | Effective Re | Use case | +| --- | --- | --- | --- | +| 1 (default) | Constant Re | Re | Standard wind-tunnel polar | +| 2 | Fixed Re·√CL | Re / √\|CL\| | Aircraft cruise (lift = weight) | +| 3 | Fixed Re·CL | Re / \|CL\| | Propeller / turbomachinery blades | + +```python +# Mode 2: as CL changes, speed adjusts → effective Re varies +result = foil.solve(alpha=5.0, Re=1e6, re_type=2) +print(result.reynolds_eff) # effective Re after Mode 2 adjustment + +# Full polar with variable Re +polar = foil.polar(alpha=(-5, 15, 0.5), Re=1e6, re_type=2) +``` + ### Inviscid analysis ```python @@ -240,8 +260,9 @@ are byte-identical. | `.panel_coords` | Repaneled coordinates `list[(x, y)]` | | `.hash` | SHA-256 hash of panel coords (cache key) | | `.with_flap(hinge_x, deflection, hinge_y_frac, n_panels)` | Return new Airfoil with flap deflected | -| `.solve(alpha, Re, mach, ncrit, max_iter, viscous, store)` | Single-point analysis | -| `.polar(alpha, Re, mach, ncrit, max_iter, viscous, store, parallel)` | Sweep over alpha range (parallel by default) | +| `.solve(alpha, Re, mach, ncrit, max_iter, viscous, store, re_type)` | Single-point analysis (`re_type`: 1, 2, or 3) | +| `.polar(alpha, Re, mach, ncrit, max_iter, viscous, store, parallel, re_type)` | Sweep over alpha range (parallel by default) | +| `.bl_distribution(alpha, Re, mach, ncrit, max_iter, re_type)` | Boundary-layer distribution at a single alpha | ### SolveResult @@ -256,7 +277,8 @@ are byte-identical. | `.x_tr_upper` | `float` | Transition x/c, upper surface | | `.x_tr_lower` | `float` | Transition x/c, lower surface | | `.alpha` | `float` | Angle of attack (degrees) | -| `.reynolds` | `float` | Reynolds number | +| `.reynolds` | `float` | Reynolds number (nominal) | +| `.reynolds_eff` | `float \| None` | Effective Reynolds after Mode 2/3 adjustment | | `.mach` | `float` | Mach number | | `.ncrit` | `float` | e^N transition criterion | | `.ld` | `float \| None` | Lift-to-drag ratio | diff --git a/flexfoil-ui/package.json b/flexfoil-ui/package.json index ec86553e..a9be3116 100644 --- a/flexfoil-ui/package.json +++ b/flexfoil-ui/package.json @@ -2,7 +2,7 @@ "name": "flexfoil-ui", "private": true, "license": "MIT", - "version": "1.1.2", + "version": "1.1.3", "type": "module", "scripts": { "dev": "vite", diff --git a/flexfoil-ui/src/components/panels/DataExplorerPanel.tsx b/flexfoil-ui/src/components/panels/DataExplorerPanel.tsx index 7c1654c2..ce69461c 100644 --- a/flexfoil-ui/src/components/panels/DataExplorerPanel.tsx +++ b/flexfoil-ui/src/components/panels/DataExplorerPanel.tsx @@ -170,7 +170,7 @@ function buildColumnDefs(): ColDef[] { field: 'is_outlier', headerName: 'Outlier', width: 80, chartDataType: 'category' as const, enableRowGroup: true, valueFormatter: (p) => p.value ? 'Yes' : '', - cellStyle: (p) => p.value ? { color: '#ef4444', fontWeight: 600 } : {}, + cellStyle: (p) => p.value ? { color: '#ef4444', fontWeight: 600 } : undefined, }, // Aerodynamic summary columns — show the alpha at which CL or L/D is maximized. // Hidden by default; become meaningful when row grouping is active. @@ -662,7 +662,7 @@ export function DataExplorerPanel() { if (col < n && row < n) { hoveredCellRef.current = { xKey: splomKeys[col], yKey: splomKeys[row] }; } - const cd = point.customdata as Array | undefined; + const cd = point.customdata as unknown as Array | undefined; const runId = cd?.[10]; hoveredRunIdRef.current = typeof runId === 'number' ? runId : null; }, [splomKeys]); diff --git a/flexfoil-ui/src/components/panels/DistributionPanel.tsx b/flexfoil-ui/src/components/panels/DistributionPanel.tsx index b4be30cb..cfdbb5d3 100644 --- a/flexfoil-ui/src/components/panels/DistributionPanel.tsx +++ b/flexfoil-ui/src/components/panels/DistributionPanel.tsx @@ -5,7 +5,7 @@ * Runs are pinned via checkbox selection in the Data Explorer table. */ -import { useMemo, useCallback, useRef, useEffect } from 'react'; +import { useMemo, useRef, useEffect } from 'react'; import Plot from 'react-plotly.js'; import { useDistributionStore } from '../../stores/distributionStore'; import { useRunStore } from '../../stores/runStore'; diff --git a/flexfoil-ui/src/components/panels/SolvePanel.tsx b/flexfoil-ui/src/components/panels/SolvePanel.tsx index 85541b31..c1d8d316 100644 --- a/flexfoil-ui/src/components/panels/SolvePanel.tsx +++ b/flexfoil-ui/src/components/panels/SolvePanel.tsx @@ -14,7 +14,7 @@ import { useCaseLogStore } from '../../stores/caseLogStore'; import { analyzeAirfoil, analyzeAirfoilInviscid, isWasmReady, type AnalysisResult } from '../../lib/wasm'; import { useSolverJobStore } from '../../stores/solverJobStore'; import { runSweep, type SweepConfig, type SweepRunData } from '../../lib/sweepEngine'; -import type { PolarPoint, SweepAxis, SweepParam } from '../../types'; +import type { PolarPoint, SweepAxis, SweepParam, ReType } from '../../types'; import type { RunInsert } from '../../lib/storageBackend'; import { parseSweepValues, formatSweepValues } from '../../lib/parseSweepValues'; @@ -61,6 +61,8 @@ export function SolvePanel() { setNcrit, setMaxIterations, setSolverMode, + reType, + setReType, upsertPolar, clearAllPolars, } = useAirfoilStore(); @@ -178,10 +180,10 @@ export function SolvePanel() { const runSolver = useCallback((coords: { x: number; y: number }[], alpha: number) => { if (isViscous) { - return analyzeAirfoil(coords, alpha, reynolds, mach, ncrit, maxIterations); + return analyzeAirfoil(coords, alpha, reynolds, mach, ncrit, maxIterations, reType); } return analyzeAirfoilInviscid(coords, alpha); - }, [isViscous, reynolds, mach, ncrit, maxIterations]); + }, [isViscous, reynolds, mach, ncrit, maxIterations, reType]); /** Cache key uses Re=0/Mach=0/Ncrit=0/maxIter=0 for inviscid to separate caches. */ const cacheRe = isViscous ? reynolds : 0; @@ -761,6 +763,7 @@ export function SolvePanel() { ncrit, maxIterations, solverMode, + reType, baseCoordinates: base, panels, flaps: geometryDesign.flaps, @@ -828,7 +831,7 @@ export function SolvePanel() { sweepAbortRef.current = null; } }, [panels, sweepPrimary, sweepSecondary, displayAlpha, reynolds, mach, ncrit, - maxIterations, solverMode, geometryDesign.flaps, name, isViscous, upsertPolar, addRun, addRunBatch, + maxIterations, solverMode, reType, geometryDesign.flaps, name, isViscous, upsertPolar, addRun, addRunBatch, jobDispatch, jobComplete, jobUpdate]); // --------------- derived --------------- @@ -892,6 +895,31 @@ export function SolvePanel() { )} + {isViscous && ( +
+
Mode
+
+ {([1, 2, 3] as ReType[]).map((m) => ( + + ))} +
+
+ {reType === 1 + ? 'Constant Re' + : reType === 2 + ? 'Fixed Re·√CL (variable speed)' + : 'Fixed Re·CL (propeller/turbo)'} +
+
+ )} + {isViscous && (