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}