use nalgebra::Vector3;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindShearModel {
None,
Logarithmic,
PowerLaw,
EkmanSpiral,
CustomLayers,
}
#[derive(Debug, Clone, Copy)]
pub struct WindLayer {
pub altitude_m: f64,
pub speed_mps: f64,
pub direction_deg: f64, }
impl WindLayer {
pub fn to_vector(&self) -> Vector3<f64> {
let ang = self.direction_deg.to_radians();
Vector3::new(
-self.speed_mps * ang.cos(), 0.0, -self.speed_mps * ang.sin(), )
}
}
#[derive(Debug, Clone)]
pub struct WindShearProfile {
pub model: WindShearModel,
pub surface_wind: WindLayer,
pub reference_height: f64, pub roughness_length: f64, pub power_exponent: f64, pub geostrophic_wind: Option<WindLayer>, pub custom_layers: Vec<WindLayer>,
}
impl Default for WindShearProfile {
fn default() -> Self {
Self {
model: WindShearModel::None,
surface_wind: WindLayer {
altitude_m: 0.0,
speed_mps: 0.0,
direction_deg: 0.0,
},
reference_height: 10.0,
roughness_length: 0.03,
power_exponent: 1.0 / 7.0,
geostrophic_wind: None,
custom_layers: Vec::new(),
}
}
}
impl WindShearProfile {
pub fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
match self.model {
WindShearModel::None => self.surface_wind.to_vector(),
WindShearModel::Logarithmic => self.logarithmic_profile(altitude_m),
WindShearModel::PowerLaw => self.power_law_profile(altitude_m),
WindShearModel::EkmanSpiral => self.ekman_spiral_profile(altitude_m),
WindShearModel::CustomLayers => self.interpolate_layers(altitude_m),
}
}
fn logarithmic_profile(&self, altitude_m: f64) -> Vector3<f64> {
let effective_altitude = if altitude_m < 0.0 {
0.001 } else if altitude_m < 0.001 {
0.001
} else {
altitude_m
};
if effective_altitude <= self.roughness_length {
return Vector3::zeros();
}
let speed_ratio = if effective_altitude > self.roughness_length
&& self.reference_height > self.roughness_length
{
(effective_altitude / self.roughness_length).ln()
/ (self.reference_height / self.roughness_length).ln()
} else {
1.0
};
self.surface_wind.to_vector() * speed_ratio.max(0.0)
}
fn power_law_profile(&self, altitude_m: f64) -> Vector3<f64> {
if altitude_m <= 0.0 {
return Vector3::zeros();
}
let speed_ratio = (altitude_m / self.reference_height).powf(self.power_exponent);
self.surface_wind.to_vector() * speed_ratio
}
fn ekman_spiral_profile(&self, altitude_m: f64) -> Vector3<f64> {
let geo_wind = self.geostrophic_wind.unwrap_or({
WindLayer {
altitude_m: 1000.0,
speed_mps: self.surface_wind.speed_mps * 1.5,
direction_deg: self.surface_wind.direction_deg - 30.0, }
});
let ekman_depth = 1000.0;
if altitude_m >= ekman_depth {
return geo_wind.to_vector();
}
let ratio = altitude_m / ekman_depth;
let speed = self.surface_wind.speed_mps * (1.0 - ratio) + geo_wind.speed_mps * ratio;
let dir1 = self.surface_wind.direction_deg.to_radians();
let mut dir2 = geo_wind.direction_deg.to_radians();
if (dir2 - dir1).abs() > PI {
if dir2 > dir1 {
dir2 -= 2.0 * PI;
} else {
dir2 += 2.0 * PI;
}
}
let direction_rad = dir1 * (1.0 - ratio) + dir2 * ratio;
Vector3::new(
-speed * direction_rad.cos(), 0.0,
-speed * direction_rad.sin(), )
}
fn interpolate_layers(&self, altitude_m: f64) -> Vector3<f64> {
if self.custom_layers.is_empty() {
return self.surface_wind.to_vector();
}
let last = self.custom_layers.len() - 1;
if altitude_m >= self.custom_layers[last].altitude_m {
return self.custom_layers[last].to_vector();
}
let mut low_idx = 0;
let mut high_idx = 0;
for (i, layer) in self.custom_layers.iter().enumerate() {
if layer.altitude_m <= altitude_m {
low_idx = i;
}
if layer.altitude_m >= altitude_m {
high_idx = i;
break;
}
}
if low_idx == high_idx {
return self.custom_layers[low_idx].to_vector();
}
let low_layer = &self.custom_layers[low_idx];
let high_layer = &self.custom_layers[high_idx];
let altitude_diff = high_layer.altitude_m - low_layer.altitude_m;
if altitude_diff.abs() < 1e-9 {
return low_layer.to_vector();
}
let ratio = (altitude_m - low_layer.altitude_m) / altitude_diff;
let low_vec = low_layer.to_vector();
let high_vec = high_layer.to_vector();
low_vec * (1.0 - ratio) + high_vec * ratio
}
}
#[derive(Debug, Clone)]
pub struct WindShearWindSock {
pub segments: Vec<(f64, f64, f64)>, pub shear_profile: Option<WindShearProfile>,
pub shooter_altitude_m: f64,
}
impl WindShearWindSock {
pub fn new(segments: Vec<(f64, f64, f64)>, shear_profile: Option<WindShearProfile>) -> Self {
Self {
segments,
shear_profile,
shooter_altitude_m: 0.0,
}
}
pub fn with_shooter_altitude(
segments: Vec<(f64, f64, f64)>,
shear_profile: Option<WindShearProfile>,
shooter_altitude_m: f64,
) -> Self {
Self {
segments,
shear_profile,
shooter_altitude_m,
}
}
pub fn vector_for_position(&self, position: Vector3<f64>) -> Vector3<f64> {
let range_m = position.x; let altitude_m = position.y;
let base_wind = self.get_range_wind(range_m);
if let Some(profile) = &self.shear_profile {
if profile.model != WindShearModel::None {
let absolute_altitude_m = altitude_m + self.shooter_altitude_m;
let altitude_vec = profile.get_wind_at_altitude(absolute_altitude_m);
if base_wind.norm() > 0.0 {
let scale_factor =
altitude_vec.norm() / profile.surface_wind.speed_mps.max(1e-9);
return base_wind * scale_factor;
}
return altitude_vec;
}
}
base_wind
}
fn get_range_wind(&self, range_m: f64) -> Vector3<f64> {
if range_m.is_nan() || self.segments.is_empty() {
return Vector3::zeros();
}
for &(speed_mps, angle_deg, until_dist) in &self.segments {
if range_m <= until_dist {
let ang = angle_deg.to_radians();
return Vector3::new(
-speed_mps * ang.cos(), 0.0,
-speed_mps * ang.sin(), );
}
}
Vector3::zeros()
}
}
pub fn boundary_layer_speed_ratio(height_rel_launch_m: f64, model: WindShearModel) -> f64 {
const Z0: f64 = 0.03; const H_REF: f64 = 10.0; const MUZZLE_HEIGHT_M: f64 = 1.5;
let height_agl = (height_rel_launch_m + MUZZLE_HEIGHT_M).max(Z0 * 1.000_1);
let ratio = match model {
WindShearModel::PowerLaw => (height_agl / H_REF).powf(1.0 / 7.0),
WindShearModel::Logarithmic => (height_agl / Z0).ln() / (H_REF / Z0).ln(),
_ => 1.0,
};
ratio.max(1.0)
}
pub fn get_wind_at_position(
position: &Vector3<f64>,
wind_segments: &[(f64, f64, f64)], enable_wind_shear: bool,
wind_shear_model: &str,
shooter_altitude_m: f64,
) -> Vector3<f64> {
let range_m = position[0];
let altitude_m = position[1];
let base_wind = if wind_segments.is_empty() {
(0.0, 0.0)
} else {
let mut found_wind = (wind_segments[0].0, wind_segments[0].1);
for seg in wind_segments {
if range_m <= seg.2 {
found_wind = (seg.0, seg.1);
break;
}
}
found_wind
};
let base_speed_mps = base_wind.0 * 0.2777778; let base_direction_deg = base_wind.1;
if !enable_wind_shear || wind_shear_model == "none" {
let ang = base_direction_deg.to_radians();
return Vector3::new(
-base_speed_mps * ang.cos(), 0.0, -base_speed_mps * ang.sin(), );
}
let model = match wind_shear_model {
"logarithmic" => WindShearModel::Logarithmic,
"power_law" | "powerlaw" => WindShearModel::PowerLaw,
"ekman_spiral" | "ekman" => WindShearModel::EkmanSpiral,
"custom_layers" | "custom" => WindShearModel::CustomLayers,
_ => WindShearModel::None,
};
let _ = shooter_altitude_m;
let speed_ratio = boundary_layer_speed_ratio(altitude_m, model);
let ang = base_direction_deg.to_radians();
Vector3::new(
-base_speed_mps * ang.cos() * speed_ratio, 0.0, -base_speed_mps * ang.sin() * speed_ratio, )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wind_layer() {
let layer_headwind = WindLayer {
altitude_m: 100.0,
speed_mps: 10.0,
direction_deg: 0.0, };
let vec = layer_headwind.to_vector();
assert!(
(vec.x - (-10.0)).abs() < 1e-6,
"0° wind should be headwind (negative X downrange)"
);
assert_eq!(vec.y, 0.0);
assert!(
(vec.z).abs() < 1e-6,
"0° wind (headwind) should have zero lateral (Z) component"
);
let layer_crosswind = WindLayer {
altitude_m: 100.0,
speed_mps: 10.0,
direction_deg: 90.0, };
let vec_cross = layer_crosswind.to_vector();
assert!(
(vec_cross.z - (-10.0)).abs() < 1e-6,
"90° wind should have negative Z lateral (from right)"
);
assert_eq!(vec_cross.y, 0.0);
assert!(
(vec_cross.x).abs() < 1e-6,
"90° wind (crosswind) should have zero downrange (X) component"
);
}
#[test]
fn test_logarithmic_profile() {
let mut profile = WindShearProfile::default();
profile.model = WindShearModel::Logarithmic;
profile.surface_wind = WindLayer {
altitude_m: 0.0,
speed_mps: 10.0,
direction_deg: 0.0,
};
let v10 = profile.get_wind_at_altitude(10.0).norm();
let v50 = profile.get_wind_at_altitude(50.0).norm();
let v100 = profile.get_wind_at_altitude(100.0).norm();
assert!(v10 > 0.0);
assert!(v50 > v10);
assert!(v100 > v50);
}
#[test]
fn test_boundary_layer_speed_ratio_flat_fire_full_wind() {
for &h in &[-15.0, -11.3, -1.0, -0.2, 0.0, 0.14, 1.5, 5.0, 8.0] {
let r_log = boundary_layer_speed_ratio(h, WindShearModel::Logarithmic);
let r_pow = boundary_layer_speed_ratio(h, WindShearModel::PowerLaw);
assert!(
(r_log - 1.0).abs() < 1e-9,
"logarithmic ratio at h={h} should be 1.0 (full wind), got {r_log}"
);
assert!(
(r_pow - 1.0).abs() < 1e-9,
"power-law ratio at h={h} should be 1.0 (full wind), got {r_pow}"
);
}
}
#[test]
fn test_boundary_layer_speed_ratio_increases_aloft() {
let r100 = boundary_layer_speed_ratio(100.0, WindShearModel::Logarithmic);
let r300 = boundary_layer_speed_ratio(300.0, WindShearModel::Logarithmic);
assert!(r100 > 1.0, "ratio at 100 m should exceed 1.0, got {r100}");
assert!(r300 > r100, "ratio should increase with altitude: {r300} !> {r100}");
assert!(
(r100 - 1.40).abs() < 0.10,
"ratio at ~100 m should be ~1.4, got {r100}"
);
}
#[test]
fn test_power_law_profile() {
let mut profile = WindShearProfile::default();
profile.model = WindShearModel::PowerLaw;
profile.surface_wind = WindLayer {
altitude_m: 0.0,
speed_mps: 10.0,
direction_deg: 0.0,
};
let v100 = profile.get_wind_at_altitude(100.0).norm();
let expected = 10.0 * (100.0_f64 / 10.0).powf(1.0 / 7.0);
assert!((v100 - expected).abs() < 0.01);
}
}
#[cfg(test)]
mod fix_validation_tests {
use super::*;
use nalgebra::Vector3;
#[test]
fn test_get_wind_at_position_flat_fire_full_crosswind() {
let pos = Vector3::new(457.0, -1.0, 0.0); let segs = [(16.09344_f64, 90.0_f64, 1000.0_f64)];
let w = get_wind_at_position(&pos, &segs, true, "logarithmic", 0.0);
let expected = 16.09344 * 0.2777778; println!("flat-fire wind vec = {:?}, |Z| = {}", w, w.z.abs());
assert!(
(w.z.abs() - expected).abs() < 0.05,
"lateral wind should be ~full {expected:.3} m/s, got {:.3}",
w.z.abs()
);
}
}