use crate::network::{
Verdict, YBusSparse, classify, connected_components, mark_unreferenced_islands,
solve_constant_admittance,
};
use crate::types::{Bus, BusType};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinearIslandStatus {
Solved,
Singular,
NoReferenceBus,
SolvedMultipleReferences,
}
#[derive(Clone, Debug)]
pub struct LinearIslandReport {
pub bus_indices: Vec<usize>,
pub slack_indices: Vec<usize>,
pub status: LinearIslandStatus,
}
#[derive(Clone, Debug)]
pub struct LinearReport {
pub islands: Vec<LinearIslandReport>,
}
pub fn linear_power_flow(buses: &mut [Bus], ybus: &YBusSparse) -> LinearReport {
let components = connected_components(ybus);
let classified = classify(buses, &components);
mark_unreferenced_islands(buses, &classified);
let mut islands = Vec::with_capacity(classified.len());
for c in &classified {
if matches!(c.verdict, Verdict::NoReferenceBus) {
islands.push(LinearIslandReport {
bus_indices: c.bus_indices.clone(),
slack_indices: c.slack_indices.clone(),
status: LinearIslandStatus::NoReferenceBus,
});
continue;
}
let unknown_idx: Vec<usize> = c
.bus_indices
.iter()
.copied()
.filter(|&i| buses[i].bus_type == BusType::PQ)
.collect();
let ok = solve_constant_admittance(buses, ybus, &unknown_idx);
let status = if !ok {
LinearIslandStatus::Singular
} else if c.slack_indices.len() > 1 {
LinearIslandStatus::SolvedMultipleReferences
} else {
LinearIslandStatus::Solved
};
islands.push(LinearIslandReport {
bus_indices: c.bus_indices.clone(),
slack_indices: c.slack_indices.clone(),
status,
});
}
LinearReport { islands }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::build_ybus;
use crate::types::Line;
use num_complex::Complex;
fn bus(idx: usize, bus_type: BusType, p_spec: f64, q_spec: f64) -> Bus {
Bus {
idx,
bus_type,
voltage_mag: 1.0,
voltage_ang: 0.0,
p_spec,
q_spec,
q_min: 0.0,
q_max: 0.0,
u_rated: 0.0,
zip_terms: Vec::new(),
}
}
#[test]
fn a_constant_impedance_load_is_reproduced_exactly() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, -0.2, -0.1),
];
let lines = vec![Line {
from: 0,
to: 1,
r: 0.02,
x: 0.06,
b_shunt: 0.0,
g_shunt: 0.0,
}];
let ybus = build_ybus(2, &lines, &[]).finish();
let report = linear_power_flow(&mut buses, &ybus);
assert_eq!(report.islands[0].status, LinearIslandStatus::Solved);
let u: Vec<Complex<f64>> = buses
.iter()
.map(|b| Complex::from_polar(b.voltage_mag, b.voltage_ang))
.collect();
let i = ybus.mul_vec(&u);
let s1 = u[1] * i[1].conj();
let expected = Complex::new(-0.2, -0.1) * buses[1].voltage_mag.powi(2);
assert!((s1 - expected).norm() < 1e-12, "{s1} vs {expected}");
}
#[test]
fn load_depresses_voltage_and_lags_the_source() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, -0.5, -0.2),
];
let lines = vec![Line {
from: 0,
to: 1,
r: 0.02,
x: 0.06,
b_shunt: 0.0,
g_shunt: 0.0,
}];
let ybus = build_ybus(2, &lines, &[]).finish();
linear_power_flow(&mut buses, &ybus);
assert!(buses[1].voltage_mag < 1.0, "{}", buses[1].voltage_mag);
assert!(buses[1].voltage_ang < 0.0, "{}", buses[1].voltage_ang);
}
#[test]
fn sourceless_island_is_reported_not_invented() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, -0.2, 0.0),
bus(2, BusType::PQ, -0.1, 0.0),
bus(3, BusType::PQ, 0.1, 0.0),
];
let lines = vec![
Line {
from: 0,
to: 1,
r: 0.02,
x: 0.06,
b_shunt: 0.0,
g_shunt: 0.0,
},
Line {
from: 2,
to: 3,
r: 0.02,
x: 0.06,
b_shunt: 0.0,
g_shunt: 0.0,
},
];
let ybus = build_ybus(4, &lines, &[]).finish();
let report = linear_power_flow(&mut buses, &ybus);
assert_eq!(report.islands.len(), 2);
assert_eq!(report.islands[0].status, LinearIslandStatus::Solved);
assert_eq!(report.islands[1].status, LinearIslandStatus::NoReferenceBus);
assert_eq!(buses[2].voltage_mag, 0.0);
}
#[test]
fn agrees_with_the_warm_start_entry_point() {
let lines = vec![
Line {
from: 0,
to: 1,
r: 0.02,
x: 0.06,
b_shunt: 0.03,
g_shunt: 0.0,
},
Line {
from: 1,
to: 2,
r: 0.04,
x: 0.10,
b_shunt: 0.02,
g_shunt: 0.0,
},
];
let ybus = build_ybus(3, &lines, &[]).finish();
let make = || {
vec![
bus(0, BusType::Slack, 0.0, 0.0),
bus(1, BusType::PQ, -0.3, -0.1),
bus(2, BusType::PQ, -0.2, -0.05),
]
};
let mut via_mode = make();
linear_power_flow(&mut via_mode, &ybus);
let mut via_warm_start = make();
crate::network::linear_initial_guess(&mut via_warm_start, &ybus);
for i in 0..3 {
assert!(
(via_mode[i].voltage_mag - via_warm_start[i].voltage_mag).abs() < 1e-15
&& (via_mode[i].voltage_ang - via_warm_start[i].voltage_ang).abs() < 1e-15,
"bus {i} disagrees"
);
}
}
}