use crate::jacobian::JacobianPattern;
use crate::network::{YBusSparse, effective_injection, power_injections};
use crate::solver::{LinearSolver, SolveStats, SolveStatus};
use crate::types::{Bus, BusType};
use gridoxide_sparse::sparse::RealSparseSystem;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConstrainedSwitch {
pub from: usize,
pub to: usize,
pub open: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RowUse {
Tie,
Pin,
}
#[derive(Clone, Debug)]
pub struct ConstrainedSolution {
pub stats: SolveStats,
pub flows: Vec<(f64, f64)>,
pub indeterminate: Vec<usize>,
}
pub fn solve_constrained(
buses: &mut [Bus],
ybus: &YBusSparse,
switches: &[ConstrainedSwitch],
tol: f64,
max_iter: usize,
) -> ConstrainedSolution {
solve_constrained_with::<RealSparseSystem>(buses, ybus, switches, tol, max_iter)
}
pub fn solve_constrained_with<S: LinearSolver>(
buses: &mut [Bus],
ybus: &YBusSparse,
switches: &[ConstrainedSwitch],
tol: f64,
max_iter: usize,
) -> ConstrainedSolution {
let n_sw = switches.len();
let empty = ConstrainedSolution {
stats: SolveStats::from_loop(SolveStatus::Singular, Vec::new()),
flows: vec![(0.0, 0.0); n_sw],
indeterminate: Vec::new(),
};
if switches
.iter()
.any(|s| s.from >= buses.len() || s.to >= buses.len())
{
return empty;
}
crate::network::mark_unreferenced_islands(
buses,
&crate::network::classify(
buses,
&components_across_switches(buses.len(), ybus, switches),
),
);
let mut dsu_angle: Vec<usize> = (0..buses.len()).collect();
let mut dsu_mag: Vec<usize> = (0..buses.len()).collect();
seed_fixed(&mut dsu_angle, buses, has_angle);
seed_fixed(&mut dsu_mag, buses, has_magnitude);
let mut rows = Vec::with_capacity(n_sw);
let mut indeterminate = Vec::new();
for (k, sw) in switches.iter().enumerate() {
let (i, j) = (sw.from, sw.to);
let connects = !sw.open && i != j;
let use_p = tie_or_pin(&mut dsu_angle, connects, i, j);
let use_q = tie_or_pin(&mut dsu_mag, connects, i, j);
if !sw.open && (use_p == RowUse::Pin || use_q == RowUse::Pin) {
indeterminate.push(k);
}
rows.push((use_p, use_q));
}
let non_slack_idx: Vec<usize> = buses
.iter()
.filter(|b| b.bus_type != BusType::Slack)
.map(|b| b.idx)
.collect();
let pq_idx: Vec<usize> = buses
.iter()
.filter(|b| b.bus_type == BusType::PQ)
.map(|b| b.idx)
.collect();
let n_angle = non_slack_idx.len();
let n_base = n_angle + pq_idx.len();
let n = n_base + 2 * n_sw;
let mut angle_col = vec![None; buses.len()];
for (pos, &i) in non_slack_idx.iter().enumerate() {
angle_col[i] = Some(pos);
}
let mut mag_col = vec![None; buses.len()];
for (pos, &i) in pq_idx.iter().enumerate() {
mag_col[i] = Some(n_angle + pos);
}
let mut warm = crate::network::YBus::new(buses.len());
for i in 0..buses.len() {
for &(j, y) in ybus.row(i) {
warm.add(i, j, y);
}
}
let y_switch = crate::switches::switch_admittance();
for sw in switches.iter().filter(|s| !s.open && s.from != s.to) {
warm.add(sw.from, sw.from, y_switch);
warm.add(sw.to, sw.to, y_switch);
warm.add(sw.from, sw.to, -y_switch);
warm.add(sw.to, sw.from, -y_switch);
}
crate::network::linear_initial_guess(buses, &warm.finish());
let pattern = JacobianPattern::analyze(buses, ybus);
let mut values: Vec<f64> = Vec::with_capacity(pattern.len());
let mut flows = vec![(0.0, 0.0); n_sw];
let mut history = Vec::with_capacity(max_iter);
let mut system: Option<S> = None;
for _ in 0..max_iter {
let (p_calc, q_calc) = power_injections(buses, ybus);
let mut rhs = vec![0.0; n];
for (row, &i) in non_slack_idx.iter().enumerate() {
rhs[row] = effective_injection(&buses[i]).0 - p_calc[i];
}
for (row, &i) in pq_idx.iter().enumerate() {
rhs[n_angle + row] = effective_injection(&buses[i]).1 - q_calc[i];
}
for (k, sw) in switches.iter().enumerate() {
let (p_s, q_s) = flows[k];
for (bus, sign) in [(sw.from, 1.0), (sw.to, -1.0)] {
if let Some(row) = angle_col[bus] {
rhs[row] -= sign * p_s;
}
if let Some(row) = mag_col[bus] {
rhs[row] -= sign * q_s;
}
}
}
for (k, sw) in switches.iter().enumerate() {
let (use_p, use_q) = rows[k];
rhs[n_base + 2 * k] = -match use_p {
RowUse::Tie => buses[sw.from].voltage_ang - buses[sw.to].voltage_ang,
RowUse::Pin => flows[k].0,
};
rhs[n_base + 2 * k + 1] = -match use_q {
RowUse::Tie => buses[sw.from].voltage_mag - buses[sw.to].voltage_mag,
RowUse::Pin => flows[k].1,
};
}
let max_mis = rhs.iter().fold(0.0f64, |a, &b| a.max(b.abs()));
history.push(max_mis);
if max_mis < tol {
return ConstrainedSolution {
stats: SolveStats::from_loop(SolveStatus::Converged, history),
flows,
indeterminate,
};
}
pattern.fill(buses, &p_calc, &q_calc, &mut values);
let mut triplets = pattern.to_triplets(&values);
for (k, sw) in switches.iter().enumerate() {
let (p_col, q_col) = (n_base + 2 * k, n_base + 2 * k + 1);
let (p_row, q_row) = (n_base + 2 * k, n_base + 2 * k + 1);
let (use_p, use_q) = rows[k];
for (bus, sign) in [(sw.from, 1.0), (sw.to, -1.0)] {
if let Some(row) = angle_col[bus] {
triplets.push((row, p_col, sign));
}
if let Some(row) = mag_col[bus] {
triplets.push((row, q_col, sign));
}
}
for (row, col_of, flow_col, active) in [
(p_row, &angle_col, p_col, use_p),
(q_row, &mag_col, q_col, use_q),
] {
let tie = f64::from(active == RowUse::Tie);
for (bus, sign) in [(sw.from, 1.0), (sw.to, -1.0)] {
if let Some(c) = col_of[bus] {
triplets.push((row, c, sign * tie));
}
}
triplets.push((row, flow_col, 1.0 - tie));
}
}
if system.is_none() {
system = S::new(n, &triplets);
}
let Some(sys) = system.as_mut() else {
return ConstrainedSolution {
stats: SolveStats::from_loop(SolveStatus::Singular, history),
flows,
indeterminate,
};
};
let Some(dx) = sys.factor_and_solve(&triplets, &rhs) else {
return ConstrainedSolution {
stats: SolveStats::from_loop(SolveStatus::Singular, history),
flows,
indeterminate,
};
};
for (pos, &i) in non_slack_idx.iter().enumerate() {
buses[i].voltage_ang += dx[pos];
}
for (pos, &i) in pq_idx.iter().enumerate() {
buses[i].voltage_mag += dx[n_angle + pos];
}
for (k, flow) in flows.iter_mut().enumerate() {
flow.0 += dx[n_base + 2 * k];
flow.1 += dx[n_base + 2 * k + 1];
}
}
ConstrainedSolution {
stats: SolveStats::from_loop(SolveStatus::MaxIterationsReached, history),
flows,
indeterminate,
}
}
fn seed_fixed(dsu: &mut [usize], buses: &[Bus], free: fn(&Bus) -> bool) {
let mut anchor: Option<usize> = None;
for (i, bus) in buses.iter().enumerate() {
if free(bus) {
continue;
}
match anchor {
None => anchor = Some(i),
Some(a) => {
let (x, y) = (find(dsu, i), find(dsu, a));
dsu[x] = y;
}
}
}
}
fn tie_or_pin(dsu: &mut [usize], connects: bool, i: usize, j: usize) -> RowUse {
if !connects {
return RowUse::Pin;
}
let (a, b) = (find(dsu, i), find(dsu, j));
if a == b {
return RowUse::Pin;
}
dsu[a] = b;
RowUse::Tie
}
fn has_angle(bus: &Bus) -> bool {
bus.bus_type != BusType::Slack
}
fn has_magnitude(bus: &Bus) -> bool {
bus.bus_type == BusType::PQ
}
fn find(dsu: &mut [usize], mut x: usize) -> usize {
while dsu[x] != x {
dsu[x] = dsu[dsu[x]];
x = dsu[x];
}
x
}
fn components_across_switches(
n: usize,
ybus: &YBusSparse,
switches: &[ConstrainedSwitch],
) -> Vec<Vec<usize>> {
let base = crate::network::connected_components(ybus);
let mut dsu: Vec<usize> = (0..n).collect();
for component in &base {
for w in component.windows(2) {
let (a, b) = (find(&mut dsu, w[0]), find(&mut dsu, w[1]));
dsu[a] = b;
}
}
for sw in switches.iter().filter(|s| !s.open) {
if sw.from < n && sw.to < n {
let (a, b) = (find(&mut dsu, sw.from), find(&mut dsu, sw.to));
dsu[a] = b;
}
}
let mut of_root: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
let mut out: Vec<Vec<usize>> = Vec::new();
for i in 0..n {
let root = find(&mut dsu, i);
let slot = *of_root.entry(root).or_insert_with(|| {
out.push(Vec::new());
out.len() - 1
});
out[slot].push(i);
}
out
}