1use serde::{Serialize, Deserialize};
7
8pub fn nse(obs: &[f64], sim: &[f64]) -> f64 {
11 if obs.is_empty() || obs.len() != sim.len() {
12 return f64::NAN;
13 }
14 let mean_o = obs.iter().sum::<f64>() / obs.len() as f64;
15 let ss_res: f64 = obs.iter().zip(sim).map(|(o, s)| (o - s).powi(2)).sum();
16 let ss_tot: f64 = obs.iter().map(|o| (o - mean_o).powi(2)).sum();
17 if ss_tot < 1e-12 {
18 return f64::NAN;
19 }
20 1.0 - ss_res / ss_tot
21}
22
23pub fn kge(obs: &[f64], sim: &[f64]) -> f64 {
26 if obs.len() < 2 || obs.len() != sim.len() {
27 return f64::NAN;
28 }
29 let n = obs.len() as f64;
30 let mean_o = obs.iter().sum::<f64>() / n;
31 let mean_s = sim.iter().sum::<f64>() / n;
32 let std_o = (obs.iter().map(|o| (o - mean_o).powi(2)).sum::<f64>() / n).sqrt();
33 let std_s = (sim.iter().map(|s| (s - mean_s).powi(2)).sum::<f64>() / n).sqrt();
34 if std_o < 1e-12 || std_s < 1e-12 {
35 return f64::NAN;
36 }
37 let cov: f64 = obs.iter().zip(sim)
39 .map(|(o, s)| (o - mean_o) * (s - mean_s))
40 .sum::<f64>() / n;
41 let r = cov / (std_o * std_s);
42 let alpha = std_s / std_o;
43 let beta = mean_s / mean_o;
44 let ed = (r - 1.0).powi(2) + (alpha - 1.0).powi(2) + (beta - 1.0).powi(2);
45 1.0 - ed.sqrt()
46}
47
48pub fn rmse(obs: &[f64], sim: &[f64]) -> f64 {
50 if obs.is_empty() || obs.len() != sim.len() {
51 return f64::NAN;
52 }
53 let ssq: f64 = obs.iter().zip(sim).map(|(o, s)| (o - s).powi(2)).sum();
54 (ssq / obs.len() as f64).sqrt()
55}
56
57pub fn pbias(obs: &[f64], sim: &[f64]) -> f64 {
59 let sum_o: f64 = obs.iter().sum();
60 if sum_o.abs() < 1e-12 {
61 return f64::NAN;
62 }
63 let sum_s: f64 = sim.iter().sum();
64 100.0 * (sum_s - sum_o) / sum_o
65}
66
67pub fn r2(obs: &[f64], sim: &[f64]) -> f64 {
69 if obs.len() < 2 || obs.len() != sim.len() {
70 return f64::NAN;
71 }
72 let n = obs.len() as f64;
73 let mean_o = obs.iter().sum::<f64>() / n;
74 let mean_s = sim.iter().sum::<f64>() / n;
75 let ss_oo: f64 = obs.iter().map(|o| (o - mean_o).powi(2)).sum();
76 let ss_ss: f64 = sim.iter().map(|s| (s - mean_s).powi(2)).sum();
77 let ss_os: f64 = obs.iter().zip(sim).map(|(o, s)| (o - mean_o) * (s - mean_s)).sum();
78 if ss_oo < 1e-12 || ss_ss < 1e-12 {
79 return f64::NAN;
80 }
81 (ss_os / (ss_oo * ss_ss).sqrt()).powi(2)
82}
83
84pub fn dc_grade(dc: f64) -> &'static str {
87 if dc >= 0.90 { "甲" }
88 else if dc >= 0.70 { "乙" }
89 else if dc >= 0.50 { "丙" }
90 else { "不合格" }
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize, Default)]
97pub struct GbtEventErrors {
98 pub peak_rel_err_pct: f64,
100 pub time_to_peak_err_steps: i64,
102 pub runoff_depth_rel_err_pct: f64,
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct QualificationTolerance {
109 pub peak_rel_pct: f64,
110 pub time_to_peak_steps: i64,
111 pub runoff_depth_rel_pct: f64,
112}
113
114impl Default for QualificationTolerance {
115 fn default() -> Self {
116 Self { peak_rel_pct: 20.0, time_to_peak_steps: 1, runoff_depth_rel_pct: 20.0 }
117 }
118}
119
120pub fn event_errors(obs: &[f64], sim: &[f64], area_km2: f64, dt_h: f64) -> GbtEventErrors {
122 let mut e = GbtEventErrors::default();
123 if obs.is_empty() || sim.is_empty() || area_km2 <= 0.0 || dt_h <= 0.0 {
124 return e;
125 }
126 let (obs_pk, obs_idx) = argmax(obs);
127 let (sim_pk, sim_idx) = argmax(sim);
128 if obs_pk.abs() > 1e-9 {
129 e.peak_rel_err_pct = 100.0 * (sim_pk - obs_pk) / obs_pk;
130 }
131 e.time_to_peak_err_steps = sim_idx as i64 - obs_idx as i64;
132 let d_obs = runoff_depth_mm(obs, area_km2, dt_h);
133 let d_sim = runoff_depth_mm(sim, area_km2, dt_h);
134 if d_obs.abs() > 1e-9 {
135 e.runoff_depth_rel_err_pct = 100.0 * (d_sim - d_obs) / d_obs;
136 }
137 e
138}
139
140pub fn event_qualified(e: &GbtEventErrors, tol: &QualificationTolerance) -> bool {
142 e.peak_rel_err_pct.abs() <= tol.peak_rel_pct
143 && e.time_to_peak_err_steps.abs() <= tol.time_to_peak_steps
144 && e.runoff_depth_rel_err_pct.abs() <= tol.runoff_depth_rel_pct
145}
146
147#[derive(Clone, Debug, Serialize, Deserialize, Default)]
149pub struct QualificationReport {
150 pub total: usize,
151 pub qualified: usize,
152 pub rate_pct: f64,
154 pub grade: String,
156}
157
158pub fn qualified_rate(qualified_flags: &[bool]) -> QualificationReport {
160 let total = qualified_flags.len();
161 let qualified = qualified_flags.iter().filter(|&&q| q).count();
162 let rate_pct = if total == 0 {
163 0.0
164 } else {
165 100.0 * qualified as f64 / total as f64
166 };
167 QualificationReport {
168 total,
169 qualified,
170 rate_pct,
171 grade: qualification_grade(rate_pct).to_string(),
172 }
173}
174
175pub fn qualification_grade(rate_pct: f64) -> &'static str {
177 if rate_pct >= 85.0 { "甲" }
178 else if rate_pct >= 70.0 { "乙" }
179 else if rate_pct >= 60.0 { "丙" }
180 else { "不合格" }
181}
182
183pub fn scheme_grade(dc_grade: &str, qual_grade: &str) -> &'static str {
185 let rank = |g: &str| -> i32 {
186 match g { "甲" => 3, "乙" => 2, "丙" => 1, _ => 0 }
187 };
188 match rank(dc_grade).min(rank(qual_grade)) {
189 3 => "甲",
190 2 => "乙",
191 1 => "丙",
192 _ => "不合格",
193 }
194}
195
196#[derive(Clone, Debug, Serialize, Deserialize, Default)]
198pub struct MetricsReport {
199 pub nse: f64,
200 pub kge: f64,
201 pub rmse: f64,
202 pub pbias: f64,
203 pub r2: f64,
204 pub grade: String,
205 pub gbt: Option<GbtEventErrors>,
207 pub qualified: Option<bool>,
209}
210
211pub fn compute_metrics(obs: &[f64], sim: &[f64]) -> MetricsReport {
213 let n = nse(obs, sim);
214 MetricsReport {
215 nse: n,
216 kge: kge(obs, sim),
217 rmse: rmse(obs, sim),
218 pbias: pbias(obs, sim),
219 r2: r2(obs, sim),
220 grade: dc_grade(n).to_string(),
221 gbt: None,
222 qualified: None,
223 }
224}
225
226pub fn compute_metrics_gbt(
228 obs: &[f64],
229 sim: &[f64],
230 area_km2: f64,
231 dt_h: f64,
232 tol: &QualificationTolerance,
233) -> MetricsReport {
234 let mut m = compute_metrics(obs, sim);
235 let e = event_errors(obs, sim, area_km2, dt_h);
236 m.qualified = Some(event_qualified(&e, tol));
237 m.gbt = Some(e);
238 m
239}
240
241#[derive(Clone, Debug, Serialize, Deserialize, Default)]
245pub struct ProbabilisticReport {
246 pub crps: f64,
248 pub brier: f64,
250 pub pod: f64,
252 pub far: f64,
254 pub csi: f64,
256}
257
258pub fn ensemble_exceedance(ensemble: &[Vec<f64>], threshold: f64) -> Vec<f64> {
260 if ensemble.is_empty() {
261 return Vec::new();
262 }
263 let n_t = ensemble.iter().map(|m| m.len()).max().unwrap_or(0);
264 let m = ensemble.len() as f64;
265 (0..n_t)
266 .map(|t| {
267 let cnt = ensemble.iter()
268 .filter(|mem| mem.get(t).map_or(false, |v| *v > threshold))
269 .count() as f64;
270 cnt / m
271 })
272 .collect()
273}
274
275pub fn crps(ensemble: &[Vec<f64>], observed: &[f64]) -> f64 {
279 if ensemble.is_empty() || observed.is_empty() {
280 return f64::NAN;
281 }
282 let m = ensemble.len();
283 let mf = m as f64;
284 let mut sum = 0.0;
285 let mut count = 0usize;
286 for (t, &y) in observed.iter().enumerate() {
287 let vals: Vec<f64> = ensemble.iter().filter_map(|mem| mem.get(t).copied()).collect();
288 if vals.len() != m {
289 continue; }
291 let mae: f64 = vals.iter().map(|x| (x - y).abs()).sum::<f64>() / mf;
292 let mut pair = 0.0;
293 for i in 0..m {
294 for j in 0..m {
295 pair += (vals[i] - vals[j]).abs();
296 }
297 }
298 let spread = pair / (2.0 * mf * mf);
299 sum += (mae - spread).max(0.0);
300 count += 1;
301 }
302 if count == 0 { f64::NAN } else { sum / count as f64 }
303}
304
305pub fn brier(prob_exceed: &[f64], observed_exceed: &[bool]) -> f64 {
307 if prob_exceed.is_empty() || prob_exceed.len() != observed_exceed.len() {
308 return f64::NAN;
309 }
310 let n = prob_exceed.len() as f64;
311 prob_exceed.iter().zip(observed_exceed.iter())
312 .map(|(p, o)| (p - if *o { 1.0 } else { 0.0 }).powi(2))
313 .sum::<f64>() / n
314}
315
316pub fn pod_far_csi(forecast_exceed: &[bool], observed_exceed: &[bool]) -> (f64, f64, f64) {
319 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()) {
323 match (*f, *o) {
324 (true, true) => hits += 1.0,
325 (false, true) => misses += 1.0,
326 (true, false) => false_alarms += 1.0,
327 (false, false) => {} }
329 }
330 let pod = if hits + misses > 0.0 { hits / (hits + misses) } else { f64::NAN };
331 let far = if hits + false_alarms > 0.0 { false_alarms / (hits + false_alarms) } else { f64::NAN };
332 let csi = if hits + misses + false_alarms > 0.0 {
333 hits / (hits + misses + false_alarms)
334 } else { f64::NAN };
335 (pod, far, csi)
336}
337
338pub fn compute_probabilistic(
341 ensemble: &[Vec<f64>],
342 observed: &[f64],
343 threshold: f64,
344) -> ProbabilisticReport {
345 let prob = ensemble_exceedance(ensemble, threshold);
346 let n = observed.len().min(prob.len());
347 let prob_aligned: Vec<f64> = prob.iter().take(n).copied().collect();
348 let obs_exceed: Vec<bool> = observed[..n].iter().map(|&q| q > threshold).collect();
349 let fcst_exceed: Vec<bool> = prob_aligned.iter().map(|&p| p > 0.5).collect();
350 let (pod, far, csi) = pod_far_csi(&fcst_exceed, &obs_exceed);
351 ProbabilisticReport {
352 crps: crps(ensemble, observed),
353 brier: brier(&prob_aligned, &obs_exceed),
354 pod, far, csi,
355 }
356}
357
358pub fn rank_histogram(ensemble: &[Vec<f64>], observed: &[f64]) -> Vec<usize> {
361 if ensemble.is_empty() || observed.is_empty() {
362 return Vec::new();
363 }
364 let m = ensemble.len();
365 let mut hist = vec![0usize; m + 1];
366 for (t, &y) in observed.iter().enumerate() {
367 let vals: Vec<f64> = ensemble.iter().filter_map(|mem| mem.get(t).copied()).collect();
368 if vals.len() != m {
369 continue;
370 }
371 let rank = vals.iter().filter(|&&x| x < y).count();
373 hist[rank] += 1;
374 }
375 hist
376}
377
378fn argmax(xs: &[f64]) -> (f64, usize) {
381 let mut best = f64::NEG_INFINITY;
382 let mut idx = 0;
383 for (i, &v) in xs.iter().enumerate() {
384 if v > best {
385 best = v;
386 idx = i;
387 }
388 }
389 (best, idx)
390}
391
392fn runoff_depth_mm(q: &[f64], area_km2: f64, dt_h: f64) -> f64 {
394 let sum_q: f64 = q.iter().sum();
395 sum_q * dt_h * 3.6 / area_km2
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_nse_perfect() {
404 let obs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
405 assert!((nse(&obs, &obs) - 1.0).abs() < 1e-10);
406 }
407
408 #[test]
409 fn test_nse_mean_prediction() {
410 let obs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
411 let sim = vec![3.0; 5]; assert!((nse(&obs, &sim) - 0.0).abs() < 1e-10);
413 }
414
415 #[test]
416 fn test_kge_perfect() {
417 let obs = vec![10.0, 20.0, 30.0, 40.0];
418 assert!((kge(&obs, &obs) - 1.0).abs() < 1e-10);
419 }
420
421 #[test]
422 fn test_rmse() {
423 let obs = vec![1.0, 2.0, 3.0];
424 let sim = vec![1.0, 2.0, 4.0];
425 assert!((rmse(&obs, &sim) - (1.0_f64 / 3.0).sqrt()).abs() < 1e-10);
427 }
428
429 #[test]
430 fn test_pbias() {
431 let obs = vec![100.0, 200.0];
432 let sim = vec![110.0, 220.0];
433 assert!((pbias(&obs, &sim) - 10.0).abs() < 1e-10);
435 }
436
437 #[test]
438 fn test_r2_perfect() {
439 let obs = vec![1.0, 2.0, 3.0, 4.0, 5.0];
440 assert!((r2(&obs, &obs) - 1.0).abs() < 1e-10);
441 }
442
443 #[test]
444 fn test_dc_grade() {
445 assert_eq!(dc_grade(0.95), "甲");
446 assert_eq!(dc_grade(0.85), "乙");
447 assert_eq!(dc_grade(0.60), "丙");
448 assert_eq!(dc_grade(0.30), "不合格");
449 }
450
451 #[test]
452 fn test_compute_metrics() {
453 let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
454 let m = compute_metrics(&obs, &obs);
455 assert!((m.nse - 1.0).abs() < 1e-10);
456 assert_eq!(m.grade, "甲");
457 assert!(m.gbt.is_none() && m.qualified.is_none(), "compute_metrics 不应填 GB/T");
458 }
459
460 #[test]
461 fn test_event_errors_perfect() {
462 let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
464 let e = event_errors(&obs, &obs, 1000.0, 1.0);
465 assert!(e.peak_rel_err_pct.abs() < 1e-9);
466 assert_eq!(e.time_to_peak_err_steps, 0);
467 assert!(e.runoff_depth_rel_err_pct.abs() < 1e-9);
468 }
469
470 #[test]
471 fn test_event_errors_peak_and_timing() {
472 let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
474 let sim = vec![10.0, 20.0, 45.0, 30.0, 10.0];
475 let e = event_errors(&obs, &sim, 1000.0, 1.0);
476 assert!((e.peak_rel_err_pct - (-10.0)).abs() < 1e-6, "峰偏 -10%, 得 {}", e.peak_rel_err_pct);
477 assert_eq!(e.time_to_peak_err_steps, -2, "超前 2 步");
478 }
479
480 #[test]
481 fn test_event_qualified_boundary() {
482 let tol = QualificationTolerance::default();
483 let ok = GbtEventErrors { peak_rel_err_pct: 19.0, time_to_peak_err_steps: 1, runoff_depth_rel_err_pct: -15.0 };
485 assert!(event_qualified(&ok, &tol));
486 let bad = GbtEventErrors { peak_rel_err_pct: 21.0, time_to_peak_err_steps: 0, runoff_depth_rel_err_pct: 0.0 };
488 assert!(!event_qualified(&bad, &tol));
489 }
490
491 #[test]
492 fn test_qualified_rate_and_grade() {
493 let flags = vec![true; 9].into_iter().chain(std::iter::once(false)).collect::<Vec<_>>();
495 let r = qualified_rate(&flags);
496 assert_eq!(r.total, 10);
497 assert_eq!(r.qualified, 9);
498 assert!((r.rate_pct - 90.0).abs() < 1e-9);
499 assert_eq!(r.grade, "甲");
500 assert_eq!(qualification_grade(75.0), "乙");
501 assert_eq!(qualification_grade(65.0), "丙");
502 assert_eq!(qualification_grade(50.0), "不合格");
503 }
504
505 #[test]
506 fn test_scheme_grade_takes_lower() {
507 assert_eq!(scheme_grade("甲", "乙"), "乙"); assert_eq!(scheme_grade("丙", "甲"), "丙");
509 assert_eq!(scheme_grade("甲", "甲"), "甲");
510 assert_eq!(scheme_grade("乙", "不合格"), "不合格");
511 }
512
513 #[test]
514 fn test_compute_metrics_gbt_qualified() {
515 let obs = vec![10.0, 20.0, 30.0, 40.0, 50.0];
516 let m = compute_metrics_gbt(&obs, &obs, 1000.0, 1.0, &Default::default());
518 assert_eq!(m.qualified, Some(true));
519 assert!(m.gbt.is_some());
520 }
521
522 #[test]
523 fn test_crps_perfect_ensemble_zero() {
524 let obs = vec![10.0, 20.0, 30.0];
526 let ens = vec![obs.clone(), obs.clone(), obs.clone()];
527 assert!(crps(&ens, &obs).abs() < 1e-9);
528 }
529
530 #[test]
531 fn test_crps_single_member_is_mae_and_spread_lowers() {
532 let obs = vec![0.0];
533 assert!((crps(&[vec![10.0]], &obs) - 10.0).abs() < 1e-9);
535 assert!((crps(&[vec![0.0], vec![10.0]], &obs) - 2.5).abs() < 1e-9);
537 }
538
539 #[test]
540 fn test_brier_perfect_and_worst() {
541 let obs = vec![true, false, true];
542 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); }
545
546 #[test]
547 fn test_pod_far_csi_contingency() {
548 let fcst = vec![true, true, true, false, true, true, false, false];
550 let obs = vec![true, true, true, true, false, false, false, false];
551 let (pod, far, csi) = pod_far_csi(&fcst, &obs);
552 assert!((pod - 0.75).abs() < 1e-9, "POD=3/4, got {}", pod);
553 assert!((far - 0.4).abs() < 1e-9, "FAR=2/5, got {}", far);
554 assert!((csi - 0.5).abs() < 1e-9, "CSI=3/6, got {}", csi);
555 }
556
557 #[test]
558 fn test_ensemble_exceedance_fraction() {
559 let ens = vec![vec![10.0, 20.0, 5.0], vec![15.0, 5.0, 5.0], vec![20.0, 25.0, 5.0]];
560 let prob = ensemble_exceedance(&ens, 12.0);
561 assert!((prob[0] - 2.0 / 3.0).abs() < 1e-9);
563 assert!((prob[1] - 2.0 / 3.0).abs() < 1e-9);
564 assert!(prob[2].abs() < 1e-9);
565 }
566
567 #[test]
568 fn test_compute_probabilistic_end_to_end() {
569 let ens = vec![vec![12.0, 28.0], vec![8.0, 32.0]];
571 let obs = vec![10.0, 30.0];
572 let r = compute_probabilistic(&ens, &obs, 20.0);
573 assert!((r.pod - 1.0).abs() < 1e-9);
576 assert!(r.far.abs() < 1e-9);
577 assert!((r.csi - 1.0).abs() < 1e-9);
578 assert!(r.brier.abs() < 1e-9);
580 }
581
582 #[test]
583 fn test_rank_histogram_bins() {
584 let ens = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
586 let obs = vec![2.0, 3.0];
587 let h = rank_histogram(&ens, &obs);
588 assert_eq!(h, vec![0, 2, 0], "两步都落 bin1, got {:?}", h);
589 let h2 = rank_histogram(&vec![vec![10.0], vec![20.0]], &[5.0]);
591 assert_eq!(h2, vec![1, 0, 0]);
592 }
593}