use super::network::{YBus, connected_components};
use num_complex::Complex;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DcBusRole {
UdcSlack,
Ground,
FixedP { p_spec: f64 },
FixedIdc { idc_spec: f64 },
Passive,
}
#[derive(Clone, Debug)]
pub struct DcBus {
pub idx: usize,
pub role: DcBusRole,
pub udc_fixed: f64,
pub voltage: f64,
pub shunt_g: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct DcLine {
pub from: usize,
pub to: usize,
pub r: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DcSolveStatus {
pub converged: bool,
pub iterations: usize,
pub isolated_buses: Vec<usize>,
}
fn build_conductance(buses: &[DcBus], lines: &[DcLine]) -> (Vec<Vec<f64>>, Vec<Vec<usize>>) {
let n = buses.len();
let mut g = vec![vec![0.0_f64; n]; n];
let mut ybus = YBus::new(n);
for line in lines {
let gij = 1.0 / line.r;
g[line.from][line.from] += gij;
g[line.to][line.to] += gij;
g[line.from][line.to] -= gij;
g[line.to][line.from] -= gij;
let c = Complex::new(gij, 0.0);
ybus.add(line.from, line.from, c);
ybus.add(line.to, line.to, c);
ybus.add(line.from, line.to, -c);
ybus.add(line.to, line.from, -c);
}
for (i, b) in buses.iter().enumerate() {
if b.shunt_g != 0.0 {
g[i][i] += b.shunt_g;
ybus.add(i, i, Complex::new(b.shunt_g, 0.0));
}
}
let components = connected_components(&ybus.finish());
(g, components)
}
fn nodal_injections(g: &[Vec<f64>], buses: &[DcBus]) -> Vec<f64> {
(0..buses.len())
.map(|i| (0..buses.len()).map(|j| g[i][j] * buses[j].voltage).sum())
.collect()
}
pub fn injected_currents(buses: &[DcBus], lines: &[DcLine]) -> Vec<f64> {
let (g, _) = build_conductance(buses, lines);
nodal_injections(&g, buses)
}
pub fn solve_dc_network(
buses: &mut [DcBus],
lines: &[DcLine],
tol: f64,
max_iter: usize,
) -> DcSolveStatus {
let (g, components) = build_conductance(buses, lines);
for b in buses.iter_mut() {
if matches!(b.role, DcBusRole::UdcSlack | DcBusRole::Ground) {
b.voltage = b.udc_fixed;
}
}
let mut isolated_buses = Vec::new();
let mut unknown_idx = Vec::new();
for component in &components {
let has_reference = component
.iter()
.any(|&i| matches!(buses[i].role, DcBusRole::UdcSlack | DcBusRole::Ground));
if has_reference {
for &i in component {
if !matches!(buses[i].role, DcBusRole::UdcSlack | DcBusRole::Ground) {
unknown_idx.push(i);
}
}
} else {
isolated_buses.extend(component.iter().copied());
}
}
unknown_idx.sort_unstable();
let fixed_avg = {
let fixed: Vec<f64> = buses
.iter()
.filter(|b| matches!(b.role, DcBusRole::UdcSlack))
.map(|b| b.udc_fixed)
.collect();
if fixed.is_empty() {
1.0
} else {
fixed.iter().sum::<f64>() / fixed.len() as f64
}
};
for &i in &unknown_idx {
if buses[i].voltage == 0.0 {
buses[i].voltage = fixed_avg;
}
}
let m = unknown_idx.len();
if m == 0 {
return DcSolveStatus {
converged: true,
iterations: 0,
isolated_buses,
};
}
for iter in 0..max_iter {
let s = nodal_injections(&g, buses);
let mut mismatch = vec![0.0; m];
let mut converged = true;
for (row, &i) in unknown_idx.iter().enumerate() {
let vi = buses[i].voltage;
mismatch[row] = match buses[i].role {
DcBusRole::FixedP { p_spec } => p_spec - vi * s[i],
DcBusRole::FixedIdc { idc_spec } => idc_spec - s[i],
DcBusRole::Passive => -s[i],
DcBusRole::UdcSlack | DcBusRole::Ground => {
unreachable!("fixed-voltage buses are excluded from unknown_idx")
}
};
if mismatch[row].abs() > tol {
converged = false;
}
}
if converged {
return DcSolveStatus {
converged: true,
iterations: iter,
isolated_buses,
};
}
let mut jac = vec![vec![0.0; m]; m];
for (row, &i) in unknown_idx.iter().enumerate() {
let vi = buses[i].voltage;
for (col, &k) in unknown_idx.iter().enumerate() {
jac[row][col] = match buses[i].role {
DcBusRole::FixedP { .. } if k == i => -(s[i] + g[i][i] * vi),
DcBusRole::FixedP { .. } => -vi * g[i][k],
DcBusRole::FixedIdc { .. } | DcBusRole::Passive => -g[i][k],
DcBusRole::UdcSlack | DcBusRole::Ground => unreachable!(),
};
}
}
let Some(delta) = gridoxide_sparse::sparse::solve_dense(jac, mismatch) else {
return DcSolveStatus {
converged: false,
iterations: iter,
isolated_buses,
};
};
for (row, &i) in unknown_idx.iter().enumerate() {
buses[i].voltage -= delta[row];
}
}
DcSolveStatus {
converged: false,
iterations: max_iter,
isolated_buses,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn passive(idx: usize) -> DcBus {
DcBus {
idx,
role: DcBusRole::Passive,
udc_fixed: 0.0,
voltage: 0.0,
shunt_g: 0.0,
}
}
#[test]
fn two_node_fixed_p_matches_analytic_solution() {
let mut buses = vec![
DcBus {
idx: 0,
role: DcBusRole::UdcSlack,
udc_fixed: 100.0,
voltage: 100.0,
shunt_g: 0.0,
},
DcBus {
idx: 1,
role: DcBusRole::FixedP { p_spec: -450.0 },
udc_fixed: 0.0,
voltage: 0.0,
shunt_g: 0.0,
},
];
let lines = vec![DcLine {
from: 0,
to: 1,
r: 2.0,
}];
let status = solve_dc_network(&mut buses, &lines, 1e-9, 50);
assert!(status.converged);
assert!(status.isolated_buses.is_empty());
assert!(
(buses[1].voltage - 90.0).abs() < 1e-6,
"V1 = {}",
buses[1].voltage
);
}
#[test]
fn three_node_passive_junction_splits_series_resistance() {
let mut buses = vec![
DcBus {
idx: 0,
role: DcBusRole::UdcSlack,
udc_fixed: 100.0,
voltage: 100.0,
shunt_g: 0.0,
},
passive(1),
DcBus {
idx: 2,
role: DcBusRole::FixedIdc { idc_spec: -5.0 },
udc_fixed: 0.0,
voltage: 0.0,
shunt_g: 0.0,
},
];
let lines = vec![
DcLine {
from: 0,
to: 1,
r: 1.0,
},
DcLine {
from: 1,
to: 2,
r: 1.0,
},
];
let status = solve_dc_network(&mut buses, &lines, 1e-9, 50);
assert!(status.converged);
assert!(
(buses[1].voltage - 95.0).abs() < 1e-6,
"V1 = {}",
buses[1].voltage
);
assert!(
(buses[2].voltage - 90.0).abs() < 1e-6,
"V2 = {}",
buses[2].voltage
);
}
#[test]
fn fixed_idc_bus_converges_immediately_since_linear() {
let mut buses = vec![
DcBus {
idx: 0,
role: DcBusRole::UdcSlack,
udc_fixed: 100.0,
voltage: 100.0,
shunt_g: 0.0,
},
DcBus {
idx: 1,
role: DcBusRole::FixedIdc { idc_spec: -10.0 },
udc_fixed: 0.0,
voltage: 0.0,
shunt_g: 0.0,
},
];
let lines = vec![DcLine {
from: 0,
to: 1,
r: 1.0,
}];
let status = solve_dc_network(&mut buses, &lines, 1e-9, 50);
assert!(status.converged);
assert!(
status.iterations <= 2,
"expected near-immediate convergence for a linear row, got {} iterations",
status.iterations
);
assert!(
(buses[1].voltage - 90.0).abs() < 1e-6,
"V1 = {}",
buses[1].voltage
);
}
#[test]
fn disconnected_dead_subgraph_does_not_perturb_live_solve() {
let mut buses = vec![
DcBus {
idx: 0,
role: DcBusRole::UdcSlack,
udc_fixed: 100.0,
voltage: 100.0,
shunt_g: 0.0,
},
DcBus {
idx: 1,
role: DcBusRole::FixedP { p_spec: -450.0 },
udc_fixed: 0.0,
voltage: 0.0,
shunt_g: 0.0,
},
passive(2),
passive(3),
];
let lines = vec![
DcLine {
from: 0,
to: 1,
r: 2.0,
},
DcLine {
from: 2,
to: 3,
r: 1.0,
},
];
let status = solve_dc_network(&mut buses, &lines, 1e-9, 50);
assert!(status.converged);
assert!(
(buses[1].voltage - 90.0).abs() < 1e-6,
"V1 = {}",
buses[1].voltage
);
let mut isolated = status.isolated_buses.clone();
isolated.sort_unstable();
assert_eq!(isolated, vec![2, 3]);
assert_eq!(buses[2].voltage, 0.0);
assert_eq!(buses[3].voltage, 0.0);
}
}