use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PanelMeshVisualization {
pub points: Vec<[f64; 2]>,
pub cp: Vec<f64>,
pub cl: f64,
pub cd: f64,
pub cp_min: f64,
pub cp_max: f64,
}
impl PanelMeshVisualization {
#[must_use]
pub fn from_panels_and_solution(
panels: &[crate::panel::Panel],
solution: &crate::panel::PanelSolution,
) -> Self {
let mut points: Vec<[f64; 2]> = panels.iter().map(|p| [p.start.0, p.start.1]).collect();
if let Some(last) = panels.last() {
points.push([last.end.0, last.end.1]);
}
let cp_min = solution.cp.iter().cloned().fold(f64::MAX, f64::min);
let cp_max = solution.cp.iter().cloned().fold(f64::MIN, f64::max);
Self {
points,
cp: solution.cp.clone(),
cl: solution.cl,
cd: solution.cd,
cp_min,
cp_max,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AirfoilProfile {
pub upper: Vec<[f64; 2]>,
pub lower: Vec<[f64; 2]>,
pub camber: Vec<[f64; 2]>,
pub designation: String,
pub chord: f64,
}
impl AirfoilProfile {
#[must_use]
pub fn from_naca(profile: &crate::airfoil::NacaProfile, num_points: usize) -> Self {
let (upper_pts, lower_pts) = profile.surface_coordinates(num_points);
let upper: Vec<[f64; 2]> = upper_pts.iter().map(|&(x, y)| [x, y]).collect();
let lower: Vec<[f64; 2]> = lower_pts.iter().map(|&(x, y)| [x, y]).collect();
let camber: Vec<[f64; 2]> = upper
.iter()
.zip(lower.iter())
.map(|(u, l)| [u[0], (u[1] + l[1]) * 0.5])
.collect();
let d1 = (profile.max_camber * 100.0).round() as u32;
let d2 = (profile.camber_position * 10.0).round() as u32;
let d34 = (profile.max_thickness * 100.0).round() as u32;
Self {
upper,
lower,
camber,
designation: format!("{d1}{d2}{d34:02}"),
chord: 1.0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FlowField2D {
pub velocities: Vec<[f64; 2]>,
pub scalar: Vec<f64>,
pub dimensions: [usize; 2],
pub origin: [f64; 2],
pub spacing: f64,
pub scalar_name: String,
pub max_speed: f64,
}
impl FlowField2D {
#[must_use]
pub fn uniform(
nx: usize,
ny: usize,
origin: [f64; 2],
spacing: f64,
freestream_u: f64,
freestream_v: f64,
) -> Self {
let count = nx * ny;
let speed = (freestream_u * freestream_u + freestream_v * freestream_v).sqrt();
Self {
velocities: vec![[freestream_u, freestream_v]; count],
scalar: vec![speed; count],
dimensions: [nx, ny],
origin,
spacing,
scalar_name: "speed".to_string(),
max_speed: speed,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BoundaryLayerProfile {
pub x_stations: Vec<f64>,
pub thickness: Vec<f64>,
pub is_laminar: Vec<bool>,
pub cf: Vec<f64>,
}
impl BoundaryLayerProfile {
#[must_use]
pub fn flat_plate(chord: f64, re_chord: f64, num_stations: usize) -> Self {
let n = num_stations.max(2);
let mut x_stations = Vec::with_capacity(n);
let mut thickness = Vec::with_capacity(n);
let mut is_laminar = Vec::with_capacity(n);
let mut cf = Vec::with_capacity(n);
for i in 0..n {
let t = (i as f64 + 0.5) / n as f64; let x = t * chord;
let re_x = re_chord * t;
x_stations.push(x);
let lam = !crate::boundary::is_turbulent(re_x);
is_laminar.push(lam);
if lam {
thickness.push(crate::boundary::blasius_thickness(x, re_x));
cf.push(crate::boundary::skin_friction_laminar(re_x));
} else {
thickness.push(crate::boundary::turbulent_thickness(x, re_x));
cf.push(crate::boundary::skin_friction_turbulent(re_x));
}
}
Self {
x_stations,
thickness,
is_laminar,
cf,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VortexVisualization {
pub filaments: Vec<VortexFilament>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct VortexFilament {
pub start: [f64; 3],
pub end: [f64; 3],
pub gamma: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn airfoil_profile_naca0012() {
let naca = crate::airfoil::NacaProfile::naca0012();
let prof = AirfoilProfile::from_naca(&naca, 20);
assert_eq!(prof.upper.len(), 20);
assert_eq!(prof.lower.len(), 20);
assert_eq!(prof.designation, "0012");
for c in &prof.camber {
assert!(c[1].abs() < 0.01, "symmetric airfoil camber should be ~0");
}
}
#[test]
fn airfoil_profile_cambered() {
let naca = crate::airfoil::NacaProfile::from_digits(2, 4, 1, 2);
let prof = AirfoilProfile::from_naca(&naca, 30);
assert_eq!(prof.designation, "2412");
let has_camber = prof.camber.iter().any(|c| c[1] > 0.001);
assert!(has_camber);
}
#[test]
fn flow_field_uniform() {
let field = FlowField2D::uniform(5, 5, [0.0, 0.0], 0.1, 10.0, 0.0);
assert_eq!(field.velocities.len(), 25);
assert!((field.max_speed - 10.0).abs() < 0.01);
}
#[test]
fn boundary_layer_flat_plate() {
let bl = BoundaryLayerProfile::flat_plate(1.0, 1e6, 10);
assert_eq!(bl.x_stations.len(), 10);
assert_eq!(bl.thickness.len(), 10);
assert!(bl.thickness.last().unwrap() > bl.thickness.first().unwrap());
assert!(bl.is_laminar[0]);
assert!(!bl.is_laminar.last().unwrap());
}
#[test]
fn vortex_viz_serializes() {
let viz = VortexVisualization {
filaments: vec![VortexFilament {
start: [0.0, 0.0, 0.0],
end: [1.0, 0.0, 0.0],
gamma: 5.0,
}],
};
let json = serde_json::to_string(&viz);
assert!(json.is_ok());
}
#[test]
fn panel_mesh_from_panels() {
let naca = crate::airfoil::NacaProfile::naca0012();
let (upper, lower) = naca.surface_coordinates(20);
let panels = crate::panel::panels_from_surface(&upper, &lower);
let solution = crate::panel::solve(&panels, 0.0).unwrap();
let viz = PanelMeshVisualization::from_panels_and_solution(&panels, &solution);
assert!(!viz.points.is_empty());
assert!(!viz.cp.is_empty());
assert!(viz.cp_min <= viz.cp_max);
}
#[test]
fn flow_field_serializes() {
let field = FlowField2D::uniform(2, 2, [0.0; 2], 1.0, 5.0, 3.0);
let json = serde_json::to_string(&field);
assert!(json.is_ok());
}
}