use num_complex::Complex;
use super::types::{Bus, BusType, Line, Line3Ph, Transformer, Transformer3PhSeq, ZipKind};
use super::sparse;
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 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 }
}
}
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, _) in ybus.row(i) {
if j != i && !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)
.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(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(0.0, ln.b1);
let y_c0 = Complex::new(0.0, 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 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 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 y0 = match (winding_from, winding_to) {
(2, 1) => {
let mut y0 = [Complex::new(0.0, 0.0); 4];
if to_status == 1 {
y0[3] = y_shunt + y_series;
}
if from_status == 1 {
let low_susceptance = -1e-8 * sn / s_base_va / uk;
y0[0] += Complex::new(0.0, low_susceptance);
}
y0
}
(1, 1) => {
let phase_shift_0 = if matches!(clock, 2 | 6 | 10) { std::f64::consts::PI } else { 0.0 };
let k = tap.norm();
let tap0 = Complex::from_polar(k, phase_shift_0);
branch_calc_param(y_series, y_shunt, tap0, from_status, to_status)
}
_ => panic!(
"transformer_seq_params only supports Dyn (winding_from=2, winding_to=1) or YNyn (winding_from=1, winding_to=1); got ({winding_from}, {winding_to})"
),
};
(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 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();
if unknown_idx.is_empty() {
return;
}
let m = unknown_idx.len();
let mut reduced_pos: Vec<Option<usize>> = vec![None; n];
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));
}
}
if let Some(sol) = sparse::solve_complex(m, &triplets, &rhs) {
for (r, &i) in unknown_idx.iter().enumerate() {
buses[i].voltage_mag = sol[r].norm();
buses[i].voltage_ang = sol[r].arg();
}
}
}
pub fn power_injections(
buses: &[Bus],
ybus: &YBusSparse,
) -> (Vec<f64>, Vec<f64>) {
let n = buses.len();
let mut p = vec![0.0; n];
let mut q = vec![0.0; n];
let v: Vec<Complex<f64>> = buses.iter().map(|b| Complex::from_polar(b.voltage_mag, b.voltage_ang)).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::*;
#[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);
}
}