Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 25 additions & 4 deletions crates/rustfoil-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,10 +17,19 @@ struct FaithfulResult {
x_tr_lower: f64,
cd_friction: f64,
cd_pressure: f64,
reynolds_eff: f64,
success: bool,
error: Option<String>,
}

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<Point> {
coords.chunks(2).map(|c| point(c[0], c[1])).collect()
}
Expand All @@ -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<f64>,
Expand All @@ -43,6 +52,7 @@ fn analyze_faithful(
mach: f64,
ncrit: f64,
max_iterations: usize,
re_type: u8,
) -> PyResult<Py<PyDict>> {
if coords.len() < 6 || coords.len() % 2 != 0 {
let d = PyDict::new(py);
Expand All @@ -67,6 +77,7 @@ fn analyze_faithful(
mach,
ncrit,
max_iterations,
re_type: re_type_from_int(re_type),
..Default::default()
};

Expand All @@ -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())?;
}
Expand Down Expand Up @@ -242,13 +254,15 @@ 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 {
alpha_deg, cl: 0.0, cd: 0.0, cm: 0.0,
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}")),
},
}
Expand All @@ -267,6 +281,7 @@ fn faithful_result_to_pydict(py: Python<'_>, r: &FaithfulResult) -> PyResult<Py<
d.set_item("x_tr_lower", r.x_tr_lower)?;
d.set_item("cd_friction", r.cd_friction)?;
d.set_item("cd_pressure", r.cd_pressure)?;
d.set_item("reynolds_eff", r.reynolds_eff)?;
d.set_item("success", r.success)?;
d.set_item("error", r.error.as_deref())?;
Ok(d.into())
Expand All @@ -276,7 +291,7 @@ fn faithful_result_to_pydict(py: Python<'_>, r: &FaithfulResult) -> PyResult<Py<
///
/// Returns a list of dicts (same schema as analyze_faithful), one per alpha.
#[pyfunction]
#[pyo3(signature = (coords, alphas, reynolds=1.0e6, mach=0.0, ncrit=9.0, max_iterations=100))]
#[pyo3(signature = (coords, alphas, reynolds=1.0e6, mach=0.0, ncrit=9.0, max_iterations=100, re_type=1))]
fn analyze_faithful_batch(
py: Python<'_>,
coords: Vec<f64>,
Expand All @@ -285,6 +300,7 @@ fn analyze_faithful_batch(
mach: f64,
ncrit: f64,
max_iterations: usize,
re_type: u8,
) -> PyResult<Vec<Py<PyDict>>> {
let err_msg = if coords.len() < 6 || coords.len() % 2 != 0 {
Some("Invalid coordinates".to_string())
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()
};

Expand Down Expand Up @@ -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<f64>,
Expand All @@ -430,6 +449,7 @@ fn get_bl_distribution(
mach: f64,
ncrit: f64,
max_iterations: usize,
re_type: u8,
) -> PyResult<Py<PyDict>> {
use rustfoil_xfoil::oper::{build_state_from_coords, solve_operating_point_from_state, coords_from_body, AlphaSpec};
use rustfoil_xfoil::XfoilOptions;
Expand All @@ -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()
};

Expand Down
27 changes: 19 additions & 8 deletions crates/rustfoil-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,13 +925,22 @@ fn bl_error(message: impl Into<String>) -> 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,
reynolds: f64,
mach: f64,
ncrit: f64,
max_iterations: usize,
re_type: u8,
) -> Result<FaithfulSnapshot, String> {
if coords.len() < 6 || coords.len() % 2 != 0 {
return Err("Invalid coordinates: need at least 3 points (6 values)".to_string());
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
};
Expand All @@ -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,
Expand Down Expand Up @@ -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),
};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down
90 changes: 90 additions & 0 deletions crates/rustfoil-xfoil/src/config.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -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,
}
}
}
Expand All @@ -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");
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion crates/rustfoil-xfoil/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading