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