use num_complex::Complex;
use crate::network::YBusSparse;
use crate::types::{Bus, BusType};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum EntryKind {
Hii = 0,
Nii = 1,
Hik = 2,
Nik = 3,
Mii = 4,
Lii = 5,
Mik = 6,
Lik = 7,
}
#[derive(Clone, Copy, Debug)]
pub struct Entry {
pub kind: EntryKind,
pub i: u32,
pub k: u32,
pub y: Complex<f64>,
}
pub struct JacobianPattern {
pub n_unknowns: usize,
rows: Vec<u32>,
cols: Vec<u32>,
entries: Vec<Entry>,
}
impl JacobianPattern {
pub fn analyze(buses: &[Bus], ybus: &YBusSparse) -> Self {
let non_slack_idx: Vec<usize> = buses
.iter()
.filter(|b| !matches!(b.bus_type, BusType::Slack))
.map(|b| b.idx)
.collect();
let pq_idx: Vec<usize> = buses
.iter()
.filter(|b| matches!(b.bus_type, BusType::PQ))
.map(|b| b.idx)
.collect();
let n_angle = non_slack_idx.len();
let n_unknowns = n_angle + pq_idx.len();
let mut non_slack_pos: Vec<Option<usize>> = vec![None; buses.len()];
for (pos, &i) in non_slack_idx.iter().enumerate() {
non_slack_pos[i] = Some(pos);
}
let mut pq_pos: Vec<Option<usize>> = vec![None; buses.len()];
for (pos, &i) in pq_idx.iter().enumerate() {
pq_pos[i] = Some(pos);
}
let capacity: usize = non_slack_idx.iter().map(|&i| ybus.row(i).len()).sum::<usize>() * 2
+ pq_idx.iter().map(|&i| ybus.row(i).len()).sum::<usize>() * 2;
let mut rows = Vec::with_capacity(capacity);
let mut cols = Vec::with_capacity(capacity);
let mut entries = Vec::with_capacity(capacity);
let mut push = |row: usize, col: usize, kind: EntryKind, i: usize, k: usize, y: Complex<f64>| {
rows.push(row as u32);
cols.push(col as u32);
entries.push(Entry { kind, i: i as u32, k: k as u32, y });
};
for (row_idx, &i) in non_slack_idx.iter().enumerate() {
for &(k, y_ik) in ybus.row(i) {
if k == i {
push(row_idx, row_idx, EntryKind::Hii, i, i, y_ik);
if let Some(col_idx) = pq_pos[i] {
push(row_idx, n_angle + col_idx, EntryKind::Nii, i, i, y_ik);
}
continue;
}
if let Some(col_idx) = non_slack_pos[k] {
push(row_idx, col_idx, EntryKind::Hik, i, k, y_ik);
}
if let Some(col_idx) = pq_pos[k] {
push(row_idx, n_angle + col_idx, EntryKind::Nik, i, k, y_ik);
}
}
}
for (row_idx, &i) in pq_idx.iter().enumerate() {
for &(k, y_ik) in ybus.row(i) {
if k == i {
let col_idx = non_slack_pos[i].expect("a PQ bus is always non-slack");
push(n_angle + row_idx, col_idx, EntryKind::Mii, i, i, y_ik);
push(n_angle + row_idx, n_angle + row_idx, EntryKind::Lii, i, i, y_ik);
continue;
}
if let Some(col_idx) = non_slack_pos[k] {
push(n_angle + row_idx, col_idx, EntryKind::Mik, i, k, y_ik);
}
if let Some(col_idx) = pq_pos[k] {
push(n_angle + row_idx, n_angle + col_idx, EntryKind::Lik, i, k, y_ik);
}
}
}
Self { n_unknowns, rows, cols, entries }
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn rows(&self) -> &[u32] {
&self.rows
}
pub fn cols(&self) -> &[u32] {
&self.cols
}
pub fn entries(&self) -> &[Entry] {
&self.entries
}
pub fn fill(&self, buses: &[Bus], p_calc: &[f64], q_calc: &[f64], values: &mut Vec<f64>) {
values.clear();
self.fill_into(buses, p_calc, q_calc, values);
}
pub fn fill_into(&self, buses: &[Bus], p_calc: &[f64], q_calc: &[f64], values: &mut Vec<f64>) {
values.reserve(self.entries.len());
for e in &self.entries {
let i = e.i as usize;
let k = e.k as usize;
let vm_i = buses[i].voltage_mag;
let (g, b) = (e.y.re, e.y.im);
let v = match e.kind {
EntryKind::Hii => -q_calc[i] - vm_i.powi(2) * b,
EntryKind::Nii => p_calc[i] / vm_i + vm_i * g,
EntryKind::Mii => p_calc[i] - vm_i.powi(2) * g,
EntryKind::Lii => q_calc[i] / vm_i - vm_i * b,
_ => {
let vm_k = buses[k].voltage_mag;
let ang = buses[i].voltage_ang - buses[k].voltage_ang;
let (sin, cos) = (ang.sin(), ang.cos());
match e.kind {
EntryKind::Hik => vm_i * vm_k * (g * sin - b * cos),
EntryKind::Nik => vm_i * (g * cos + b * sin),
EntryKind::Mik => -vm_i * vm_k * (g * cos + b * sin),
EntryKind::Lik => vm_i * (g * sin - b * cos),
_ => unreachable!("diagonal kinds handled above"),
}
}
};
values.push(v);
}
}
pub fn fill_identity_into(&self, values: &mut Vec<f64>) {
values.reserve(self.entries.len());
for k in 0..self.entries.len() {
values.push(if self.rows[k] == self.cols[k] { 1.0 } else { 0.0 });
}
}
pub fn to_triplets(&self, values: &[f64]) -> Vec<(usize, usize, f64)> {
self.rows
.iter()
.zip(&self.cols)
.zip(values)
.map(|((&r, &c), &v)| (r as usize, c as usize, v))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::{build_ybus, power_injections};
use crate::solver::jacobian_triplets_reference;
use crate::types::Line;
fn mixed_network() -> (Vec<Bus>, YBusSparse) {
let bus = |idx, bus_type, vm, p, q| Bus {
idx,
bus_type,
voltage_mag: vm,
voltage_ang: 0.0,
p_spec: p,
q_spec: q,
q_min: -9.0,
q_max: 9.0,
u_rated: 0.0,
zip_terms: Vec::new(),
};
let buses = vec![
bus(0, BusType::Slack, 1.06, 0.0, 0.0),
bus(1, BusType::PV, 1.04, 0.4, 0.0),
bus(2, BusType::PQ, 1.0, -0.6, -0.2),
bus(3, BusType::PQ, 1.0, -0.3, -0.1),
bus(4, BusType::PQ, 1.0, -0.5, -0.15),
];
let line = |from, to, r, x, b| Line { from, to, r, x, b_shunt: b, g_shunt: 0.0 };
let lines = vec![
line(0, 1, 0.02, 0.06, 0.03),
line(0, 2, 0.08, 0.24, 0.025),
line(1, 2, 0.06, 0.18, 0.02),
line(1, 3, 0.06, 0.18, 0.02),
line(2, 4, 0.04, 0.12, 0.015),
line(3, 4, 0.01, 0.03, 0.01),
];
let ybus = build_ybus(buses.len(), &lines, &[]).finish();
(buses, ybus)
}
#[test]
fn pattern_matches_reference_bit_for_bit() {
let (mut buses, ybus) = mixed_network();
let pattern = JacobianPattern::analyze(&buses, &ybus);
let mut values = Vec::new();
for step in 0..4 {
for (j, b) in buses.iter_mut().enumerate() {
b.voltage_ang = -0.01 * (step * (j + 1)) as f64;
if b.bus_type == BusType::PQ {
b.voltage_mag = 1.0 - 0.013 * step as f64 * (j + 1) as f64;
}
}
let (p_calc, q_calc) = power_injections(&buses, &ybus);
let expected = jacobian_triplets_reference(&buses, &ybus, &p_calc, &q_calc);
pattern.fill(&buses, &p_calc, &q_calc, &mut values);
let got = pattern.to_triplets(&values);
assert_eq!(got.len(), expected.len(), "nonzero count at step {step}");
for (idx, (g, e)) in got.iter().zip(&expected).enumerate() {
assert_eq!((g.0, g.1), (e.0, e.1), "entry {idx} position at step {step}");
assert_eq!(
g.2.to_bits(),
e.2.to_bits(),
"entry {idx} value at step {step}: {} vs {}",
g.2,
e.2
);
}
}
}
#[test]
fn pattern_matches_reference_on_network_json() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests/data/network.json");
let raw = std::fs::read_to_string(path).expect("read network.json");
let network: crate::json::NetworkData = serde_json::from_str(&raw).expect("parse");
let buses = network.buses;
let ybus = build_ybus(buses.len(), &network.lines, &[]).finish();
let (p_calc, q_calc) = power_injections(&buses, &ybus);
let expected = jacobian_triplets_reference(&buses, &ybus, &p_calc, &q_calc);
let pattern = JacobianPattern::analyze(&buses, &ybus);
let mut values = Vec::new();
pattern.fill(&buses, &p_calc, &q_calc, &mut values);
let got = pattern.to_triplets(&values);
assert_eq!(got.len(), expected.len());
for (idx, (g, e)) in got.iter().zip(&expected).enumerate() {
assert_eq!((g.0, g.1), (e.0, e.1), "entry {idx} position");
assert_eq!(g.2.to_bits(), e.2.to_bits(), "entry {idx} value");
}
}
#[test]
fn fill_overwrites_a_reused_buffer() {
let (buses, ybus) = mixed_network();
let pattern = JacobianPattern::analyze(&buses, &ybus);
let (p_calc, q_calc) = power_injections(&buses, &ybus);
let mut fresh = Vec::new();
pattern.fill(&buses, &p_calc, &q_calc, &mut fresh);
let mut dirty = vec![f64::NAN; pattern.len() * 3];
pattern.fill(&buses, &p_calc, &q_calc, &mut dirty);
assert_eq!(dirty.len(), pattern.len());
assert_eq!(dirty, fresh);
}
}