use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::str::FromStr;
use metfor::{self, Celsius, HectoPascal, Knots, Meters, WindSpdDir};
use optional::Optioned;
use sounding_base::{Sounding, StationInfo};
pub mod index_tests;
pub mod layer_tests;
pub mod level_tests;
#[allow(unused_macros)] macro_rules! check_file_complete {
($test_name:ident, $fname:expr) => {
#[test]
fn $test_name() {
let (snd, ivals, fvals) = utils::load_test_file($fname);
assert!(
sounding_validate::validate(&snd).is_ok(),
"Failed validation."
);
let ival_keys = [
"num dendritic zones",
"num warm dry bulb aloft",
"num warm wet bulb aloft",
"num inversions",
"num freezing level",
"num wet bulb zeros",
];
let fval_keys = [
"dendritic zone pressures",
"warm dry bulb layer pressures",
"cold surface layer pressures",
"warm wet bulb layer pressures",
"6km agl layer pressures",
"700-500 hPa layer heights",
"inversion layer pressures",
"freezing level pressures",
"wet bulb zero pressures",
"max wet bulb aloft",
"max wet bulb pressure",
"max temperature aloft",
"max temperature pressure",
"warm layer max t",
"haines_low",
"haines_mid",
"haines_high",
"haines",
"hdw",
"kindex",
"swet",
"total_totals",
"precipitable_water",
];
for key in ival_keys.iter() {
assert!(ivals.contains_key(*key), *key);
}
for key in fval_keys.iter() {
assert!(fvals.contains_key(*key), *key);
}
for key in ivals.keys() {
assert!(ival_keys.contains(&key.as_str()), "extra ival key found");
}
for key in fvals.keys() {
assert!(fval_keys.contains(&key.as_str()), "extra fval key found");
}
}
};
}
#[allow(unused_macros)] macro_rules! test_file {
($test_mod_name:ident, $fname:expr) => {
mod $test_mod_name {
use std::collections::HashMap;
use crate::utils;
use sounding_base::Sounding;
fn load_data() -> (Sounding, HashMap<String, i64>, HashMap<String, Vec<f64>>) {
utils::load_test_file($fname)
}
mod levels {
use crate::utils::level_tests;
use crate::$test_mod_name::load_data;
#[test]
fn freezing_level() {
let (snd, ivals, fvals) = load_data();
level_tests::test_freezing_levels(&snd, &ivals, &fvals);
}
#[test]
fn wet_bulb_zero() {
let (snd, ivals, fvals) = load_data();
level_tests::test_wet_bulb_zero_levels(&snd, &ivals, &fvals);
}
#[test]
fn max_wet_bulb_in_profile() {
let (snd, _, fvals) = load_data();
level_tests::test_max_wet_bulb_in_profile(&snd, &fvals);
}
#[test]
fn max_temperature_in_profile() {
let (snd, _, fvals) = load_data();
level_tests::test_max_temperature(&snd, &fvals);
}
}
mod layers {
use crate::utils::layer_tests;
use crate::$test_mod_name::load_data;
#[test]
fn dendritic_layers() {
let (snd, ivals, fvals) = load_data();
layer_tests::test_dendritic_layers(&snd, &ivals, &fvals);
}
#[test]
fn warm_dry_bulb_aloft_and_cold_sfc_layers() {
let (snd, ivals, fvals) = load_data();
layer_tests::test_warm_dry_bulb_aloft_and_cold_sfc_layers(&snd, &ivals, &fvals);
}
#[test]
fn warm_wet_bulb_aloft() {
let (snd, ivals, fvals) = load_data();
layer_tests::test_warm_wet_bulb_aloft(&snd, &ivals, &fvals);
}
#[test]
fn layer_agl() {
let (snd, _, fvals) = load_data();
layer_tests::test_layer_agl(&snd, &fvals);
}
#[test]
fn pressure_layer() {
let (snd, _, fvals) = load_data();
layer_tests::test_pressure_layer(&snd, &fvals);
}
#[test]
fn inversions() {
let (snd, ivals, fvals) = load_data();
layer_tests::test_inversion_layers(&snd, &ivals, &fvals);
}
}
mod indexes {
use crate::utils::index_tests;
use crate::$test_mod_name::load_data;
use sounding_analysis;
#[test]
fn test_haines_low() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::haines_low,
"haines_low",
0,
0,
);
}
#[test]
fn test_haines_mid() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::haines_mid,
"haines_mid",
0,
0,
);
}
#[test]
fn test_haines_high() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::haines_high,
"haines_high",
0,
0,
);
}
#[test]
fn test_haines() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::haines,
"haines",
0,
0,
);
}
#[test]
fn test_hdw() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::hot_dry_windy,
"hdw",
5.0,
0.0,
);
}
#[test]
fn test_kindex() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::kindex,
"kindex",
metfor::Celsius(0.1),
metfor::Celsius(-1000.0),
);
}
#[test]
fn test_swet() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::swet,
"swet",
1.0,
-1000.0,
);
}
#[test]
fn test_total_totals() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::total_totals,
"total_totals",
1.0,
0.0,
);
}
#[test]
fn test_precipitable_water() {
let (snd, _, fvals) = load_data();
index_tests::test_index(
&snd,
&fvals,
sounding_analysis::precipitable_water,
"precipitable_water",
metfor::Mm(0.5),
metfor::Mm(0.0),
);
}
}
}
};
}
pub fn load_test_file(fname: &str) -> (Sounding, HashMap<String, i64>, HashMap<String, Vec<f64>>) {
let mut test_path = PathBuf::new();
test_path.push("test_data");
test_path.push(fname);
load_test_csv_sounding(&test_path)
}
#[allow(dead_code)] fn approx_equal(tgt: f64, guess: f64, tol: f64) -> bool {
use std::f64;
assert!(tol > 0.0);
let passed = f64::abs(tgt - guess) <= tol;
if !passed {
println!("left = {} and right = {}", tgt, guess);
}
passed
}
fn load_test_csv_sounding(
location: &PathBuf,
) -> (Sounding, HashMap<String, i64>, HashMap<String, Vec<f64>>) {
let mut f = File::open(location).expect(&format!("Error opening file: {:#?}", location));
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect(&format!("Error reading file: {:#?}", location));
let lines: Vec<&str> = contents.split('\n').collect();
let mut line_iter = lines.iter();
let mut height: Vec<Optioned<Meters>> = Vec::with_capacity(lines.len());
let mut temp: Vec<Optioned<Celsius>> = Vec::with_capacity(lines.len());
let mut wb: Vec<Optioned<Celsius>> = Vec::with_capacity(lines.len());
let mut dp: Vec<Optioned<Celsius>> = Vec::with_capacity(lines.len());
let mut press: Vec<Optioned<HectoPascal>> = Vec::with_capacity(lines.len());
let mut wind: Vec<Optioned<WindSpdDir<Knots>>> = Vec::with_capacity(lines.len());
for line in line_iter.by_ref() {
if line.starts_with("### Surface Data ###")
|| line.starts_with("### Analysis Int Section ###")
|| line.starts_with("### Analysis Float Section ###")
{
break;
}
let tokens: Vec<&str> = line.split(',').collect();
if tokens.len() < 6 {
continue;
}
let t_c: Option<Celsius> = f64::from_str(tokens[1]).ok().map(Celsius);
let dp_c: Option<Celsius> = f64::from_str(tokens[2]).ok().map(Celsius);
let press_hpa: Option<HectoPascal> = f64::from_str(tokens[3]).ok().map(HectoPascal).into();
let wb_c: Option<Celsius> = t_c
.and_then(|t| dp_c.and_then(|dp| press_hpa.and_then(|p| metfor::wet_bulb(t, dp, p))));
let wspd = f64::from_str(tokens[4]).ok();
let wdir = f64::from_str(tokens[5]).ok();
let wind_val: Option<WindSpdDir<Knots>> = wspd.and_then(|wspd| {
wdir.map(|wdir| WindSpdDir {
speed: Knots(wspd),
direction: wdir,
})
});
height.push(f64::from_str(tokens[0]).ok().map(Meters).into());
temp.push(t_c.into());
wb.push(wb_c.into());
dp.push(dp_c.into());
press.push(press_hpa.into());
wind.push(wind_val.into());
}
let mut snd = Sounding::new()
.with_height_profile(height)
.with_temperature_profile(temp)
.with_wet_bulb_profile(wb)
.with_dew_point_profile(dp)
.with_pressure_profile(press)
.with_wind_profile(wind);
for line in line_iter.by_ref() {
if line.starts_with("### Analysis Int Section ###")
|| line.starts_with("### Analysis Float Section ###")
{
break;
}
let tokens: Vec<&str> = line.split(',').collect();
if tokens.len() < 6 {
continue;
}
let height = f64::from_str(tokens[0]).ok().map(Meters);
let t_c = f64::from_str(tokens[1]).ok().map(Celsius);
let dp_c = f64::from_str(tokens[2]).ok().map(Celsius);
let press_hpa = f64::from_str(tokens[3]).ok().map(HectoPascal);
let wspd = f64::from_str(tokens[4]).ok();
let wdir = f64::from_str(tokens[5]).ok();
let wind = wspd.and_then(|wspd| {
wdir.map(|wdir| WindSpdDir {
speed: Knots(wspd),
direction: wdir,
})
});
snd = snd
.with_sfc_temperature(t_c)
.with_station_info(StationInfo::new().with_elevation(height))
.with_sfc_dew_point(dp_c)
.with_station_pressure(press_hpa)
.with_sfc_wind(wind)
}
let mut target_int_vals = HashMap::new();
for line in line_iter.by_ref() {
if line.starts_with("### Analysis Float Section ###") {
break;
}
let tokens: Vec<String> = line
.split(',')
.filter_map(|val| {
let v = val.trim();
if v != "" {
Some(v.to_owned())
} else {
None
}
})
.collect();
if tokens.len() < 2 {
continue;
}
let key = tokens[0].to_owned();
let value = i64::from_str(&tokens[1]).unwrap();
target_int_vals.insert(key, value);
}
let mut target_float_vals = HashMap::new();
for line in line_iter.by_ref() {
let tokens: Vec<String> = line
.split(',')
.filter_map(|val| {
let v = val.trim();
if v != "" {
Some(v.to_owned())
} else {
None
}
})
.collect();
if tokens.len() < 1 {
continue;
}
let key = tokens[0].to_owned();
let mut values = vec![];
for token in tokens.iter().skip(1) {
values.push(f64::from_str(token).unwrap());
}
target_float_vals.insert(key, values);
}
(snd, target_int_vals, target_float_vals)
}