use gridoxide::constrained::{ConstrainedSwitch, solve_constrained};
use gridoxide::network::{YBus, build_ybus, stamp_shunts};
use gridoxide::solver::{SolveStatus, newton_raphson};
use gridoxide::switches::switch_admittance;
use gridoxide::types::{Bus, BusType, Line, Transformer};
use num_complex::Complex;
fn bus(idx: usize, bus_type: BusType, p: f64, q: f64) -> Bus {
Bus {
idx,
bus_type,
voltage_mag: 1.0,
voltage_ang: 0.0,
p_spec: p,
q_spec: q,
q_min: -f64::INFINITY,
q_max: f64::INFINITY,
u_rated: 400e3,
zip_terms: Vec::new(),
}
}
fn line(from: usize, to: usize, r: f64, x: f64) -> Line {
Line {
from,
to,
r,
x,
b_shunt: 0.0,
g_shunt: 0.0,
}
}
fn switch_branch(from: usize, to: usize, open: bool) -> Transformer {
let status = u8::from(!open);
Transformer {
from,
to,
from_status: status,
to_status: status,
y_series: switch_admittance(),
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::new(1.0, 0.0),
}
}
fn ybus_of(
n: usize,
lines: &[Line],
transformers: &[Transformer],
) -> gridoxide::network::YBusSparse {
let mut y: YBus = build_ybus(n, lines, transformers);
stamp_shunts(&mut y, &[]);
y.finish()
}
fn both_ways(
buses: &[Bus],
lines: &[Line],
switches: &[ConstrainedSwitch],
) -> (
Vec<Bus>,
Vec<Bus>,
gridoxide::constrained::ConstrainedSolution,
) {
let stamped: Vec<Transformer> = switches
.iter()
.map(|s| switch_branch(s.from, s.to, s.open))
.collect();
let mut reg = buses.to_vec();
let y_reg = ybus_of(reg.len(), lines, &stamped);
let reports = newton_raphson(&mut reg, &y_reg, 1e-10, 30);
assert!(
reports.iter().all(|r| matches!(
r.status,
gridoxide::solver::IslandStatus::Converged
| gridoxide::solver::IslandStatus::NoReferenceBus
)),
"the regularized reference did not solve: {reports:?}"
);
let mut con = buses.to_vec();
let y_con = ybus_of(con.len(), lines, &[]);
let solution = solve_constrained(&mut con, &y_con, switches, 1e-10, 30);
(reg, con, solution)
}
fn agreement_tolerance(n_closed: usize) -> f64 {
1e-9 + 2.0 * n_closed as f64 * gridoxide::switches::switch_reactance()
}
fn assert_states_agree(reg: &[Bus], con: &[Bus], tol: f64) {
for (i, (r, c)) in reg.iter().zip(con).enumerate() {
assert!(
(r.voltage_mag - c.voltage_mag).abs() < tol,
"bus {i} magnitude: regularized {} vs constrained {}",
r.voltage_mag,
c.voltage_mag
);
assert!(
(r.voltage_ang - c.voltage_ang).abs() < tol,
"bus {i} angle: regularized {} vs constrained {}",
r.voltage_ang,
c.voltage_ang
);
}
}
#[test]
fn a_closed_switch_carries_the_load_and_both_treatments_agree() {
let buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, 0.0, 0.0),
bus(2, BusType::PQ, -0.8, -0.3),
];
let lines = vec![line(0, 1, 0.01, 0.1)];
let switches = vec![ConstrainedSwitch {
from: 1,
to: 2,
open: false,
}];
let (reg, con, solution) = both_ways(&buses, &lines, &switches);
assert_eq!(solution.stats.status, SolveStatus::Converged);
assert!(
solution.indeterminate.is_empty(),
"nothing here is indeterminate"
);
assert_states_agree(
®,
&con,
agreement_tolerance(switches.iter().filter(|s| !s.open).count()),
);
assert!((con[1].voltage_mag - con[2].voltage_mag).abs() < 1e-12);
assert!((con[1].voltage_ang - con[2].voltage_ang).abs() < 1e-12);
let (p, q) = solution.flows[0];
assert!(
(p - 0.8).abs() < 1e-9,
"switch carries P = {p}, expected the load's 0.8"
);
assert!(
(q - 0.3).abs() < 1e-9,
"switch carries Q = {q}, expected the load's 0.3"
);
}
#[test]
fn an_open_switch_carries_nothing_and_ties_nothing() {
let buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, -0.5, -0.2),
bus(2, BusType::PQ, -0.3, -0.1),
];
let lines = vec![line(0, 1, 0.01, 0.1)];
let switches = vec![ConstrainedSwitch {
from: 1,
to: 2,
open: true,
}];
let (reg, con, solution) = both_ways(&buses, &lines, &switches);
assert_eq!(solution.stats.status, SolveStatus::Converged);
assert_eq!(
solution.flows[0],
(0.0, 0.0),
"an open switch carries exactly nothing"
);
assert_states_agree(
®,
&con,
agreement_tolerance(switches.iter().filter(|s| !s.open).count()),
);
assert_eq!(con[2].voltage_mag, 0.0);
}
#[test]
fn the_two_treatments_agree_across_a_switching_campaign() {
let buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, -0.2, -0.05),
bus(2, BusType::PQ, -0.1, -0.02),
bus(3, BusType::PQ, -0.6, -0.25),
];
let lines = vec![
line(0, 1, 0.01, 0.1),
line(0, 2, 0.02, 0.15),
line(1, 3, 0.015, 0.12),
];
let mut previous: Option<f64> = None;
for open in [false, true, false] {
let switches = vec![ConstrainedSwitch {
from: 2,
to: 3,
open,
}];
let (reg, con, solution) = both_ways(&buses, &lines, &switches);
assert_eq!(
solution.stats.status,
SolveStatus::Converged,
"open = {open}"
);
assert_states_agree(
®,
&con,
agreement_tolerance(switches.iter().filter(|s| !s.open).count()),
);
let p = solution.flows[0].0;
if open {
assert_eq!(p, 0.0);
} else {
assert!(
p.abs() > 1e-3,
"a closed tie in a ring should carry something, got {p}"
);
}
if !open {
if let Some(before) = previous {
assert!((p - before).abs() < 1e-12);
}
previous = Some(p);
}
}
}
#[test]
fn a_loop_of_closed_switches_is_reported_as_indeterminate() {
let buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, 0.0, 0.0),
bus(2, BusType::PQ, -0.4, -0.15),
];
let lines = vec![line(0, 1, 0.01, 0.1)];
let switches = vec![
ConstrainedSwitch {
from: 1,
to: 2,
open: false,
},
ConstrainedSwitch {
from: 1,
to: 2,
open: false,
},
];
let (reg, con, solution) = both_ways(&buses, &lines, &switches);
assert_eq!(solution.stats.status, SolveStatus::Converged);
assert_states_agree(
®,
&con,
agreement_tolerance(switches.iter().filter(|s| !s.open).count()),
);
assert_eq!(
solution.indeterminate,
vec![1],
"the loop-closing switch is the second one"
);
assert_eq!(
solution.flows[1],
(0.0, 0.0),
"an indeterminate flow is pinned, not guessed"
);
let (p, q) = solution.flows[0];
assert!(
(p - 0.4).abs() < 1e-9 && (q - 0.15).abs() < 1e-9,
"got ({p}, {q})"
);
}
#[test]
fn a_chain_of_switches_ties_every_intermediate_bus() {
let n = 8;
let mut buses = vec![bus(0, BusType::Slack, 0.0, 0.0)];
for i in 1..n {
buses.push(bus(i, BusType::PQ, 0.0, 0.0));
}
buses[n - 1].p_spec = -0.5;
buses[n - 1].q_spec = -0.2;
let lines = vec![line(0, 1, 0.01, 0.1)];
let switches: Vec<ConstrainedSwitch> = (1..n - 1)
.map(|i| ConstrainedSwitch {
from: i,
to: i + 1,
open: false,
})
.collect();
let (reg, con, solution) = both_ways(&buses, &lines, &switches);
assert_eq!(solution.stats.status, SolveStatus::Converged);
assert!(solution.indeterminate.is_empty());
assert_states_agree(
®,
&con,
agreement_tolerance(switches.iter().filter(|s| !s.open).count()),
);
for i in 1..n - 1 {
assert!(
(con[i].voltage_mag - con[i + 1].voltage_mag).abs() < 1e-12
&& (con[i].voltage_ang - con[i + 1].voltage_ang).abs() < 1e-12,
"switch {i} did not tie its ends"
);
}
for (k, (p, q)) in solution.flows.iter().enumerate() {
assert!(
(p - 0.5).abs() < 1e-9 && (q - 0.2).abs() < 1e-9,
"switch {k} carries ({p}, {q}), not the load"
);
}
}
#[test]
fn many_switches_do_not_degrade_convergence() {
let n = 102;
let mut buses = vec![bus(0, BusType::Slack, 0.0, 0.0)];
for i in 1..n {
buses.push(bus(i, BusType::PQ, 0.0, 0.0));
}
buses[n - 1].p_spec = -0.5;
buses[n - 1].q_spec = -0.2;
let lines = vec![line(0, 1, 0.01, 0.1)];
let switches: Vec<ConstrainedSwitch> = (1..n - 1)
.map(|i| ConstrainedSwitch {
from: i,
to: i + 1,
open: false,
})
.collect();
let mut con = buses.clone();
let y = ybus_of(n, &lines, &[]);
let solution = solve_constrained(&mut con, &y, &switches, 1e-10, 30);
assert_eq!(
solution.stats.status,
SolveStatus::Converged,
"100 switches did not converge"
);
assert!(
solution.stats.iterations() <= 5,
"took {} iterations",
solution.stats.iterations()
);
for (p, _) in &solution.flows {
assert!((p - 0.5).abs() < 1e-9);
}
}
#[test]
fn a_switch_between_two_references_is_indeterminate_rather_than_singular() {
let buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::Slack, 0.0, 0.0),
bus(2, BusType::PQ, -0.3, -0.1),
];
let lines = vec![line(0, 2, 0.01, 0.1)];
let switches = vec![ConstrainedSwitch {
from: 0,
to: 1,
open: false,
}];
let mut con = buses.clone();
let y = ybus_of(3, &lines, &[]);
let solution = solve_constrained(&mut con, &y, &switches, 1e-10, 30);
assert_eq!(solution.stats.status, SolveStatus::Converged);
assert_eq!(solution.indeterminate, vec![0]);
assert_eq!(solution.flows[0], (0.0, 0.0));
}
#[test]
fn islands_are_decided_across_closed_switches() {
let buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, -0.25, -0.1),
];
let lines: Vec<Line> = Vec::new();
let switches = vec![ConstrainedSwitch {
from: 0,
to: 1,
open: false,
}];
let mut con = buses.clone();
let y = ybus_of(2, &lines, &[]);
let solution = solve_constrained(&mut con, &y, &switches, 1e-10, 30);
assert_eq!(solution.stats.status, SolveStatus::Converged);
assert!(
con[1].voltage_mag > 0.5,
"bus 1 was de-energized despite a closed switch to the source"
);
assert!((con[1].voltage_mag - con[0].voltage_mag).abs() < 1e-12);
let (p, q) = solution.flows[0];
assert!(
(p - 0.25).abs() < 1e-9 && (q - 0.1).abs() < 1e-9,
"got ({p}, {q})"
);
}