use pavan::airfoil::NacaProfile;
use pavan::boundary;
use pavan::forces::{self, AeroForce};
use pavan::panel;
use pavan::vehicle::AeroBody;
use pavan::vlm;
use pavan::wind::WindField;
use pavan::*;
#[test]
fn full_flight_computation() {
let body = AeroBody::light_aircraft();
let forces = body.compute_forces(60.0, 2000.0, 5.0_f64.to_radians());
assert!(forces.lift > 0.0);
assert!(forces.drag > 0.0);
assert!(
forces.lift > forces.drag,
"L should exceed D at moderate AoA"
);
}
#[test]
fn atmosphere_consistency() {
let t = atmosphere::standard_temperature(0.0);
let p = atmosphere::standard_pressure(0.0);
let rho = atmosphere::standard_density(0.0);
let p_check = rho * atmosphere::GAS_CONSTANT_AIR * t;
assert!(
(p - p_check).abs() < 10.0,
"ideal gas law should hold at sea level"
);
}
#[test]
fn airfoil_thickness_reasonable() {
let profile = NacaProfile::naca0012();
let (upper, lower) = profile.surface_coordinates(100);
let max_thickness: f64 = upper
.iter()
.zip(lower.iter())
.map(|(u, l)| u.1 - l.1)
.fold(0.0_f64, f64::max);
assert!(
(max_thickness - 0.12).abs() < 0.02,
"NACA 0012 max thickness should be ~12%, got {max_thickness}"
);
}
#[test]
fn boundary_layer_transition() {
let re_lam = 100_000.0;
let re_turb = 1_000_000.0;
assert!(!boundary::is_turbulent(re_lam));
assert!(boundary::is_turbulent(re_turb));
}
#[test]
fn atmosphere_to_forces_pipeline() {
let alt = 3000.0;
let velocity = 70.0;
let chord = 1.5;
let rho = atmosphere::standard_density(alt);
let t = atmosphere::standard_temperature(alt);
let mu = forces::air_dynamic_viscosity(t);
let re = forces::reynolds_number(rho, velocity, chord, mu);
let q = atmosphere::dynamic_pressure(rho, velocity);
assert!(rho > 0.0 && rho < 1.225);
assert!(
re > 1e6,
"Re should be > 1M for aircraft conditions, got {re}"
);
assert!(q > 0.0);
let cl = lift_coefficient_thin_airfoil(5.0_f64.to_radians());
let cd = drag_coefficient(0.027, cl, 7.5, 0.8);
let l = forces::lift(q, 16.2, cl);
let d = forces::drag(q, 16.2, cd);
assert!(l > d, "lift should exceed drag at 5° AoA");
}
#[test]
fn atmosphere_consistency_at_multiple_altitudes() {
for h in [0.0, 2000.0, 5000.0, 8000.0, 11_000.0, 15_000.0, 20_000.0] {
let t = atmosphere::standard_temperature(h);
let p = atmosphere::standard_pressure(h);
let rho = atmosphere::standard_density(h);
let p_check = rho * atmosphere::GAS_CONSTANT_AIR * t;
let rel_err = (p - p_check).abs() / p;
assert!(
rel_err < 1e-6,
"ideal gas law violated at {h}m: P={p}, ρRT={p_check}"
);
}
}
#[test]
fn boundary_layer_with_real_reynolds() {
let rho = atmosphere::standard_density(0.0);
let t = atmosphere::standard_temperature(0.0);
let mu = forces::air_dynamic_viscosity(t);
let velocity = 50.0;
let x = 1.0;
let re = forces::reynolds_number(rho, velocity, x, mu);
assert!(
boundary::is_turbulent(re),
"Re at sea level/50m/s should be turbulent"
);
let delta_lam = boundary::blasius_thickness(x, re);
let delta_turb = boundary::turbulent_thickness(x, re);
assert!(delta_turb > delta_lam);
assert!(delta_turb < 0.05, "BL thickness should be reasonable at 1m");
}
#[test]
fn serde_round_trip_aero_force() {
let f = forces::compute_aero_force(1.225, 100.0, 10.0, 0.5, 0.05, -0.1, 1.5);
let json = serde_json::to_string(&f).expect("serialize AeroForce");
let back: AeroForce = serde_json::from_str(&json).expect("deserialize AeroForce");
assert!((back.lift - f.lift).abs() < f64::EPSILON);
assert!((back.drag - f.drag).abs() < f64::EPSILON);
assert!((back.moment - f.moment).abs() < f64::EPSILON);
}
#[test]
fn serde_round_trip_wind_field() {
let w = WindField::from_speed_direction(15.0, 0.5);
let json = serde_json::to_string(&w).expect("serialize WindField");
let back: WindField = serde_json::from_str(&json).expect("deserialize WindField");
for i in 0..3 {
assert!((back.velocity[i] - w.velocity[i]).abs() < f64::EPSILON);
}
}
#[test]
fn serde_round_trip_aero_body() {
let body = AeroBody::light_aircraft();
let json = serde_json::to_string(&body).expect("serialize AeroBody");
let back: AeroBody = serde_json::from_str(&json).expect("deserialize AeroBody");
assert!((back.reference_area - body.reference_area).abs() < f64::EPSILON);
assert!((back.cd0 - body.cd0).abs() < f64::EPSILON);
assert!((back.aspect_ratio - body.aspect_ratio).abs() < f64::EPSILON);
}
#[test]
fn wind_profile_altitude_interaction() {
let v_ref = 10.0;
let z_ref = 10.0;
let z0 = 0.03;
let v_100m = pavan::wind::log_wind_profile(v_ref, 100.0, z_ref, z0);
let v_200m = pavan::wind::log_wind_profile(v_ref, 200.0, z_ref, z0);
assert!(v_200m > v_100m, "wind should increase with height");
let rho_low = atmosphere::standard_density(100.0);
let rho_high = atmosphere::standard_density(5000.0);
let q_low = atmosphere::dynamic_pressure(rho_low, v_100m);
let q_high = atmosphere::dynamic_pressure(rho_high, v_200m);
assert!(q_low > 0.0 && q_high > 0.0);
}
#[test]
fn glider_vs_aircraft_full_envelope() {
let glider = AeroBody::glider();
let aircraft = AeroBody::light_aircraft();
for deg in [2.0_f64, 3.0, 5.0, 7.0] {
let alpha = deg.to_radians();
let fg = glider.compute_forces(30.0, 0.0, alpha);
let fa = aircraft.compute_forces(30.0, 0.0, alpha);
let ld_g = fg.lift / fg.drag;
let ld_a = fa.lift / fa.drag;
assert!(ld_g > ld_a, "glider should have better L/D at {deg}° AoA");
}
}
#[test]
fn panel_method_airfoil_to_forces_pipeline() {
let profile = NacaProfile::naca0012();
let (upper, lower) = profile.surface_coordinates(80);
let panels = panel::panels_from_surface(&upper, &lower);
let alpha = 5.0_f64.to_radians();
let sol = panel::solve(&panels, alpha).expect("panel solve");
let rho = atmosphere::standard_density(0.0);
let f = forces::compute_aero_force(rho, 60.0, 16.2, sol.cl, sol.cd, sol.cm, 1.5);
assert!(
f.lift > 0.0,
"panel method should produce positive lift at 5°"
);
assert!(f.drag >= 0.0);
}
#[test]
fn panel_method_vs_thin_airfoil_comparison() {
let profile = NacaProfile::naca0012();
let (upper, lower) = profile.surface_coordinates(100);
let panels = panel::panels_from_surface(&upper, &lower);
let alpha = 3.0_f64.to_radians();
let sol = panel::solve(&panels, alpha).expect("solve");
let cl_thin = lift_coefficient_thin_airfoil(alpha);
let rel_diff = (sol.cl - cl_thin).abs() / cl_thin;
assert!(
rel_diff < 0.3,
"panel Cl={:.3} vs thin Cl={:.3}, diff={:.0}%",
sol.cl,
cl_thin,
rel_diff * 100.0
);
}
#[test]
fn panel_method_serde_round_trip() {
let profile = NacaProfile::naca0012();
let (upper, lower) = profile.surface_coordinates(50);
let panels = panel::panels_from_surface(&upper, &lower);
let sol = panel::solve(&panels, 0.0).expect("solve");
let json = serde_json::to_string(&sol).expect("serialize");
let back: PanelSolution = serde_json::from_str(&json).expect("deserialize");
assert!((back.cl - sol.cl).abs() < f64::EPSILON);
assert_eq!(back.cp.len(), sol.cp.len());
}
#[test]
fn panel_method_alpha_sweep() {
let profile = NacaProfile::naca0012();
let (upper, lower) = profile.surface_coordinates(80);
let panels = panel::panels_from_surface(&upper, &lower);
let alphas: Vec<f64> = (-5..=10).map(|d| (d as f64).to_radians()).collect();
let results = panel::solve_multi(&panels, &alphas).expect("solve_multi");
for i in 1..results.len() {
assert!(
results[i].cl > results[i - 1].cl,
"Cl should increase monotonically with AoA"
);
}
}
#[test]
fn vlm_to_forces_pipeline() {
let wing = vlm::WingGeometry::rectangular(6.0, 1.0, 10, 2);
let panels = vlm::generate_panels(&wing);
let sol = vlm::solve(&panels, &wing, 5.0_f64.to_radians(), 60.0).expect("vlm solve");
let rho = atmosphere::standard_density(0.0);
let q = atmosphere::dynamic_pressure(rho, 60.0);
let s = wing.reference_area();
let l = forces::lift(q, s, sol.cl);
let d = forces::drag(q, s, sol.cdi);
assert!(
l > 0.0,
"VLM should produce positive lift, got CL={}",
sol.cl
);
assert!(
d > 0.0,
"VLM should produce positive induced drag, got CDi={}",
sol.cdi
);
assert!(l > d, "lift should exceed induced drag");
}
#[test]
fn vlm_cdi_matches_formula() {
let wing = vlm::WingGeometry::rectangular(8.0, 1.0, 12, 2);
let panels = vlm::generate_panels(&wing);
let sol = vlm::solve(&panels, &wing, 5.0_f64.to_radians(), 1.0).expect("solve");
let ar = wing.aspect_ratio();
let cdi_formula = sol.cl * sol.cl / (std::f64::consts::PI * sol.oswald_efficiency * ar);
let rel_err = (sol.cdi - cdi_formula).abs() / sol.cdi.max(1e-10);
assert!(
rel_err < 0.05,
"CDi from VLM ({}) should match formula ({}) within 5%",
sol.cdi,
cdi_formula
);
}
#[test]
fn vlm_serde_round_trip_wing() {
let wing = vlm::WingGeometry::tapered(10.0, 2.0, 1.0, 8, 2);
let json = serde_json::to_string(&wing).expect("serialize");
let back: WingGeometry = serde_json::from_str(&json).expect("deserialize");
assert!((back.span - wing.span).abs() < f64::EPSILON);
assert!((back.tip_chord - wing.tip_chord).abs() < f64::EPSILON);
}
#[cfg(feature = "cfd")]
mod cfd_tests {
use pavan::airfoil::NacaProfile;
use pavan::cfd::{self, AirfoilCfd, AirfoilCfdConfig, CfdSnapshot};
use pavan::panel;
fn small_config() -> AirfoilCfdConfig {
let mut cfg = AirfoilCfdConfig::default_for(50.0, 0.0);
cfg.grid_nx = 60;
cfg.grid_ny = 30;
cfg.domain_size = (6.0, 3.0);
cfg.dt = 0.001;
cfg.pressure_iterations = 20;
cfg.use_multigrid = false;
cfg.surface_points = 40;
cfg
}
#[test]
fn full_pipeline_panel_to_cfd() {
let profile = NacaProfile::naca0012();
let cfg = small_config();
let mut cfd = AirfoilCfd::new(&profile, &cfg).expect("new");
let (upper, lower) = profile.surface_coordinates(60);
let panels = panel::panels_from_surface(&upper, &lower);
let sol = panel::solve(&panels, 0.0).expect("panel solve");
cfd::init_from_panel(&mut cfd, &sol, &panels).expect("init");
let history = cfd.run(5).expect("run");
assert_eq!(history.len(), 5);
assert!(history[4].max_speed > 0.0);
}
#[test]
fn serde_round_trip_cfd_snapshot() {
let profile = NacaProfile::naca0012();
let cfg = small_config();
let mut cfd = AirfoilCfd::new(&profile, &cfg).expect("new");
let snap = cfd.step().expect("step");
let json = serde_json::to_string(&snap).expect("serialize");
let back: CfdSnapshot = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.step, snap.step);
assert!((back.cl - snap.cl).abs() < f64::EPSILON);
}
}