sounding_base/
sounding.rs

1//! Data type and methods to store an atmospheric sounding.
2
3use chrono::NaiveDateTime;
4use metfor::{Celsius, HectoPascal, Kelvin, Knots, Meters, Mm, PaPS, Quantity, WindSpdDir};
5use optional::Optioned;
6
7use crate::data_row::DataRow;
8use crate::station_info::StationInfo;
9
10/// All the variables stored in the sounding.
11///
12/// The upper air profile variables are stored in parallel vectors. If a profile lacks a certain
13/// variable, e.g. cloud fraction, that whole vector has length 0 instead of being full of missing
14/// values.
15///
16#[derive(Clone, Debug, Default)]
17pub struct Sounding {
18    // Description of the source of the sounding.
19    source: Option<String>,
20
21    // Station info
22    station: StationInfo,
23
24    // Valid time of sounding
25    valid_time: Option<NaiveDateTime>,
26    // Difference in model initialization time and `valid_time` in hours.
27    lead_time: Optioned<i32>,
28
29    // Profiles
30    pressure: Vec<Optioned<HectoPascal>>,
31    temperature: Vec<Optioned<Celsius>>,
32    wet_bulb: Vec<Optioned<Celsius>>,
33    dew_point: Vec<Optioned<Celsius>>,
34    theta_e: Vec<Optioned<Kelvin>>,
35    wind: Vec<Optioned<WindSpdDir<Knots>>>,
36    pvv: Vec<Optioned<PaPS>>,
37    height: Vec<Optioned<Meters>>,
38    cloud_fraction: Vec<Optioned<f64>>,
39
40    // Surface variables
41    mslp: Optioned<HectoPascal>,
42    station_pressure: Optioned<HectoPascal>,
43    sfc_temperature: Optioned<Celsius>,
44    sfc_dew_point: Optioned<Celsius>,
45    low_cloud: Optioned<f64>,
46    mid_cloud: Optioned<f64>,
47    high_cloud: Optioned<f64>,
48    precipitation: Optioned<Mm>,
49    sfc_wind: Optioned<WindSpdDir<Knots>>,
50}
51
52macro_rules! make_profile_setter {
53    ($(#[$attr:meta])* => $name:tt, $sfc_val:ident, $inner_type:tt, $p_var:ident) => {
54        $(#[$attr])*
55        pub fn $name(self, mut profile: Vec<Optioned<$inner_type>>) -> Self {
56            if !profile.is_empty() {
57                profile.insert(0, self.$sfc_val);
58            }
59            Self {$p_var: profile, ..self}
60        }
61    };
62    ($(#[$attr:meta])* => $name:tt, $method:ident(), $inner_type:tt, $p_var:ident) => {
63        $(#[$attr])*
64        pub fn $name(self, mut profile: Vec<Optioned<$inner_type>>) -> Self {
65            if !profile.is_empty() {
66                profile.insert(0, self.$method().into());
67            }
68            Self {$p_var: profile, ..self}
69        }
70    };
71    ($(#[$attr:meta])* => $name:tt, $sfc_val:expr, $inner_type:tt, $p_var:ident) => {
72        $(#[$attr])*
73        pub fn $name(self, mut profile: Vec<Optioned<$inner_type>>) -> Self {
74            if !profile.is_empty() {
75                profile.insert(0, $sfc_val);
76            }
77            Self {$p_var: profile, ..self}
78        }
79    };
80}
81
82impl Sounding {
83    /// Create a new sounding with default values. This is a proxy for default with a clearer name.
84    ///
85    /// # Examples
86    ///
87    /// ```rust
88    /// use sounding_base::Sounding;
89    ///
90    /// let snd = Sounding::new();
91    /// println!("{:?}", snd);
92    /// ```
93    #[inline]
94    pub fn new() -> Self {
95        Sounding::default()
96    }
97
98    /// Add a source description to this sounding.
99    ///
100    /// # Examples
101    ///
102    /// ```rust
103    /// use sounding_base::Sounding;
104    ///
105    /// let snd = Sounding::new().with_source_description("An empty sounding.".to_owned());
106    /// let snd = snd.with_source_description(
107    ///     Some("Still empty, just added a description.".to_owned()),
108    /// );
109    /// let _snd = snd.with_source_description(None);
110    ///
111    /// ```
112    #[inline]
113    pub fn with_source_description<S>(mut self, desc: S) -> Self
114    where
115        Option<String>: From<S>,
116    {
117        self.source = Option::from(desc);
118        self
119    }
120
121    /// Retrieve a source description for this sounding.
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// use sounding_base::Sounding;
127    ///
128    /// let snd = Sounding::new().with_source_description("An empty sounding.".to_owned());
129    /// assert_eq!(snd.source_description().unwrap(), "An empty sounding.");
130    ///
131    /// let snd = snd.with_source_description(None);
132    /// assert!(snd.source_description().is_none());
133    ///
134    /// ```
135    #[inline]
136    pub fn source_description(&self) -> Option<&str> {
137        self.source.as_ref().map(|s| s.as_ref())
138    }
139
140    /// Builder function for setting the station info.
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use sounding_base::{Sounding, StationInfo};
146    ///
147    /// let stn = StationInfo::new();
148    /// let _snd = Sounding::new().with_station_info(stn);
149    ///
150    /// ```
151    #[inline]
152    pub fn with_station_info(mut self, new_value: StationInfo) -> Self {
153        self.station = new_value;
154        self
155    }
156
157    /// Get the station info
158    ///
159    /// # Examples
160    ///
161    /// ```rust
162    /// use sounding_base::StationInfo;
163    /// # use sounding_base::doctest::make_test_sounding;
164    ///
165    /// let snd = make_test_sounding();
166    /// let stn: StationInfo = snd.station_info();
167    ///
168    /// println!("{:?}", stn);
169    ///
170    /// ```
171    #[inline]
172    pub fn station_info(&self) -> StationInfo {
173        self.station
174    }
175
176    make_profile_setter!(
177        /// Builder method for the pressure profile.
178        ///
179        /// # Examples
180        /// ```rust
181        /// use sounding_base::Sounding;
182        /// use metfor::HectoPascal;
183        /// use optional::{some, Optioned};
184        /// 
185        /// let data = vec![1000.0, 925.0, 850.0, 700.0, 500.0, 300.0, 250.0, 200.0, 150.0, 100.0];
186        /// let pressure_data: Vec<Optioned<HectoPascal>> = data.into_iter()
187        ///     .map(HectoPascal)
188        ///     .map(some)
189        ///     .collect();
190        ///
191        /// let _snd = Sounding::new()
192        ///     .with_pressure_profile(pressure_data);
193        /// ```
194        #[inline]
195        => with_pressure_profile, station_pressure, HectoPascal, pressure
196    );
197
198    /// Get the pressure profile
199    ///
200    /// # Examples
201    ///
202    /// ```rust
203    /// use sounding_base::Sounding;
204    /// # use sounding_base::doctest::make_test_sounding;
205    ///
206    /// let snd = make_test_sounding();
207    /// let data = snd.pressure_profile();
208    ///
209    /// for p in data {
210    ///     if let Some(p) = p.into_option() {
211    ///         println!("{:?}", p);
212    ///     } else {
213    ///         println!("missing value!");
214    ///     }
215    /// }
216    ///
217    /// // Uninitialized profiles just return an empty vector.
218    /// let snd = Sounding::new();
219    /// let data = snd.pressure_profile();
220    /// assert!(data.is_empty());
221    ///
222    /// ```
223    #[inline]
224    pub fn pressure_profile(&self) -> &[Optioned<HectoPascal>] {
225        &self.pressure
226    }
227
228    make_profile_setter!(
229        /// Builder method for the temperature profile.
230        ///
231        /// See `with_pressure_profile` for an example of usage, keeping in mind the units type may
232        /// be different.
233        #[inline]
234        => with_temperature_profile, sfc_temperature, Celsius, temperature
235    );
236
237    /// Get the temperature profile.
238    ///
239    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
240    /// be different.
241    #[inline]
242    pub fn temperature_profile(&self) -> &[Optioned<Celsius>] {
243        &self.temperature
244    }
245
246    make_profile_setter!(
247        /// Builder method for the dew point profile.
248        ///
249        /// See `with_pressure_profile` for an example of usage, keeping in mind the units type may
250        /// be different.
251        #[inline]
252        => with_dew_point_profile, sfc_dew_point, Celsius, dew_point
253    );
254
255    /// Get the dew point profile.
256    ///
257    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
258    /// be different.
259    #[inline]
260    pub fn dew_point_profile(&self) -> &[Optioned<Celsius>] {
261        &self.dew_point
262    }
263
264    make_profile_setter!(
265        /// Builder method for the wet bulb profile.
266        ///
267        /// See `with_pressure_profile` for an example of usage, keeping in mind the units type may
268        /// be different.
269        #[inline]
270        => with_wet_bulb_profile, surface_wet_bulb(), Celsius, wet_bulb
271    );
272
273    /// Get the wet bulb temperature profile.
274    ///
275    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
276    /// be different.
277    #[inline]
278    pub fn wet_bulb_profile(&self) -> &[Optioned<Celsius>] {
279        &self.wet_bulb
280    }
281
282    make_profile_setter!(
283        /// Builder method for the theta e profile.
284        ///
285        /// See `with_pressure_profile` for an example of usage, keeping in mind the units type may
286        /// be different.
287        #[inline]
288        => with_theta_e_profile, surface_theta_e(), Kelvin, theta_e
289    );
290
291    /// Get the equivalent potential temperature profile.
292    ///
293    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
294    /// be different.
295    #[inline]
296    pub fn theta_e_profile(&self) -> &[Optioned<Kelvin>] {
297        &self.theta_e
298    }
299
300    /// Builder method for the wind profile.
301    ///
302    /// See `set_pressure_profile` for an example of usage, keeping in mind the units type may
303    /// be different.
304    #[inline]
305    pub fn with_wind_profile(self, mut profile: Vec<Optioned<WindSpdDir<Knots>>>) -> Self {
306        if !profile.is_empty() {
307            profile.insert(0, self.sfc_wind);
308        }
309        Self {
310            wind: profile,
311            ..self
312        }
313    }
314
315    /// Get the wind profile.
316    ///
317    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
318    /// be different.
319    #[inline]
320    pub fn wind_profile(&self) -> &[Optioned<WindSpdDir<Knots>>] {
321        &self.wind
322    }
323
324    make_profile_setter!(
325        /// Builder method for the pressure vertical velocity profile.
326        ///
327        /// See `set_pressure_profile` for an example of usage, keeping in mind the units type may
328        /// be different.
329        #[inline]
330        => with_pvv_profile, optional::some(PaPS(0.0)), PaPS, pvv
331    );
332
333    /// Get the pressure vertical velocity profile.
334    ///
335    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
336    /// be different.
337    #[inline]
338    pub fn pvv_profile(&self) -> &[Optioned<PaPS>] {
339        &self.pvv
340    }
341
342    make_profile_setter!(
343        /// Builder method for the geopotential height profile.
344        ///
345        /// See `set_pressure_profile` for an example of usage, keeping in mind the units type may
346        /// be different.
347        #[inline]
348        => with_height_profile, surface_height(), Meters, height
349    );
350
351    /// Get the geopotential height profile.
352    ///
353    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
354    /// be different.
355    #[inline]
356    pub fn height_profile(&self) -> &[Optioned<Meters>] {
357        &self.height
358    }
359
360    make_profile_setter!(
361        /// Builder method for the cloud cover profile.
362        ///
363        /// See `set_pressure_profile` for an example of usage, keeping in mind the units type may
364        /// be different.
365        #[inline]
366        => with_cloud_fraction_profile, optional::some(0.0), f64, cloud_fraction
367    );
368
369    /// Get the cloud fraction profile.
370    ///
371    /// See `pressure_profile` for an example of using getters, keeping in mind the units type may
372    /// be different.
373    #[inline]
374    pub fn cloud_fraction_profile(&self) -> &[Optioned<f64>] {
375        &self.cloud_fraction
376    }
377
378    /// Builder method for the mean sea level pressure.
379    ///
380    /// # Examples
381    ///```rust
382    /// use metfor::{HectoPascal, Millibar};
383    /// use sounding_base::Sounding;
384    /// use optional::{some, none};
385    ///
386    /// let _snd = Sounding::new().with_mslp(HectoPascal(1021.5));
387    /// let _snd = Sounding::new().with_mslp(some(HectoPascal(1021.5)));
388    /// let _snd = Sounding::new().with_mslp(none::<HectoPascal>());
389    /// let _snd = Sounding::new().with_mslp(Millibar(1021.5));
390    /// let _snd = Sounding::new().with_mslp(some(Millibar(1021.5)));
391    /// let _snd = Sounding::new().with_mslp(none::<Millibar>());
392    ///```
393    #[inline]
394    pub fn with_mslp<T, U>(mut self, value: T) -> Self
395    where
396        Optioned<U>: From<T>,
397        U: optional::Noned + metfor::Pressure,
398        HectoPascal: From<U>,
399    {
400        let pressure: Optioned<U> = Optioned::from(value);
401        let pressure: Optioned<HectoPascal> = pressure.map_t(HectoPascal::from);
402        self.mslp = pressure;
403        self
404    }
405
406    /// Get the mean sea level pressure
407    #[inline]
408    pub fn mslp(&self) -> Optioned<HectoPascal> {
409        self.mslp
410    }
411
412    /// Biulder method for the station pressure.
413    ///
414    /// # Examples
415    ///```rust
416    /// use metfor::{HectoPascal, Millibar};
417    /// use sounding_base::Sounding;
418    /// use optional::{some, none};
419    ///
420    /// let _snd = Sounding::new().with_station_pressure(HectoPascal(1021.5));
421    /// let _snd = Sounding::new().with_station_pressure(some(HectoPascal(1021.5)));
422    /// let _snd = Sounding::new().with_station_pressure(none::<HectoPascal>());
423    /// let _snd = Sounding::new().with_station_pressure(Millibar(1021.5));
424    /// let _snd = Sounding::new().with_station_pressure(some(Millibar(1021.5)));
425    /// let _snd = Sounding::new().with_station_pressure(none::<Millibar>());
426    ///```
427    #[inline]
428    pub fn with_station_pressure<T, U>(mut self, value: T) -> Self
429    where
430        Optioned<U>: From<T>,
431        U: optional::Noned + metfor::Pressure,
432        HectoPascal: From<U>,
433    {
434        let pressure: Optioned<U> = Optioned::from(value);
435        let pressure: Optioned<HectoPascal> = pressure.map_t(HectoPascal::from);
436
437        // Add it in to the profile.
438        if !self.pressure.is_empty() {
439            self.pressure[0] = pressure;
440        }
441
442        self.station_pressure = pressure;
443        self.update_sfc_wet_bulb_theta_e(); // updates wet bulb and theta_e profiles
444        self
445    }
446
447    /// Get the mean sea level pressure.
448    #[inline]
449    pub fn station_pressure(&self) -> Optioned<HectoPascal> {
450        self.station_pressure
451    }
452
453    /// Builder method the surface temperature.
454    ///
455    /// # Examples
456    ///```rust
457    /// use metfor::{Fahrenheit, Celsius, Kelvin};
458    /// use sounding_base::Sounding;
459    /// use optional::{some, none};
460    ///
461    /// let _snd = Sounding::new().with_sfc_temperature(Celsius(20.0));
462    /// let _snd = Sounding::new().with_sfc_temperature(some(Celsius(20.0)));
463    /// let _snd = Sounding::new().with_sfc_temperature(none::<Celsius>());
464    /// let _snd = Sounding::new().with_sfc_temperature(Kelvin(290.0));
465    /// let _snd = Sounding::new().with_sfc_temperature(some(Kelvin(290.0)));
466    /// let _snd = Sounding::new().with_sfc_temperature(none::<Kelvin>());
467    /// let _snd = Sounding::new().with_sfc_temperature(Fahrenheit(72.1));
468    /// let _snd = Sounding::new().with_sfc_temperature(some(Fahrenheit(72.1)));
469    /// let _snd = Sounding::new().with_sfc_temperature(none::<Fahrenheit>());
470    ///```
471    #[inline]
472    pub fn with_sfc_temperature<T, U>(mut self, value: T) -> Self
473    where
474        Optioned<U>: From<T>,
475        U: optional::Noned + metfor::Temperature,
476        Celsius: From<U>,
477    {
478        let sfc_temperature: Optioned<U> = Optioned::from(value);
479        let sfc_temperature: Optioned<Celsius> = sfc_temperature.map_t(Celsius::from);
480
481        // Add it in to the profile.
482        if !self.temperature.is_empty() {
483            self.temperature[0] = sfc_temperature;
484        }
485
486        self.sfc_temperature = sfc_temperature;
487        self.update_sfc_wet_bulb_theta_e(); // updates wet bulb and theta_e profiles
488        self
489    }
490
491    /// Get the surface temperature.
492    #[inline]
493    pub fn sfc_temperature(&self) -> Optioned<Celsius> {
494        self.sfc_temperature
495    }
496
497    /// Set the surface dew point.
498    ///
499    /// # Examples
500    ///```rust
501    /// use metfor::{Fahrenheit, Celsius, Kelvin};
502    /// use sounding_base::Sounding;
503    /// use optional::{some, none};
504    ///
505    /// let _snd = Sounding::new().with_sfc_dew_point(Celsius(20.0));
506    /// let _snd = Sounding::new().with_sfc_dew_point(some(Celsius(20.0)));
507    /// let _snd = Sounding::new().with_sfc_dew_point(none::<Celsius>());
508    /// let _snd = Sounding::new().with_sfc_dew_point(Kelvin(290.0));
509    /// let _snd = Sounding::new().with_sfc_dew_point(some(Kelvin(290.0)));
510    /// let _snd = Sounding::new().with_sfc_dew_point(none::<Kelvin>());
511    /// let _snd = Sounding::new().with_sfc_dew_point(Fahrenheit(72.1));
512    /// let _snd = Sounding::new().with_sfc_dew_point(some(Fahrenheit(72.1)));
513    /// let _snd = Sounding::new().with_sfc_dew_point(none::<Fahrenheit>());
514    ///```
515    #[inline]
516    pub fn with_sfc_dew_point<T, U>(mut self, value: T) -> Self
517    where
518        Optioned<U>: From<T>,
519        U: optional::Noned + metfor::Temperature,
520        Celsius: From<U>,
521    {
522        let sfc_dew_point: Optioned<U> = Optioned::from(value);
523        let sfc_dew_point: Optioned<Celsius> = sfc_dew_point.map_t(Celsius::from);
524
525        // Add it in to the profile.
526        if !self.dew_point.is_empty() {
527            self.dew_point[0] = sfc_dew_point;
528        }
529
530        self.sfc_dew_point = sfc_dew_point;
531        self.update_sfc_wet_bulb_theta_e(); // updates wet bulb and theta_e profiles
532        self
533    }
534
535    /// Get the surface dew point.
536    #[inline]
537    pub fn sfc_dew_point(&self) -> Optioned<Celsius> {
538        self.sfc_dew_point
539    }
540
541    /// Set the surface wind.
542    ///
543    /// # Examples
544    ///```rust
545    /// use sounding_base::Sounding;
546    /// use metfor::{WindSpdDir, WindUV, Knots, MetersPSec};
547    /// use optional::{some, none};
548    ///
549    /// let _snd = Sounding::new()
550    ///     .with_sfc_wind(WindSpdDir{speed: Knots(10.0), direction: 270.0});
551    ///
552    /// let _snd = Sounding::new()
553    ///     .with_sfc_wind(some(WindSpdDir{speed: Knots(10.0), direction: 270.0}));
554    ///
555    /// let _snd = Sounding::new().with_sfc_wind(none::<WindSpdDir<_>>());
556    ///
557    /// let _snd = Sounding::new()
558    ///     .with_sfc_wind(some(WindUV{u: MetersPSec(-7.3), v: MetersPSec(5.2)}));
559    /// let _snd = Sounding::new()
560    ///     .with_sfc_wind(WindUV{u: MetersPSec(-7.3), v: MetersPSec(5.2)});
561    ///
562    /// let _snd = Sounding::new().with_sfc_wind(none::<WindUV<MetersPSec>>());
563    ///```
564    #[inline]
565    pub fn with_sfc_wind<T, U>(mut self, value: T) -> Self
566    where
567        Optioned<U>: From<T>,
568        U: optional::Noned + Copy,
569        WindSpdDir<Knots>: From<U>,
570    {
571        let sfc_wind: Optioned<U> = Optioned::from(value);
572        let sfc_wind: Optioned<WindSpdDir<Knots>> = sfc_wind.map_t(WindSpdDir::from);
573
574        if !self.wind.is_empty() {
575            self.wind[0] = sfc_wind;
576        }
577
578        Self { sfc_wind, ..self }
579    }
580
581    /// Get the surface wind.
582    #[inline]
583    pub fn sfc_wind(&self) -> Optioned<WindSpdDir<Knots>> {
584        self.sfc_wind
585    }
586
587    /// Builder method for the precipitation.
588    ///
589    /// # Examples
590    ///```rust
591    /// use sounding_base::Sounding;
592    /// use metfor::{Inches, Mm, Cm};
593    /// use optional::{some, none};
594    ///
595    /// let _snd = Sounding::new().with_precipitation(Inches(1.0));
596    /// let _snd = Sounding::new().with_precipitation(some(Inches(1.0)));
597    /// let _snd = Sounding::new().with_precipitation(none::<Inches>());
598    /// let _snd = Sounding::new().with_precipitation(some(Cm(2.5)));
599    /// let _snd = Sounding::new().with_precipitation(Cm(2.5));
600    /// let _snd = Sounding::new().with_precipitation(none::<Cm>());
601    /// let _snd = Sounding::new().with_precipitation(some(Mm(25.0)));
602    /// let _snd = Sounding::new().with_precipitation(Mm(25.0));
603    /// let _snd = Sounding::new().with_precipitation(none::<Mm>());
604    ///```
605    #[inline]
606    pub fn with_precipitation<T, U>(self, value: T) -> Self
607    where
608        Optioned<U>: From<T>,
609        U: optional::Noned + metfor::Length,
610        Mm: From<U>,
611    {
612        let precipitation: Optioned<U> = Optioned::from(value);
613        let precipitation: Optioned<Mm> = precipitation.map_t(Mm::from);
614
615        Self {
616            precipitation,
617            ..self
618        }
619    }
620
621    /// Get the precipitation.
622    #[inline]
623    pub fn precipitation(&self) -> Optioned<Mm> {
624        self.precipitation
625    }
626
627    /// Builder method for the low cloud amount in the range 0.0 to 1.0.
628    ///
629    /// # Examples
630    ///```rust
631    /// use sounding_base::Sounding;
632    /// use optional::{some, none};
633    ///
634    /// let _snd = Sounding::new().with_low_cloud(0.5);
635    /// let _snd = Sounding::new().with_low_cloud(some(0.5));
636    /// let _snd = Sounding::new().with_low_cloud(none());
637    ///```
638    #[inline]
639    pub fn with_low_cloud<T>(self, value: T) -> Self
640    where
641        Optioned<f64>: From<T>,
642    {
643        let low_cloud: Optioned<f64> = Optioned::from(value);
644
645        debug_assert!({
646            if let Some(cld) = low_cloud.into_option() {
647                cld >= 0.0 && cld <= 1.0
648            } else {
649                true
650            }
651        });
652
653        Self { low_cloud, ..self }
654    }
655
656    /// Get the low cloud
657    #[inline]
658    pub fn low_cloud(&self) -> Optioned<f64> {
659        self.low_cloud
660    }
661
662    /// Builder method for the mid cloud amount in the range 0.0 to 1.0.
663    ///
664    /// # Examples
665    ///```rust
666    /// use sounding_base::Sounding;
667    /// use optional::{some, none};
668    ///
669    /// let _snd = Sounding::new().with_mid_cloud(0.5);
670    /// let _snd = Sounding::new().with_mid_cloud(some(0.5));
671    /// let _snd = Sounding::new().with_mid_cloud(none());
672    ///```
673    #[inline]
674    pub fn with_mid_cloud<T>(self, value: T) -> Self
675    where
676        Optioned<f64>: From<T>,
677    {
678        let mid_cloud: Optioned<f64> = Optioned::from(value);
679
680        debug_assert!({
681            if let Some(cld) = mid_cloud.into_option() {
682                cld >= 0.0 && cld <= 1.0
683            } else {
684                true
685            }
686        });
687
688        Self { mid_cloud, ..self }
689    }
690
691    /// Get the mid cloud
692    #[inline]
693    pub fn mid_cloud(&self) -> Optioned<f64> {
694        self.mid_cloud
695    }
696
697    /// Builder method for the high cloud amount in the range 0.0 to 1.0.
698    ///
699    /// # Examples
700    ///```rust
701    /// use sounding_base::Sounding;
702    /// use optional::{some, none};
703    ///
704    /// let _snd = Sounding::new().with_high_cloud(0.5);
705    /// let _snd = Sounding::new().with_high_cloud(some(0.5));
706    /// let _snd = Sounding::new().with_high_cloud(none());
707    ///```
708    #[inline]
709    pub fn with_high_cloud<T>(self, value: T) -> Self
710    where
711        Optioned<f64>: From<T>,
712    {
713        let high_cloud: Optioned<f64> = Optioned::from(value);
714
715        debug_assert!({
716            if let Some(cld) = high_cloud.into_option() {
717                cld >= 0.0 && cld <= 1.0
718            } else {
719                true
720            }
721        });
722
723        Self { high_cloud, ..self }
724    }
725
726    /// Get the high cloud
727    #[inline]
728    pub fn high_cloud(&self) -> Optioned<f64> {
729        self.high_cloud
730    }
731
732    /// Difference in model initialization time and `valid_time` in hours.
733    ///
734    /// # Examples
735    /// ```rust
736    /// use sounding_base::Sounding;
737    ///
738    /// let _snd = Sounding::new().with_lead_time(24);
739    /// let snd = Sounding::new().with_lead_time(Some(24));
740    ///
741    /// assert_eq!(snd.lead_time().unwrap(), 24);
742    /// ```
743    #[inline]
744    pub fn with_lead_time<T>(mut self, lt: T) -> Self
745    where
746        Optioned<i32>: From<T>,
747    {
748        self.lead_time = Optioned::from(lt);
749        self
750    }
751
752    /// Difference in model initialization time and `valid_time` in hours.
753    #[inline]
754    pub fn lead_time(&self) -> Optioned<i32> {
755        self.lead_time
756    }
757
758    /// Valid time of the sounding.
759    #[inline]
760    pub fn valid_time(&self) -> Option<NaiveDateTime> {
761        self.valid_time
762    }
763
764    /// Builder method to set the valid time of the sounding.
765    ///
766    /// # Examples
767    /// ```rust
768    /// use sounding_base::Sounding;
769    /// use chrono::NaiveDate;
770    ///
771    /// let vtime = NaiveDate::from_ymd(2019, 1, 1).and_hms(12, 0, 0);
772    /// let _snd = Sounding::new().with_valid_time(vtime);
773    /// let _snd = Sounding::new().with_valid_time(Some(vtime));
774    /// ```
775    #[inline]
776    pub fn with_valid_time<T>(mut self, valid_time: T) -> Self
777    where
778        Option<NaiveDateTime>: From<T>,
779    {
780        self.valid_time = Option::from(valid_time);
781        self
782    }
783
784    /// Get a bottom up iterator over the data rows. The first value returned from the iterator is
785    /// surface values.
786    ///
787    /// # Examples
788    ///
789    /// ```rust
790    /// use metfor::{HectoPascal, Millibar, Celsius};
791    /// use optional::some;
792    /// use sounding_base::Sounding;
793    ///
794    /// let pres: Vec<_> = vec![1000.0, 925.0, 850.0].into_iter()
795    ///     .map(HectoPascal).map(some).collect();
796    /// let temps: Vec<_> = vec![20.0, 18.0, 17.0].into_iter()
797    ///     .map(Celsius).map(some).collect();
798    ///
799    /// let snd = Sounding::new()
800    ///     .with_pressure_profile(pres)
801    ///     .with_temperature_profile(temps)
802    ///     .with_station_pressure(Millibar(1014.0));
803    ///
804    /// let mut iter = snd.bottom_up();
805    ///
806    /// let mut row = iter.next().unwrap();
807    /// assert_eq!(row.pressure.unwrap(), HectoPascal(1014.0)); // Surface values first!
808    /// assert!(row.temperature.is_none());  // We never set a surface temprature!
809    /// assert!(row.wind.is_none()); // We never set wind profile.
810    ///
811    /// row = iter.next().unwrap();
812    /// assert_eq!(row.pressure.unwrap(), HectoPascal(1000.0));
813    /// assert_eq!(row.temperature.unwrap(), Celsius(20.0));
814    /// assert!(row.wind.is_none()); // We never set wind profile.
815    ///
816    /// row = iter.next().unwrap();
817    /// assert_eq!(row.pressure.unwrap(), HectoPascal(925.0));
818    /// assert_eq!(row.temperature.unwrap(), Celsius(18.0));
819    /// assert!(row.wind.is_none()); // We never set wind profile.
820    ///
821    /// row = iter.next().unwrap();
822    /// assert_eq!(row.pressure.unwrap(), HectoPascal(850.0));
823    /// assert_eq!(row.temperature.unwrap(), Celsius(17.0));
824    /// assert!(row.wind.is_none()); // We never set wind profile.
825    ///
826    /// let row_opt = iter.next();
827    /// assert!(row_opt.is_none());
828    /// ```
829    #[inline]
830    pub fn bottom_up<'a>(&'a self) -> impl Iterator<Item = DataRow> + 'a {
831        ProfileIterator {
832            next_idx: 0,
833            direction: 1,
834            src: self,
835        }
836    }
837
838    /// Get a top down iterator over the data rows. The last value returned is the surface values.
839    ///
840    /// # Examples
841    ///
842    /// ```rust
843    /// use metfor::{HectoPascal, Millibar, Celsius};
844    /// use optional::some;
845    /// use sounding_base::Sounding;
846    ///
847    /// let pres: Vec<_> = vec![1000.0, 925.0, 850.0].into_iter()
848    ///     .map(HectoPascal).map(some).collect();
849    /// let temps: Vec<_> = vec![20.0, 18.0, 17.0].into_iter()
850    ///     .map(Celsius).map(some).collect();
851    ///
852    /// let snd = Sounding::new()
853    ///     .with_pressure_profile(pres)
854    ///     .with_temperature_profile(temps)
855    ///     .with_station_pressure(Millibar(1014.0));
856    ///
857    /// let mut iter = snd.top_down();
858    ///
859    /// let mut row = iter.next().unwrap();
860    /// assert_eq!(row.pressure.unwrap(), HectoPascal(850.0));
861    /// assert_eq!(row.temperature.unwrap(), Celsius(17.0));
862    /// assert!(row.wind.is_none()); // We never set wind profile.
863    ///
864    /// row = iter.next().unwrap();
865    /// assert_eq!(row.pressure.unwrap(), HectoPascal(925.0));
866    /// assert_eq!(row.temperature.unwrap(), Celsius(18.0));
867    /// assert!(row.wind.is_none()); // We never set wind profile.
868    ///
869    /// row = iter.next().unwrap();
870    /// assert_eq!(row.pressure.unwrap(), HectoPascal(1000.0));
871    /// assert_eq!(row.temperature.unwrap(), Celsius(20.0));
872    /// assert!(row.wind.is_none()); // We never set wind profile.
873    ///
874    /// row = iter.next().unwrap();
875    /// assert_eq!(row.pressure.unwrap(), HectoPascal(1014.0)); // Surface values first!
876    /// assert!(row.temperature.is_none());  // We never set a surface temprature!
877    /// assert!(row.wind.is_none()); // We never set wind profile.
878    ///
879    /// let row_opt = iter.next();
880    /// assert!(row_opt.is_none());
881    /// ```
882    #[inline]
883    pub fn top_down<'a>(&'a self) -> impl Iterator<Item = DataRow> + 'a {
884        ProfileIterator {
885            next_idx: (self.pressure.len() - 1) as isize,
886            direction: -1,
887            src: self,
888        }
889    }
890
891    /// Get a row of data values from this sounding.
892    ///
893    /// # Examples
894    ///
895    /// ```rust
896    /// use metfor::{HectoPascal, Millibar, Celsius};
897    /// use optional::some;
898    /// use sounding_base::Sounding;
899    ///
900    /// let pres: Vec<_> = vec![1000.0, 925.0, 850.0].into_iter()
901    ///     .map(HectoPascal).map(some).collect();
902    /// let temps: Vec<_> = vec![20.0, 18.0, 17.0].into_iter()
903    ///     .map(Celsius).map(some).collect();
904    ///
905    /// let snd = Sounding::new()
906    ///     .with_pressure_profile(pres)
907    ///     .with_temperature_profile(temps)
908    ///     .with_station_pressure(Millibar(1014.0));
909    ///
910    /// let row = snd.data_row(0).unwrap(); // This is the surface
911    /// assert_eq!(row.pressure.unwrap(), HectoPascal(1014.0));
912    /// assert!(row.temperature.is_none()); // We never set a surface temperature.
913    ///
914    /// let row = snd.data_row(1).unwrap(); // This is the lowest layer above the surface.
915    /// assert_eq!(row.pressure.unwrap(), HectoPascal(1000.0));
916    /// assert_eq!(row.temperature.unwrap(), Celsius(20.0));
917    ///
918    /// assert!(snd.data_row(4).is_none()); // There weren't that many rows!
919    /// ```
920    #[inline]
921    pub fn data_row(&self, idx: usize) -> Option<DataRow> {
922        macro_rules! copy_to_result {
923            ($result:ident, $profile:ident, $idx:ident) => {
924                match self.$profile.get($idx) {
925                    None => {}
926                    Some(opt_val) => $result.$profile = *opt_val,
927                }
928            };
929        }
930
931        if idx >= self.pressure.len() {
932            return None;
933        }
934
935        let mut result = DataRow::default();
936
937        copy_to_result!(result, pressure, idx);
938        copy_to_result!(result, temperature, idx);
939        copy_to_result!(result, wet_bulb, idx);
940        copy_to_result!(result, dew_point, idx);
941        copy_to_result!(result, theta_e, idx);
942        copy_to_result!(result, wind, idx);
943        copy_to_result!(result, pvv, idx);
944        copy_to_result!(result, height, idx);
945        copy_to_result!(result, cloud_fraction, idx);
946
947        Some(result)
948    }
949
950    /// Get the surface values in a `DataRow` format.
951    #[inline]
952    pub fn surface_as_data_row(&self) -> Option<DataRow> {
953        self.data_row(0)
954    }
955
956    /// Given a target pressure, return the row of data values closest to this one.
957    pub fn fetch_nearest_pnt<P>(&self, target_p: P) -> DataRow
958    where
959        HectoPascal: From<P>,
960        P: metfor::Pressure,
961    {
962        let tgt_p = HectoPascal::from(target_p);
963
964        let mut idx: usize = 0;
965        let mut best_abs_diff: f64 = ::std::f64::MAX;
966        for (i, &p_opt) in self.pressure.iter().enumerate() {
967            if let Some(p) = p_opt.into_option() {
968                let abs_diff = (tgt_p.unpack() - p.unpack()).abs();
969                if abs_diff < best_abs_diff {
970                    best_abs_diff = abs_diff;
971                    idx = i;
972                }
973                if abs_diff > best_abs_diff {
974                    break;
975                }
976            }
977        }
978
979        if idx == 0 {
980            self.surface_as_data_row().unwrap()
981        } else {
982            self.data_row(idx - 1).unwrap()
983        }
984    }
985
986    #[inline]
987    fn surface_wet_bulb(&self) -> Option<Celsius> {
988        let sfc_t = self.sfc_temperature.into_option()?;
989        let sfc_p = self.station_pressure.into_option()?;
990        let sfc_dp = self.sfc_dew_point.into_option()?;
991
992        metfor::wet_bulb(sfc_t, sfc_dp, sfc_p)
993    }
994
995    #[inline]
996    fn surface_theta_e(&self) -> Option<Kelvin> {
997        let sfc_t = self.sfc_temperature.into_option()?;
998        let sfc_p = self.station_pressure.into_option()?;
999        let sfc_dp = self.sfc_dew_point.into_option()?;
1000
1001        metfor::theta_e(sfc_t, sfc_dp, sfc_p)
1002    }
1003
1004    #[inline]
1005    fn surface_height(&self) -> Option<Meters> {
1006        self.station_info().elevation().into_option()
1007    }
1008
1009    #[inline]
1010    fn update_sfc_wet_bulb_theta_e(&mut self) {
1011        if let (Some(sfc_p), Some(sfc_t), Some(sfc_dp)) = (
1012            self.station_pressure.into_option(),
1013            self.sfc_temperature.into_option(),
1014            self.sfc_dew_point.into_option(),
1015        ) {
1016            if !self.wet_bulb.is_empty() {
1017                self.wet_bulb[0] = metfor::wet_bulb(sfc_t, sfc_dp, sfc_p).into();
1018            }
1019
1020            if !self.theta_e.is_empty() {
1021                self.theta_e[0] = metfor::theta_e(sfc_t, sfc_dp, sfc_p).into();
1022            }
1023        }
1024    }
1025}
1026
1027/// Iterator over the data rows of a sounding. This may be a top down or bottom up iterator where
1028/// either the last or first row returned is the surface data.
1029struct ProfileIterator<'a> {
1030    next_idx: isize,
1031    direction: isize, // +1 for bottom up, -1 for top down
1032    src: &'a Sounding,
1033}
1034
1035impl<'a> Iterator for ProfileIterator<'a> {
1036    type Item = DataRow;
1037
1038    #[inline]
1039    fn next(&mut self) -> Option<Self::Item> {
1040        let result = self.src.data_row(self.next_idx as usize);
1041        self.next_idx += self.direction;
1042        result
1043    }
1044}
1045
1046// FIXME: only configure for test and doc tests, not possible as of 1.31
1047#[doc(hidden)]
1048pub mod doctest {
1049    use super::*;
1050
1051    pub fn make_test_sounding() -> super::Sounding {
1052        use optional::some;
1053
1054        let p = vec![
1055            some(HectoPascal(1000.0)),
1056            some(HectoPascal(925.0)),
1057            some(HectoPascal(850.0)),
1058            some(HectoPascal(700.0)),
1059        ];
1060        let t = vec![
1061            some(Celsius(20.0)),
1062            some(Celsius(18.0)),
1063            some(Celsius(10.0)),
1064            some(Celsius(2.0)),
1065        ];
1066
1067        Sounding::new()
1068            .with_pressure_profile(p)
1069            .with_temperature_profile(t)
1070            .with_sfc_temperature(some(Celsius(21.0)))
1071            .with_station_pressure(some(HectoPascal(1005.0)))
1072    }
1073}
1074
1075#[cfg(test)]
1076mod test {
1077    use super::*;
1078
1079    #[test]
1080    fn test_profile() {
1081        let snd = doctest::make_test_sounding();
1082
1083        println!("snd = {:#?}", snd);
1084        assert!(snd.pressure_profile().iter().all(|t| t.is_some()));
1085        assert!(snd.temperature_profile().iter().all(|t| t.is_some()));
1086        assert_eq!(
1087            snd.pressure_profile()
1088                .iter()
1089                .filter(|p| p.is_some())
1090                .count(),
1091            5
1092        );
1093
1094        assert_eq!(
1095            snd.temperature_profile()
1096                .iter()
1097                .filter(|t| t.is_some())
1098                .count(),
1099            5
1100        );
1101    }
1102}