sounding_validate/
validate.rs1use crate::error::*;
2use metfor::{Celsius, HectoPascal, Meters, WindSpdDir};
3use sounding_base::Sounding;
4
5use optional::{some, Optioned};
6
7macro_rules! validate_f64_positive {
8    ($var:expr, $var_name:expr, $err_list:ident) => {
9        if let Some(val) = $var.into_option() {
10            let val: f64 = metfor::Quantity::unpack(val);
11            if val < 0.0 {
12                $err_list.push_error(Err(ValidationError::InvalidNegativeValue($var_name, val)));
13            }
14        }
15    };
16}
17
18macro_rules! validate_wind_direction {
19    ($var:expr, $err_list:ident) => {
20        if let Some(val) = $var.into() {
21            if val < 0.0 || val > 360.0 {
22                $err_list.push_error(Err(ValidationError::InvalidWindDirection(val)));
23            }
24        }
25    };
26}
27
28pub fn validate(snd: &Sounding) -> Result<(), ValidationErrors> {
31    let mut err_return = ValidationErrors::new();
32
33    let pressure = snd.pressure_profile();
34
35    err_return.push_error(check_pressure_exists(pressure));
41
42    let len = pressure.len();
43    let temperature = snd.temperature_profile();
44    let wet_bulb = snd.wet_bulb_profile();
45    let dew_point = snd.dew_point_profile();
46    let theta_e = snd.theta_e_profile();
47    let wind = snd.wind_profile();
48    let omega = snd.pvv_profile();
49    let height = snd.height_profile();
50    let cloud_fraction = snd.cloud_fraction_profile();
51
52    err_return.push_error(validate_vector_len(temperature, len, "Temperature"));
53    err_return.push_error(validate_vector_len(wet_bulb, len, "Wet bulb temperature"));
54    err_return.push_error(validate_vector_len(dew_point, len, "Dew point"));
55    err_return.push_error(validate_vector_len(theta_e, len, "Theta-e"));
56    err_return.push_error(validate_vector_len(wind, len, "Wind"));
57    err_return.push_error(validate_vector_len(
58        omega,
59        len,
60        "Omega (pressure vertical velocity)",
61    ));
62    err_return.push_error(validate_vector_len(height, len, "Height"));
63    err_return.push_error(validate_vector_len(cloud_fraction, len, "Cloud fraction"));
64
65    err_return.push_error(check_vertical_height_pressure(snd));
69
70    check_temp_wet_bulb_dew_point(snd, &mut err_return);
72
73    for wind_val in wind {
75        if let Some(WindSpdDir {
76            speed: spd,
77            direction: dir,
78        }) = wind_val.into_option()
79        {
80            validate_f64_positive!(some(spd), "Wind speed", err_return);
81            validate_wind_direction!(dir, err_return);
82        }
83    }
84
85    for cld in cloud_fraction {
87        validate_f64_positive!(*cld, "Cloud fraction", err_return);
88    }
89
90    validate_f64_positive!(snd.low_cloud(), "Low cloud", err_return);
93    validate_f64_positive!(snd.mid_cloud(), "Mid cloud", err_return);
94    validate_f64_positive!(snd.high_cloud(), "Hi cloud", err_return);
95
96    if let Some(WindSpdDir {
97        speed: spd,
98        direction: dir,
99    }) = snd.sfc_wind().into_option()
100    {
101        validate_f64_positive!(some(spd), "Wind speed", err_return);
102        validate_wind_direction!(dir, err_return);
103    }
104
105    validate_f64_positive!(snd.mslp(), "MSLP", err_return);
106
107    validate_f64_positive!(snd.station_pressure(), "Station pressure", err_return);
108
109    err_return.check_any()
110}
111
112fn check_pressure_exists(pressure: &[Optioned<HectoPascal>]) -> Result<(), ValidationError> {
113    if pressure.is_empty() {
114        Err(ValidationError::NoPressureProfile)
115    } else {
116        Ok(())
117    }
118}
119
120fn validate_vector_len<T>(
121    vec: &[T],
122    len: usize,
123    var_name: &'static str,
124) -> Result<(), ValidationError> {
125    if !vec.is_empty() && vec.len() != len {
126        Err(ValidationError::InvalidVectorLength(
127            var_name,
128            vec.len(),
129            len,
130        ))
131    } else {
132        Ok(())
133    }
134}
135
136fn check_vertical_height_pressure(snd: &Sounding) -> Result<(), ValidationError> {
137    let pressure = snd
140        .pressure_profile()
141        .into_iter()
142        .filter_map(|val| val.into_option())
143        .map(|HectoPascal(val)| val);
144    let mut pressure_one_level_down = snd
145        .station_pressure()
146        .map_t(|HectoPascal(val)| val)
147        .unwrap_or(::std::f64::MAX);
148    for pres in pressure {
149        if pressure_one_level_down < pres {
150            return Err(ValidationError::PressureNotDecreasingWithHeight);
151        }
152        pressure_one_level_down = pres;
153    }
154
155    let height = snd
157        .height_profile()
158        .into_iter()
159        .filter_map(|val| val.into_option())
160        .map(|Meters(val)| val);
161    let mut height_one_level_down = snd
162        .station_info()
163        .elevation()
164        .map(|Meters(val)| val)
165        .unwrap_or(::std::f64::MIN);
166    for hght in height {
167        if height_one_level_down > hght {
168            return Err(ValidationError::PressureNotDecreasingWithHeight);
169        }
170        height_one_level_down = hght;
171    }
172
173    Ok(())
174}
175
176fn check_temp_wet_bulb_dew_point(snd: &Sounding, ve: &mut ValidationErrors) {
177    let temperature = snd.temperature_profile();
178    let wet_bulb = snd.wet_bulb_profile();
179    let dew_point = snd.dew_point_profile();
180
181    for (t, wb) in temperature.iter().zip(wet_bulb.iter()) {
183        if let (Some(Celsius(t)), Some(Celsius(wb))) = (t.into_option(), wb.into_option()) {
184            if t < wb {
185                ve.push_error(Err(ValidationError::TemperatureLessThanWetBulb(t, wb)));
186            }
187        }
188    }
189    for (t, dp) in temperature.iter().zip(dew_point.iter()) {
190        if let (Some(Celsius(t)), Some(Celsius(dp))) = (t.into_option(), dp.into_option()) {
191            if t < dp {
192                ve.push_error(Err(ValidationError::TemperatureLessThanDewPoint(t, dp)));
193            }
194        }
195    }
196    for (wb, dp) in wet_bulb.iter().zip(dew_point.iter()) {
197        if let (Some(Celsius(wb)), Some(Celsius(dp))) = (wb.into_option(), dp.into_option()) {
198            if wb < dp {
199                ve.push_error(Err(ValidationError::WetBulbLessThanDewPoint(wb, dp)));
200            }
201        }
202    }
203}