Skip to main content

hydro_core/
metrics.rs

1//! Evaluation metrics for hydrological model comparison.
2//!
3//! Includes Nash-Sutcliffe Efficiency (= DC 确定性系数), Kling-Gupta Efficiency,
4//! RMSE, Percent Bias, R², and GB/T 22482-2008 accuracy grading.
5
6use serde::{Serialize, Deserialize};
7
8/// Nash-Sutcliffe Efficiency (= 确定性系数 DC in Chinese standard).
9/// 1.0 = perfect; 0.0 = as good as the observed mean; <0 = worse than mean.
10pub 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
23/// Kling-Gupta Efficiency (Gupta et al. 2009).
24/// Decomposes into correlation (r), variability (α), and bias (β).
25pub 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    // Pearson correlation
38    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
48/// Root Mean Square Error.
49pub 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
57/// Percent Bias (positive = overprediction, negative = underprediction).
58pub 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
67/// Coefficient of Determination (R²).
68pub 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
84/// GB/T 22482-2008 accuracy grade from DC (= NSE).
85/// 甲 (excellent) > 0.90; 乙 (good) 0.70–0.90; 丙 (acceptable) 0.50–0.70.
86pub 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// ── GB/T 22482-2008 事件误差 + 合格率 ────────────────────────────────
94
95/// 单场洪水的 GB/T 22482 事件误差(相对误差 %,峰现误差以时段步数计)。
96#[derive(Clone, Debug, Serialize, Deserialize, Default)]
97pub struct GbtEventErrors {
98    /// 洪峰相对误差 % = 100·(Qpk_sim − Qpk_obs)/Qpk_obs
99    pub peak_rel_err_pct: f64,
100    /// 峰现时间误差(时段步数)= argmax(sim) − argmax(obs);正=滞后,负=超前
101    pub time_to_peak_err_steps: i64,
102    /// 径流深相对误差 % = 100·(D_sim − D_obs)/D_obs;D 由水量/面积折算(mm)
103    pub runoff_depth_rel_err_pct: f64,
104}
105
106/// GB/T 22482 许可误差(默认:洪峰 ±20%、峰现 ±1 时段、径流深 ±20%)。
107#[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
120/// 计算单场洪水的 GB/T 22482 事件误差。`area_km2` 用于水量→径流深折算。
121pub 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
140/// 单场洪水是否合格(三项误差均在许可范围内)。
141pub 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/// 合格率汇总报告(多场洪水)。
148#[derive(Clone, Debug, Serialize, Deserialize, Default)]
149pub struct QualificationReport {
150    pub total: usize,
151    pub qualified: usize,
152    /// 合格率 % = 100·qualified/total
153    pub rate_pct: f64,
154    /// 甲/乙/丙/不合格(甲≥85、乙 70–85、丙 60–70)
155    pub grade: String,
156}
157
158/// 由各场洪水的合格标志聚合合格率。
159pub 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
175/// GB/T 22482 合格率评级:甲≥85%、乙 70–85%、丙 60–70%、否则不合格。
176pub 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
183/// 方案等级 = DC 等级 与 合格率等级 取低(丙级为洪水预报业务最低要求)。
184pub 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/// Full metrics report for one model's simulation vs observation.
197#[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    /// GB/T 22482 事件误差(需提供 area/dt;否则 None)。
206    pub gbt: Option<GbtEventErrors>,
207    /// 单场洪水是否合格(GB/T 22482 许可误差内)。
208    pub qualified: Option<bool>,
209}
210
211/// Compute all metrics at once(不含 GB/T 事件误差;gbt/qualified = None)。
212pub 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
226/// 全量指标 + GB/T 22482 事件误差 + 合格判定。
227pub 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// ── 概率/集合指标(CRPS / POD-FAR-CSI / Brier)────────────────────────
242
243/// 集合概率预报验证报告。
244#[derive(Clone, Debug, Serialize, Deserialize, Default)]
245pub struct ProbabilisticReport {
246    /// 连续排名概率分数(fair CRPS,越低越好;完美集合=0)。
247    pub crps: f64,
248    /// Brier 分数(阈值超限概率的均方误差,0=完美,1=最差)。
249    pub brier: f64,
250    /// 命中率 POD = hits/(hits+misses)。
251    pub pod: f64,
252    /// 误警率 FAR = false_alarms/(hits+false_alarms)。
253    pub far: f64,
254    /// 临界成功指数 CSI = hits/(hits+misses+false_alarms)。
255    pub csi: f64,
256}
257
258/// 逐时步集合超限概率(成员 > 阈值 的比例)。成员长度不齐时按各自是否可达该时步计。
259pub 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
275/// fair CRPS(Gneiting & Raftery 2007;Ferro 2017):逐时步平均。
276/// 每步 `CRPS_t = (1/M)Σ|x_m−y| − (1/(2M²))ΣΣ|x_m−x_n|`;完美集合(成员全=y)→ 0。
277/// 单成员集合退化为 MAE。成员该时步缺失则跳过该步。
278pub 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; // 该时步成员不全,跳过
290        }
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
305/// Brier 分数:超限概率 vs 超限指示的均方误差。0=完美,1=最差。
306pub 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
316/// 阈值超限列联表 → (POD, FAR, CSI)。
317/// POD = a/(a+b)、FAR = c/(a+c)、CSI = a/(a+b+c);对应分母为 0 时该量返回 NAN。
318pub fn pod_far_csi(forecast_exceed: &[bool], observed_exceed: &[bool]) -> (f64, f64, f64) {
319    let mut hits = 0.0;         // a: 预报超 ∧ 观测超
320    let mut misses = 0.0;       // b: 预报未超 ∧ 观测超
321    let mut false_alarms = 0.0; // c: 预报超 ∧ 观测未超
322    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) => {} // 正确拒绝
328        }
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
338/// 集合概率预报全量验证。forecast 超限 = 集合超限概率 > 0.5;observed 超限 = obs > 阈值。
339/// 各时步按 observed 与集合的最短长度对齐。
340pub 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
358/// 集合秩直方图(Talagrand):逐时步把观测在 M 个成员中的秩(0..=M)累计成 M+1 个桶。
359/// 桶越均匀 → 集合越可靠(可信);U 形 → 欠散布(过自信);钟形 → 过散布。
360pub 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        // rank = 严格小于观测的成员数(观测落在该秩区间)
372        let rank = vals.iter().filter(|&&x| x < y).count();
373        hist[rank] += 1;
374    }
375    hist
376}
377
378// ── helpers ──
379
380fn 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
392/// 径流深(mm)= ΣQ·dt_h·3.6 / area_km2(Q m³/s, dt_h 小时, area km²)。
393fn 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]; // mean
412        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        // RMSE = sqrt((0+0+1)/3) = sqrt(1/3) ≈ 0.577
426        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        // PBIAS = 100*(330-300)/300 = 10%
434        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        // obs==sim → 三项误差全 0;径流深 = ΣQ·1·3.6/1000 = 150·3.6/1000 = 0.54 mm
463        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        // obs 峰在第 4 步=50;sim 峰在第 2 步=45(超前 2 步,峰值偏小 10%)
473        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        // 三项都在限内 → 合格
484        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        // 峰超限 → 不合格
487        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        // 9/10 合格 = 90% → 甲
494        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("甲", "乙"), "乙"); // 取低
508        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        // sim 完全一致 → 合格
517        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        // 成员全 = obs → CRPS = 0
525        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        // 单成员 [10] → CRPS = MAE = 10(spread 项=0)
534        assert!((crps(&[vec![10.0]], &obs) - 10.0).abs() < 1e-9);
535        // 双成员 [0,10]:mae=(0+10)/2=5;spread=(1/8)·20=2.5 → CRPS=2.5
536        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); // 完美
543        assert!((brier(&[0.0, 1.0, 0.0], &obs) - 1.0).abs() < 1e-9); // 最差
544    }
545
546    #[test]
547    fn test_pod_far_csi_contingency() {
548        // 3 命中 + 1 漏报 + 2 误报(+ 2 正确拒绝)
549        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        // t0:{10,15,20}→2/3; t1:{20,5,25}→2/3; t2:{5,5,5}→0
562        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        // 2 成员,obs=[10,30];阈值=20。成员1=[12,28],成员2=[8,32]
570        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        // 超限概率:t0={12,8}→0(都≤20); t1={28,32}→1(都>20)
574        // fcst 超限(>0.5):[false,true]; obs 超限(>20):[false,true] → 完美:POD=1,FAR=0,CSI=1
575        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        // Brier:prob=[0,1],obs指示=[0,1] → 0
579        assert!(r.brier.abs() < 1e-9);
580    }
581
582    #[test]
583    fn test_rank_histogram_bins() {
584        // 2 成员,t0:成员[1,3] obs=2 → rank=1(只有1<2);t1:成员[2,4] obs=3 → rank=1(只有2<3)
585        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        // 观测恒低于所有成员 → 全落 bin0
590        let h2 = rank_histogram(&vec![vec![10.0], vec![20.0]], &[5.0]);
591        assert_eq!(h2, vec![1, 0, 0]);
592    }
593}