use nalgebra::DMatrix;
use num_complex::Complex64;
use sprs::{CsMat, TriMat};
pub fn build_jacobian_sparse(
ybus: &CsMat<Complex64>,
v_mag: &[f64],
v_ang: &[f64],
p_calc: &[f64],
q_calc: &[f64],
pq_indices: &[usize],
pvpq_indices: &[usize],
) -> CsMat<f64> {
let n = v_mag.len();
let npvpq = pvpq_indices.len();
let npq = pq_indices.len();
let j_size = npvpq + npq;
let nnz_bound = 8 * ybus.nnz();
let mut tri: TriMat<f64> = TriMat::with_capacity((j_size, j_size), nnz_bound);
let mut pvpq_map = vec![usize::MAX; n];
for (row, &i) in pvpq_indices.iter().enumerate() {
pvpq_map[i] = row;
}
let mut pq_map = vec![usize::MAX; n];
for (row, &i) in pq_indices.iter().enumerate() {
pq_map[i] = row;
}
for (&yij_val, (i, j)) in ybus.iter() {
let in_pvpq_i = pvpq_map[i] != usize::MAX;
let in_pq_i = pq_map[i] != usize::MAX;
if i == j {
let g_ii = yij_val.re;
let b_ii = yij_val.im;
let v2 = v_mag[i] * v_mag[i];
if in_pvpq_i {
let row = pvpq_map[i];
tri.add_triplet(row, row, -q_calc[i] - b_ii * v2);
if in_pq_i {
let col = pq_map[i];
tri.add_triplet(row, npvpq + col, p_calc[i] + g_ii * v2);
}
}
if in_pq_i {
let row = pq_map[i];
let pvpq_col = pvpq_map[i];
tri.add_triplet(npvpq + row, pvpq_col, p_calc[i] - g_ii * v2);
tri.add_triplet(npvpq + row, npvpq + row, q_calc[i] - b_ii * v2);
}
} else {
let theta_ij = v_ang[i] - v_ang[j];
let (sin_ij, cos_ij) = theta_ij.sin_cos();
let vm_ij = v_mag[i] * v_mag[j];
let g = yij_val.re;
let b = yij_val.im;
let gs_bc = g * sin_ij - b * cos_ij; let gc_bs = g * cos_ij + b * sin_ij;
let in_pvpq_j = pvpq_map[j] != usize::MAX;
let in_pq_j = pq_map[j] != usize::MAX;
if in_pvpq_i {
let row = pvpq_map[i];
if in_pvpq_j {
tri.add_triplet(row, pvpq_map[j], vm_ij * gs_bc);
}
if in_pq_j {
tri.add_triplet(row, npvpq + pq_map[j], vm_ij * gc_bs);
}
}
if in_pq_i {
let row = pq_map[i];
if in_pvpq_j {
tri.add_triplet(npvpq + row, pvpq_map[j], -vm_ij * gc_bs);
}
if in_pq_j {
tri.add_triplet(npvpq + row, npvpq + pq_map[j], vm_ij * gs_bc);
}
}
}
}
tri.to_csr()
}
pub fn build_jacobian(
ybus: &CsMat<Complex64>,
v_mag: &[f64],
v_ang: &[f64],
p_calc: &[f64],
q_calc: &[f64],
pq_indices: &[usize],
pvpq_indices: &[usize],
) -> DMatrix<f64> {
let csmat = build_jacobian_sparse(ybus, v_mag, v_ang, p_calc, q_calc, pq_indices, pvpq_indices);
let n = csmat.rows();
let mut dense = DMatrix::zeros(n, n);
for (&v, (r, c)) in csmat.iter() {
dense[(r, c)] = v;
}
dense
}
#[cfg(feature = "parallel")]
pub fn build_jacobian_parallel(
ybus: &CsMat<Complex64>,
v_mag: &[f64],
v_ang: &[f64],
p_calc: &[f64],
q_calc: &[f64],
pq_indices: &[usize],
pvpq_indices: &[usize],
) -> DMatrix<f64> {
let csmat = build_jacobian_sparse(ybus, v_mag, v_ang, p_calc, q_calc, pq_indices, pvpq_indices);
let n = csmat.rows();
let mut dense = DMatrix::zeros(n, n);
for (&v, (r, c)) in csmat.iter() {
dense[(r, c)] = v;
}
dense
}
#[cfg(test)]
mod tests {
use super::*;
use num_complex::Complex64;
use sprs::TriMat;
fn make_3bus_ybus() -> CsMat<Complex64> {
let y01 = Complex64::new(0.01, 0.05).inv();
let y12 = Complex64::new(0.02, 0.08).inv();
let y_sh0 = Complex64::new(0.0, 0.001);
let mut tri: TriMat<Complex64> = TriMat::new((3, 3));
tri.add_triplet(0, 0, y01 + y_sh0);
tri.add_triplet(1, 1, y01 + y12);
tri.add_triplet(2, 2, y12);
tri.add_triplet(0, 1, -y01);
tri.add_triplet(1, 0, -y01);
tri.add_triplet(1, 2, -y12);
tri.add_triplet(2, 1, -y12);
tri.to_csc()
}
#[test]
fn jacobian_sparse_matches_dense_3bus() {
let ybus = make_3bus_ybus();
let pvpq_indices = &[1usize, 2];
let pq_indices = &[1usize, 2];
let v_mag = [1.0_f64, 0.98, 0.96];
let v_ang = [0.0_f64, -0.03, -0.06];
let p_calc = {
let v: Vec<Complex64> = v_mag
.iter()
.zip(v_ang.iter())
.map(|(&m, &a)| Complex64::from_polar(m, a))
.collect();
let mut p = [0.0_f64; 3];
for (&yij, (i, j)) in ybus.iter() {
let s = v[i] * (yij * v[j]).conj();
p[i] += s.re;
}
p
};
let q_calc = {
let v: Vec<Complex64> = v_mag
.iter()
.zip(v_ang.iter())
.map(|(&m, &a)| Complex64::from_polar(m, a))
.collect();
let mut q = [0.0_f64; 3];
for (&yij, (i, j)) in ybus.iter() {
let s = v[i] * (yij * v[j]).conj();
q[i] += s.im;
}
q
};
let jac_dense = build_jacobian(
&ybus,
&v_mag,
&v_ang,
&p_calc,
&q_calc,
pq_indices,
pvpq_indices,
);
let jac_sparse = build_jacobian_sparse(
&ybus,
&v_mag,
&v_ang,
&p_calc,
&q_calc,
pq_indices,
pvpq_indices,
);
let n = jac_dense.nrows();
assert_eq!(n, jac_sparse.rows(), "Jacobian row count must match");
assert_eq!(n, jac_sparse.cols(), "Jacobian col count must match");
for r in 0..n {
for c in 0..n {
let dense_val = jac_dense[(r, c)];
let sparse_val = jac_sparse.get(r, c).copied().unwrap_or(0.0);
assert!(
(dense_val - sparse_val).abs() < 1e-12,
"Jacobian[{r},{c}]: dense={dense_val:.15e} sparse={sparse_val:.15e}"
);
}
}
}
#[test]
fn jacobian_sparse_nnz_bounded_ieee14() {
let net = crate::testcases::ieee::ieee14().expect("IEEE 14-bus must load");
let ybus = net.admittance_matrix().expect("Y-bus must build");
let mut pq_indices = Vec::new();
let mut pv_indices = Vec::new();
for (i, bus) in net.buses.iter().enumerate() {
match bus.bus_type {
crate::network::bus::BusType::PQ => pq_indices.push(i),
crate::network::bus::BusType::PV => pv_indices.push(i),
crate::network::bus::BusType::Slack => {}
}
}
let mut pvpq_indices = pv_indices.clone();
pvpq_indices.extend_from_slice(&pq_indices);
pvpq_indices.sort();
let n = net.bus_count();
let v_mag = net.buses.iter().map(|b| b.vm).collect::<Vec<_>>();
let v_ang = net.buses.iter().map(|b| b.va).collect::<Vec<_>>();
let (p_calc, q_calc) = {
let v: Vec<Complex64> = v_mag
.iter()
.zip(v_ang.iter())
.map(|(&m, &a)| Complex64::from_polar(m, a))
.collect();
let mut p = vec![0.0_f64; n];
let mut q = vec![0.0_f64; n];
for (&yij, (i, j)) in ybus.iter() {
let s = v[i] * (yij * v[j]).conj();
p[i] += s.re;
q[i] += s.im;
}
(p, q)
};
let jac_sparse = build_jacobian_sparse(
&ybus,
&v_mag,
&v_ang,
&p_calc,
&q_calc,
&pq_indices,
&pvpq_indices,
);
let j_size = pvpq_indices.len() + pq_indices.len();
let dense_bound = (j_size * j_size) as f64;
let nnz = jac_sparse.nnz();
assert!(
(nnz as f64) < 0.4 * dense_bound,
"Jacobian nnz={nnz} must be < 40% of j_size²={dense_bound:.0} for IEEE 14-bus"
);
}
}