use std::path::PathBuf;
use gridoxide::opf::ipm::IpmSolver;
use gridoxide::rao::crac::*;
use gridoxide::rao::evaluate::AcOptions;
use gridoxide::rao::validate::{ValidationOptions, Verdict, validate};
use gridoxide::rao::{Network, Resolution, SearchOptions, crac_json, run};
use gridoxide::ucte;
mod openrao;
fn ucte_fixture(name: &str) -> PathBuf {
openrao::fixture(name)
}
fn rao_fixture(name: &str) -> PathBuf {
openrao::fixture(name)
}
struct Case {
net: ucte::UcteImport,
crac: Crac,
}
fn case(crac_name: &str) -> Case {
let net = ucte::read(ucte_fixture("TestCase12Nodes.uct")).expect("network");
let (crac, _) = crac_json::read(rao_fixture(crac_name)).expect("crac");
Case { net, crac }
}
impl Case {
fn resolution(&self) -> Resolution {
Resolution::new(&self.crac, &self.net.branch_ids)
}
fn network(&self) -> Network<'_> {
Network {
generation: &self.net.generation,
buses: &self.net.buses,
lines: &self.net.lines,
transformers: &self.net.transformers,
branch_ids: &self.net.branch_ids,
bus_ids: &[],
initially_open: &[],
bus_countries: &[],
shunts: &[],
tap_changers: &[],
base_mva: self.net.base_mva,
}
}
fn plan(&self) -> gridoxide::rao::Plan {
let mut solver = IpmSolver::new();
run(
&self.crac,
&self.network(),
&self.resolution(),
&mut solver,
&SearchOptions::default(),
)
}
fn options(&self) -> ValidationOptions<'_> {
ValidationOptions {
ac: AcOptions {
shunts: &self.net.shunts,
..Default::default()
},
..Default::default()
}
}
}
#[test]
fn every_perimeter_of_the_plan_gets_a_verdict() {
let c = case("crac-for-12nodes.json");
let plan = c.plan();
let report = validate(&c.crac, &c.network(), &c.resolution(), &plan, &c.options());
assert_eq!(
report.perimeters.len(),
plan.perimeters().count(),
"a plan's perimeters and its verdicts have to correspond one to one"
);
for (v, p) in report.perimeters.iter().zip(plan.perimeters()) {
assert_eq!(v.states, p.states);
assert_eq!(
v.dc_margin_mw, p.final_margin_mw,
"the DC figure is the search's own"
);
}
}
#[test]
fn the_two_models_disagree_and_the_disagreement_is_reported() {
let c = case("crac-for-12nodes.json");
let plan = c.plan();
let report = validate(&c.crac, &c.network(), &c.resolution(), &plan, &c.options());
let biggest = report
.perimeters
.iter()
.map(|p| p.regression_mw().abs())
.fold(0.0f64, f64::max);
assert!(
biggest > 1e-6,
"AC reproduced the DC margins exactly, which means it did not run"
);
assert!(
report.ac_margin_mw.is_finite(),
"the plan should have measurable AC margins"
);
}
#[test]
fn a_threshold_no_plan_can_meet_rejects_it() {
let c = case("crac-for-12nodes.json");
let plan = c.plan();
let options = ValidationOptions {
min_margin_mw: 1e6,
..c.options()
};
let report = validate(&c.crac, &c.network(), &c.resolution(), &plan, &options);
assert!(!report.is_accepted());
assert!(report.rejected().count() > 0);
for p in report.rejected() {
assert!(
matches!(p.verdict, Verdict::Insecure { .. } | Verdict::Diverged),
"{:?}",
p.verdict
);
}
}
#[test]
fn a_regression_bound_catches_a_model_disagreement_that_is_still_secure() {
let c = case("crac-for-12nodes.json");
let plan = c.plan();
let lax = validate(&c.crac, &c.network(), &c.resolution(), &plan, &c.options());
let worst = lax
.perimeters
.iter()
.filter(|p| p.verdict != Verdict::Diverged)
.map(|p| p.regression_mw())
.fold(f64::NEG_INFINITY, f64::max);
if worst <= 0.0 {
return;
}
let options = ValidationOptions {
min_margin_mw: f64::NEG_INFINITY,
max_regression_mw: worst / 2.0,
..c.options()
};
let strict = validate(&c.crac, &c.network(), &c.resolution(), &plan, &options);
assert!(
strict
.rejected()
.any(|p| matches!(p.verdict, Verdict::Regressed { .. })),
"a bound below the observed regression should reject: {:?}",
strict
.perimeters
.iter()
.map(|p| (&p.verdict, p.regression_mw()))
.collect::<Vec<_>>()
);
}
#[test]
fn a_secure_plan_is_accepted_under_the_default_thresholds() {
let c = case("crac-for-12nodes.json");
let plan = c.plan();
let report = validate(&c.crac, &c.network(), &c.resolution(), &plan, &c.options());
for (v, p) in report.perimeters.iter().zip(plan.perimeters()) {
if p.final_margin_mw >= 0.0 && v.ac_margin_mw >= 0.0 {
assert!(
v.verdict.is_accepted(),
"{:?} for {:?}",
v.verdict,
v.states
);
}
}
}