use std::path::{Path, PathBuf};
use powerio::{
BalancedNetwork, BranchCharging, BusId, BusType, Error, SourceFormat, TargetFormat,
convert_file, parse_file, parse_str, write_as,
};
use serde_json::Value;
fn fixture() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/opfdataset/example_0.json")
}
fn fixture_text() -> String {
std::fs::read_to_string(fixture()).unwrap()
}
fn assert_close(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() <= 1.0e-10 * actual.abs().max(expected.abs()).max(1.0),
"{actual} != {expected}"
);
}
#[test]
fn parses_official_schema_complete_solved_snapshot() {
let parsed = parse_file(fixture(), None).unwrap();
let net = &parsed.network;
assert_eq!(net.source_format, SourceFormat::DeepMindOpfDataJson);
assert_eq!(net.name, "example_0");
assert_close(net.base_mva, 100.0);
assert_eq!(net.buses.len(), 14);
assert_eq!(net.generators.len(), 5);
assert_eq!(net.loads.len(), 11);
assert_eq!(net.shunts.len(), 1);
assert_eq!(net.branches.len(), 20);
assert_eq!(net.buses[0].id, BusId(1));
assert_eq!(net.buses[0].kind, BusType::Ref);
assert_eq!(net.buses[1].kind, BusType::Pv);
assert_close(net.buses[0].vm, 1.060_000_010_369_160_5);
assert_close(net.buses[0].va, 0.0);
assert_close(net.buses[0].vmin, 0.94);
assert_close(net.buses[0].vmax, 1.06);
let generator = &net.generators[0];
assert_eq!(generator.bus, BusId(1));
assert_close(generator.pg, 286.070_948_069_333_44);
assert_close(generator.qg, 3.883_803_174_297_159);
assert_close(generator.pmin, 0.0);
assert_close(generator.pmax, 340.0);
assert_close(generator.vg, net.buses[0].vm);
assert_close(generator.mbase, 100.0);
let cost = generator.cost.as_ref().unwrap();
assert_eq!(cost.model, 2);
assert_eq!(cost.ncost, 3);
assert_close(cost.coeffs[0], 0.0);
assert_close(cost.coeffs[1], 7.920_951);
assert_close(cost.coeffs[2], 0.0);
assert_eq!(net.loads[0].bus, BusId(2));
assert_close(net.loads[0].p, 20.649_686_030_854_52);
assert_close(net.loads[0].q, 14.970_012_102_581_254);
assert_eq!(net.shunts[0].bus, BusId(9));
assert_close(net.shunts[0].g, 0.0);
assert_close(net.shunts[0].b, 19.0);
let line = &net.branches[0];
assert_eq!((line.from, line.to), (BusId(1), BusId(2)));
assert_close(line.r, 0.01938);
assert_close(line.x, 0.05917);
assert_close(line.b, 0.0528);
assert_eq!(
line.charging,
Some(BranchCharging::new(0.0, 0.0264, 0.0, 0.0264))
);
assert_close(line.rate_a, 472.0);
assert_close(line.angmin, -30.0);
assert_close(line.angmax, 30.0);
let flow = line.solution.unwrap();
assert_close(flow.pf, 200.987_451_502_624_43);
assert_close(flow.qf, -4.314_632_222_308_059);
assert_close(flow.pt, -194.019_590_717_250_68);
assert_close(flow.qt, 19.820_585_478_075_1);
let transformer = &net.branches[17];
assert_eq!((transformer.from, transformer.to), (BusId(4), BusId(7)));
assert_close(transformer.x, 0.20912);
assert_close(transformer.tap, 0.978);
assert_close(transformer.rate_a, 141.0);
assert_eq!(parsed.warnings.len(), 2, "{:?}", parsed.warnings);
assert!(parsed.warnings[0].contains("solver initial values"));
assert!(parsed.warnings[1].contains("synthesized IDs"));
}
#[test]
fn detects_aliases_and_echoes_the_official_source_exactly() {
let source = fixture_text();
for alias in [
"opfdata-json",
"opfdata",
"OPFData",
"deepmind-opfdata-json",
"deepmind-opfdata",
"gridopt-json",
"gridopt",
] {
let parsed = parse_str(&source, alias).unwrap();
assert_eq!(
parsed.network.source_format,
SourceFormat::DeepMindOpfDataJson
);
}
let parsed = parse_file(fixture(), None).unwrap();
let echo = write_as(&parsed.network, TargetFormat::DeepMindOpfDataJson).unwrap();
assert_eq!(echo.text, source);
assert!(echo.warnings.is_empty());
}
#[test]
fn converts_to_classical_json_and_matpower_with_fidelity_warnings() {
let power_models = convert_file(fixture(), TargetFormat::PowerModelsJson, None).unwrap();
let back = parse_str(&power_models.text, "powermodels-json")
.unwrap()
.network;
assert_eq!(back.buses.len(), 14);
assert_eq!(back.generators.len(), 5);
assert_eq!(back.branches.len(), 20);
assert_close(back.generators[0].pg, 286.070_948_069_333_44);
assert!(
power_models
.warnings
.iter()
.any(|warning| warning.contains("solver initial values"))
);
let matpower = convert_file(fixture(), TargetFormat::Matpower, None).unwrap();
assert!(matpower.text.contains("mpc.bus"));
assert!(matpower.text.contains("mpc.gencost"));
assert!(
matpower
.warnings
.iter()
.any(|warning| warning.contains("branch solution value"))
);
}
#[test]
fn opfdata_target_without_retained_source_is_unsupported() {
let err = write_as(
&BalancedNetwork::new("memory", 100.0),
TargetFormat::DeepMindOpfDataJson,
)
.unwrap_err();
assert!(matches!(
err,
Error::WriteUnsupported {
format: "opfdata-json"
}
));
}
fn modified(mut edit: impl FnMut(&mut Value)) -> String {
let mut value: Value = serde_json::from_str(&fixture_text()).unwrap();
edit(&mut value);
serde_json::to_string(&value).unwrap()
}
fn recompute_objective(value: &mut Value) {
let generators = value["grid"]["nodes"]["generator"].as_array().unwrap();
let solution = value["solution"]["nodes"]["generator"].as_array().unwrap();
let objective = generators
.iter()
.zip(solution)
.map(|(generator, solved)| {
let row = generator.as_array().unwrap();
let pg = solved[0].as_f64().unwrap();
row[8].as_f64().unwrap() * pg * pg
+ row[9].as_f64().unwrap() * pg
+ row[10].as_f64().unwrap()
})
.sum::<f64>();
value["metadata"]["objective"] = Value::from(objective);
}
#[test]
fn accepts_variable_fulltop_and_n_minus_one_element_counts() {
let line_outage = modified(|value| {
for path in [
"/grid/edges/ac_line/senders",
"/grid/edges/ac_line/receivers",
"/grid/edges/ac_line/features",
"/solution/edges/ac_line/senders",
"/solution/edges/ac_line/receivers",
"/solution/edges/ac_line/features",
] {
value
.pointer_mut(path)
.unwrap()
.as_array_mut()
.unwrap()
.pop();
}
});
let parsed = parse_str(&line_outage, "opfdata").unwrap();
assert_eq!(parsed.network.generators.len(), 5);
assert_eq!(parsed.network.branches.len(), 19);
assert!(!parsed.warnings.iter().any(|w| w.contains("objective")));
let generator_outage = modified(|value| {
let removed = value["grid"]["nodes"]["generator"]
.as_array_mut()
.unwrap()
.len()
- 1;
value["grid"]["nodes"]["generator"]
.as_array_mut()
.unwrap()
.pop();
value["solution"]["nodes"]["generator"]
.as_array_mut()
.unwrap()
.pop();
let senders = value["grid"]["edges"]["generator_link"]["senders"]
.as_array_mut()
.unwrap();
let link = senders
.iter()
.position(|sender| sender.as_u64() == Some(removed as u64))
.unwrap();
senders.remove(link);
value["grid"]["edges"]["generator_link"]["receivers"]
.as_array_mut()
.unwrap()
.remove(link);
recompute_objective(value);
});
let parsed = parse_str(&generator_outage, "opfdata").unwrap();
assert_eq!(parsed.network.generators.len(), 4);
assert_eq!(parsed.network.branches.len(), 20);
assert!(!parsed.warnings.iter().any(|w| w.contains("objective")));
}
#[test]
fn maps_general_quadratic_costs_from_per_unit_to_mw() {
let source = modified(|value| {
value["grid"]["nodes"]["generator"][0][8] = Value::from(100.0);
value["grid"]["nodes"]["generator"][0][9] = Value::from(200.0);
value["grid"]["nodes"]["generator"][0][10] = Value::from(3.0);
recompute_objective(value);
});
let parsed = parse_str(&source, "opfdata").unwrap();
let cost = parsed.network.generators[0].cost.as_ref().unwrap();
assert_close(cost.coeffs[0], 0.01);
assert_close(cost.coeffs[1], 2.0);
assert_close(cost.coeffs[2], 3.0);
assert!(!parsed.warnings.iter().any(|w| w.contains("objective")));
}
#[test]
fn retains_published_schema_extensions_and_warns_on_projection() {
let source = modified(|value| {
value["dataset_revision"] = Value::from(2);
value["metadata"]["solver"] = Value::from("example");
});
let parsed = parse_str(&source, "opfdata").unwrap();
assert!(
parsed
.warnings
.iter()
.any(|warning| warning.contains("`dataset_revision`"))
);
assert!(
parsed
.warnings
.iter()
.any(|warning| warning.contains("`metadata.solver`"))
);
let echo = write_as(&parsed.network, TargetFormat::DeepMindOpfDataJson).unwrap();
assert_eq!(echo.text, source);
}
#[test]
fn rejects_bad_base_bus_type_and_feature_width() {
let bad_base = modified(|value| value["grid"]["context"][0][0][0] = Value::from(0.0));
let err = parse_str(&bad_base, "opfdata").unwrap_err();
assert!(
err.to_string().contains("baseMVA must be positive"),
"{err}"
);
let bad_type = modified(|value| value["grid"]["nodes"]["bus"][0][1] = Value::from(9.0));
let err = parse_str(&bad_type, "opfdata").unwrap_err();
assert!(err.to_string().contains("invalid bus type 9"), "{err}");
let bad_width = modified(|value| {
value["grid"]["edges"]["ac_line"]["features"][0]
.as_array_mut()
.unwrap()
.pop();
});
let err = parse_str(&bad_width, "opfdata").unwrap_err();
assert!(err.to_string().contains("invalid OPFData schema"), "{err}");
}
#[test]
fn rejects_bad_links_and_solution_topology() {
let duplicate_link = modified(|value| {
value["grid"]["edges"]["generator_link"]["senders"][1] = Value::from(0);
});
let err = parse_str(&duplicate_link, "opfdata").unwrap_err();
assert!(err.to_string().contains("more than one link"), "{err}");
let bad_endpoint = modified(|value| {
value["grid"]["edges"]["load_link"]["receivers"][0] = Value::from(99);
});
let err = parse_str(&bad_endpoint, "opfdata").unwrap_err();
assert!(err.to_string().contains("bus index 99"), "{err}");
let topology_mismatch = modified(|value| {
value["solution"]["edges"]["transformer"]["receivers"][0] = Value::from(8);
});
let err = parse_str(&topology_mismatch, "opfdata").unwrap_err();
assert!(err.to_string().contains("topology differs"), "{err}");
}
#[test]
fn warns_when_objective_does_not_match_and_guides_unsupported_inputs() {
let wrong_objective = modified(|value| value["metadata"]["objective"] = Value::from(-1.0));
let parsed = parse_str(&wrong_objective, "opfdata").unwrap();
assert!(
parsed
.warnings
.iter()
.any(|warning| warning.contains("metadata.objective"))
);
for path in ["cache.pt", "group.tar.gz"] {
let err = parse_file(path, Some("opfdata")).unwrap_err();
assert!(err.to_string().contains("extract"), "{path}: {err}");
}
}