use serde::{Serialize, Deserialize};
pub fn nse(obs: &[f64], sim: &[f64]) -> f64 {
if obs.is_empty() || obs.len() != sim.len() {
return f64::NAN;
}
let mean_o = obs.iter().sum::<f64>() / obs.len() as f64;
let ss_res: f64 = obs.iter().zip(sim).map(|(o, s)| (o - s).powi(2)).sum();
let ss_tot: f64 = obs.iter().map(|o| (o - mean_o).powi(2)).sum();
if ss_tot < 1e-12 {
return f64::NAN;
}
1.0 - ss_res / ss_tot
}
pub fn kge(obs: &[f64], sim: &[f64]) -> f64 {
if obs.len() < 2 || obs.len() != sim.len() {
return f64::NAN;
}
let n = obs.len() as f64;
let mean_o = obs.iter().sum::<f64>() / n;
let mean_s = sim.iter().sum::<f64>() / n;
let std_o = (obs.iter().map(|o| (o - mean_o).powi(2)).sum::<f64>() / n).sqrt();
let std_s = (sim.iter().map(|s| (s - mean_s).powi(2)).sum::<f64>() / n).sqrt();
if std_o < 1e-12 || std_s < 1e-12 {
return f64::NAN;
}
let cov: f64 = obs.iter().zip(sim)
.map(|(o, s)| (o - mean_o) * (s - mean_s))
.sum::<f64>() / n;
let r = cov / (std_o * std_s);
let alpha = std_s / std_o;
let beta = mean_s / mean_o;
let ed = (r - 1.0).powi(2) + (alpha - 1.0).powi(2) + (beta - 1.0).powi(2);
1.0 - ed.sqrt()
}
pub fn rmse(obs: &[f64], sim: &[f64]) -> f64 {
if obs.is_empty() || obs.len() != sim.len() {
return f64::NAN;
}
let ssq: f64 = obs.iter().zip(sim).map(|(o, s)| (o - s).powi(2)).sum();
(ssq / obs.len() as f64).sqrt()
}
pub fn pbias(obs: &[f64], sim: &[f64]) -> f64 {
let sum_o: f64 = obs.iter().sum();
if sum_o.abs() < 1e-12 {
return f64::NAN;
}
let sum_s: f64 = sim.iter().sum();
100.0 * (sum_s - sum_o) / sum_o
}
pub fn r2(obs: &[f64], sim: &[f64]) -> f64 {
if obs.len() < 2 || obs.len() != sim.len() {
return f64::NAN;
}
let n = obs.len() as f64;
let mean_o = obs.iter().sum::<f64>() / n;
let mean_s = sim.iter().sum::<f64>() / n;
let ss_oo: f64 = obs.iter().map(|o| (o - mean_o).powi(2)).sum();
let ss_ss: f64 = sim.iter().map(|s| (s - mean_s).powi(2)).sum();
let ss_os: f64 = obs.iter().zip(sim).map(|(o, s)| (o - mean_o) * (s - mean_s)).sum();
if ss_oo < 1e-12 || ss_ss < 1e-12 {
return f64::NAN;
}
(ss_os / (ss_oo * ss_ss).sqrt()).powi(2)
}
pub fn dc_grade(dc: f64) -> &'static str {
if dc >= 0.90 { "甲" }
else if dc >= 0.70 { "乙" }
else if dc >= 0.50 { "丙" }
else { "不合格" }
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct GbtEventErrors {
pub peak_rel_err_pct: f64,
pub time_to_peak_err_steps: i64,
pub runoff_depth_rel_err_pct: f64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QualificationTolerance {
pub peak_rel_pct: f64,
pub time_to_peak_steps: i64,
pub runoff_depth_rel_pct: f64,
}
impl Default for QualificationTolerance {
fn default() -> Self {
Self { peak_rel_pct: 20.0, time_to_peak_steps: 1, runoff_depth_rel_pct: 20.0 }
}
}
pub fn event_errors(obs: &[f64], sim: &[f64], area_km2: f64, dt_h: f64) -> GbtEventErrors {
let mut e = GbtEventErrors::default();
if obs.is_empty() || sim.is_empty() || area_km2 <= 0.0 || dt_h <= 0.0 {
return e;
}
let (obs_pk, obs_idx) = argmax(obs);
let (sim_pk, sim_idx) = argmax(sim);
if obs_pk.abs() > 1e-9 {
e.peak_rel_err_pct = 100.0 * (sim_pk - obs_pk) / obs_pk;
}
e.time_to_peak_err_steps = sim_idx as i64 - obs_idx as i64;
let d_obs = runoff_depth_mm(obs, area_km2, dt_h);
let d_sim = runoff_depth_mm(sim, area_km2, dt_h);
if d_obs.abs() > 1e-9 {
e.runoff_depth_rel_err_pct = 100.0 * (d_sim - d_obs) / d_obs;
}
e
}
pub fn event_qualified(e: &GbtEventErrors, tol: &QualificationTolerance) -> bool {
e.peak_rel_err_pct.abs() <= tol.peak_rel_pct
&& e.time_to_peak_err_steps.abs() <= tol.time_to_peak_steps
&& e.runoff_depth_rel_err_pct.abs() <= tol.runoff_depth_rel_pct
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct QualificationReport {
pub total: usize,
pub qualified: usize,
pub rate_pct: f64,
pub grade: String,
}
pub fn qualified_rate(qualified_flags: &[bool]) -> QualificationReport {
let total = qualified_flags.len();
let qualified = qualified_flags.iter().filter(|&&q| q).count();
let rate_pct = if total == 0 {
0.0
} else {
100.0 * qualified as f64 / total as f64
};
QualificationReport {
total,
qualified,
rate_pct,
grade: qualification_grade(rate_pct).to_string(),
}
}
pub fn qualification_grade(rate_pct: f64) -> &'static str {
if rate_pct >= 85.0 { "甲" }
else if rate_pct >= 70.0 { "乙" }
else if rate_pct >= 60.0 { "丙" }
else { "不合格" }
}
pub fn scheme_grade(dc_grade: &str, qual_grade: &str) -> &'static str {
let rank = |g: &str| -> i32 {
match g { "甲" => 3, "乙" => 2, "丙" => 1, _ => 0 }
};
match rank(dc_grade).min(rank(qual_grade)) {
3 => "甲",
2 => "乙",
1 => "丙",
_ => "不合格",
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct MetricsReport {
pub nse: f64,
pub kge: f64,
pub rmse: f64,
pub pbias: f64,
pub r2: f64,
pub grade: String,
pub gbt: Option<GbtEventErrors>,
pub qualified: Option<bool>,
}
pub fn compute_metrics(obs: &[f64], sim: &[f64]) -> MetricsReport {
let n = nse(obs, sim);
MetricsReport {
nse: n,
kge: kge(obs, sim),
rmse: rmse(obs, sim),
pbias: pbias(obs, sim),
r2: r2(obs, sim),
grade: dc_grade(n).to_string(),
gbt: None,
qualified: None,
}
}
pub fn compute_metrics_gbt(
obs: &[f64],
sim: &[f64],
area_km2: f64,
dt_h: f64,
tol: &QualificationTolerance,
) -> MetricsReport {
let mut m = compute_metrics(obs, sim);
let e = event_errors(obs, sim, area_km2, dt_h);
m.qualified = Some(event_qualified(&e, tol));
m.gbt = Some(e);
m
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct ProbabilisticReport {
pub crps: f64,
pub brier: f64,
pub pod: f64,
pub far: f64,
pub csi: f64,
}
pub fn ensemble_exceedance(ensemble: &[Vec<f64>], threshold: f64) -> Vec<f64> {
if ensemble.is_empty() {
return Vec::new();
}
let n_t = ensemble.iter().map(|m| m.len()).max().unwrap_or(0);
let m = ensemble.len() as f64;
(0..n_t)
.map(|t| {
let cnt = ensemble.iter()
.filter(|mem| mem.get(t).map_or(false, |v| *v > threshold))
.count() as f64;
cnt / m
})
.collect()
}
pub fn crps(ensemble: &[Vec<f64>], observed: &[f64]) -> f64 {
if ensemble.is_empty() || observed.is_empty() {
return f64::NAN;
}
let m = ensemble.len();
let mf = m as f64;
let mut sum = 0.0;
let mut count = 0usize;
for (t, &y) in observed.iter().enumerate() {
let vals: Vec<f64> = ensemble.iter().filter_map(|mem| mem.get(t).copied()).collect();
if vals.len() != m {
continue; }
let mae: f64 = vals.iter().map(|x| (x - y).abs()).sum::<f64>() / mf;
let mut pair = 0.0;
for i in 0..m {
for j in 0..m {
pair += (vals[i] - vals[j]).abs();
}
}
let spread = pair / (2.0 * mf * mf);
sum += (mae - spread).max(0.0);
count += 1;
}
if count == 0 { f64::NAN } else { sum / count as f64 }
}
pub fn brier(prob_exceed: &[f64], observed_exceed: &[bool]) -> f64 {
if prob_exceed.is_empty() || prob_exceed.len() != observed_exceed.len() {
return f64::NAN;
}
let n = prob_exceed.len() as f64;
prob_exceed.iter().zip(observed_exceed.iter())
.map(|(p, o)| (p - if *o { 1.0 } else { 0.0 }).powi(2))
.sum::<f64>() / n
}
pub fn pod_far_csi(forecast_exceed: &[bool], observed_exceed: &[bool]) -> (f64, f64, f64) {
let mut hits = 0.0; let mut misses = 0.0; let mut false_alarms = 0.0; for (f, o) in forecast_exceed.iter().zip(observed_exceed.iter()) {
match (*f, *o) {
(true, true) => hits += 1.0,
(false, true) => misses += 1.0,
(true, false) => false_alarms += 1.0,
(false, false) => {} }
}
let pod = if hits + misses > 0.0 { hits / (hits + misses) } else { f64::NAN };
let far = if hits + false_alarms > 0.0 { false_alarms / (hits + false_alarms) } else { f64::NAN };
let csi = if hits + misses + false_alarms > 0.0 {
hits / (hits + misses + false_alarms)
} else { f64::NAN };
(pod, far, csi)
}
pub fn compute_probabilistic(
ensemble: &[Vec<f64>],
observed: &[f64],
threshold: f64,
) -> ProbabilisticReport {
let prob = ensemble_exceedance(ensemble, threshold);
let n = observed.len().min(prob.len());
let prob_aligned: Vec<f64> = prob.iter().take(n).copied().collect();
let obs_exceed: Vec<bool> = observed[..n].iter().map(|&q| q > threshold).collect();
let fcst_exceed: Vec<bool> = prob_aligned.iter().map(|&p| p > 0.5).collect();
let (pod, far, csi) = pod_far_csi(&fcst_exceed, &obs_exceed);
ProbabilisticReport {
crps: crps(ensemble, observed),
brier: brier(&prob_aligned, &obs_exceed),
pod, far, csi,
}
}
pub fn rank_histogram(ensemble: &[Vec<f64>], observed: &[f64]) -> Vec<usize> {
if ensemble.is_empty() || observed.is_empty() {
return Vec::new();
}
let m = ensemble.len();
let mut hist = vec![0usize; m + 1];
for (t, &y) in observed.iter().enumerate() {
let vals: Vec<f64> = ensemble.iter().filter_map(|mem| mem.get(t).copied()).collect();
if vals.len() != m {
continue;
}
let rank = vals.iter().filter(|&&x| x < y).count();
hist[rank] += 1;
}
hist
}
fn argmax(xs: &[f64]) -> (f64, usize) {
let mut best = f64::NEG_INFINITY;
let mut idx = 0;
for (i, &v) in xs.iter().enumerate() {
if v > best {
best = v;
idx = i;
}
}
(best, idx)
}
fn runoff_depth_mm(q: &[f64], area_km2: f64, dt_h: f64) -> f64 {
let sum_q: f64 = q.iter().sum();
sum_q * dt_h * 3.6 / area_km2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nse_perfect() {
let obs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
assert!((nse(&obs, &obs) - 1.0).abs() < 1e-10);
}
#[test]
fn test_nse_mean_prediction() {
let obs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let sim = vec![3.0; 5]; assert!((nse(&obs, &sim) - 0.0).abs() < 1e-10);
}
#[test]
fn test_kge_perfect() {
let obs = vec![10.0, 20.0, 30.0, 40.0];
assert!((kge(&obs, &obs) - 1.0).abs() < 1e-10);
}
#[test]
fn test_rmse() {
let obs = vec![1.0, 2.0, 3.0];
let sim = vec![1.0, 2.0, 4.0];
assert!((rmse(&obs, &sim) - (1.0_f64 / 3.0).sqrt()).abs() < 1e-10);
}
#[test]
fn test_pbias() {
let obs = vec![100.0, 200.0];
let sim = vec![110.0, 220.0];
assert!((pbias(&obs, &sim) - 10.0).abs() < 1e-10);
}
#[test]
fn test_r2_perfect() {
let obs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
assert!((r2(&obs, &obs) - 1.0).abs() < 1e-10);
}
#[test]
fn test_dc_grade() {
assert_eq!(dc_grade(0.95), "甲");
assert_eq!(dc_grade(0.85), "乙");
assert_eq!(dc_grade(0.60), "丙");
assert_eq!(dc_grade(0.30), "不合格");
}
#[test]
fn test_compute_metrics() {
let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let m = compute_metrics(&obs, &obs);
assert!((m.nse - 1.0).abs() < 1e-10);
assert_eq!(m.grade, "甲");
assert!(m.gbt.is_none() && m.qualified.is_none(), "compute_metrics 不应填 GB/T");
}
#[test]
fn test_event_errors_perfect() {
let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let e = event_errors(&obs, &obs, 1000.0, 1.0);
assert!(e.peak_rel_err_pct.abs() < 1e-9);
assert_eq!(e.time_to_peak_err_steps, 0);
assert!(e.runoff_depth_rel_err_pct.abs() < 1e-9);
}
#[test]
fn test_event_errors_peak_and_timing() {
let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let sim = vec![10.0, 20.0, 45.0, 30.0, 10.0];
let e = event_errors(&obs, &sim, 1000.0, 1.0);
assert!((e.peak_rel_err_pct - (-10.0)).abs() < 1e-6, "峰偏 -10%, 得 {}", e.peak_rel_err_pct);
assert_eq!(e.time_to_peak_err_steps, -2, "超前 2 步");
}
#[test]
fn test_event_qualified_boundary() {
let tol = QualificationTolerance::default();
let ok = GbtEventErrors { peak_rel_err_pct: 19.0, time_to_peak_err_steps: 1, runoff_depth_rel_err_pct: -15.0 };
assert!(event_qualified(&ok, &tol));
let bad = GbtEventErrors { peak_rel_err_pct: 21.0, time_to_peak_err_steps: 0, runoff_depth_rel_err_pct: 0.0 };
assert!(!event_qualified(&bad, &tol));
}
#[test]
fn test_qualified_rate_and_grade() {
let flags = vec![true; 9].into_iter().chain(std::iter::once(false)).collect::<Vec<_>>();
let r = qualified_rate(&flags);
assert_eq!(r.total, 10);
assert_eq!(r.qualified, 9);
assert!((r.rate_pct - 90.0).abs() < 1e-9);
assert_eq!(r.grade, "甲");
assert_eq!(qualification_grade(75.0), "乙");
assert_eq!(qualification_grade(65.0), "丙");
assert_eq!(qualification_grade(50.0), "不合格");
}
#[test]
fn test_scheme_grade_takes_lower() {
assert_eq!(scheme_grade("甲", "乙"), "乙"); assert_eq!(scheme_grade("丙", "甲"), "丙");
assert_eq!(scheme_grade("甲", "甲"), "甲");
assert_eq!(scheme_grade("乙", "不合格"), "不合格");
}
#[test]
fn test_compute_metrics_gbt_qualified() {
let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let m = compute_metrics_gbt(&obs, &obs, 1000.0, 1.0, &Default::default());
assert_eq!(m.qualified, Some(true));
assert!(m.gbt.is_some());
}
#[test]
fn test_crps_perfect_ensemble_zero() {
let obs = vec![10.0, 20.0, 30.0];
let ens = vec![obs.clone(), obs.clone(), obs.clone()];
assert!(crps(&ens, &obs).abs() < 1e-9);
}
#[test]
fn test_crps_single_member_is_mae_and_spread_lowers() {
let obs = vec![0.0];
assert!((crps(&[vec![10.0]], &obs) - 10.0).abs() < 1e-9);
assert!((crps(&[vec![0.0], vec![10.0]], &obs) - 2.5).abs() < 1e-9);
}
#[test]
fn test_brier_perfect_and_worst() {
let obs = vec![true, false, true];
assert!(brier(&[1.0, 0.0, 1.0], &obs).abs() < 1e-9); assert!((brier(&[0.0, 1.0, 0.0], &obs) - 1.0).abs() < 1e-9); }
#[test]
fn test_pod_far_csi_contingency() {
let fcst = vec![true, true, true, false, true, true, false, false];
let obs = vec![true, true, true, true, false, false, false, false];
let (pod, far, csi) = pod_far_csi(&fcst, &obs);
assert!((pod - 0.75).abs() < 1e-9, "POD=3/4, got {}", pod);
assert!((far - 0.4).abs() < 1e-9, "FAR=2/5, got {}", far);
assert!((csi - 0.5).abs() < 1e-9, "CSI=3/6, got {}", csi);
}
#[test]
fn test_ensemble_exceedance_fraction() {
let ens = vec![vec![10.0, 20.0, 5.0], vec![15.0, 5.0, 5.0], vec![20.0, 25.0, 5.0]];
let prob = ensemble_exceedance(&ens, 12.0);
assert!((prob[0] - 2.0 / 3.0).abs() < 1e-9);
assert!((prob[1] - 2.0 / 3.0).abs() < 1e-9);
assert!(prob[2].abs() < 1e-9);
}
#[test]
fn test_compute_probabilistic_end_to_end() {
let ens = vec![vec![12.0, 28.0], vec![8.0, 32.0]];
let obs = vec![10.0, 30.0];
let r = compute_probabilistic(&ens, &obs, 20.0);
assert!((r.pod - 1.0).abs() < 1e-9);
assert!(r.far.abs() < 1e-9);
assert!((r.csi - 1.0).abs() < 1e-9);
assert!(r.brier.abs() < 1e-9);
}
#[test]
fn test_rank_histogram_bins() {
let ens = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
let obs = vec![2.0, 3.0];
let h = rank_histogram(&ens, &obs);
assert_eq!(h, vec![0, 2, 0], "两步都落 bin1, got {:?}", h);
let h2 = rank_histogram(&vec![vec![10.0], vec![20.0]], &[5.0]);
assert_eq!(h2, vec![1, 0, 0]);
}
}