1use std::f64::consts::{PI, TAU};
20use std::fmt;
21
22use hpr_core::DVec3;
23use hpr_core::interp::Side;
24use serde::{Deserialize, Serialize};
25
26use crate::error::{AtmosError, finite, positive};
27
28#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
30pub struct WindSample {
31 pub velocity_enu_m_s: DVec3,
33 pub extrapolated: Option<Side>,
37}
38
39pub trait Wind: fmt::Debug + Send + Sync {
41 fn wind(&self, height_msl_m: f64) -> Result<WindSample, AtmosError>;
47}
48
49pub fn velocity_from_speed_direction(speed_m_s: f64, direction_from_rad: f64) -> DVec3 {
51 DVec3::new(
52 -speed_m_s * direction_from_rad.sin(),
53 -speed_m_s * direction_from_rad.cos(),
54 0.0,
55 )
56}
57
58fn wrap_direction(angle_rad: f64) -> f64 {
60 let wrapped = angle_rad.rem_euclid(TAU);
61 if wrapped >= TAU { 0.0 } else { wrapped }
63}
64
65fn check_direction(direction_from_rad: f64) -> Result<f64, AtmosError> {
66 Ok(wrap_direction(finite(
67 "wind direction (rad)",
68 direction_from_rad,
69 )?))
70}
71
72fn check_speed(speed_m_s: f64) -> Result<f64, AtmosError> {
73 let speed = finite("wind speed (m/s)", speed_m_s)?;
74 if speed < 0.0 {
75 return Err(AtmosError::Domain {
76 what: "wind speed (m/s)",
77 value: speed,
78 });
79 }
80 Ok(speed)
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
85#[serde(try_from = "ConstantWindData", into = "ConstantWindData")]
86pub struct ConstantWind {
87 speed_m_s: f64,
88 direction_from_rad: f64,
89}
90
91#[derive(Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93struct ConstantWindData {
94 speed_m_s: f64,
95 direction_from_rad: f64,
96}
97
98impl TryFrom<ConstantWindData> for ConstantWind {
99 type Error = AtmosError;
100
101 fn try_from(data: ConstantWindData) -> Result<Self, AtmosError> {
102 ConstantWind::new(data.speed_m_s, data.direction_from_rad)
103 }
104}
105
106impl From<ConstantWind> for ConstantWindData {
107 fn from(wind: ConstantWind) -> Self {
108 ConstantWindData {
109 speed_m_s: wind.speed_m_s,
110 direction_from_rad: wind.direction_from_rad,
111 }
112 }
113}
114
115impl ConstantWind {
116 pub fn new(speed_m_s: f64, direction_from_rad: f64) -> Result<Self, AtmosError> {
122 Ok(ConstantWind {
123 speed_m_s: check_speed(speed_m_s)?,
124 direction_from_rad: check_direction(direction_from_rad)?,
125 })
126 }
127
128 pub fn calm() -> Self {
130 ConstantWind {
131 speed_m_s: 0.0,
132 direction_from_rad: 0.0,
133 }
134 }
135
136 pub fn speed_m_s(&self) -> f64 {
138 self.speed_m_s
139 }
140
141 pub fn direction_from_rad(&self) -> f64 {
143 self.direction_from_rad
144 }
145}
146
147impl Wind for ConstantWind {
148 fn wind(&self, height_msl_m: f64) -> Result<WindSample, AtmosError> {
149 finite("height (m)", height_msl_m)?;
150 Ok(WindSample {
151 velocity_enu_m_s: velocity_from_speed_direction(
152 self.speed_m_s,
153 self.direction_from_rad,
154 ),
155 extrapolated: None,
156 })
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
173#[serde(try_from = "PowerLawWindData", into = "PowerLawWindData")]
174pub struct PowerLawWind {
175 reference_speed_m_s: f64,
176 reference_height_agl_m: f64,
177 exponent: f64,
178 direction_from_rad: f64,
179 ground_msl_m: f64,
180}
181
182#[derive(Serialize, Deserialize)]
183#[serde(deny_unknown_fields)]
184struct PowerLawWindData {
185 reference_speed_m_s: f64,
186 reference_height_agl_m: f64,
187 exponent: f64,
188 direction_from_rad: f64,
189 ground_msl_m: f64,
190}
191
192impl TryFrom<PowerLawWindData> for PowerLawWind {
193 type Error = AtmosError;
194
195 fn try_from(d: PowerLawWindData) -> Result<Self, AtmosError> {
196 PowerLawWind::new(
197 d.reference_speed_m_s,
198 d.reference_height_agl_m,
199 d.exponent,
200 d.direction_from_rad,
201 d.ground_msl_m,
202 )
203 }
204}
205
206impl From<PowerLawWind> for PowerLawWindData {
207 fn from(w: PowerLawWind) -> Self {
208 PowerLawWindData {
209 reference_speed_m_s: w.reference_speed_m_s,
210 reference_height_agl_m: w.reference_height_agl_m,
211 exponent: w.exponent,
212 direction_from_rad: w.direction_from_rad,
213 ground_msl_m: w.ground_msl_m,
214 }
215 }
216}
217
218impl PowerLawWind {
219 pub fn new(
227 reference_speed_m_s: f64,
228 reference_height_agl_m: f64,
229 exponent: f64,
230 direction_from_rad: f64,
231 ground_msl_m: f64,
232 ) -> Result<Self, AtmosError> {
233 let exponent = finite("power-law exponent", exponent)?;
234 if exponent < 0.0 {
235 return Err(AtmosError::Domain {
236 what: "power-law exponent",
237 value: exponent,
238 });
239 }
240 Ok(PowerLawWind {
241 reference_speed_m_s: check_speed(reference_speed_m_s)?,
242 reference_height_agl_m: positive("reference height (m)", reference_height_agl_m)?,
243 exponent,
244 direction_from_rad: check_direction(direction_from_rad)?,
245 ground_msl_m: finite("ground height (m)", ground_msl_m)?,
246 })
247 }
248
249 pub fn reference_speed_m_s(&self) -> f64 {
251 self.reference_speed_m_s
252 }
253
254 pub fn reference_height_agl_m(&self) -> f64 {
256 self.reference_height_agl_m
257 }
258
259 pub fn exponent(&self) -> f64 {
261 self.exponent
262 }
263
264 pub fn direction_from_rad(&self) -> f64 {
266 self.direction_from_rad
267 }
268
269 pub fn ground_msl_m(&self) -> f64 {
271 self.ground_msl_m
272 }
273}
274
275impl Wind for PowerLawWind {
276 fn wind(&self, height_msl_m: f64) -> Result<WindSample, AtmosError> {
277 let z = finite("height (m)", height_msl_m)? - self.ground_msl_m;
278 let (speed, extrapolated) = if z > 0.0 {
279 let ratio = z / self.reference_height_agl_m;
280 (self.reference_speed_m_s * ratio.powf(self.exponent), None)
281 } else {
282 (0.0, (z < 0.0).then_some(Side::Below))
283 };
284 Ok(WindSample {
285 velocity_enu_m_s: velocity_from_speed_direction(speed, self.direction_from_rad),
286 extrapolated,
287 })
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
302#[serde(try_from = "LogLawWindData", into = "LogLawWindData")]
303pub struct LogLawWind {
304 reference_speed_m_s: f64,
305 reference_height_agl_m: f64,
306 roughness_length_m: f64,
307 direction_from_rad: f64,
308 ground_msl_m: f64,
309}
310
311#[derive(Serialize, Deserialize)]
312#[serde(deny_unknown_fields)]
313struct LogLawWindData {
314 reference_speed_m_s: f64,
315 reference_height_agl_m: f64,
316 roughness_length_m: f64,
317 direction_from_rad: f64,
318 ground_msl_m: f64,
319}
320
321impl TryFrom<LogLawWindData> for LogLawWind {
322 type Error = AtmosError;
323
324 fn try_from(d: LogLawWindData) -> Result<Self, AtmosError> {
325 LogLawWind::new(
326 d.reference_speed_m_s,
327 d.reference_height_agl_m,
328 d.roughness_length_m,
329 d.direction_from_rad,
330 d.ground_msl_m,
331 )
332 }
333}
334
335impl From<LogLawWind> for LogLawWindData {
336 fn from(w: LogLawWind) -> Self {
337 LogLawWindData {
338 reference_speed_m_s: w.reference_speed_m_s,
339 reference_height_agl_m: w.reference_height_agl_m,
340 roughness_length_m: w.roughness_length_m,
341 direction_from_rad: w.direction_from_rad,
342 ground_msl_m: w.ground_msl_m,
343 }
344 }
345}
346
347impl LogLawWind {
348 pub fn new(
357 reference_speed_m_s: f64,
358 reference_height_agl_m: f64,
359 roughness_length_m: f64,
360 direction_from_rad: f64,
361 ground_msl_m: f64,
362 ) -> Result<Self, AtmosError> {
363 let z0 = positive("roughness length (m)", roughness_length_m)?;
364 let z_ref = positive("reference height (m)", reference_height_agl_m)?;
365 if z_ref <= z0 {
366 return Err(AtmosError::Domain {
367 what: "reference height above the roughness length (m)",
368 value: z_ref,
369 });
370 }
371 Ok(LogLawWind {
372 reference_speed_m_s: check_speed(reference_speed_m_s)?,
373 reference_height_agl_m: z_ref,
374 roughness_length_m: z0,
375 direction_from_rad: check_direction(direction_from_rad)?,
376 ground_msl_m: finite("ground height (m)", ground_msl_m)?,
377 })
378 }
379
380 pub fn reference_speed_m_s(&self) -> f64 {
382 self.reference_speed_m_s
383 }
384
385 pub fn reference_height_agl_m(&self) -> f64 {
387 self.reference_height_agl_m
388 }
389
390 pub fn roughness_length_m(&self) -> f64 {
392 self.roughness_length_m
393 }
394
395 pub fn direction_from_rad(&self) -> f64 {
397 self.direction_from_rad
398 }
399
400 pub fn ground_msl_m(&self) -> f64 {
402 self.ground_msl_m
403 }
404}
405
406impl Wind for LogLawWind {
407 fn wind(&self, height_msl_m: f64) -> Result<WindSample, AtmosError> {
408 let z = finite("height (m)", height_msl_m)? - self.ground_msl_m;
409 let z0 = self.roughness_length_m;
410 let speed = if z > z0 {
411 self.reference_speed_m_s * (z / z0).ln() / (self.reference_height_agl_m / z0).ln()
412 } else {
413 0.0
414 };
415 Ok(WindSample {
416 velocity_enu_m_s: velocity_from_speed_direction(speed, self.direction_from_rad),
417 extrapolated: (z < 0.0).then_some(Side::Below),
418 })
419 }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425#[non_exhaustive]
426pub enum WindInterpolation {
427 #[default]
432 SpeedDirection,
433 Components,
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
440#[serde(deny_unknown_fields)]
441pub struct WindLevel {
442 pub height_msl_m: f64,
444 pub speed_m_s: f64,
446 pub direction_from_rad: f64,
448}
449
450#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
459#[serde(try_from = "LayeredWindData", into = "LayeredWindData")]
460pub struct LayeredWind {
461 levels: Vec<WindLevel>,
462 interpolation: WindInterpolation,
463}
464
465#[derive(Serialize, Deserialize)]
466#[serde(deny_unknown_fields)]
467struct LayeredWindData {
468 levels: Vec<WindLevel>,
469 #[serde(default)]
470 interpolation: WindInterpolation,
471}
472
473impl TryFrom<LayeredWindData> for LayeredWind {
474 type Error = AtmosError;
475
476 fn try_from(data: LayeredWindData) -> Result<Self, AtmosError> {
477 LayeredWind::new(data.levels, data.interpolation)
478 }
479}
480
481impl From<LayeredWind> for LayeredWindData {
482 fn from(wind: LayeredWind) -> Self {
483 LayeredWindData {
484 levels: wind.levels,
485 interpolation: wind.interpolation,
486 }
487 }
488}
489
490impl LayeredWind {
491 pub fn new(
499 levels: Vec<WindLevel>,
500 interpolation: WindInterpolation,
501 ) -> Result<Self, AtmosError> {
502 if levels.is_empty() {
503 return Err(AtmosError::NoLevels);
504 }
505 let mut checked = Vec::with_capacity(levels.len());
506 for (index, level) in levels.into_iter().enumerate() {
507 let height = finite("wind level height (m)", level.height_msl_m)?;
508 if let Some(previous) = checked.last().map(|l: &WindLevel| l.height_msl_m)
509 && height <= previous
510 {
511 return Err(AtmosError::HeightsNotIncreasing { index });
512 }
513 checked.push(WindLevel {
514 height_msl_m: height,
515 speed_m_s: check_speed(level.speed_m_s)?,
516 direction_from_rad: check_direction(level.direction_from_rad)?,
517 });
518 }
519 Ok(LayeredWind {
520 levels: checked,
521 interpolation,
522 })
523 }
524
525 pub fn levels(&self) -> &[WindLevel] {
527 &self.levels
528 }
529
530 pub fn interpolation(&self) -> WindInterpolation {
532 self.interpolation
533 }
534}
535
536impl Wind for LayeredWind {
537 fn wind(&self, height_msl_m: f64) -> Result<WindSample, AtmosError> {
538 let z = finite("height (m)", height_msl_m)?;
539 let levels = &self.levels;
540 let (first, last) = match (levels.first(), levels.last()) {
542 (Some(first), Some(last)) => (first, last),
543 _ => return Err(AtmosError::NoLevels),
544 };
545 let hold = |level: &WindLevel, side| WindSample {
546 velocity_enu_m_s: velocity_from_speed_direction(
547 level.speed_m_s,
548 level.direction_from_rad,
549 ),
550 extrapolated: side,
551 };
552 if z < first.height_msl_m {
553 return Ok(hold(first, Some(Side::Below)));
554 }
555 if z > last.height_msl_m {
556 return Ok(hold(last, Some(Side::Above)));
557 }
558 let upper = levels.partition_point(|l| l.height_msl_m <= z);
560 if upper >= levels.len() {
561 return Ok(hold(last, None));
562 }
563 let (a, b) = (&levels[upper - 1], &levels[upper]);
564 let t = (z - a.height_msl_m) / (b.height_msl_m - a.height_msl_m);
565 let velocity = match self.interpolation {
566 WindInterpolation::SpeedDirection => {
567 let speed = a.speed_m_s + t * (b.speed_m_s - a.speed_m_s);
568 let (from_a, from_b) = match (a.speed_m_s == 0.0, b.speed_m_s == 0.0) {
570 (true, false) => (b.direction_from_rad, b.direction_from_rad),
571 (false, true) => (a.direction_from_rad, a.direction_from_rad),
572 _ => (a.direction_from_rad, b.direction_from_rad),
573 };
574 let mut turn = from_b - from_a;
575 if turn > PI {
577 turn -= TAU;
578 } else if turn <= -PI {
579 turn += TAU;
580 }
581 velocity_from_speed_direction(speed, from_a + t * turn)
582 }
583 WindInterpolation::Components => {
584 let va = velocity_from_speed_direction(a.speed_m_s, a.direction_from_rad);
585 let vb = velocity_from_speed_direction(b.speed_m_s, b.direction_from_rad);
586 va + t * (vb - va)
587 }
588 };
589 Ok(WindSample {
590 velocity_enu_m_s: velocity,
591 extrapolated: None,
592 })
593 }
594}
595
596#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598#[serde(tag = "model", rename_all = "snake_case")]
599#[non_exhaustive]
600pub enum WindModel {
601 Constant(ConstantWind),
603 PowerLaw(PowerLawWind),
605 LogLaw(LogLawWind),
607 Layered(LayeredWind),
609}
610
611impl Wind for WindModel {
612 fn wind(&self, height_msl_m: f64) -> Result<WindSample, AtmosError> {
613 match self {
614 WindModel::Constant(w) => w.wind(height_msl_m),
615 WindModel::PowerLaw(w) => w.wind(height_msl_m),
616 WindModel::LogLaw(w) => w.wind(height_msl_m),
617 WindModel::Layered(w) => w.wind(height_msl_m),
618 }
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 fn deg(degrees: f64) -> f64 {
627 degrees.to_radians()
628 }
629
630 fn assert_close(actual: DVec3, expected: DVec3, tolerance: f64) {
631 assert!(
632 (actual - expected).length() <= tolerance,
633 "{actual:?} vs {expected:?}"
634 );
635 }
636
637 fn speed_and_direction_from(velocity: DVec3) -> (f64, f64) {
638 let speed = velocity.x.hypot(velocity.y);
639 (speed, wrap_direction((-velocity.x).atan2(-velocity.y)))
640 }
641
642 #[test]
645 fn layered_wind_interpolates_speed_and_heading() {
646 let wind = LayeredWind::new(
648 vec![
649 WindLevel {
650 height_msl_m: 1010.0,
651 speed_m_s: 4.0,
652 direction_from_rad: deg(350.0),
653 },
654 WindLevel {
655 height_msl_m: 2010.0,
656 speed_m_s: 12.0,
657 direction_from_rad: deg(30.0),
658 },
659 ],
660 WindInterpolation::SpeedDirection,
661 )
662 .unwrap();
663
664 let mid = wind.wind(1510.0).unwrap();
666 assert_eq!(mid.extrapolated, None);
667 let (speed, direction) = speed_and_direction_from(mid.velocity_enu_m_s);
668 assert!((speed - 8.0).abs() < 1e-12);
669 assert!((direction - deg(10.0)).abs() < 1e-12);
670
671 let (speed, direction) =
673 speed_and_direction_from(wind.wind(1260.0).unwrap().velocity_enu_m_s);
674 assert!((speed - 6.0).abs() < 1e-12);
675 assert!(direction.min(TAU - direction) < 1e-12);
676
677 let near_surface = wind.wind(1010.001).unwrap().velocity_enu_m_s;
679 let surface = velocity_from_speed_direction(4.0, deg(350.0));
680 assert_close(near_surface, surface, 1e-4);
681 assert_close(wind.wind(1010.0).unwrap().velocity_enu_m_s, surface, 1e-15);
682 assert_close(
683 wind.wind(2010.0).unwrap().velocity_enu_m_s,
684 velocity_from_speed_direction(12.0, deg(30.0)),
685 1e-14,
686 );
687 }
688
689 #[test]
690 fn components_interpolation_averages_the_vectors() {
691 let levels = vec![
692 WindLevel {
693 height_msl_m: 0.0,
694 speed_m_s: 10.0,
695 direction_from_rad: deg(270.0),
696 },
697 WindLevel {
698 height_msl_m: 100.0,
699 speed_m_s: 10.0,
700 direction_from_rad: deg(0.0),
701 },
702 ];
703 let components = LayeredWind::new(levels.clone(), WindInterpolation::Components).unwrap();
704 assert_close(
706 components.wind(50.0).unwrap().velocity_enu_m_s,
707 DVec3::new(5.0, -5.0, 0.0),
708 1e-14,
709 );
710 let polar = LayeredWind::new(levels, WindInterpolation::SpeedDirection).unwrap();
712 let (speed, direction) =
713 speed_and_direction_from(polar.wind(50.0).unwrap().velocity_enu_m_s);
714 assert!((speed - 10.0).abs() < 1e-12);
715 assert!((direction - deg(315.0)).abs() < 1e-12);
716 }
717
718 #[test]
721 fn wind_grows_out_of_calm_without_turning() {
722 for (calm_below, calm_height) in [(true, 0.0), (false, 1000.0)] {
723 let calm = WindLevel {
724 height_msl_m: calm_height,
725 speed_m_s: 0.0,
726 direction_from_rad: 0.0,
727 };
728 let windy = WindLevel {
729 height_msl_m: 1000.0 - calm_height,
730 speed_m_s: 10.0,
731 direction_from_rad: deg(180.0),
732 };
733 let levels = if calm_below {
734 vec![calm, windy]
735 } else {
736 vec![windy, calm]
737 };
738 let wind = LayeredWind::new(levels, WindInterpolation::SpeedDirection).unwrap();
739 assert_close(
740 wind.wind(500.0).unwrap().velocity_enu_m_s,
741 DVec3::new(0.0, 5.0, 0.0),
742 1e-12,
743 );
744 }
745 }
746
747 #[test]
748 fn opposite_directions_turn_clockwise() {
749 let wind = LayeredWind::new(
750 vec![
751 WindLevel {
752 height_msl_m: 0.0,
753 speed_m_s: 5.0,
754 direction_from_rad: deg(90.0),
755 },
756 WindLevel {
757 height_msl_m: 10.0,
758 speed_m_s: 5.0,
759 direction_from_rad: deg(270.0),
760 },
761 ],
762 WindInterpolation::SpeedDirection,
763 )
764 .unwrap();
765 let (_, direction) = speed_and_direction_from(wind.wind(5.0).unwrap().velocity_enu_m_s);
766 assert!((direction - deg(180.0)).abs() < 1e-12);
767 }
768
769 #[test]
770 fn layered_wind_holds_and_flags_beyond_its_levels() {
771 let wind = LayeredWind::new(
772 vec![
773 WindLevel {
774 height_msl_m: 100.0,
775 speed_m_s: 3.0,
776 direction_from_rad: deg(180.0),
777 },
778 WindLevel {
779 height_msl_m: 900.0,
780 speed_m_s: 9.0,
781 direction_from_rad: deg(200.0),
782 },
783 ],
784 WindInterpolation::default(),
785 )
786 .unwrap();
787 let below = wind.wind(0.0).unwrap();
788 assert_eq!(below.extrapolated, Some(Side::Below));
789 assert_close(below.velocity_enu_m_s, DVec3::new(0.0, 3.0, 0.0), 1e-14);
790 let above = wind.wind(5000.0).unwrap();
791 assert_eq!(above.extrapolated, Some(Side::Above));
792 assert_close(
793 above.velocity_enu_m_s,
794 velocity_from_speed_direction(9.0, deg(200.0)),
795 1e-15,
796 );
797 let single = LayeredWind::new(
799 vec![WindLevel {
800 height_msl_m: 10.0,
801 speed_m_s: 2.0,
802 direction_from_rad: 0.0,
803 }],
804 WindInterpolation::default(),
805 )
806 .unwrap();
807 assert_eq!(single.wind(10.0).unwrap().extrapolated, None);
808 assert_eq!(single.wind(11.0).unwrap().extrapolated, Some(Side::Above));
809 }
810
811 #[test]
812 fn meteorological_direction_convention() {
813 let west = ConstantWind::new(10.0, deg(270.0)).unwrap();
815 assert_close(
816 west.wind(0.0).unwrap().velocity_enu_m_s,
817 DVec3::new(10.0, 0.0, 0.0),
818 1e-14,
819 );
820 let north = ConstantWind::new(10.0, 0.0).unwrap();
821 assert_close(
822 north.wind(123.0).unwrap().velocity_enu_m_s,
823 DVec3::new(0.0, -10.0, 0.0),
824 1e-14,
825 );
826 assert_eq!(
827 ConstantWind::calm().wind(0.0).unwrap().velocity_enu_m_s,
828 DVec3::ZERO
829 );
830 let wrapped = ConstantWind::new(1.0, deg(-90.0)).unwrap();
832 assert!((wrapped.direction_from_rad - deg(270.0)).abs() < 1e-12);
833 }
834
835 #[test]
836 fn power_law_passes_through_its_reference() {
837 let wind = PowerLawWind::new(5.0, 10.0, 1.0 / 7.0, deg(270.0), 1400.0).unwrap();
838 let at_reference = wind.wind(1410.0).unwrap();
839 assert_close(
840 at_reference.velocity_enu_m_s,
841 DVec3::new(5.0, 0.0, 0.0),
842 1e-14,
843 );
844 let at_80 = wind.wind(1480.0).unwrap().velocity_enu_m_s.x;
845 assert!((at_80 - 5.0 * 8.0_f64.powf(1.0 / 7.0)).abs() < 1e-13);
846 assert_eq!(wind.wind(1400.0).unwrap().velocity_enu_m_s, DVec3::ZERO);
847 let below = wind.wind(1390.0).unwrap();
848 assert_eq!(below.velocity_enu_m_s, DVec3::ZERO);
849 assert_eq!(below.extrapolated, Some(Side::Below));
850 }
851
852 #[test]
853 fn log_law_passes_through_its_reference_and_vanishes_at_the_roughness_length() {
854 let wind = LogLawWind::new(6.0, 10.0, 0.03, deg(180.0), 0.0).unwrap();
855 assert_close(
856 wind.wind(10.0).unwrap().velocity_enu_m_s,
857 DVec3::new(0.0, 6.0, 0.0),
858 1e-14,
859 );
860 let at_100 = wind.wind(100.0).unwrap().velocity_enu_m_s.y;
861 let expected = 6.0 * (100.0_f64 / 0.03).ln() / (10.0_f64 / 0.03).ln();
862 assert!((at_100 - expected).abs() < 1e-13);
863 assert_eq!(wind.wind(0.03).unwrap().velocity_enu_m_s, DVec3::ZERO);
864 assert_eq!(wind.wind(-1.0).unwrap().extrapolated, Some(Side::Below));
865 }
866
867 #[test]
868 fn invalid_inputs_are_rejected() {
869 assert!(ConstantWind::new(-1.0, 0.0).is_err());
870 assert!(ConstantWind::new(1.0, f64::NAN).is_err());
871 assert!(PowerLawWind::new(1.0, 0.0, 0.14, 0.0, 0.0).is_err());
872 assert!(PowerLawWind::new(1.0, 10.0, -0.1, 0.0, 0.0).is_err());
873 assert!(LogLawWind::new(1.0, 0.01, 0.03, 0.0, 0.0).is_err());
874 assert!(LogLawWind::new(1.0, 10.0, 0.0, 0.0, 0.0).is_err());
875 assert!(matches!(
876 LayeredWind::new(vec![], WindInterpolation::default()),
877 Err(AtmosError::NoLevels)
878 ));
879 let level = |h| WindLevel {
880 height_msl_m: h,
881 speed_m_s: 1.0,
882 direction_from_rad: 0.0,
883 };
884 assert!(matches!(
885 LayeredWind::new(vec![level(0.0), level(0.0)], WindInterpolation::default()),
886 Err(AtmosError::HeightsNotIncreasing { index: 1 })
887 ));
888 assert!(ConstantWind::calm().wind(f64::INFINITY).is_err());
889 }
890
891 #[test]
892 fn wind_models_round_trip_through_json() {
893 let models = vec![
894 WindModel::Constant(ConstantWind::new(3.0, 1.0).unwrap()),
895 WindModel::PowerLaw(PowerLawWind::new(5.0, 10.0, 0.14, 2.0, 100.0).unwrap()),
896 WindModel::LogLaw(LogLawWind::new(5.0, 10.0, 0.05, 3.0, 100.0).unwrap()),
897 WindModel::Layered(
898 LayeredWind::new(
899 vec![WindLevel {
900 height_msl_m: 5.0,
901 speed_m_s: 1.0,
902 direction_from_rad: 0.5,
903 }],
904 WindInterpolation::Components,
905 )
906 .unwrap(),
907 ),
908 ];
909 for model in models {
910 let json = serde_json::to_string(&model).unwrap();
911 let back: WindModel = serde_json::from_str(&json).unwrap();
912 assert_eq!(back, model, "{json}");
913 assert_eq!(back.wind(50.0).unwrap(), model.wind(50.0).unwrap());
914 }
915 let negative = r#"{"model":"constant","speed_m_s":-3.0,"direction_from_rad":0.0}"#;
916 assert!(serde_json::from_str::<WindModel>(negative).is_err());
917 let unknown = r#"{"model":"constant","speed_m_s":3.0,"direction_from_rad":0.0,"x":1}"#;
918 assert!(serde_json::from_str::<WindModel>(unknown).is_err());
919 }
920}