use num_complex::Complex;
use crate::topology::bus_view::{BusView, RetainedSwitch};
use crate::topology::ideal_connection_z;
use crate::topology::model::SwitchIdx;
use crate::types::Transformer;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SwitchTreatment {
#[default]
Merge,
Regularize,
Constrain,
}
pub fn switch_reactance() -> f64 {
ideal_connection_z()
}
pub fn switch_admittance() -> Complex<f64> {
Complex::new(1.0, 0.0) / Complex::new(0.0, switch_reactance())
}
pub fn regularized_branches(view: &BusView) -> Vec<Transformer> {
let y = switch_admittance();
view.retained()
.iter()
.filter(|r| !view.is_degenerate(r))
.map(|r| {
let status = u8::from(!r.open);
Transformer {
from: r.buses[0].0,
to: r.buses[1].0,
from_status: status,
to_status: status,
y_series: y,
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::new(1.0, 0.0),
}
})
.collect()
}
pub fn degenerate_switches(view: &BusView) -> Vec<RetainedSwitch> {
view.retained()
.iter()
.filter(|r| view.is_degenerate(r))
.copied()
.collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProbeShape {
Chain,
Star,
Ring,
}
pub fn conditioning_probe(
counts: &[usize],
shape: ProbeShape,
y_series: Complex<f64>,
tol: f64,
max_iter: usize,
) -> Vec<(usize, Option<usize>, f64)> {
use crate::network::build_ybus;
use crate::solver::{JacobianBackend, PersistentSolver, SolveStatus};
use crate::types::{Bus, BusType, Line};
counts
.iter()
.map(|&n| {
let switch_nodes = match shape {
ProbeShape::Chain | ProbeShape::Star => n + 1,
ProbeShape::Ring => n.max(2),
};
let n_buses = switch_nodes + 1;
let load = n_buses - 1;
let mut buses: Vec<Bus> = (0..n_buses)
.map(|i| Bus {
idx: i,
bus_type: if i == 0 { BusType::Slack } else { BusType::PQ },
voltage_mag: 1.0,
voltage_ang: 0.0,
p_spec: 0.0,
q_spec: 0.0,
q_min: -f64::INFINITY,
q_max: f64::INFINITY,
u_rated: 0.0,
zip_terms: Vec::new(),
})
.collect();
buses[load].p_spec = -0.1;
buses[load].q_spec = -0.03;
let switch = |from: usize, to: usize| Transformer {
from,
to,
from_status: 1,
to_status: 1,
y_series,
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::new(1.0, 0.0),
};
let switches: Vec<Transformer> = match shape {
ProbeShape::Chain => (0..n).map(|i| switch(i, i + 1)).collect(),
ProbeShape::Star => (0..n).map(|i| switch(0, i + 1)).collect(),
ProbeShape::Ring => (0..switch_nodes)
.map(|i| switch(i, (i + 1) % switch_nodes))
.collect(),
};
let feeder_from = switch_nodes - 1;
let lines = vec![Line {
from: feeder_from,
to: load,
r: 0.01,
x: 0.05,
b_shunt: 0.0,
g_shunt: 0.0,
}];
let ybus = build_ybus(n_buses, &lines, &switches).finish();
crate::network::linear_initial_guess(&mut buses, &ybus);
let (_, stats) = PersistentSolver::new(JacobianBackend::default())
.solve_with_stats(&mut buses, &ybus, tol, max_iter);
let finite = buses
.iter()
.all(|b| b.voltage_mag.is_finite() && b.voltage_ang.is_finite());
let ok = stats.status == SolveStatus::Converged && finite;
(n, ok.then(|| stats.iterations()), buses[load].voltage_mag)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::topology::model::{NodeBreakerTopology, NodeIdx, Switch, SwitchKind};
use crate::topology::{RetentionPolicy, bus_view};
fn sw(a: usize, b: usize, open: bool) -> Switch {
Switch {
kind: SwitchKind::Breaker,
nodes: [NodeIdx(a), NodeIdx(b)],
open,
in_service: true,
}
}
#[test]
fn a_switch_branch_is_inductive_at_the_ideal_connection_stiffness() {
let y = switch_admittance();
assert!(y.im < 0.0, "switch admittance {y} is capacitive");
let z = Complex::new(1.0, 0.0) / y;
assert!(z.im > 0.0, "switch reactance {} is negative", z.im);
assert!((z.im - switch_reactance()).abs() < 1e-18);
assert!(
(y.norm() - crate::topology::IDEAL_CONNECTION_Y.norm()).abs() < 1.0,
"{} vs {}",
y.norm(),
crate::topology::IDEAL_CONNECTION_Y.norm()
);
let dc = crate::linear::dc_branches(
&[],
®ularized_branches(&{
let mut t = NodeBreakerTopology::new(2);
t.add_switch(sw(0, 1, false));
bus_view(&t, &RetentionPolicy::RetainAll)
}),
crate::linear::DcOptions::default(),
);
assert_eq!(dc.len(), 1);
assert!(dc[0].b > 0.0, "DC susceptance came out {}", dc[0].b);
}
#[test]
fn open_and_closed_switches_differ_only_in_status() {
let mut topo = NodeBreakerTopology::new(3);
topo.add_switch(sw(0, 1, false));
topo.add_switch(sw(1, 2, true));
let view = bus_view(&topo, &RetentionPolicy::RetainAll);
let branches = regularized_branches(&view);
assert_eq!(branches.len(), 2);
assert_eq!((branches[0].from_status, branches[0].to_status), (1, 1));
assert_eq!((branches[1].from_status, branches[1].to_status), (0, 0));
assert_eq!(branches[0].y_series, branches[1].y_series);
assert_eq!((branches[0].from, branches[0].to), (0, 1));
assert_eq!((branches[1].from, branches[1].to), (1, 2));
}
#[test]
fn flipping_a_switch_preserves_the_ybus_sparsity_pattern() {
use crate::network::build_ybus;
let pattern = |open: bool| {
let mut topo = NodeBreakerTopology::new(3);
topo.add_switch(sw(0, 1, open));
topo.add_switch(sw(1, 2, false));
let view = bus_view(&topo, &RetentionPolicy::RetainAll);
let ybus = build_ybus(3, &[], ®ularized_branches(&view)).finish();
(0..ybus.n())
.map(|i| ybus.row(i).iter().map(|&(j, _)| j).collect::<Vec<_>>())
.collect::<Vec<_>>()
};
assert_eq!(pattern(false), pattern(true));
}
#[test]
fn degenerate_switches_are_separated_out() {
let mut topo = NodeBreakerTopology::new(2);
topo.add_switch(sw(0, 1, false)); topo.add_switch(Switch {
kind: SwitchKind::Junction, nodes: [NodeIdx(0), NodeIdx(1)],
open: false,
in_service: true,
});
let view = bus_view(&topo, &RetentionPolicy::RetainAll);
assert_eq!(view.retained().len(), 1);
assert!(regularized_branches(&view).is_empty());
assert_eq!(degenerate_switches(&view).len(), 1);
}
#[test]
fn merge_all_produces_no_switch_branches() {
let mut topo = NodeBreakerTopology::new(3);
topo.add_switch(sw(0, 1, false));
topo.add_switch(sw(1, 2, true));
let view = bus_view(&topo, &RetentionPolicy::MergeAll);
assert!(regularized_branches(&view).is_empty());
assert!(degenerate_switches(&view).is_empty());
}
#[test]
fn a_closed_switch_holds_its_two_buses_together() {
use crate::network::build_ybus;
use crate::solver::newton_raphson;
use crate::types::{Bus, BusType, Line};
let mut buses: Vec<Bus> = (0..3)
.map(|i| Bus {
idx: i,
bus_type: if i == 0 { BusType::Slack } else { BusType::PQ },
voltage_mag: 1.0,
voltage_ang: 0.0,
p_spec: 0.0,
q_spec: 0.0,
q_min: -f64::INFINITY,
q_max: f64::INFINITY,
u_rated: 0.0,
zip_terms: Vec::new(),
})
.collect();
buses[2].p_spec = -0.5;
buses[2].q_spec = -0.2;
let mut topo = NodeBreakerTopology::new(3);
topo.add_switch(sw(0, 1, false));
let view = bus_view(&topo, &RetentionPolicy::RetainAll);
let lines = vec![Line {
from: 1,
to: 2,
r: 0.02,
x: 0.06,
b_shunt: 0.0,
g_shunt: 0.0,
}];
let ybus = build_ybus(3, &lines, ®ularized_branches(&view)).finish();
newton_raphson(&mut buses, &ybus, 1e-8, 30);
let drop = (buses[0].voltage_mag - buses[1].voltage_mag).abs();
let across_line = (buses[1].voltage_mag - buses[2].voltage_mag).abs();
assert!(
drop < 1e-4,
"a closed switch dropped {drop} p.u., which is not 'ideal enough'"
);
assert!(
drop < across_line / 100.0,
"{drop} vs {across_line} across a real branch"
);
}
#[test]
fn regularized_switches_converge_at_every_count_this_probe_can_build() {
let counts = [1usize, 20, 29, 90, 1266, 1464];
for shape in [ProbeShape::Chain, ProbeShape::Star, ProbeShape::Ring] {
for (label, y) in [
("inductive", switch_admittance()),
("capacitive", crate::topology::IDEAL_CONNECTION_Y),
] {
for (n, iters, vm) in conditioning_probe(&counts, shape, y, 1e-6, 30) {
assert!(
iters.is_some(),
"{shape:?}/{label}: {n} switches failed to converge — \
the switch-count ceiling has been reproduced"
);
assert!(
(vm - 1.0).abs() < 0.05,
"{shape:?}/{label}: {n} switches converged to an implausible \
|V| = {vm}"
);
}
}
}
}
#[test]
fn an_open_switch_separates_its_two_buses() {
use crate::network::{build_ybus, connected_components};
let mut topo = NodeBreakerTopology::new(2);
topo.add_switch(sw(0, 1, true));
let view = bus_view(&topo, &RetentionPolicy::RetainAll);
let ybus = build_ybus(2, &[], ®ularized_branches(&view)).finish();
assert!(
ybus.row(0).iter().any(|&(j, _)| j == 1),
"structural entry was dropped"
);
assert_eq!(
ybus.get(0, 1),
Complex::new(0.0, 0.0),
"an open switch still conducts"
);
assert_eq!(connected_components(&ybus).len(), 2);
}
}
#[derive(Clone, Debug)]
pub struct NodeBreakerNetwork {
pub buses: Vec<crate::types::Bus>,
pub lines: Vec<crate::types::Line>,
pub transformers: Vec<Transformer>,
pub shunts: Vec<crate::network::ShuntAdm>,
pub view: BusView,
pub topology: crate::topology::NodeBreakerTopology,
pub switch_labels: Vec<String>,
branch_of_retained: Vec<Option<usize>>,
pub zero_injection: Vec<bool>,
}
impl NodeBreakerNetwork {
#[allow(clippy::too_many_arguments)]
pub fn new(
buses: Vec<crate::types::Bus>,
lines: Vec<crate::types::Line>,
mut transformers: Vec<Transformer>,
shunts: Vec<crate::network::ShuntAdm>,
view: BusView,
topology: crate::topology::NodeBreakerTopology,
switch_labels: Vec<String>,
zero_injection: Vec<bool>,
treatment: SwitchTreatment,
) -> Self {
let mut branch_of_retained = vec![None; view.retained().len()];
if treatment == SwitchTreatment::Regularize {
let base = lines.len() + transformers.len();
let mut stamped = 0;
for (n, r) in view.retained().iter().enumerate() {
if view.is_degenerate(r) {
continue;
}
branch_of_retained[n] = Some(base + stamped);
stamped += 1;
}
transformers.extend(regularized_branches(&view));
debug_assert_eq!(lines.len() + transformers.len(), base + stamped);
}
Self {
buses,
lines,
transformers,
shunts,
view,
topology,
switch_labels,
branch_of_retained,
zero_injection,
}
}
pub fn switch_kind(&self, switch: SwitchIdx) -> Option<crate::topology::SwitchKind> {
self.topology.switches.get(switch.0).map(|s| s.kind)
}
pub fn switch_label(&self, switch: SwitchIdx) -> String {
self.switch_labels
.get(switch.0)
.cloned()
.unwrap_or_else(|| format!("switch{}", switch.0))
}
pub fn n_branches(&self) -> usize {
self.lines.len() + self.transformers.len()
}
pub fn branch_of_retained(&self, n: usize) -> Option<usize> {
self.branch_of_retained.get(n).copied().flatten()
}
pub fn branch_of_switch(&self, switch: SwitchIdx) -> Option<usize> {
let n = self
.view
.retained()
.iter()
.position(|r| r.switch == switch)?;
self.branch_of_retained(n)
}
pub fn switch_branches(&self) -> Vec<(SwitchIdx, usize)> {
self.view
.retained()
.iter()
.enumerate()
.filter_map(|(n, r)| self.branch_of_retained(n).map(|b| (r.switch, b)))
.collect()
}
pub fn set_switch_open(&mut self, switch: SwitchIdx, open: bool) -> bool {
let Some(branch) = self.branch_of_switch(switch) else {
return false;
};
let status = u8::from(!open);
let t = &mut self.transformers[branch - self.lines.len()];
t.from_status = status;
t.to_status = status;
true
}
pub fn is_switch_open(&self, switch: SwitchIdx) -> Option<bool> {
let branch = self.branch_of_switch(switch)?;
Some(self.transformers[branch - self.lines.len()].from_status == 0)
}
pub fn constrained_switches(&self) -> Vec<crate::constrained::ConstrainedSwitch> {
self.view
.retained()
.iter()
.map(|r| crate::constrained::ConstrainedSwitch {
from: r.buses[0].0,
to: r.buses[1].0,
open: r.open,
})
.collect()
}
pub fn switch_flow(
&self,
switch: SwitchIdx,
voltages: &[num_complex::Complex<f64>],
) -> Option<(f64, f64)> {
let branch = self.branch_of_switch(switch)?;
let params = crate::branch_flow::branch_params(&self.lines, &self.transformers);
Some(crate::branch_flow::terminal_flow(
¶ms[branch],
crate::branch_flow::Terminal::From,
voltages,
))
}
}