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.cos(), 0.0, -self.speed_mps * ang.sin(), )
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.cos(), 0.0,
175 -speed * direction_rad.sin(), )
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 last = self.custom_layers.len() - 1;
191 if altitude_m >= self.custom_layers[last].altitude_m {
192 return self.custom_layers[last].to_vector();
193 }
194
195 let mut low_idx = 0;
197 let mut high_idx = 0;
198
199 for (i, layer) in self.custom_layers.iter().enumerate() {
200 if layer.altitude_m <= altitude_m {
201 low_idx = i;
202 }
203 if layer.altitude_m >= altitude_m {
204 high_idx = i;
205 break;
206 }
207 }
208
209 if low_idx == high_idx {
211 return self.custom_layers[low_idx].to_vector();
212 }
213
214 let low_layer = &self.custom_layers[low_idx];
216 let high_layer = &self.custom_layers[high_idx];
217
218 let altitude_diff = high_layer.altitude_m - low_layer.altitude_m;
220 if altitude_diff.abs() < 1e-9 {
221 return low_layer.to_vector();
222 }
223
224 let ratio = (altitude_m - low_layer.altitude_m) / altitude_diff;
225
226 let low_vec = low_layer.to_vector();
228 let high_vec = high_layer.to_vector();
229
230 low_vec * (1.0 - ratio) + high_vec * ratio
231 }
232}
233
234#[derive(Debug, Clone)]
236pub struct WindShearWindSock {
237 pub segments: Vec<(f64, f64, f64)>, pub shear_profile: Option<WindShearProfile>,
239 pub shooter_altitude_m: f64,
240}
241
242impl WindShearWindSock {
243 pub fn new(segments: Vec<(f64, f64, f64)>, shear_profile: Option<WindShearProfile>) -> Self {
244 Self {
245 segments,
246 shear_profile,
247 shooter_altitude_m: 0.0,
248 }
249 }
250
251 pub fn with_shooter_altitude(
252 segments: Vec<(f64, f64, f64)>,
253 shear_profile: Option<WindShearProfile>,
254 shooter_altitude_m: f64,
255 ) -> Self {
256 Self {
257 segments,
258 shear_profile,
259 shooter_altitude_m,
260 }
261 }
262
263 pub fn vector_for_position(&self, position: Vector3<f64>) -> Vector3<f64> {
266 let range_m = position.x; let altitude_m = position.y; let base_wind = self.get_range_wind(range_m);
271
272 if let Some(profile) = &self.shear_profile {
273 if profile.model != WindShearModel::None {
274 let absolute_altitude_m = altitude_m + self.shooter_altitude_m;
277 let altitude_vec = profile.get_wind_at_altitude(absolute_altitude_m);
278
279 if base_wind.norm() > 0.0 {
281 let scale_factor =
282 altitude_vec.norm() / profile.surface_wind.speed_mps.max(1e-9);
283 return base_wind * scale_factor;
284 }
285
286 return altitude_vec;
287 }
288 }
289
290 base_wind
291 }
292
293 fn get_range_wind(&self, range_m: f64) -> Vector3<f64> {
296 if range_m.is_nan() || self.segments.is_empty() {
297 return Vector3::zeros();
298 }
299
300 for &(speed_mps, angle_deg, until_dist) in &self.segments {
302 if range_m <= until_dist {
303 let ang = angle_deg.to_radians();
304 return Vector3::new(
305 -speed_mps * ang.cos(), 0.0,
307 -speed_mps * ang.sin(), );
309 }
310 }
311
312 Vector3::zeros()
314 }
315}
316
317pub fn boundary_layer_speed_ratio(height_rel_launch_m: f64, model: WindShearModel) -> f64 {
334 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);
339 let ratio = match model {
340 WindShearModel::PowerLaw => (height_agl / H_REF).powf(1.0 / 7.0),
341 WindShearModel::Logarithmic => (height_agl / Z0).ln() / (H_REF / Z0).ln(),
342 _ => 1.0,
344 };
345 ratio.max(1.0)
346}
347
348pub fn get_wind_at_position(
363 position: &Vector3<f64>,
364 wind_segments: &[(f64, f64, f64)], enable_wind_shear: bool,
366 wind_shear_model: &str,
367 shooter_altitude_m: f64,
368) -> Vector3<f64> {
369 let range_m = position[0];
371 let altitude_m = position[1]; let base_wind = if wind_segments.is_empty() {
375 (0.0, 0.0)
376 } else {
377 let mut found_wind = (wind_segments[0].0, wind_segments[0].1);
379 for seg in wind_segments {
380 if range_m <= seg.2 {
381 found_wind = (seg.0, seg.1);
382 break;
383 }
384 }
385 found_wind
386 };
387
388 let base_speed_mps = base_wind.0 * 0.2777778; let base_direction_deg = base_wind.1;
391
392 if !enable_wind_shear || wind_shear_model == "none" {
393 let ang = base_direction_deg.to_radians();
395 return Vector3::new(
396 -base_speed_mps * ang.cos(), 0.0, -base_speed_mps * ang.sin(), );
400 }
401
402 let model = match wind_shear_model {
409 "logarithmic" => WindShearModel::Logarithmic,
410 "power_law" | "powerlaw" => WindShearModel::PowerLaw,
411 "ekman_spiral" | "ekman" => WindShearModel::EkmanSpiral,
412 "custom_layers" | "custom" => WindShearModel::CustomLayers,
413 _ => WindShearModel::None,
414 };
415
416 let _ = shooter_altitude_m;
420
421 let speed_ratio = boundary_layer_speed_ratio(altitude_m, model);
422 let ang = base_direction_deg.to_radians();
423 Vector3::new(
424 -base_speed_mps * ang.cos() * speed_ratio, 0.0, -base_speed_mps * ang.sin() * speed_ratio, )
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_wind_layer() {
436 let layer_headwind = WindLayer {
441 altitude_m: 100.0,
442 speed_mps: 10.0,
443 direction_deg: 0.0, };
445
446 let vec = layer_headwind.to_vector();
447 assert!(
448 (vec.x - (-10.0)).abs() < 1e-6,
449 "0° wind should be headwind (negative X downrange)"
450 );
451 assert_eq!(vec.y, 0.0);
452 assert!(
453 (vec.z).abs() < 1e-6,
454 "0° wind (headwind) should have zero lateral (Z) component"
455 );
456
457 let layer_crosswind = WindLayer {
459 altitude_m: 100.0,
460 speed_mps: 10.0,
461 direction_deg: 90.0, };
463
464 let vec_cross = layer_crosswind.to_vector();
465 assert!(
466 (vec_cross.z - (-10.0)).abs() < 1e-6,
467 "90° wind should have negative Z lateral (from right)"
468 );
469 assert_eq!(vec_cross.y, 0.0);
470 assert!(
471 (vec_cross.x).abs() < 1e-6,
472 "90° wind (crosswind) should have zero downrange (X) component"
473 );
474 }
475
476 #[test]
477 fn test_logarithmic_profile() {
478 let mut profile = WindShearProfile::default();
479 profile.model = WindShearModel::Logarithmic;
480 profile.surface_wind = WindLayer {
481 altitude_m: 0.0,
482 speed_mps: 10.0,
483 direction_deg: 0.0,
484 };
485
486 let v10 = profile.get_wind_at_altitude(10.0).norm();
488 let v50 = profile.get_wind_at_altitude(50.0).norm();
489 let v100 = profile.get_wind_at_altitude(100.0).norm();
490
491 assert!(v10 > 0.0);
492 assert!(v50 > v10);
493 assert!(v100 > v50);
494 }
495
496 #[test]
497 fn test_boundary_layer_speed_ratio_flat_fire_full_wind() {
498 for &h in &[-15.0, -11.3, -1.0, -0.2, 0.0, 0.14, 1.5, 5.0, 8.0] {
501 let r_log = boundary_layer_speed_ratio(h, WindShearModel::Logarithmic);
502 let r_pow = boundary_layer_speed_ratio(h, WindShearModel::PowerLaw);
503 assert!(
504 (r_log - 1.0).abs() < 1e-9,
505 "logarithmic ratio at h={h} should be 1.0 (full wind), got {r_log}"
506 );
507 assert!(
508 (r_pow - 1.0).abs() < 1e-9,
509 "power-law ratio at h={h} should be 1.0 (full wind), got {r_pow}"
510 );
511 }
512 }
513
514 #[test]
515 fn test_boundary_layer_speed_ratio_increases_aloft() {
516 let r100 = boundary_layer_speed_ratio(100.0, WindShearModel::Logarithmic);
518 let r300 = boundary_layer_speed_ratio(300.0, WindShearModel::Logarithmic);
519 assert!(r100 > 1.0, "ratio at 100 m should exceed 1.0, got {r100}");
520 assert!(r300 > r100, "ratio should increase with altitude: {r300} !> {r100}");
521 assert!(
523 (r100 - 1.40).abs() < 0.10,
524 "ratio at ~100 m should be ~1.4, got {r100}"
525 );
526 }
527
528 #[test]
529 fn test_power_law_profile() {
530 let mut profile = WindShearProfile::default();
531 profile.model = WindShearModel::PowerLaw;
532 profile.surface_wind = WindLayer {
533 altitude_m: 0.0,
534 speed_mps: 10.0,
535 direction_deg: 0.0,
536 };
537
538 let v100 = profile.get_wind_at_altitude(100.0).norm();
540 let expected = 10.0 * (100.0_f64 / 10.0).powf(1.0 / 7.0);
541 assert!((v100 - expected).abs() < 0.01);
542 }
543}
544
545#[cfg(test)]
546mod fix_validation_tests {
547 use super::*;
548 use nalgebra::Vector3;
549
550 #[test]
551 fn test_get_wind_at_position_flat_fire_full_crosswind() {
552 let pos = Vector3::new(457.0, -1.0, 0.0); let segs = [(16.09344_f64, 90.0_f64, 1000.0_f64)];
556 let w = get_wind_at_position(&pos, &segs, true, "logarithmic", 0.0);
557 let expected = 16.09344 * 0.2777778; println!("flat-fire wind vec = {:?}, |Z| = {}", w, w.z.abs());
559 assert!(
560 (w.z.abs() - expected).abs() < 0.05,
561 "lateral wind should be ~full {expected:.3} m/s, got {:.3}",
562 w.z.abs()
563 );
564 }
565}