use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BandRequirement {
pub freq_range: (f64, f64),
pub max_prd: f64,
pub min_r: f64,
}
impl BandRequirement {
pub fn new(lo: f64, hi: f64, max_prd: f64, min_r: f64) -> Self {
Self {
freq_range: (lo, hi),
max_prd,
min_r,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EcsLevel {
pub name: String,
pub level: char,
pub max_prd: f64,
pub min_r: f64,
pub max_snr_loss: f64,
pub min_cr: f64,
pub band_fidelity: BTreeMap<String, BandRequirement>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ComplianceResult {
pub grade: char,
pub violations: Vec<String>,
}
impl ComplianceResult {
pub fn passed(&self) -> bool {
self.grade != '\0'
}
pub fn grade_str(&self) -> String {
if self.grade == '\0' {
String::new()
} else {
self.grade.to_string()
}
}
}
pub fn levels() -> Vec<EcsLevel> {
let mut out = Vec::with_capacity(4);
out.push(EcsLevel {
name: "Lossless".to_string(),
level: 'L',
max_prd: 0.0,
min_r: 1.0,
max_snr_loss: 0.0,
min_cr: 0.8,
band_fidelity: BTreeMap::new(),
});
out.push(EcsLevel {
name: "Near-Lossless".to_string(),
level: 'N',
max_prd: 5.0,
min_r: 0.99,
max_snr_loss: 2.0,
min_cr: 1.0,
band_fidelity: BTreeMap::new(),
});
{
let mut bands = BTreeMap::new();
bands.insert("delta".to_string(), BandRequirement::new(0.5, 4.0, 5.0, 0.98));
bands.insert("theta".to_string(), BandRequirement::new(4.0, 8.0, 7.0, 0.97));
bands.insert("alpha".to_string(), BandRequirement::new(8.0, 13.0, 8.0, 0.96));
bands.insert("beta".to_string(), BandRequirement::new(13.0, 30.0, 12.0, 0.93));
bands.insert("gamma".to_string(), BandRequirement::new(30.0, 50.0, 20.0, 0.85));
out.push(EcsLevel {
name: "Clinical".to_string(),
level: 'C',
max_prd: 9.0,
min_r: 0.95,
max_snr_loss: 3.0,
min_cr: 20.0,
band_fidelity: bands,
});
}
{
let mut bands = BTreeMap::new();
bands.insert("delta".to_string(), BandRequirement::new(0.5, 4.0, 10.0, 0.95));
bands.insert("theta".to_string(), BandRequirement::new(4.0, 8.0, 12.0, 0.93));
bands.insert("alpha".to_string(), BandRequirement::new(8.0, 13.0, 15.0, 0.90));
bands.insert("beta".to_string(), BandRequirement::new(13.0, 30.0, 25.0, 0.80));
bands.insert("gamma".to_string(), BandRequirement::new(30.0, 50.0, 40.0, 0.60));
out.push(EcsLevel {
name: "Monitoring".to_string(),
level: 'M',
max_prd: 20.0,
min_r: 0.85,
max_snr_loss: 6.0,
min_cr: 100.0,
band_fidelity: bands,
});
}
{
let mut bands = BTreeMap::new();
bands.insert("delta".to_string(), BandRequirement::new(0.5, 4.0, 20.0, 0.85));
bands.insert("theta".to_string(), BandRequirement::new(4.0, 8.0, 25.0, 0.80));
bands.insert("alpha".to_string(), BandRequirement::new(8.0, 13.0, 30.0, 0.75));
bands.insert("beta".to_string(), BandRequirement::new(13.0, 30.0, 40.0, 0.65));
bands.insert("gamma".to_string(), BandRequirement::new(30.0, 50.0, 60.0, 0.40));
out.push(EcsLevel {
name: "Alerting".to_string(),
level: 'A',
max_prd: 40.0,
min_r: 0.70,
max_snr_loss: 10.0,
min_cr: 200.0,
band_fidelity: bands,
});
}
out
}
pub fn level_by_char(c: char) -> Option<EcsLevel> {
levels().into_iter().find(|l| l.level == c)
}
fn check_lossy(
level: &EcsLevel,
r: f64,
prd: f64,
cr: f64,
per_band: &[(String, f64, f64)],
) -> Vec<String> {
let mut v = Vec::new();
if r < level.min_r {
v.push(format!("global R {r:.4} < {:.4}", level.min_r));
}
if prd > level.max_prd {
v.push(format!("global PRD {prd:.2}% > {:.2}%", level.max_prd));
}
if cr < level.min_cr {
v.push(format!("CR {cr:.1} < {:.1}", level.min_cr));
}
for (band_name, req) in &level.band_fidelity {
if let Some((_, br, bp)) = per_band.iter().find(|(n, _, _)| n == band_name) {
if *br < req.min_r {
v.push(format!("{band_name} R {br:.4} < {:.4}", req.min_r));
}
if *bp > req.max_prd {
v.push(format!("{band_name} PRD {bp:.2}% > {:.2}%", req.max_prd));
}
}
}
v
}
pub fn grade(
r: f64,
prd: f64,
cr: f64,
_snr_loss: f64,
per_band: &[(String, f64, f64)],
) -> ComplianceResult {
let tiers = levels();
if prd == 0.0 && cr >= 0.8 {
return ComplianceResult {
grade: 'L',
violations: Vec::new(),
};
}
let lossy_order = ['N', 'C', 'M', 'A'];
let mut prev_violations: Vec<String> = Vec::new();
for code in lossy_order {
let level = tiers
.iter()
.find(|l| l.level == code)
.expect("lossy tier present in table");
let violations = check_lossy(level, r, prd, cr, per_band);
if violations.is_empty() {
return ComplianceResult {
grade: code,
violations: prev_violations,
};
}
prev_violations = violations;
}
ComplianceResult {
grade: '\0',
violations: prev_violations,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn good_clinical_bands() -> Vec<(String, f64, f64)> {
vec![
("delta".to_string(), 0.99, 3.0),
("theta".to_string(), 0.98, 4.0),
("alpha".to_string(), 0.97, 5.0),
("beta".to_string(), 0.95, 8.0),
("gamma".to_string(), 0.90, 12.0),
]
}
#[test]
fn table_has_five_tiers_in_order() {
let t = levels();
assert_eq!(t.len(), 5);
assert_eq!(t[0].level, 'L');
assert_eq!(t[1].level, 'N');
assert_eq!(t[2].level, 'C');
assert_eq!(t[3].level, 'M');
assert_eq!(t[4].level, 'A');
assert_eq!(t[0].min_cr, 0.8);
assert!(t[0].band_fidelity.is_empty());
assert_eq!(t[1].max_prd, 5.0);
assert_eq!(t[1].min_r, 0.99);
assert_eq!(t[1].min_cr, 1.0);
assert!(t[1].band_fidelity.is_empty());
}
#[test]
fn near_lossless_tier() {
let res = grade(0.995, 3.0, 2.0, 0.0, &[]);
assert_eq!(res.grade, 'N');
assert!(res.violations.is_empty());
let res = grade(0.96, 7.0, 25.0, 0.0, &good_clinical_bands());
assert_eq!(res.grade, 'C');
assert!(res.violations.iter().any(|s| s.contains("PRD")));
let res = grade(0.9999, 0.1, 0.9, 0.0, &[]);
assert_ne!(res.grade, 'N');
}
#[test]
fn lossless_short_circuit() {
let res = grade(1.0, 0.0, 0.8, 0.0, &[]);
assert_eq!(res.grade, 'L');
assert!(res.violations.is_empty());
assert!(res.passed());
let res2 = grade(0.0, 0.0, 5.0, 0.0, &[]);
assert_eq!(res2.grade, 'L');
}
#[test]
fn lossless_blocked_by_cr_floor() {
let res = grade(0.0, 0.0, 0.5, 0.0, &[]);
assert_ne!(res.grade, 'L');
}
#[test]
fn clinical_pass() {
let res = grade(0.96, 5.0, 25.0, 0.0, &good_clinical_bands());
assert_eq!(res.grade, 'C');
assert!(res.violations.iter().any(|s| s.contains('R')));
}
#[test]
fn clinical_pass_global_only() {
let res = grade(0.96, 5.0, 25.0, 0.0, &[]);
assert_eq!(res.grade, 'C');
}
#[test]
fn below_floor() {
let res = grade(0.5, 50.0, 1.0, 0.0, &[]);
assert_eq!(res.grade, '\0');
assert!(!res.passed());
assert_eq!(res.grade_str(), "");
assert!(!res.violations.is_empty());
}
#[test]
fn descends_to_monitoring_with_clinical_todo() {
let res = grade(0.90, 15.0, 120.0, 0.0, &[]);
assert_eq!(res.grade, 'M');
assert!(res.violations.iter().any(|s| s.contains("PRD")));
assert!(res.violations.iter().any(|s| s.contains('R')));
}
#[test]
fn band_failure_drops_tier() {
let mut bands = good_clinical_bands();
bands[4] = ("gamma".to_string(), 0.70, 12.0);
let res = grade(0.96, 5.0, 120.0, 0.0, &bands);
assert_eq!(res.grade, 'M');
assert!(res.violations.iter().any(|s| s.contains("gamma")));
}
}