1use nalgebra::Vector3;
10use std::f64::consts::PI;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum WindShearModel {
15 None,
16 Logarithmic,
17 PowerLaw,
18 EkmanSpiral,
19 CustomLayers,
20}
21
22#[derive(Debug, Clone, Copy)]
24pub struct WindLayer {
25 pub altitude_m: f64,
26 pub speed_mps: f64,
27 pub direction_deg: f64, }
29
30impl WindLayer {
31 pub fn to_vector(&self) -> Vector3<f64> {
34 let ang = self.direction_deg.to_radians();
35 Vector3::new(
36 -self.speed_mps * ang.sin(), 0.0, -self.speed_mps * ang.cos(), )
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct WindShearProfile {
46 pub model: WindShearModel,
47 pub surface_wind: WindLayer,
48 pub reference_height: f64, pub roughness_length: f64, pub power_exponent: f64, pub geostrophic_wind: Option<WindLayer>, pub custom_layers: Vec<WindLayer>,
53}
54
55impl Default for WindShearProfile {
56 fn default() -> Self {
57 Self {
58 model: WindShearModel::None,
59 surface_wind: WindLayer {
60 altitude_m: 0.0,
61 speed_mps: 0.0,
62 direction_deg: 0.0,
63 },
64 reference_height: 10.0,
65 roughness_length: 0.03,
66 power_exponent: 1.0 / 7.0,
67 geostrophic_wind: None,
68 custom_layers: Vec::new(),
69 }
70 }
71}
72
73impl WindShearProfile {
74 pub fn get_wind_at_altitude(&self, altitude_m: f64) -> Vector3<f64> {
76 match self.model {
77 WindShearModel::None => self.surface_wind.to_vector(),
78 WindShearModel::Logarithmic => self.logarithmic_profile(altitude_m),
79 WindShearModel::PowerLaw => self.power_law_profile(altitude_m),
80 WindShearModel::EkmanSpiral => self.ekman_spiral_profile(altitude_m),
81 WindShearModel::CustomLayers => self.interpolate_layers(altitude_m),
82 }
83 }
84
85 fn logarithmic_profile(&self, altitude_m: f64) -> Vector3<f64> {
88 let effective_altitude = if altitude_m < 0.0 {
91 0.001 } else if altitude_m < 0.001 {
94 0.001
96 } else {
97 altitude_m
98 };
99
100 if effective_altitude <= self.roughness_length {
102 return Vector3::zeros();
103 }
104
105 let speed_ratio = if effective_altitude > self.roughness_length
107 && self.reference_height > self.roughness_length
108 {
109 (effective_altitude / self.roughness_length).ln()
110 / (self.reference_height / self.roughness_length).ln()
111 } else {
112 1.0
113 };
114
115 self.surface_wind.to_vector() * speed_ratio.max(0.0)
117 }
118
119 fn power_law_profile(&self, altitude_m: f64) -> Vector3<f64> {
121 if altitude_m <= 0.0 {
122 return Vector3::zeros();
123 }
124
125 let speed_ratio = (altitude_m / self.reference_height).powf(self.power_exponent);
127
128 self.surface_wind.to_vector() * speed_ratio
130 }
131
132 fn ekman_spiral_profile(&self, altitude_m: f64) -> Vector3<f64> {
134 let geo_wind = self.geostrophic_wind.unwrap_or({
136 WindLayer {
137 altitude_m: 1000.0,
138 speed_mps: self.surface_wind.speed_mps * 1.5,
139 direction_deg: self.surface_wind.direction_deg - 30.0, }
141 });
142
143 let ekman_depth = 1000.0; if altitude_m >= ekman_depth {
147 return geo_wind.to_vector();
148 }
149
150 let ratio = altitude_m / ekman_depth;
152
153 let speed = self.surface_wind.speed_mps * (1.0 - ratio) + geo_wind.speed_mps * ratio;
155
156 let dir1 = self.surface_wind.direction_deg.to_radians();
158 let mut dir2 = geo_wind.direction_deg.to_radians();
159
160 if (dir2 - dir1).abs() > PI {
162 if dir2 > dir1 {
163 dir2 -= 2.0 * PI;
164 } else {
165 dir2 += 2.0 * PI;
166 }
167 }
168
169 let direction_rad = dir1 * (1.0 - ratio) + dir2 * ratio;
170
171 Vector3::new(
173 -speed * direction_rad.sin(), 0.0,
175 -speed * direction_rad.cos(), )
177 }
178
179 fn interpolate_layers(&self, altitude_m: f64) -> Vector3<f64> {
181 if self.custom_layers.is_empty() {
182 return self.surface_wind.to_vector();
183 }
184
185 let mut low_idx = 0;
187 let mut high_idx = 0;
188
189 for (i, layer) in self.custom_layers.iter().enumerate() {
190 if layer.altitude_m <= altitude_m {
191 low_idx = i;
192 }
193 if layer.altitude_m >= altitude_m {
194 high_idx = i;
195 break;
196 }
197 }
198
199 if low_idx == high_idx {
201 return self.custom_layers[low_idx].to_vector();
202 }
203
204 let low_layer = &self.custom_layers[low_idx];
206 let high_layer = &self.custom_layers[high_idx];
207
208 let altitude_diff = high_layer.altitude_m - low_layer.altitude_m;
210 if altitude_diff.abs() < 1e-9 {
211 return low_layer.to_vector();
212 }
213
214 let ratio = (altitude_m - low_layer.altitude_m) / altitude_diff;
215
216 let low_vec = low_layer.to_vector();
218 let high_vec = high_layer.to_vector();
219
220 low_vec * (1.0 - ratio) + high_vec * ratio
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct WindShearWindSock {
227 pub segments: Vec<(f64, f64, f64)>, pub shear_profile: Option<WindShearProfile>,
229 pub shooter_altitude_m: f64,
230}
231
232impl WindShearWindSock {
233 pub fn new(segments: Vec<(f64, f64, f64)>, shear_profile: Option<WindShearProfile>) -> Self {
234 Self {
235 segments,
236 shear_profile,
237 shooter_altitude_m: 0.0,
238 }
239 }
240
241 pub fn with_shooter_altitude(
242 segments: Vec<(f64, f64, f64)>,
243 shear_profile: Option<WindShearProfile>,
244 shooter_altitude_m: f64,
245 ) -> Self {
246 Self {
247 segments,
248 shear_profile,
249 shooter_altitude_m,
250 }
251 }
252
253 pub fn vector_for_position(&self, position: Vector3<f64>) -> Vector3<f64> {
256 let range_m = position.z; let altitude_m = position.y; let base_wind = self.get_range_wind(range_m);
261
262 if let Some(profile) = &self.shear_profile {
263 if profile.model != WindShearModel::None {
264 let absolute_altitude_m = altitude_m + self.shooter_altitude_m;
267 let altitude_vec = profile.get_wind_at_altitude(absolute_altitude_m);
268
269 if base_wind.norm() > 0.0 {
271 let scale_factor =
272 altitude_vec.norm() / profile.surface_wind.speed_mps.max(1e-9);
273 return base_wind * scale_factor;
274 }
275
276 return altitude_vec;
277 }
278 }
279
280 base_wind
281 }
282
283 fn get_range_wind(&self, range_m: f64) -> Vector3<f64> {
286 if range_m.is_nan() || self.segments.is_empty() {
287 return Vector3::zeros();
288 }
289
290 for &(speed_mps, angle_deg, until_dist) in &self.segments {
292 if range_m <= until_dist {
293 let ang = angle_deg.to_radians();
294 return Vector3::new(
295 -speed_mps * ang.sin(), 0.0,
297 -speed_mps * ang.cos(), );
299 }
300 }
301
302 Vector3::zeros()
304 }
305}
306
307pub fn get_wind_at_position(
322 position: &Vector3<f64>,
323 wind_segments: &[(f64, f64, f64)], enable_wind_shear: bool,
325 wind_shear_model: &str,
326 shooter_altitude_m: f64,
327) -> Vector3<f64> {
328 let range_m = position[2];
330 let altitude_m = position[1]; let base_wind = if wind_segments.is_empty() {
334 (0.0, 0.0)
335 } else {
336 let mut found_wind = (wind_segments[0].0, wind_segments[0].1);
338 for seg in wind_segments {
339 if range_m <= seg.2 {
340 found_wind = (seg.0, seg.1);
341 break;
342 }
343 }
344 found_wind
345 };
346
347 let base_speed_mps = base_wind.0 * 0.2777778; let base_direction_deg = base_wind.1;
350
351 if !enable_wind_shear || wind_shear_model == "none" {
352 let ang = base_direction_deg.to_radians();
354 return Vector3::new(
355 -base_speed_mps * ang.sin(), 0.0, -base_speed_mps * ang.cos(), );
359 }
360
361 let mut profile = WindShearProfile::default();
363 profile.model = match wind_shear_model {
364 "logarithmic" => WindShearModel::Logarithmic,
365 "power_law" | "powerlaw" => WindShearModel::PowerLaw,
366 "ekman_spiral" | "ekman" => WindShearModel::EkmanSpiral,
367 "custom_layers" | "custom" => WindShearModel::CustomLayers,
368 _ => WindShearModel::None,
369 };
370 profile.surface_wind = WindLayer {
371 altitude_m: 0.0,
372 speed_mps: base_speed_mps,
373 direction_deg: base_direction_deg,
374 };
375
376 let absolute_altitude_m = altitude_m + shooter_altitude_m;
378
379 if absolute_altitude_m.abs() < 0.1 && altitude_m.abs() < 0.1 {
382 let ang = base_direction_deg.to_radians();
384 return Vector3::new(
385 -base_speed_mps * ang.sin() * 0.5, 0.0,
387 -base_speed_mps * ang.cos() * 0.5,
388 );
389 }
390
391 if range_m > 800.0 {
394 let altitude_factor = (1.0 + absolute_altitude_m / 100.0).min(2.0).max(0.1);
396 let sheared_speed = base_speed_mps * altitude_factor;
397 let ang = base_direction_deg.to_radians();
398 return Vector3::new(-sheared_speed * ang.sin(), 0.0, -sheared_speed * ang.cos());
399 }
400
401 let clamped_altitude = absolute_altitude_m.max(-10.0).min(1000.0);
403 profile.get_wind_at_altitude(clamped_altitude)
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_wind_layer() {
412 let layer_headwind = WindLayer {
417 altitude_m: 100.0,
418 speed_mps: 10.0,
419 direction_deg: 0.0, };
421
422 let vec = layer_headwind.to_vector();
423 assert!(
424 (vec.x).abs() < 1e-6,
425 "0° wind (headwind) should have zero lateral (X) component"
426 );
427 assert_eq!(vec.y, 0.0);
428 assert!(
429 (vec.z - (-10.0)).abs() < 1e-6,
430 "0° wind should be headwind (negative Z downrange)"
431 );
432
433 let layer_crosswind = WindLayer {
435 altitude_m: 100.0,
436 speed_mps: 10.0,
437 direction_deg: 90.0, };
439
440 let vec_cross = layer_crosswind.to_vector();
441 assert!(
442 (vec_cross.x - (-10.0)).abs() < 1e-6,
443 "90° wind should have negative X lateral (from right)"
444 );
445 assert_eq!(vec_cross.y, 0.0);
446 assert!(
447 (vec_cross.z).abs() < 1e-6,
448 "90° wind (crosswind) should have zero downrange (Z) component"
449 );
450 }
451
452 #[test]
453 fn test_logarithmic_profile() {
454 let mut profile = WindShearProfile::default();
455 profile.model = WindShearModel::Logarithmic;
456 profile.surface_wind = WindLayer {
457 altitude_m: 0.0,
458 speed_mps: 10.0,
459 direction_deg: 0.0,
460 };
461
462 let v10 = profile.get_wind_at_altitude(10.0).norm();
464 let v50 = profile.get_wind_at_altitude(50.0).norm();
465 let v100 = profile.get_wind_at_altitude(100.0).norm();
466
467 assert!(v10 > 0.0);
468 assert!(v50 > v10);
469 assert!(v100 > v50);
470 }
471
472 #[test]
473 fn test_power_law_profile() {
474 let mut profile = WindShearProfile::default();
475 profile.model = WindShearModel::PowerLaw;
476 profile.surface_wind = WindLayer {
477 altitude_m: 0.0,
478 speed_mps: 10.0,
479 direction_deg: 0.0,
480 };
481
482 let v100 = profile.get_wind_at_altitude(100.0).norm();
484 let expected = 10.0 * (100.0_f64 / 10.0).powf(1.0 / 7.0);
485 assert!((v100 - expected).abs() < 0.01);
486 }
487}