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;