#![allow(dead_code)]
use std::path::Path;
use gridoxide::cgmes::CimDataset;
use gridoxide::types::Bus;
pub struct ExpectedVoltage {
pub tn_mrid: String,
pub v: f64,
pub angle_deg: f64,
}
pub fn expected_voltages(ds: &CimDataset) -> Vec<ExpectedVoltage> {
let mut out = Vec::new();
let Some(mrids) = ds.by_type.get("SvVoltage") else {
return out;
};
for mrid in mrids {
let sv = &ds.entries[mrid];
let (Some(tn), Some(v), Some(angle_deg)) = (
sv.get_ref("SvVoltage.TopologicalNode"),
sv.get_f64("SvVoltage.v"),
sv.get_f64("SvVoltage.angle"),
) else {
continue;
};
out.push(ExpectedVoltage {
tn_mrid: tn.to_string(),
v,
angle_deg,
});
}
out
}
pub fn assert_matches_sv(
solved: &[Bus],
bus_index: &std::collections::HashMap<String, usize>,
expected: &[ExpectedVoltage],
tol: f64,
) {
for exp in expected {
let Some(&idx) = bus_index.get(&exp.tn_mrid) else {
continue;
};
let bus = &solved[idx];
let expected_v_pu = (exp.v * 1e3) / bus.u_rated;
assert!(
(bus.voltage_mag - expected_v_pu).abs() < tol,
"TopologicalNode {}: voltage_mag {} vs expected {} (tol {tol})",
exp.tn_mrid,
bus.voltage_mag,
expected_v_pu,
);
let expected_ang_rad = exp.angle_deg.to_radians();
let diff = (bus.voltage_ang - expected_ang_rad).rem_euclid(2.0 * std::f64::consts::PI);
let diff = if diff > std::f64::consts::PI {
diff - 2.0 * std::f64::consts::PI
} else {
diff
};
assert!(
diff.abs() < tol,
"TopologicalNode {}: voltage_ang {} vs expected {} (tol {tol})",
exp.tn_mrid,
bus.voltage_ang,
expected_ang_rad,
);
}
}
pub fn assert_matches_sv_percentile(
solved: &[Bus],
bus_index: &std::collections::HashMap<String, usize>,
expected: &[ExpectedVoltage],
median_tol: f64,
p90_tol: f64,
p99_tol: f64,
) {
let mut errs: Vec<f64> = Vec::new();
for exp in expected {
let Some(&idx) = bus_index.get(&exp.tn_mrid) else {
continue;
};
let bus = &solved[idx];
let expected_v_pu = (exp.v * 1e3) / bus.u_rated;
errs.push((bus.voltage_mag - expected_v_pu).abs());
}
errs.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = errs.len();
let median = errs[n / 2];
let p90 = errs[n * 9 / 10];
let p99 = errs[n * 99 / 100];
eprintln!(
"voltage_mag abs error (pu): n={n} median={median:.5} p90={p90:.5} p99={p99:.5} max={:.5}",
errs[n - 1]
);
assert!(
median < median_tol,
"median voltage_mag error {median} too high (expected < {median_tol})"
);
assert!(
p90 < p90_tol,
"p90 voltage_mag error {p90} too high (expected < {p90_tol})"
);
assert!(
p99 < p99_tol,
"p99 voltage_mag error {p99} too high (expected < {p99_tol})"
);
}
pub fn fixture_dir() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join(
"tests/data/CGMES-Test-Configurations/v3.0/MicroGrid/MicroGid-BaseCase/MicroGrid-BE-MAS",
)
}
pub struct ExpectedInjection {
pub equipment_mrid: String,
pub p: f64,
pub q: f64,
}
pub fn expected_injections(ds: &CimDataset, s_base_mva: f64) -> Vec<ExpectedInjection> {
let mut equipment_of: std::collections::HashMap<&str, &str> = Default::default();
if let Some(mrids) = ds.by_type.get("Terminal") {
for mrid in mrids {
if let Some(eq) = ds.entries[mrid].get_ref("Terminal.ConductingEquipment") {
equipment_of.insert(mrid.as_str(), eq);
}
}
}
let mut out = Vec::new();
let Some(mrids) = ds.by_type.get("SvPowerFlow") else {
return out;
};
for mrid in mrids {
let sv = &ds.entries[mrid];
let (Some(term), Some(p), Some(q)) = (
sv.get_ref("SvPowerFlow.Terminal"),
sv.get_f64("SvPowerFlow.p"),
sv.get_f64("SvPowerFlow.q"),
) else {
continue;
};
let Some(eq) = equipment_of.get(term) else {
continue;
};
out.push(ExpectedInjection {
equipment_mrid: (*eq).to_string(),
p: -p / s_base_mva,
q: -q / s_base_mva,
});
}
out
}