use num_complex::Complex;
use crate::network::{BusVoltages, 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>,
src: Vec<u32>,
diag: Vec<(u32, Complex<f64>)>,
pairs: Vec<(u32, u32, Complex<f64>)>,
}
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 src = Vec::with_capacity(capacity);
let pair_base = 4 * n_angle;
let mut push = |row: usize,
col: usize,
kind: EntryKind,
i: usize,
k: usize,
y: Complex<f64>,
slot: usize| {
rows.push(row as u32);
cols.push(col as u32);
entries.push(Entry {
kind,
i: i as u32,
k: k as u32,
y,
});
src.push(slot as u32);
};
let has_column = |k: usize| non_slack_pos[k].is_some() || pq_pos[k].is_some();
let mut diag: Vec<(u32, Complex<f64>)> = non_slack_idx
.iter()
.map(|&i| (i as u32, Complex::new(0.0, 0.0)))
.collect();
let mut pairs: Vec<(u32, u32, Complex<f64>)> = Vec::new();
let mut first_pair = vec![0usize; buses.len()];
for (row_idx, &i) in non_slack_idx.iter().enumerate() {
first_pair[i] = pairs.len();
let d = 4 * row_idx;
for &(k, y_ik) in ybus.row(i) {
if k == i {
diag[row_idx].1 = y_ik;
push(row_idx, row_idx, EntryKind::Hii, i, i, y_ik, d);
if let Some(col_idx) = pq_pos[i] {
push(
row_idx,
n_angle + col_idx,
EntryKind::Nii,
i,
i,
y_ik,
d + 1,
);
}
continue;
}
if !has_column(k) {
continue;
}
let at = pair_base + 4 * pairs.len();
pairs.push((i as u32, k as u32, y_ik));
if let Some(col_idx) = non_slack_pos[k] {
push(row_idx, col_idx, EntryKind::Hik, i, k, y_ik, at);
}
if let Some(col_idx) = pq_pos[k] {
push(
row_idx,
n_angle + col_idx,
EntryKind::Nik,
i,
k,
y_ik,
at + 1,
);
}
}
}
for (row_idx, &i) in pq_idx.iter().enumerate() {
let d = 4 * non_slack_pos[i].expect("a PQ bus is always non-slack");
let mut pair = first_pair[i];
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,
d + 2,
);
push(
n_angle + row_idx,
n_angle + row_idx,
EntryKind::Lii,
i,
i,
y_ik,
d + 3,
);
continue;
}
if !has_column(k) {
continue;
}
debug_assert_eq!((pairs[pair].0, pairs[pair].1), (i as u32, k as u32));
let at = pair_base + 4 * pair;
if let Some(col_idx) = non_slack_pos[k] {
push(
n_angle + row_idx,
col_idx,
EntryKind::Mik,
i,
k,
y_ik,
at + 2,
);
}
if let Some(col_idx) = pq_pos[k] {
push(
n_angle + row_idx,
n_angle + col_idx,
EntryKind::Lik,
i,
k,
y_ik,
at + 3,
);
}
pair += 1;
}
}
Self {
n_unknowns,
rows,
cols,
entries,
src,
diag,
pairs,
}
}
pub fn permute(&mut self, order: &[usize]) {
assert_eq!(
order.len(),
self.entries.len(),
"order must cover every entry"
);
let mut seen = vec![false; order.len()];
for &i in order {
assert!(
!std::mem::replace(&mut seen[i], true),
"order repeats entry {i}"
);
}
self.rows = order.iter().map(|&i| self.rows[i]).collect();
self.cols = order.iter().map(|&i| self.cols[i]).collect();
self.entries = order.iter().map(|&i| self.entries[i]).collect();
self.src = order.iter().map(|&i| self.src[i]).collect();
}
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>) {
self.fill_from(&BusVoltages::of(buses), p_calc, q_calc, values);
}
pub fn fill_with(
&self,
v: &BusVoltages,
p_calc: &[f64],
q_calc: &[f64],
values: &mut Vec<f64>,
) {
values.clear();
self.fill_from(v, p_calc, q_calc, values);
}
fn fill_from(&self, v: &BusVoltages, p_calc: &[f64], q_calc: &[f64], values: &mut Vec<f64>) {
let vm = &v.vm;
let mut slots = Vec::with_capacity(4 * (self.diag.len() + self.pairs.len()));
for &(i, y) in &self.diag {
let i = i as usize;
let (vm_i, g, b) = (vm[i], y.re, y.im);
slots.extend([
-q_calc[i] - vm_i.powi(2) * b,
p_calc[i] / vm_i + vm_i * g,
p_calc[i] - vm_i.powi(2) * g,
q_calc[i] / vm_i - vm_i * b,
]);
}
for &(i, k, y) in &self.pairs {
let (i, k) = (i as usize, k as usize);
let (vm_i, vm_k, g, b) = (vm[i], vm[k], y.re, y.im);
let (s_i, c_i, s_k, c_k) = (v.sin[i], v.cos[i], v.sin[k], v.cos[k]);
let sin = s_i * c_k - c_i * s_k;
let cos = c_i * c_k + s_i * s_k;
slots.extend([
vm_i * vm_k * (g * sin - b * cos),
vm_i * (g * cos + b * sin),
-vm_i * vm_k * (g * cos + b * sin),
vm_i * (g * sin - b * cos),
]);
}
values.extend(self.src.iter().map(|&s| slots[s as usize]));
}
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::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)
}
fn assert_same_value(got: f64, expected: f64, what: &str) {
assert!(
(got - expected).abs() <= 1e-13 * expected.abs().max(1.0),
"{what}: {got} vs {expected}"
);
}
#[test]
fn pattern_matches_reference() {
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_same_value(g.2, e.2, &format!("entry {idx} value at step {step}"));
}
}
}
#[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_same_value(g.2, e.2, &format!("entry {idx} value"));
}
}
#[test]
fn permute_reorders_the_entries() {
let (buses, ybus) = mixed_network();
let (p_calc, q_calc) = power_injections(&buses, &ybus);
let mut pattern = JacobianPattern::analyze(&buses, &ybus);
let mut values = Vec::new();
pattern.fill(&buses, &p_calc, &q_calc, &mut values);
let before = pattern.to_triplets(&values);
let order: Vec<usize> = (0..pattern.len()).rev().collect();
pattern.permute(&order);
pattern.fill(&buses, &p_calc, &q_calc, &mut values);
let after = pattern.to_triplets(&values);
let expected: Vec<_> = order.iter().map(|&i| before[i]).collect();
assert_eq!(after, expected);
}
#[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);
}
fn jacobian_triplets_reference(
buses: &[Bus],
ybus: &YBusSparse,
p_calc: &[f64],
q_calc: &[f64],
) -> Vec<(usize, usize, f64)> {
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 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 cap = jacobian_triplet_capacity(ybus, &non_slack_idx, &pq_idx);
build_jacobian_triplets(
buses,
ybus,
&non_slack_idx,
&pq_idx,
&non_slack_pos,
&pq_pos,
n_angle,
p_calc,
q_calc,
cap,
)
}
fn jacobian_triplet_capacity(
ybus: &YBusSparse,
non_slack_idx: &[usize],
pq_idx: &[usize],
) -> usize {
let non_slack_degree: usize = non_slack_idx.iter().map(|&i| ybus.row(i).len()).sum();
let pq_degree: usize = pq_idx.iter().map(|&i| ybus.row(i).len()).sum();
2 * non_slack_degree + 2 * pq_degree
}
#[allow(clippy::too_many_arguments)]
fn build_jacobian_triplets(
buses: &[Bus],
ybus: &YBusSparse,
non_slack_idx: &[usize],
pq_idx: &[usize],
non_slack_pos: &[Option<usize>],
pq_pos: &[Option<usize>],
n_angle: usize,
p_calc: &[f64],
q_calc: &[f64],
triplet_capacity: usize,
) -> Vec<(usize, usize, f64)> {
let vm: Vec<f64> = buses.iter().map(|b| b.voltage_mag).collect();
let va: Vec<f64> = buses.iter().map(|b| b.voltage_ang).collect();
let mut triplets = Vec::with_capacity(triplet_capacity);
for (row_idx, &i) in non_slack_idx.iter().enumerate() {
for &(k, y_ik) in ybus.row(i) {
if k == i {
triplets.push((row_idx, row_idx, -q_calc[i] - vm[i].powi(2) * y_ik.im));
if let Some(col_idx) = pq_pos[i] {
triplets.push((
row_idx,
n_angle + col_idx,
p_calc[i] / vm[i] + vm[i] * y_ik.re,
));
}
continue;
}
let angle_ik = va[i] - va[k];
if let Some(col_idx) = non_slack_pos[k] {
let h_ik =
vm[i] * vm[k] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos());
triplets.push((row_idx, col_idx, h_ik));
}
if let Some(col_idx) = pq_pos[k] {
let n_ik = vm[i] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin());
triplets.push((row_idx, n_angle + col_idx, n_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");
triplets.push((
n_angle + row_idx,
col_idx,
p_calc[i] - vm[i].powi(2) * y_ik.re,
));
triplets.push((
n_angle + row_idx,
n_angle + row_idx,
q_calc[i] / vm[i] - vm[i] * y_ik.im,
));
continue;
}
let angle_ik = va[i] - va[k];
if let Some(col_idx) = non_slack_pos[k] {
let m_ik =
-vm[i] * vm[k] * (y_ik.re * angle_ik.cos() + y_ik.im * angle_ik.sin());
triplets.push((n_angle + row_idx, col_idx, m_ik));
}
if let Some(col_idx) = pq_pos[k] {
let l_ik = vm[i] * (y_ik.re * angle_ik.sin() - y_ik.im * angle_ik.cos());
triplets.push((n_angle + row_idx, n_angle + col_idx, l_ik));
}
}
}
triplets
}
}