#![allow(clippy::float_cmp)]
use powerio::{
DEFAULT_BASE_FREQUENCY, Network, TargetFormat, parse_pandapower_json, parse_psse, parse_str,
write_as, write_pandapower_json, write_psse,
};
#[test]
fn psse_base_frequency_round_trips() {
let raw = "0, 100.00, 33, 0, 0, 50.00 / fifty hertz\n\
CASE\nCOMMENT\n\
1,'B1 ', 230.0,3,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
2,'B2 ', 138.0,1,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
0 / END OF BUS DATA, BEGIN LOAD DATA\n\
Q\n";
let net = parse_psse(raw).unwrap();
assert_eq!(net.base_frequency, 50.0);
let text = write_psse(&net).text;
let reparsed = parse_psse(&text).unwrap();
assert_eq!(reparsed.base_frequency, 50.0);
}
#[test]
fn psse_missing_base_frequency_defaults_to_sixty() {
let raw = "0, 100.00\nCASE\nCOMMENT\n\
1,'B1 ', 230.0,3,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
0 / END OF BUS DATA, BEGIN LOAD DATA\nQ\n";
let net = parse_psse(raw).unwrap();
assert_eq!(net.base_frequency, DEFAULT_BASE_FREQUENCY);
}
#[test]
fn pandapower_f_hz_round_trips_with_line_charging() {
let raw = "0, 100.00, 33, 0, 0, 60.00 / x\nCASE\nCOMMENT\n\
1,'B1 ', 230.0,3,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
2,'B2 ', 230.0,1,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
0 / END OF BUS DATA, BEGIN LOAD DATA\n\
0 / END OF LOAD DATA, BEGIN FIXED SHUNT DATA\n\
0 / END OF FIXED SHUNT DATA, BEGIN GENERATOR DATA\n\
0 / END OF GENERATOR DATA, BEGIN BRANCH DATA\n\
1,2,'1 ',0.01,0.05,0.02,100.0,0.0,0.0,0,0,0,0,1,1,0,1,1\n\
0 / END OF BRANCH DATA, BEGIN TRANSFORMER DATA\n\
0 / END OF TRANSFORMER DATA, BEGIN AREA DATA\nQ\n";
let mut net = parse_psse(raw).unwrap();
net.base_frequency = 50.0;
let b0 = net.branches[0].b;
let pp = write_pandapower_json(&net).text;
let back = parse_pandapower_json(&pp).unwrap().network;
assert_eq!(back.base_frequency, 50.0);
assert!(
(back.branches[0].b - b0).abs() < 1e-9,
"line charging changed across the f_hz hop: {} != {b0}",
back.branches[0].b
);
}
#[test]
fn dropped_frequency_warns_for_formats_without_a_field() {
let raw = "0, 100.00, 33, 0, 0, 50.00 / x\nCASE\nCOMMENT\n\
1,'B1 ', 230.0,3,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
0 / END OF BUS DATA, BEGIN LOAD DATA\nQ\n";
let net = parse_psse(raw).unwrap();
for target in [
TargetFormat::Matpower,
TargetFormat::PowerModelsJson,
TargetFormat::EgretJson,
TargetFormat::PowerWorld,
] {
let conv = write_as(&net, target).unwrap();
assert!(
conv.warnings.iter().any(|w| w.contains("frequency")),
"{target:?} should warn that it drops the 50 Hz label, got {:?}",
conv.warnings
);
}
for target in [TargetFormat::Psse { rev: 33 }, TargetFormat::PandapowerJson] {
let conv = write_as(&net, target).unwrap();
assert!(
!conv.warnings.iter().any(|w| w.contains("frequency")),
"{target:?} carries the frequency and should not warn, got {:?}",
conv.warnings
);
}
}
#[test]
fn json_transport_round_trips_and_defaults() {
let raw = "0, 100.00, 33, 0, 0, 50.00 / x\nCASE\nCOMMENT\n\
1,'B1 ', 230.0,3,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
0 / END OF BUS DATA, BEGIN LOAD DATA\nQ\n";
let net = parse_psse(raw).unwrap();
let json = net.to_json().unwrap();
assert_eq!(Network::from_json(&json).unwrap().base_frequency, 50.0);
let without: serde_json::Value = {
let mut v: serde_json::Value = serde_json::from_str(&json).unwrap();
v.as_object_mut().unwrap().remove("base_frequency");
v
};
let restored = Network::from_json(&without.to_string()).unwrap();
assert_eq!(restored.base_frequency, DEFAULT_BASE_FREQUENCY);
}
#[test]
fn parse_str_psse_reads_frequency() {
let raw = "0, 100.00, 33, 0, 0, 50.00 / x\nCASE\nCOMMENT\n\
1,'B1 ', 230.0,3,1,1,1,1.0,0.0,1.1,0.9,1.1,0.9\n\
0 / END OF BUS DATA, BEGIN LOAD DATA\nQ\n";
let parsed = parse_str(raw, "psse").unwrap();
assert_eq!(parsed.network.base_frequency, 50.0);
}