use num_complex::Complex;
use crate::network::{Classified, Verdict, classify, mark_unreferenced_islands};
use crate::topology::{IDEAL_CONNECTION_Y, clamp_branch_impedance, ideal_connection_z, union_all};
use crate::types::{Bus, BusType, Line, Transformer};
use gridoxide_sparse::sparse::RealFactorization;
use super::{DcApproximation, DcOptions};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DcBranch {
pub index: usize,
pub from: usize,
pub to: usize,
pub b: f64,
pub shift: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DcIslandStatus {
Solved,
SolvedMultipleReferences,
Singular,
NoReferenceBus,
}
#[derive(Clone, Debug)]
pub struct DcIslandReport {
pub bus_indices: Vec<usize>,
pub slack_indices: Vec<usize>,
pub status: DcIslandStatus,
pub slack_pickup: f64,
}
#[derive(Clone, Debug)]
pub struct DcSolution {
pub islands: Vec<DcIslandReport>,
pub branch_p: Vec<f64>,
pub max_residual: f64,
pub negative_reactance_branches: Vec<usize>,
pub ignored_branches: Vec<usize>,
}
fn dc_injection(bus: &Bus) -> f64 {
bus.p_spec + bus.zip_terms.iter().map(|z| z.s_const.re).sum::<f64>()
}
fn susceptance(r: f64, x: f64, approximation: DcApproximation) -> f64 {
match approximation {
DcApproximation::IgnoreR => 1.0 / clamp_branch_impedance(0.0, x).1,
DcApproximation::IgnoreG => {
let (r, x) = clamp_branch_impedance(r, x);
x / (r * r + x * x)
}
}
}
pub fn dc_branches(lines: &[Line], transformers: &[Transformer], opts: DcOptions) -> Vec<DcBranch> {
let mut out = Vec::with_capacity(lines.len() + transformers.len());
for (i, ln) in lines.iter().enumerate() {
if ln.from == ln.to {
continue;
}
let b = susceptance(ln.r, ln.x, opts.approximation);
if !b.is_finite() {
continue;
}
out.push(DcBranch {
index: i,
from: ln.from,
to: ln.to,
b,
shift: 0.0,
});
}
for (j, t) in transformers.iter().enumerate() {
let index = lines.len() + j;
if t.from == t.to {
continue;
}
if t.from_status == 0 || t.to_status == 0 {
continue;
}
let k = t.tap.norm();
let mut b = if t.y_series == IDEAL_CONNECTION_Y {
1.0 / ideal_connection_z()
} else {
let z = Complex::new(1.0, 0.0) / t.y_series;
susceptance(z.re, z.im, opts.approximation)
};
if opts.use_transformer_ratio && k.is_finite() && k > 0.0 {
b /= k;
}
if !b.is_finite() {
continue;
}
out.push(DcBranch {
index,
from: t.from,
to: t.to,
b,
shift: t.tap.arg(),
});
}
out
}
fn dc_components(n: usize, branches: &[DcBranch]) -> Vec<Vec<usize>> {
let mut uf = union_all(n, branches.iter().map(|b| (b.from, b.to)));
let mut comp_of_root = vec![usize::MAX; n];
let mut components: Vec<Vec<usize>> = Vec::new();
for i in 0..n {
let root = uf.find(i);
if comp_of_root[root] == usize::MAX {
comp_of_root[root] = components.len();
components.push(Vec::new());
}
components[comp_of_root[root]].push(i);
}
components
}
pub(crate) struct Partition {
pub(crate) classified: Vec<Classified>,
pub(crate) island_branches: Vec<Vec<usize>>,
}
pub(crate) fn partition(buses: &[Bus], branches: &[DcBranch]) -> Partition {
let n = buses.len();
let components = dc_components(n, branches);
let mut island_of = vec![usize::MAX; n];
for (ci, members) in components.iter().enumerate() {
for &i in members {
island_of[i] = ci;
}
}
let mut island_branches: Vec<Vec<usize>> = vec![Vec::new(); components.len()];
for (pos, br) in branches.iter().enumerate() {
island_branches[island_of[br.from]].push(pos);
}
Partition {
classified: classify(buses, &components),
island_branches,
}
}
pub(crate) fn reduced_b_triplets(
branches: &[DcBranch],
positions: &[usize],
reduced_pos: &[usize],
) -> Vec<(usize, usize, f64)> {
let mut triplets = Vec::with_capacity(positions.len() * 4);
for &pos in positions {
let br = &branches[pos];
let (rf, rt) = (reduced_pos[br.from], reduced_pos[br.to]);
if rf != NOT_UNKNOWN {
triplets.push((rf, rf, br.b));
if rt != NOT_UNKNOWN {
triplets.push((rf, rt, -br.b));
}
}
if rt != NOT_UNKNOWN {
triplets.push((rt, rt, br.b));
if rf != NOT_UNKNOWN {
triplets.push((rt, rf, -br.b));
}
}
}
triplets
}
pub(crate) const NOT_UNKNOWN: usize = usize::MAX;
pub fn dc_power_flow(
buses: &mut [Bus],
lines: &[Line],
transformers: &[Transformer],
opts: DcOptions,
) -> DcSolution {
let n = buses.len();
let n_branches = lines.len() + transformers.len();
let branches = dc_branches(lines, transformers, opts);
let Partition {
classified,
island_branches,
} = partition(buses, &branches);
mark_unreferenced_islands(buses, &classified);
let mut contributing = vec![false; n_branches];
let mut negative_reactance_branches = Vec::new();
for br in &branches {
contributing[br.index] = true;
if br.b < 0.0 {
negative_reactance_branches.push(br.index);
}
}
let ignored_branches: Vec<usize> = (0..n_branches).filter(|&i| !contributing[i]).collect();
let mut branch_p = vec![0.0; n_branches];
let mut max_residual: f64 = 0.0;
let mut islands = Vec::with_capacity(classified.len());
let mut reduced_pos = vec![NOT_UNKNOWN; n];
for (ci, c) in classified.iter().enumerate() {
if matches!(c.verdict, Verdict::NoReferenceBus) {
islands.push(DcIslandReport {
bus_indices: c.bus_indices.clone(),
slack_indices: c.slack_indices.clone(),
status: DcIslandStatus::NoReferenceBus,
slack_pickup: 0.0,
});
continue;
}
let mut unknown: Vec<usize> = Vec::new();
for &i in &c.bus_indices {
if buses[i].bus_type != BusType::Slack {
reduced_pos[i] = unknown.len();
unknown.push(i);
}
}
let m = unknown.len();
let mut solved = true;
if m > 0 {
let triplets = reduced_b_triplets(&branches, &island_branches[ci], &reduced_pos);
let mut rhs = vec![0.0; m];
for (r, &i) in unknown.iter().enumerate() {
rhs[r] = dc_injection(&buses[i]);
}
for &pos in &island_branches[ci] {
let br = &branches[pos];
let (rf, rt) = (reduced_pos[br.from], reduced_pos[br.to]);
let shift_p = br.b * br.shift;
if rf != NOT_UNKNOWN {
rhs[rf] += shift_p;
if rt == NOT_UNKNOWN {
rhs[rf] += br.b * buses[br.to].voltage_ang;
}
}
if rt != NOT_UNKNOWN {
rhs[rt] -= shift_p;
if rf == NOT_UNKNOWN {
rhs[rt] += br.b * buses[br.from].voltage_ang;
}
}
}
match RealFactorization::new(m, &triplets).and_then(|sys| sys.solve(&rhs)) {
Some(theta) => {
for (r, &i) in unknown.iter().enumerate() {
buses[i].voltage_ang = theta[r];
if buses[i].bus_type == BusType::PQ {
buses[i].voltage_mag = 1.0;
}
}
}
None => solved = false,
}
}
if !solved {
islands.push(DcIslandReport {
bus_indices: c.bus_indices.clone(),
slack_indices: c.slack_indices.clone(),
status: DcIslandStatus::Singular,
slack_pickup: 0.0,
});
continue;
}
let mut net_out = vec![0.0; n];
for &pos in &island_branches[ci] {
let br = &branches[pos];
let p = br.b * (buses[br.from].voltage_ang - buses[br.to].voltage_ang - br.shift);
branch_p[br.index] = p;
net_out[br.from] += p;
net_out[br.to] -= p;
}
for &i in &c.bus_indices {
if buses[i].bus_type != BusType::Slack {
max_residual = max_residual.max((net_out[i] - dc_injection(&buses[i])).abs());
}
}
let slack_pickup: f64 = c.slack_indices.iter().map(|&s| net_out[s]).sum();
let status = if c.slack_indices.len() > 1 {
DcIslandStatus::SolvedMultipleReferences
} else {
DcIslandStatus::Solved
};
islands.push(DcIslandReport {
bus_indices: c.bus_indices.clone(),
slack_indices: c.slack_indices.clone(),
status,
slack_pickup,
});
}
DcSolution {
islands,
branch_p,
max_residual,
negative_reactance_branches,
ignored_branches,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bus(idx: usize, bus_type: BusType, p_spec: f64) -> Bus {
Bus {
idx,
bus_type,
voltage_mag: 1.0,
voltage_ang: 0.0,
p_spec,
q_spec: 0.0,
q_min: 0.0,
q_max: 0.0,
u_rated: 0.0,
zip_terms: Vec::new(),
}
}
fn line(from: usize, to: usize, x: f64) -> Line {
Line {
from,
to,
r: 0.0,
x,
b_shunt: 0.0,
g_shunt: 0.0,
}
}
#[test]
fn radial_three_bus_matches_closed_form() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0),
bus(1, BusType::PQ, -0.3),
bus(2, BusType::PQ, -0.5),
];
let lines = vec![line(0, 1, 0.1), line(1, 2, 0.2)];
let sol = dc_power_flow(&mut buses, &lines, &[], DcOptions::default());
assert_eq!(sol.islands.len(), 1);
assert_eq!(sol.islands[0].status, DcIslandStatus::Solved);
let theta1 = -0.8 * 0.1;
let theta2 = theta1 - 0.5 * 0.2;
assert!(
(buses[1].voltage_ang - theta1).abs() < 1e-12,
"{}",
buses[1].voltage_ang
);
assert!(
(buses[2].voltage_ang - theta2).abs() < 1e-12,
"{}",
buses[2].voltage_ang
);
assert!((sol.branch_p[0] - 0.8).abs() < 1e-12, "{}", sol.branch_p[0]);
assert!((sol.branch_p[1] - 0.5).abs() < 1e-12, "{}", sol.branch_p[1]);
assert!((sol.islands[0].slack_pickup - 0.8).abs() < 1e-12);
assert!(sol.max_residual < 1e-12, "{}", sol.max_residual);
}
#[test]
fn parallel_paths_split_inversely_with_reactance() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0),
bus(1, BusType::PQ, 0.0),
bus(2, BusType::PQ, -0.6),
];
let lines = vec![line(0, 2, 0.2), line(0, 1, 0.2), line(1, 2, 0.2)];
let sol = dc_power_flow(&mut buses, &lines, &[], DcOptions::default());
assert!((sol.branch_p[0] - 0.4).abs() < 1e-12, "{}", sol.branch_p[0]);
assert!((sol.branch_p[1] - 0.2).abs() < 1e-12, "{}", sol.branch_p[1]);
assert!((sol.branch_p[2] - 0.2).abs() < 1e-12, "{}", sol.branch_p[2]);
assert!(sol.max_residual < 1e-12, "{}", sol.max_residual);
}
fn pst(from: usize, to: usize, x: f64, alpha: f64) -> Transformer {
Transformer {
from,
to,
from_status: 1,
to_status: 1,
y_series: Complex::new(1.0, 0.0) / Complex::new(0.0, x),
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::from_polar(1.0, alpha),
}
}
#[test]
fn radial_phase_shifter_displaces_the_angle_and_carries_nothing() {
let alpha = 0.05_f64;
let mut buses = vec![bus(0, BusType::Slack, 0.0), bus(1, BusType::PQ, 0.0)];
let sol = dc_power_flow(
&mut buses,
&[],
&[pst(0, 1, 0.25, alpha)],
DcOptions::default(),
);
assert!(
(buses[1].voltage_ang - (-alpha)).abs() < 1e-12,
"{}",
buses[1].voltage_ang
);
assert!(sol.branch_p[0].abs() < 1e-12, "{}", sol.branch_p[0]);
assert!(sol.islands[0].slack_pickup.abs() < 1e-12);
}
#[test]
fn phase_shifter_in_a_loop_drives_circulating_flow() {
let alpha = 0.05_f64;
let (x_line, x_pst) = (0.2_f64, 0.25_f64);
let (b_l, b_t) = (1.0 / x_line, 1.0 / x_pst);
let mut buses = vec![bus(0, BusType::Slack, 0.0), bus(1, BusType::PQ, 0.0)];
let lines = vec![line(0, 1, x_line)];
let sol = dc_power_flow(
&mut buses,
&lines,
&[pst(0, 1, x_pst, alpha)],
DcOptions::default(),
);
let circulating = b_l * b_t * alpha / (b_l + b_t);
assert!(
(sol.branch_p[0] - circulating).abs() < 1e-12,
"line: {}",
sol.branch_p[0]
);
assert!(
(sol.branch_p[1] + circulating).abs() < 1e-12,
"pst: {}",
sol.branch_p[1]
);
assert!(
(buses[1].voltage_ang - (-b_t * alpha / (b_l + b_t))).abs() < 1e-12,
"{}",
buses[1].voltage_ang
);
assert!(
sol.islands[0].slack_pickup.abs() < 1e-12,
"{}",
sol.islands[0].slack_pickup
);
assert!(sol.max_residual < 1e-12);
}
#[test]
fn transformer_ratio_divides_susceptance_once() {
let x = 0.1_f64;
let k = 1.05_f64;
let t = Transformer {
from: 0,
to: 1,
from_status: 1,
to_status: 1,
y_series: Complex::new(1.0, 0.0) / Complex::new(0.0, x),
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::from_polar(k, 0.0),
};
let got = dc_branches(&[], std::slice::from_ref(&t), DcOptions::default());
assert!((got[0].b - 1.0 / (x * k)).abs() < 1e-12, "{}", got[0].b);
let no_ratio = DcOptions {
use_transformer_ratio: false,
..Default::default()
};
let got = dc_branches(&[], std::slice::from_ref(&t), no_ratio);
assert!((got[0].b - 1.0 / x).abs() < 1e-12, "{}", got[0].b);
}
#[test]
fn ignore_g_uses_the_true_series_susceptance() {
let ln = Line {
from: 0,
to: 1,
r: 0.1,
x: 0.1,
b_shunt: 0.0,
g_shunt: 0.0,
};
let opts = DcOptions {
approximation: DcApproximation::IgnoreG,
..Default::default()
};
let got = dc_branches(std::slice::from_ref(&ln), &[], opts);
let y_series = Complex::new(1.0, 0.0) / Complex::new(ln.r, ln.x);
assert!((got[0].b - (-y_series.im)).abs() < 1e-12, "{}", got[0].b);
assert!((got[0].b - 5.0).abs() < 1e-12, "{}", got[0].b);
let got = dc_branches(std::slice::from_ref(&ln), &[], DcOptions::default());
assert!((got[0].b - 10.0).abs() < 1e-12, "{}", got[0].b);
}
#[test]
fn self_loops_and_open_terminals_are_ignored() {
let lines = vec![line(0, 1, 0.1), line(1, 1, 0.2)];
let transformers = vec![Transformer {
from: 0,
to: 1,
from_status: 1,
to_status: 0,
y_series: Complex::new(1.0, 0.0) / Complex::new(0.0, 0.1),
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::new(1.0, 0.0),
}];
let got = dc_branches(&lines, &transformers, DcOptions::default());
assert_eq!(got.len(), 1);
assert_eq!(got[0].index, 0);
let mut buses = vec![bus(0, BusType::Slack, 0.0), bus(1, BusType::PQ, -0.4)];
let sol = dc_power_flow(&mut buses, &lines, &transformers, DcOptions::default());
assert_eq!(sol.ignored_branches, vec![1, 2]);
assert_eq!(sol.branch_p[1], 0.0);
assert_eq!(sol.branch_p[2], 0.0);
}
#[test]
fn sourceless_island_is_reported_not_invented() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0),
bus(1, BusType::PQ, -0.4),
bus(2, BusType::PQ, -0.1),
bus(3, BusType::PQ, 0.1),
];
let lines = vec![line(0, 1, 0.1), line(2, 3, 0.2)];
let sol = dc_power_flow(&mut buses, &lines, &[], DcOptions::default());
assert_eq!(sol.islands.len(), 2);
assert_eq!(sol.islands[0].status, DcIslandStatus::Solved);
assert_eq!(sol.islands[1].status, DcIslandStatus::NoReferenceBus);
assert_eq!(sol.islands[1].bus_indices, vec![2, 3]);
assert_eq!(buses[2].voltage_ang, 0.0);
assert_eq!(sol.branch_p[1], 0.0);
}
#[test]
fn two_references_in_one_island_still_solve() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0),
bus(1, BusType::PQ, -0.6),
bus(2, BusType::Slack, 0.0),
];
let lines = vec![line(0, 1, 0.2), line(1, 2, 0.2)];
let sol = dc_power_flow(&mut buses, &lines, &[], DcOptions::default());
assert_eq!(
sol.islands[0].status,
DcIslandStatus::SolvedMultipleReferences
);
assert!(sol.max_residual < 1e-12, "{}", sol.max_residual);
assert!((sol.islands[0].slack_pickup - 0.6).abs() < 1e-12);
assert!((sol.branch_p[0] - 0.3).abs() < 1e-12, "{}", sol.branch_p[0]);
}
#[test]
fn nonzero_slack_angle_offsets_the_whole_island() {
let offset = 0.2_f64;
let lines = vec![line(0, 1, 0.1), line(1, 2, 0.2)];
let mut base = vec![
bus(0, BusType::Slack, 0.0),
bus(1, BusType::PQ, -0.3),
bus(2, BusType::PQ, -0.5),
];
dc_power_flow(&mut base, &lines, &[], DcOptions::default());
let mut shifted = vec![
bus(0, BusType::Slack, 0.0),
bus(1, BusType::PQ, -0.3),
bus(2, BusType::PQ, -0.5),
];
shifted[0].voltage_ang = offset;
let sol = dc_power_flow(&mut shifted, &lines, &[], DcOptions::default());
for i in 1..3 {
assert!(
(shifted[i].voltage_ang - (base[i].voltage_ang + offset)).abs() < 1e-12,
"bus {i}: {} vs {}",
shifted[i].voltage_ang,
base[i].voltage_ang
);
}
assert!(sol.max_residual < 1e-12);
}
#[test]
fn link_admittance_yields_a_positive_susceptance() {
let t = Transformer {
from: 0,
to: 1,
from_status: 1,
to_status: 1,
y_series: IDEAL_CONNECTION_Y,
y_shunt: Complex::new(0.0, 0.0),
tap: Complex::new(1.0, 0.0),
};
let z = Complex::new(1.0, 0.0) / IDEAL_CONNECTION_Y;
assert!(
z.im < 0.0,
"fixture assumption broken: link reactance is {}",
z.im
);
let got = dc_branches(&[], std::slice::from_ref(&t), DcOptions::default());
assert!(got[0].b > 0.0, "link susceptance came out {}", got[0].b);
assert!(
(got[0].b - 1.0 / ideal_connection_z()).abs() < 1e-6,
"{}",
got[0].b
);
}
#[test]
fn series_capacitor_keeps_its_negative_susceptance() {
let lines = vec![line(0, 1, 0.2), line(0, 1, -0.1)];
let mut buses = vec![bus(0, BusType::Slack, 0.0), bus(1, BusType::PQ, -0.5)];
let sol = dc_power_flow(&mut buses, &lines, &[], DcOptions::default());
let got = dc_branches(&lines, &[], DcOptions::default());
assert!((got[1].b - (-10.0)).abs() < 1e-12, "{}", got[1].b);
assert_eq!(sol.negative_reactance_branches, vec![1]);
}
#[test]
fn a_pv_bus_keeps_its_voltage_setpoint() {
let mut buses = vec![
bus(0, BusType::Slack, 0.0),
bus(1, BusType::PV, 0.2),
bus(2, BusType::PQ, -0.5),
];
buses[0].voltage_mag = 1.06;
buses[1].voltage_mag = 1.04;
let lines = vec![line(0, 1, 0.1), line(1, 2, 0.2), line(0, 2, 0.3)];
dc_power_flow(&mut buses, &lines, &[], DcOptions::default());
assert_eq!(buses[0].voltage_mag, 1.06, "slack setpoint was overwritten");
assert_eq!(buses[1].voltage_mag, 1.04, "PV setpoint was overwritten");
assert_eq!(buses[2].voltage_mag, 1.0);
assert!(buses[1].voltage_ang != 0.0);
}
#[test]
fn components_match_the_documented_ordering_contract() {
let branches = vec![
DcBranch {
index: 0,
from: 3,
to: 1,
b: 1.0,
shift: 0.0,
},
DcBranch {
index: 1,
from: 4,
to: 2,
b: 1.0,
shift: 0.0,
},
];
assert_eq!(
dc_components(5, &branches),
vec![vec![0], vec![1, 3], vec![2, 4]]
);
}
}