1use crate::sounding::Sounding;
10use crate::ProfileField;
11use nalgebra::Vector3;
12
13fn horizontal_speed(w: Vector3<f64>) -> f64 {
15 w.x.hypot(w.z)
16}
17
18fn direction_from_deg(w: Vector3<f64>) -> f64 {
21 (w.z.atan2(w.x).to_degrees() + 180.0).rem_euclid(360.0)
22}
23
24const LAYER_TOPS_M: [f64; 4] = [1_000.0, 3_000.0, 6_000.0, 10_000.0];
27
28#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct LayerError {
31 pub top_m: f64,
34 pub mean_abs_speed_error_mps: f64,
36 pub levels: usize,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct ValidationReport {
43 pub levels: usize,
45 pub speed_rmse_mps: f64,
47 pub direction_rmse_deg: f64,
49 pub layers: Vec<LayerError>,
51}
52
53pub fn validate(profile: &ProfileField, sounding: &Sounding) -> ValidationReport {
57 let mut speed_sq = 0.0;
58 let mut direction_sq = 0.0;
59 let mut layer_abs = vec![0.0; LAYER_TOPS_M.len() + 1];
60 let mut layer_n = vec![0usize; LAYER_TOPS_M.len() + 1];
61 for level in &sounding.levels {
62 let wind = profile.wind_at(level.altitude_m);
63 let speed_err = horizontal_speed(wind) - level.speed_mps;
64 speed_sq += speed_err * speed_err;
65 let mut dir_err = (direction_from_deg(wind) - level.direction_from_deg) % 360.0;
66 if dir_err > 180.0 {
67 dir_err -= 360.0;
68 } else if dir_err < -180.0 {
69 dir_err += 360.0;
70 }
71 direction_sq += dir_err * dir_err;
72 let layer = LAYER_TOPS_M
73 .iter()
74 .position(|top| level.altitude_m < *top)
75 .unwrap_or(LAYER_TOPS_M.len());
76 layer_abs[layer] += speed_err.abs();
77 layer_n[layer] += 1;
78 }
79 let n = sounding.levels.len().max(1) as f64;
80 ValidationReport {
81 levels: sounding.levels.len(),
82 speed_rmse_mps: (speed_sq / n).sqrt(),
83 direction_rmse_deg: (direction_sq / n).sqrt(),
84 layers: layer_abs
85 .into_iter()
86 .zip(layer_n)
87 .enumerate()
88 .filter(|(_, (_, count))| *count > 0)
89 .map(|(i, (abs, count))| LayerError {
90 top_m: LAYER_TOPS_M.get(i).copied().unwrap_or(f64::INFINITY),
91 mean_abs_speed_error_mps: abs / count as f64,
92 levels: count,
93 })
94 .collect(),
95 }
96}