use std::path::{Path, PathBuf};
use gridoxide::cgmes::{cgmes_to_network, load_profiles};
fn config(dir: &str) -> Option<PathBuf> {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/CGMES-Test-Configurations/v3.0")
.join(dir);
path.exists().then_some(path)
}
fn profiles(dir: &Path) -> Vec<PathBuf> {
let mut v: Vec<PathBuf> = std::fs::read_dir(dir)
.expect("configuration directory")
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|e| e == "xml"))
.collect();
v.sort();
v
}
fn network(dir: &str) -> Option<gridoxide::cgmes::CgmesNetwork> {
let dir = config(dir)?;
let paths = profiles(&dir);
let refs: Vec<&Path> = paths.iter().map(|p| p.as_path()).collect();
let ds = load_profiles(&refs).expect("failed to decode CGMES profiles");
Some(cgmes_to_network(&ds, 100e6).expect("conversion failed"))
}
#[test]
fn the_current_position_reads_back_exactly() {
let configs = [
"Svedala/Svedala-Merged",
"FullGrid/FullGrid-Merged",
"SmallGrid/SmallGrid-Merged",
"PowerFlow/PowerFlow",
"PST/PST_PhaseTapChangerLinear_Type1",
"PST/PST_PhaseTapChangerLinear_Type2",
"PST/PST_PhaseTapChangerTable_Type3",
"MiniGrid/MiniGrid-Merged",
];
let mut checked = 0;
let mut skipped = Vec::new();
for name in configs {
let Some(net) = network(name) else {
skipped.push(name);
continue;
};
assert_eq!(
net.tap_changers.len(),
net.transformers.len(),
"{name}: the tap table must be parallel to the transformers"
);
for (i, changer) in net.tap_changers.iter().enumerate() {
let Some(changer) = changer else { continue };
let from_table = changer.current().unwrap_or_else(|| {
panic!("{name}: transformer {i} is at a position outside its own table")
});
let on_branch = net.transformers[i].tap;
assert_eq!(
(from_table.re.to_bits(), from_table.im.to_bits()),
(on_branch.re.to_bits(), on_branch.im.to_bits()),
"{name}: transformer {i} at position {} — table says {from_table}, branch carries {on_branch}",
changer.position
);
if let Some(series) = &changer.series {
let idx = (changer.position - changer.low) as usize;
assert_eq!(
(series[idx].re.to_bits(), series[idx].im.to_bits()),
(
net.transformers[i].y_series.re.to_bits(),
net.transformers[i].y_series.im.to_bits()
),
"{name}: transformer {i}'s per-step admittance disagrees with the branch"
);
}
checked += 1;
}
}
assert!(
checked >= 20,
"expected the fixture set to contribute a good number of tap changers, got {checked} \
(skipped: {skipped:?})"
);
}
#[test]
fn svedala_retains_every_position_of_every_changer() {
let Some(net) = network("Svedala/Svedala-Merged") else {
eprintln!("skipping: Svedala fixture not checked out");
return;
};
let changers: Vec<_> = net.tap_changers.iter().flatten().collect();
assert_eq!(changers.len(), 11, "Svedala declares eleven tap changers");
for c in &changers {
assert!(c.len() >= 2, "a one-position changer is not a changer");
assert_eq!(c.steps.len(), (c.high() - c.low + 1) as usize);
assert!(
c.position >= c.low && c.position <= c.high(),
"position {} outside [{}, {}]",
c.position,
c.low,
c.high()
);
for p in c.low..=c.high() {
assert!(c.at(p).is_some(), "position {p} missing");
assert!(
c.ratio(p).unwrap() > 0.0,
"position {p} has a non-positive ratio"
);
}
}
}
#[test]
fn a_phase_shifter_rounds_to_a_position_it_has() {
let Some(net) = network("PST/PST_PhaseTapChangerTable_Type3") else {
eprintln!("skipping: PST Type3 fixture not checked out");
return;
};
let changer = net
.tap_changers
.iter()
.flatten()
.next()
.expect("the PST fixture has a phase tap changer");
for p in changer.low..=changer.high() {
let angle = changer.angle_deg(p).expect("every position has an angle");
let rounded = changer
.nearest_to_angle(angle)
.expect("rounds to something");
assert!(
(changer.angle_deg(rounded).unwrap() - angle).abs() < 1e-12,
"position {p} at {angle}° rounded to {rounded} at {}°",
changer.angle_deg(rounded).unwrap()
);
}
}
#[test]
fn setting_a_position_moves_the_branch_and_stays_in_range() {
let Some(mut net) = network("PST/PST_PhaseTapChangerLinear_Type1") else {
eprintln!("skipping: PST Type1 fixture not checked out");
return;
};
let i = net
.tap_changers
.iter()
.position(|c| c.is_some())
.expect("the PST fixture has a tap changer");
let changer = net.tap_changers[i].as_mut().unwrap();
let (low, high) = (changer.low, changer.high());
let varies = changer.series.is_some();
let before = net.transformers[i].tap;
let before_y = net.transformers[i].y_series;
assert!(changer.set_position(&mut net.transformers[i], high));
assert_ne!(
net.transformers[i].tap, before,
"the extreme tap must differ from the current one"
);
if varies {
assert_ne!(
net.transformers[i].y_series, before_y,
"this changer's reactance varies with position, so the branch admittance must too"
);
}
let at_high = net.transformers[i].tap;
let changer = net.tap_changers[i].as_mut().unwrap();
assert!(!changer.set_position(&mut net.transformers[i], high + 1));
assert!(!changer.set_position(&mut net.transformers[i], low - 1));
assert_eq!(net.transformers[i].tap, at_high);
assert_eq!(net.tap_changers[i].as_ref().unwrap().position, high);
}