use super::types::{Bus, BusType, Line, Line3Ph, Transformer, Transformer3PhSeq, ZipKind};
use gridoxide_sparse::sparse;
use num_complex::Complex;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ShuntAdm {
pub at: usize,
pub y: Complex<f64>,
}
pub struct ShuntAdm3Ph {
pub at: usize,
pub y1: Complex<f64>,
pub y0: Complex<f64>,
}
pub(crate) fn seq_to_phase_shunt(y1: Complex<f64>, y0: Complex<f64>) -> [[Complex<f64>; 3]; 3] {
let d = (y0 + 2.0 * y1) / 3.0;
let o = (y0 - y1) / 3.0;
let mut m = [[o; 3]; 3];
for i in 0..3 {
m[i][i] = d;
}
m
}
pub struct YBus {
n: usize,
entries: Vec<(usize, usize, Complex<f64>)>,
}
impl YBus {
pub fn new(n: usize) -> Self {
Self {
n,
entries: Vec::new(),
}
}
pub fn add(&mut self, i: usize, j: usize, val: Complex<f64>) {
self.entries.push((i, j, val));
}
pub fn into_entries(self) -> Vec<(usize, usize, Complex<f64>)> {
self.entries
}
pub fn finish(self) -> YBusSparse {
let mut merged: std::collections::HashMap<(usize, usize), Complex<f64>> =
std::collections::HashMap::new();
for &(i, j, v) in &self.entries {
*merged.entry((i, j)).or_insert(Complex::new(0.0, 0.0)) += v;
}
let mut adjacency: Vec<Vec<(usize, Complex<f64>)>> = vec![Vec::new(); self.n];
for (&(i, j), &v) in &merged {
adjacency[i].push((j, v));
}
for row in &mut adjacency {
row.sort_unstable_by_key(|&(j, _)| j);
}
let matrix = sparse::SparseMatrix::build(self.n, &self.entries)
.expect("Y-bus triplet set should always form a valid sparse matrix");
YBusSparse {
n: self.n,
adjacency,
matrix,
}
}
}
#[derive(Clone)]
pub struct YBusSparse {
n: usize,
adjacency: Vec<Vec<(usize, Complex<f64>)>>,
matrix: sparse::SparseMatrix,
}
impl YBusSparse {
pub fn n(&self) -> usize {
self.n
}
pub fn row(&self, i: usize) -> &[(usize, Complex<f64>)] {
&self.adjacency[i]
}
pub fn get(&self, i: usize, j: usize) -> Complex<f64> {
self.adjacency[i]
.binary_search_by_key(&j, |&(col, _)| col)
.map(|idx| self.adjacency[i][idx].1)
.unwrap_or(Complex::new(0.0, 0.0))
}
pub fn mul_vec(&self, v: &[Complex<f64>]) -> Vec<Complex<f64>> {
self.matrix.mul_vec(v)
}
}
pub fn connected_components(ybus: &YBusSparse) -> Vec<Vec<usize>> {
let n = ybus.n();
let mut visited = vec![false; n];
let mut components = Vec::new();
for start in 0..n {
if visited[start] {
continue;
}
let mut members = Vec::new();
let mut stack = vec![start];
visited[start] = true;
while let Some(i) = stack.pop() {
members.push(i);
for &(j, y) in ybus.row(i) {
if j != i && y != Complex::new(0.0, 0.0) && !visited[j] {
visited[j] = true;
stack.push(j);
}
}
}
members.sort_unstable();
components.push(members);
}
components
}
pub(crate) enum Verdict {
NoReferenceBus,
AmbiguousReferenceBus,
Solvable,
}
pub(crate) struct Classified {
pub(crate) bus_indices: Vec<usize>,
pub(crate) slack_indices: Vec<usize>,
pub(crate) verdict: Verdict,
}
pub(crate) fn classify(buses: &[Bus], components: &[Vec<usize>]) -> Vec<Classified> {
components
.iter()
.map(|members| {
let slack_indices: Vec<usize> = members
.iter()
.copied()
.filter(|&i| buses[i].bus_type == BusType::Slack && buses[i].voltage_mag != 0.0)
.collect();
let verdict = match slack_indices.len() {
0 => Verdict::NoReferenceBus,
1 => Verdict::Solvable,
_ => Verdict::AmbiguousReferenceBus,
};
Classified {
bus_indices: members.clone(),
slack_indices,
verdict,
}
})
.collect()
}
pub(crate) fn mark_unreferenced_islands(buses: &mut [Bus], classified: &[Classified]) {
for c in classified {
if matches!(c.verdict, Verdict::NoReferenceBus) {
for &i in &c.bus_indices {
buses[i].bus_type = BusType::Slack;
buses[i].voltage_mag = 0.0;
buses[i].voltage_ang = 0.0;
buses[i].p_spec = 0.0;
buses[i].q_spec = 0.0;
}
}
}
}
pub fn build_ybus_with_outages(
n: usize,
lines: &[Line],
transformers: &[Transformer],
outaged: &[bool],
) -> YBus {
let out = |idx: usize| outaged.get(idx).copied().unwrap_or(false);
let in_service_lines: Vec<Line> = lines
.iter()
.enumerate()
.filter(|(i, _)| !out(*i))
.map(|(_, l)| l.clone())
.collect();
let in_service_transformers: Vec<Transformer> = transformers
.iter()
.enumerate()
.filter(|(j, _)| !out(lines.len() + j))
.map(|(_, t)| t.clone())
.collect();
let mut y = build_ybus(n, &in_service_lines, &in_service_transformers);
let zero = Complex::new(0.0, 0.0);
let mut stamp_pattern = |from: usize, to: usize| {
y.add(from, from, zero);
if from != to {
y.add(from, to, zero);
y.add(to, from, zero);
y.add(to, to, zero);
}
};
for (i, ln) in lines.iter().enumerate() {
if out(i) {
stamp_pattern(ln.from, ln.to);
}
}
for (j, t) in transformers.iter().enumerate() {
if out(lines.len() + j) {
stamp_pattern(t.from, t.to);
}
}
y
}
pub fn structural_component_count(
n: usize,
lines: &[Line],
transformers: &[Transformer],
outaged: &[bool],
) -> usize {
let out = |idx: usize| outaged.get(idx).copied().unwrap_or(false);
let mut uf = crate::topology::UnionFind::new(n);
for (i, ln) in lines.iter().enumerate() {
if !out(i) && ln.from != ln.to {
uf.union(ln.from, ln.to);
}
}
for (j, t) in transformers.iter().enumerate() {
if !out(lines.len() + j) && t.from != t.to {
uf.union(t.from, t.to);
}
}
let mut roots: Vec<usize> = (0..n).map(|i| uf.find(i)).collect();
roots.sort_unstable();
roots.dedup();
roots.len()
}
pub fn build_ybus(n: usize, lines: &[Line], transformers: &[Transformer]) -> YBus {
let mut y = YBus::new(n);
for ln in lines {
if ln.from == ln.to {
y.add(ln.from, ln.from, Complex::new(ln.g_shunt, ln.b_shunt));
continue;
}
let z = Complex::new(ln.r, ln.x);
let y_line = Complex::new(1.0, 0.0) / z;
let y_shunt_half = Complex::new(ln.g_shunt / 2.0, ln.b_shunt / 2.0);
y.add(ln.from, ln.from, y_line + y_shunt_half);
y.add(ln.to, ln.to, y_line + y_shunt_half);
y.add(ln.from, ln.to, -y_line);
y.add(ln.to, ln.from, -y_line);
}
stamp_transformers(&mut y, transformers);
y
}
pub fn line3ph_blocks(ln: &Line3Ph) -> [[[Complex<f64>; 3]; 3]; 4] {
let y_c1 = Complex::new(ln.g1, ln.b1);
let y_c0 = Complex::new(ln.g0, ln.b0);
let zero = [[Complex::new(0.0, 0.0); 3]; 3];
if ln.from == ln.to {
let m = seq_to_phase_shunt(y_c1, y_c0);
return [m, zero, zero, zero];
}
let y1 = Complex::new(1.0, 0.0) / Complex::new(ln.r1, ln.x1);
let y0 = Complex::new(1.0, 0.0) / Complex::new(ln.r0, ln.x0);
let d_s = (y0 + 2.0 * y1) / 3.0;
let o_s = (y0 - y1) / 3.0;
let d_sh = (y_c0 + 2.0 * y_c1) / 6.0;
let o_sh = (y_c0 - y_c1) / 6.0;
let mut diag = zero;
let mut mutual = zero;
for p in 0..3 {
for q in 0..3 {
let ys = if p == q { d_s } else { o_s };
let ysh = if p == q { d_sh } else { o_sh };
diag[p][q] = ys + ysh;
mutual[p][q] = -ys;
}
}
[diag, mutual, mutual, diag]
}
pub fn transformer3ph_blocks(t: &Transformer3PhSeq) -> [[[Complex<f64>; 3]; 3]; 4] {
[0, 1, 2, 3].map(|i| fortescue_to_phase(t.y0[i], t.y1[i], t.y2[i]))
}
pub fn build_ybus_3ph(n: usize, lines: &[Line3Ph]) -> YBus {
let mut y = YBus::new(3 * n);
for ln in lines {
let [yff, yft, ytf, ytt] = line3ph_blocks(ln);
for (block, (bi, bj)) in [yff, yft, ytf, ytt].iter().zip([
(ln.from, ln.from),
(ln.from, ln.to),
(ln.to, ln.from),
(ln.to, ln.to),
]) {
for p in 0..3 {
for q in 0..3 {
y.add(3 * bi + p, 3 * bj + q, block[p][q]);
}
}
}
}
y
}
pub fn tap_ratio_from_voltages(u_num: f64, u_denom: f64, clock: i32) -> Complex<f64> {
Complex::from_polar(u_num / u_denom, clock as f64 * std::f64::consts::PI / 6.0)
}
pub fn half_open_branch_shunt(y_series: Complex<f64>, y_shunt: Complex<f64>) -> Complex<f64> {
if y_shunt == Complex::new(0.0, 0.0) {
return Complex::new(0.0, 0.0);
}
let one = Complex::new(1.0, 0.0);
y_shunt * 0.5 + one / (one / y_series + Complex::new(2.0, 0.0) / y_shunt)
}
pub fn branch_calc_param(
y_series: Complex<f64>,
y_shunt: Complex<f64>,
tap: Complex<f64>,
from_status: u8,
to_status: u8,
) -> [Complex<f64>; 4] {
let zero = Complex::new(0.0, 0.0);
let k = tap.norm();
match (from_status, to_status) {
(1, 1) => {
let y_diag = y_series + y_shunt * 0.5;
[
y_diag / (k * k),
-y_series / tap.conj(),
-y_series / tap,
y_diag,
]
}
(1, 0) => {
let branch_shunt = half_open_branch_shunt(y_series, y_shunt);
[branch_shunt / (k * k), zero, zero, zero]
}
(0, 1) => {
let branch_shunt = half_open_branch_shunt(y_series, y_shunt);
[zero, zero, zero, branch_shunt]
}
_ => [zero, zero, zero, zero],
}
}
fn stamp_transformers(ybus: &mut YBus, transformers: &[Transformer]) {
for t in transformers {
let [yff, yft, ytf, ytt] =
branch_calc_param(t.y_series, t.y_shunt, t.tap, t.from_status, t.to_status);
ybus.add(t.from, t.from, yff);
ybus.add(t.from, t.to, yft);
ybus.add(t.to, t.from, ytf);
ybus.add(t.to, t.to, ytt);
}
}
fn fortescue_to_phase(
y0: Complex<f64>,
y1: Complex<f64>,
y2: Complex<f64>,
) -> [[Complex<f64>; 3]; 3] {
let one = Complex::new(1.0, 0.0);
let a = Complex::from_polar(1.0, 2.0 * std::f64::consts::PI / 3.0);
let a2 = a * a;
let big_a = [[one, one, one], [one, a2, a], [one, a, a2]];
let a_inv = [[one, one, one], [one, a, a2], [one, a2, a]];
let y_seq = [y0, y1, y2];
let mut out = [[Complex::new(0.0, 0.0); 3]; 3];
for p in 0..3 {
for q in 0..3 {
let mut sum = Complex::new(0.0, 0.0);
for k in 0..3 {
sum += big_a[p][k] * y_seq[k] * a_inv[k][q] / 3.0;
}
out[p][q] = sum;
}
}
out
}
pub const WYE: u8 = 0;
pub const WYE_N: u8 = 1;
pub const DELTA: u8 = 2;
pub const ZIGZAG: u8 = 3;
pub const ZIGZAG_N: u8 = 4;
fn zero_seq_available(this_side: u8, other_side: u8) -> bool {
match this_side {
WYE_N => other_side == WYE_N || other_side == DELTA,
ZIGZAG_N => true,
_ => false,
}
}
pub fn transformer_seq_params(
y_series: Complex<f64>,
y_shunt: Complex<f64>,
tap: Complex<f64>,
from_status: u8,
to_status: u8,
winding_from: u8,
winding_to: u8,
sn: f64,
uk: f64,
s_base_va: f64,
clock: i32,
) -> ([Complex<f64>; 4], [Complex<f64>; 4], [Complex<f64>; 4]) {
let zero = Complex::new(0.0, 0.0);
let y1 = branch_calc_param(y_series, y_shunt, tap, from_status, to_status);
let y2 = branch_calc_param(y_series, y_shunt, tap.conj(), from_status, to_status);
let k = tap.norm();
let y0_shunt = y_shunt;
let mut y0 = [zero; 4];
if winding_from == WYE_N && winding_to == WYE_N {
let phase_shift_0 = if matches!(clock, 2 | 6 | 10) {
std::f64::consts::PI
} else {
0.0
};
let tap0 = Complex::from_polar(k, phase_shift_0);
y0 = branch_calc_param(y_series, y0_shunt, tap0, from_status, to_status);
} else if winding_from == WYE_N && from_status == 1 {
let mut y = y0_shunt;
if winding_to == DELTA {
y += y_series;
}
if y != zero {
y0[0] = y / (k * k);
}
} else if winding_to == WYE_N && to_status == 1 {
let mut y = y0_shunt;
if winding_from == DELTA {
y += y_series;
}
if y != zero {
y0[3] = y;
}
}
if winding_from == ZIGZAG_N && from_status == 1 {
y0[0] = y_series * 10.0 / (k * k);
}
if winding_to == ZIGZAG_N && to_status == 1 {
y0[3] = y_series * 10.0;
}
let low_susceptance = -1e-8 * sn / s_base_va / uk;
if !zero_seq_available(winding_from, winding_to) && from_status == 1 {
y0[0] += Complex::new(0.0, low_susceptance);
}
if !zero_seq_available(winding_to, winding_from) && to_status == 1 {
y0[3] += Complex::new(0.0, low_susceptance);
}
(y0, y1, y2)
}
pub fn stamp_transformers_3ph(ybus: &mut YBus, transformers: &[Transformer3PhSeq]) {
for t in transformers {
let blocks = transformer3ph_blocks(t);
let ends = [
(t.from, t.from),
(t.from, t.to),
(t.to, t.from),
(t.to, t.to),
];
for (block, (bi, bj)) in blocks.iter().zip(ends) {
for p in 0..3 {
for q in 0..3 {
ybus.add(3 * bi + p, 3 * bj + q, block[p][q]);
}
}
}
}
}
pub fn stamp_shunts(ybus: &mut YBus, shunts: &[ShuntAdm]) {
for s in shunts {
ybus.add(s.at, s.at, s.y);
}
}
pub fn stamp_shunts_3ph(ybus: &mut YBus, shunts: &[ShuntAdm3Ph]) {
for s in shunts {
let m = seq_to_phase_shunt(s.y1, s.y0);
for p in 0..3 {
for q in 0..3 {
ybus.add(3 * s.at + p, 3 * s.at + q, m[p][q]);
}
}
}
}
pub fn source_impedance_pu(sk: f64, rx_ratio: f64, s_base_va: f64) -> (f64, f64) {
let z_s_pu = s_base_va / sk;
let x_s = z_s_pu / (rx_ratio * rx_ratio + 1.0_f64).sqrt();
(rx_ratio * x_s, x_s)
}
pub fn source_impedance_pu_seq(
sk: f64,
rx_ratio: f64,
z01_ratio: f64,
s_base_va: f64,
) -> (f64, f64, f64, f64) {
let (r1, x1) = source_impedance_pu(sk, rx_ratio, s_base_va);
let (r0, x0) = (r1 * z01_ratio, x1 * z01_ratio);
(r1, x1, r0, x0)
}
pub fn transformer_tap(
u1: f64,
u2: f64,
tap_side: u8,
tap_pos: i32,
tap_min: i32,
tap_max: i32,
tap_nom: i32,
tap_size: f64,
clock: i32,
u1_rated: f64,
u2_rated: f64,
) -> Complex<f64> {
let tap_pos = tap_pos.clamp(tap_min.min(tap_max), tap_min.max(tap_max));
let delta = (tap_pos - tap_nom) as f64 * tap_size;
let (u1_tapped, u2_tapped) = if tap_side == 0 {
(u1 + delta, u2)
} else {
(u1, u2 + delta)
};
tap_ratio_from_voltages(u1_tapped * u2_rated, u2_tapped * u1_rated, clock)
}
pub fn transformer_admittances_ex(
u_field: f64,
u_base: f64,
sn: f64,
uk: f64,
pk: f64,
i0: f64,
p0: f64,
s_base_va: f64,
) -> (Complex<f64>, Complex<f64>) {
let base_y_to = s_base_va / (u_base * u_base);
let uk_sign = if uk >= 0.0 { 1.0 } else { -1.0 };
let z_abs = uk.abs() * u_field * u_field / sn;
let r_ohm = pk * u_field * u_field / (sn * sn);
let x_sq = z_abs * z_abs - r_ohm * r_ohm;
let x_ohm = uk_sign * if x_sq > 0.0 { x_sq.sqrt() } else { 0.0 };
let y_series = Complex::new(1.0, 0.0) / Complex::new(r_ohm, x_ohm) / base_y_to;
let g_fe = p0 / (u_field * u_field);
let y_sh_abs = i0 * sn / (u_field * u_field);
let b_sq = y_sh_abs * y_sh_abs - g_fe * g_fe;
let b_m = if b_sq > 0.0 { -b_sq.sqrt() } else { 0.0 };
let y_shunt = Complex::new(g_fe, b_m) / base_y_to;
(y_series, y_shunt)
}
pub fn transformer_admittances(
u2: f64,
sn: f64,
uk: f64,
pk: f64,
i0: f64,
p0: f64,
s_base_va: f64,
) -> (Complex<f64>, Complex<f64>) {
transformer_admittances_ex(u2, u2, sn, uk, pk, i0, p0, s_base_va)
}
pub fn three_winding_star_params(
sn_1: f64,
sn_2: f64,
sn_3: f64,
uk_12: f64,
uk_13: f64,
uk_23: f64,
pk_12: f64,
pk_13: f64,
pk_23: f64,
) -> ((f64, f64, f64), (f64, f64, f64)) {
let uk_12r = uk_12 * sn_1 / sn_1.min(sn_2);
let uk_13r = uk_13 * sn_1 / sn_1.min(sn_3);
let uk_23r = uk_23 * sn_1 / sn_2.min(sn_3);
let uk_t1p = 0.5 * (uk_12r + uk_13r - uk_23r);
let uk_t2p = 0.5 * (uk_12r + uk_23r - uk_13r);
let uk_t3p = 0.5 * (uk_13r + uk_23r - uk_12r);
let uk = (uk_t1p, uk_t2p * (sn_2 / sn_1), uk_t3p * (sn_3 / sn_1));
let pk_12r = pk_12 * (sn_1 / sn_1.min(sn_2)).powi(2);
let pk_13r = pk_13 * (sn_1 / sn_1.min(sn_3)).powi(2);
let pk_23r = pk_23 * (sn_1 / sn_2.min(sn_3)).powi(2);
let pk_t1p = 0.5 * (pk_12r + pk_13r - pk_23r);
let pk_t2p = 0.5 * (pk_12r + pk_23r - pk_13r);
let pk_t3p = 0.5 * (pk_13r + pk_23r - pk_12r);
let pk = (
pk_t1p,
pk_t2p * (sn_2 / sn_1).powi(2),
pk_t3p * (sn_3 / sn_1).powi(2),
);
(uk, pk)
}
pub fn effective_injection(bus: &Bus) -> (f64, f64) {
let mut s = Complex::new(bus.p_spec, bus.q_spec);
let vmag = bus.voltage_mag;
for zt in &bus.zip_terms {
s += match zt.kind {
ZipKind::ConstPower => zt.s_const,
ZipKind::ConstImpedance => zt.s_const * vmag * vmag,
ZipKind::ConstCurrent => zt.s_const * vmag,
};
}
(s.re, s.im)
}
pub fn solve_constant_admittance(
buses: &mut [Bus],
ybus: &YBusSparse,
unknown_idx: &[usize],
) -> bool {
let m = unknown_idx.len();
if m == 0 {
return true;
}
let mut reduced_pos: Vec<Option<usize>> = vec![None; buses.len()];
for (r, &i) in unknown_idx.iter().enumerate() {
reduced_pos[i] = Some(r);
}
let mut triplets: Vec<(usize, usize, Complex<f64>)> = Vec::new();
let mut rhs = vec![Complex::new(0.0, 0.0); m];
for (r, &i) in unknown_idx.iter().enumerate() {
let (p, q) = effective_injection(&buses[i]);
let y_load = -Complex::new(p, q).conj();
let mut diag_seen = false;
for &(j, y_ij) in ybus.row(i) {
let y_ij = if j == i {
diag_seen = true;
y_ij + y_load
} else {
y_ij
};
match reduced_pos[j] {
Some(c) => triplets.push((r, c, y_ij)),
None => {
let u_j = Complex::from_polar(buses[j].voltage_mag, buses[j].voltage_ang);
rhs[r] -= y_ij * u_j;
}
}
}
if !diag_seen {
triplets.push((r, r, y_load));
}
}
match sparse::solve_complex(m, &triplets, &rhs) {
Some(sol) => {
for (r, &i) in unknown_idx.iter().enumerate() {
buses[i].voltage_mag = sol[r].norm();
buses[i].voltage_ang = sol[r].arg();
}
true
}
None => false,
}
}
pub fn linear_initial_guess(buses: &mut [Bus], ybus: &YBusSparse) {
let n = buses.len();
let unknown_idx: Vec<usize> = (0..n)
.filter(|&i| matches!(buses[i].bus_type, BusType::PQ))
.collect();
solve_constant_admittance(buses, ybus, &unknown_idx);
}
#[derive(Clone, Debug, Default)]
pub struct BusVoltages {
pub vm: Vec<f64>,
pub sin: Vec<f64>,
pub cos: Vec<f64>,
}
impl BusVoltages {
pub fn of(buses: &[Bus]) -> Self {
let mut v = Self::default();
v.update(buses);
v
}
pub fn update(&mut self, buses: &[Bus]) {
self.vm.clear();
self.sin.clear();
self.cos.clear();
for b in buses {
let (s, c) = b.voltage_ang.sin_cos();
self.vm.push(b.voltage_mag);
self.sin.push(s);
self.cos.push(c);
}
}
}
pub fn power_injections(buses: &[Bus], ybus: &YBusSparse) -> (Vec<f64>, Vec<f64>) {
power_injections_from(&BusVoltages::of(buses), ybus)
}
pub fn power_injections_from(v: &BusVoltages, ybus: &YBusSparse) -> (Vec<f64>, Vec<f64>) {
let n = v.vm.len();
let mut p = vec![0.0; n];
let mut q = vec![0.0; n];
let v: Vec<Complex<f64>> = (0..n)
.map(|k| Complex::new(v.vm[k] * v.cos[k], v.vm[k] * v.sin[k]))
.collect();
let i = ybus.mul_vec(&v);
for k in 0..n {
let s = v[k] * i[k].conj();
p[k] = s.re;
q[k] = s.im;
}
(p, q)
}
#[cfg(test)]
mod tests {
use super::*;
fn seq_fixture() -> (Complex<f64>, Complex<f64>, Complex<f64>) {
let y_series = Complex::new(2.0, -6.0);
let y_shunt = Complex::new(1e-4, -5e-4);
let tap = Complex::from_polar(1.05, std::f64::consts::PI / 6.0);
(y_series, y_shunt, tap)
}
fn seq_params_for(winding_from: u8, winding_to: u8, clock: i32) -> [Complex<f64>; 4] {
let (y_series, y_shunt, tap) = seq_fixture();
let (y0, _, _) = transformer_seq_params(
y_series,
y_shunt,
tap,
1,
1,
winding_from,
winding_to,
2e7,
0.05,
1e6,
clock,
);
y0
}
#[test]
fn ynd_gets_a_from_side_zero_sequence_path() {
let (y_series, y_shunt, tap) = seq_fixture();
let k = tap.norm();
let y0 = seq_params_for(WYE_N, DELTA, 1);
let expected_yff = (y_shunt + y_series) / (k * k);
assert!(
(y0[0] - expected_yff).norm() < 1e-15,
"yff = {}, want {expected_yff}",
y0[0]
);
let low = -1e-8 * 2e7 / 1e6 / 0.05;
assert!(
(y0[3] - Complex::new(0.0, low)).norm() < 1e-18,
"ytt = {}",
y0[3]
);
assert_eq!(y0[1], Complex::new(0.0, 0.0));
assert_eq!(y0[2], Complex::new(0.0, 0.0));
}
#[test]
fn dyn_is_the_mirror_of_ynd() {
let (y_series, y_shunt, _) = seq_fixture();
let y0 = seq_params_for(DELTA, WYE_N, 5);
assert!(
(y0[3] - (y_shunt + y_series)).norm() < 1e-15,
"ytt = {}",
y0[3]
);
let low = -1e-8 * 2e7 / 1e6 / 0.05;
assert!(
(y0[0] - Complex::new(0.0, low)).norm() < 1e-18,
"yff = {}",
y0[0]
);
}
#[test]
fn ynyn_flips_the_zero_sequence_tap_when_reverse_connected() {
let straight = seq_params_for(WYE_N, WYE_N, 0);
let reversed = seq_params_for(WYE_N, WYE_N, 6);
assert!((straight[0] - reversed[0]).norm() < 1e-15);
assert!((straight[3] - reversed[3]).norm() < 1e-15);
assert!(
(straight[1] + reversed[1]).norm() < 1e-15,
"{} vs {}",
straight[1],
reversed[1]
);
assert!(
(straight[2] + reversed[2]).norm() < 1e-15,
"{} vs {}",
straight[2],
reversed[2]
);
assert!(straight[1].norm() > 0.0);
}
#[test]
fn a_pair_with_no_zero_sequence_path_is_not_left_floating() {
let y0 = seq_params_for(WYE, DELTA, 1);
let low = -1e-8 * 2e7 / 1e6 / 0.05;
assert!(
(y0[0] - Complex::new(0.0, low)).norm() < 1e-18,
"yff = {}",
y0[0]
);
assert!(
(y0[3] - Complex::new(0.0, low)).norm() < 1e-18,
"ytt = {}",
y0[3]
);
}
#[test]
fn zigzag_n_carries_ten_times_the_series_admittance() {
let (y_series, _, _) = seq_fixture();
let y0 = seq_params_for(WYE, ZIGZAG_N, 1);
assert!((y0[3] - y_series * 10.0).norm() < 1e-15, "ytt = {}", y0[3]);
}
#[test]
fn an_open_terminal_contributes_no_zero_sequence_term() {
let (y_series, y_shunt, tap) = seq_fixture();
let (y0, _, _) = transformer_seq_params(
y_series, y_shunt, tap, 1, 0, WYE_N, DELTA, 2e7, 0.05, 1e6, 1,
);
assert_eq!(y0[3], Complex::new(0.0, 0.0));
}
fn bus_at(idx: usize, bus_type: BusType, voltage_mag: f64) -> Bus {
Bus {
idx,
bus_type,
voltage_mag,
voltage_ang: 0.0,
p_spec: 0.0,
q_spec: 0.0,
q_min: 0.0,
q_max: 0.0,
u_rated: 0.0,
zip_terms: Vec::new(),
}
}
#[test]
fn a_de_energized_placeholder_is_not_a_reference_bus() {
let buses = vec![
bus_at(0, BusType::Slack, 0.0), bus_at(1, BusType::PQ, 1.0),
bus_at(2, BusType::Slack, 1.05), bus_at(3, BusType::PQ, 1.0),
];
let components = vec![vec![0, 1], vec![2, 3]];
let classified = classify(&buses, &components);
assert!(
matches!(classified[0].verdict, Verdict::NoReferenceBus),
"a component whose only slack sits at V = 0 has no reference"
);
assert!(classified[0].slack_indices.is_empty());
assert!(matches!(classified[1].verdict, Verdict::Solvable));
assert_eq!(classified[1].slack_indices, vec![2]);
let mut buses = buses;
mark_unreferenced_islands(&mut buses, &classified);
assert_eq!(buses[1].bus_type, BusType::Slack);
assert_eq!(buses[1].voltage_mag, 0.0);
assert_eq!(
buses[3].bus_type,
BusType::PQ,
"the referenced island is untouched"
);
}
#[test]
fn a_real_reference_beside_a_placeholder_still_counts() {
let buses = vec![
bus_at(0, BusType::Slack, 0.0),
bus_at(1, BusType::Slack, 1.05),
bus_at(2, BusType::PQ, 1.0),
];
let classified = classify(&buses, &[vec![0, 1, 2]]);
assert!(matches!(classified[0].verdict, Verdict::Solvable));
assert_eq!(classified[0].slack_indices, vec![1]);
}
#[test]
fn tap_ratio_accounts_for_nameplate_versus_node_rating() {
let k = transformer_tap(10750.0, 420.0, 0, 3, 5, 1, 3, 250.0, 0, 10500.0, 420.0);
assert!(
(k.norm() - 10750.0 / 10500.0).abs() < 1e-12,
"expected the 2.4% off-nominal ratio, got {}",
k.norm()
);
}
#[test]
fn a_matched_transformer_at_nominal_tap_is_unity() {
let k = transformer_tap(
110000.0, 10500.0, 0, 0, -12, 12, 0, 1320.0, 0, 110000.0, 10500.0,
);
assert!((k.norm() - 1.0).abs() < 1e-12, "got {}", k.norm());
}
#[test]
fn the_tap_position_shifts_the_tapped_side() {
let k = transformer_tap(
110000.0, 10500.0, 0, -3, -12, 12, 0, 1320.0, 0, 110000.0, 10500.0,
);
assert!(
(k.norm() - (110000.0 - 3960.0) / 110000.0).abs() < 1e-12,
"got {}",
k.norm()
);
let k = transformer_tap(
110000.0, 10500.0, 1, -3, -12, 12, 0, 100.0, 0, 110000.0, 10500.0,
);
assert!(
(k.norm() - 10500.0 / (10500.0 - 300.0)).abs() < 1e-12,
"got {}",
k.norm()
);
}
#[test]
fn the_clock_still_sets_the_phase() {
let k = transformer_tap(10750.0, 420.0, 0, 3, 5, 1, 3, 250.0, 5, 10500.0, 420.0);
assert!(
(k.arg() - 5.0 * std::f64::consts::PI / 6.0).abs() < 1e-12,
"got {}",
k.arg()
);
}
#[test]
fn connected_components_single_cluster() {
let mut y = YBus::new(3);
y.add(0, 1, Complex::new(1.0, -2.0));
y.add(1, 0, Complex::new(1.0, -2.0));
y.add(1, 2, Complex::new(1.0, -2.0));
y.add(2, 1, Complex::new(1.0, -2.0));
let ybus = y.finish();
let components = connected_components(&ybus);
assert_eq!(components, vec![vec![0, 1, 2]]);
}
#[test]
fn connected_components_multiple_clusters() {
let mut y = YBus::new(5);
y.add(0, 1, Complex::new(1.0, -2.0));
y.add(1, 0, Complex::new(1.0, -2.0));
y.add(2, 2, Complex::new(0.0, 1e-6)); y.add(3, 4, Complex::new(1.0, -2.0));
y.add(4, 3, Complex::new(1.0, -2.0));
let ybus = y.finish();
let components = connected_components(&ybus);
assert_eq!(components, vec![vec![0, 1], vec![2], vec![3, 4]]);
}
#[test]
fn connected_components_bus_with_no_ybus_entries_at_all() {
let mut y = YBus::new(2);
y.add(0, 0, Complex::new(0.0, 1e-6));
let ybus = y.finish();
let components = connected_components(&ybus);
assert_eq!(components, vec![vec![0], vec![1]]);
}
fn test_bus(idx: usize, bus_type: BusType) -> Bus {
Bus {
idx,
bus_type,
voltage_mag: 1.0,
voltage_ang: 0.0,
p_spec: 0.3,
q_spec: 0.1,
q_min: -f64::INFINITY,
q_max: f64::INFINITY,
u_rated: 0.0,
zip_terms: Vec::new(),
}
}
#[test]
fn classify_exactly_one_slack_is_solvable() {
let buses = vec![test_bus(0, BusType::Slack), test_bus(1, BusType::PQ)];
let classified = classify(&buses, &[vec![0, 1]]);
assert_eq!(classified.len(), 1);
assert!(matches!(classified[0].verdict, Verdict::Solvable));
assert_eq!(classified[0].slack_indices, vec![0]);
}
#[test]
fn classify_zero_slack_is_no_reference_bus() {
let buses = vec![test_bus(0, BusType::PQ), test_bus(1, BusType::PV)];
let classified = classify(&buses, &[vec![0, 1]]);
assert!(matches!(classified[0].verdict, Verdict::NoReferenceBus));
assert!(classified[0].slack_indices.is_empty());
}
#[test]
fn classify_two_slack_is_ambiguous() {
let buses = vec![test_bus(0, BusType::Slack), test_bus(1, BusType::Slack)];
let classified = classify(&buses, &[vec![0, 1]]);
assert!(matches!(
classified[0].verdict,
Verdict::AmbiguousReferenceBus
));
assert_eq!(classified[0].slack_indices, vec![0, 1]);
}
#[test]
fn mark_unreferenced_islands_zeroes_no_reference_component_only() {
let mut buses = vec![
test_bus(0, BusType::PQ), test_bus(1, BusType::PV), test_bus(2, BusType::Slack), test_bus(3, BusType::PQ), ];
let components = vec![vec![0, 1], vec![2, 3]];
let classified = classify(&buses, &components);
mark_unreferenced_islands(&mut buses, &classified);
for &i in &[0, 1] {
assert_eq!(buses[i].bus_type, BusType::Slack);
assert_eq!(buses[i].voltage_mag, 0.0);
assert_eq!(buses[i].voltage_ang, 0.0);
assert_eq!(buses[i].p_spec, 0.0);
assert_eq!(buses[i].q_spec, 0.0);
}
assert_eq!(buses[2].bus_type, BusType::Slack);
assert_eq!(buses[3].bus_type, BusType::PQ);
assert_eq!(buses[3].p_spec, 0.3);
assert_eq!(buses[3].q_spec, 0.1);
}
#[test]
fn mark_unreferenced_islands_leaves_ambiguous_component_untouched() {
let mut buses = vec![
test_bus(0, BusType::Slack),
test_bus(1, BusType::Slack),
test_bus(2, BusType::PQ),
];
let components = vec![vec![0, 1, 2]];
let classified = classify(&buses, &components);
mark_unreferenced_islands(&mut buses, &classified);
assert_eq!(buses[2].bus_type, BusType::PQ);
assert_eq!(buses[2].p_spec, 0.3);
}
}