use crate::measurement::{AngleFrame, Measurement, MeasurementKind, Target};
use crate::se::functional::{
Partial, current_partials_of, local_current_partials_of, power_partials_of,
};
use crate::types::Bus;
use super::SeNetwork;
pub struct StateLayout {
pub n_buses: usize,
pub angle_ref: Option<usize>,
theta_pos: Vec<Option<usize>>,
vmag_offset: usize,
}
impl StateLayout {
pub fn new(buses: &[Bus], measurements: &[Measurement], net: &SeNetwork) -> Self {
let phase_is_measured = measurements
.iter()
.any(|m| m.kind == MeasurementKind::VoltageAngle && m.weight() > 0.0);
let angle_ref = (!phase_is_measured).then(|| net.reference_bus(buses));
let mut theta_pos = vec![None; buses.len()];
let mut next = 0;
for i in 0..buses.len() {
if Some(i) != angle_ref {
theta_pos[i] = Some(next);
next += 1;
}
}
Self {
n_buses: buses.len(),
angle_ref,
theta_pos,
vmag_offset: next,
}
}
pub fn n_unknowns(&self) -> usize {
self.vmag_offset + self.n_buses
}
pub fn theta(&self, bus: usize) -> Option<usize> {
self.theta_pos[bus]
}
pub fn vmag(&self, bus: usize) -> usize {
self.vmag_offset + bus
}
}
pub type Row = Vec<(usize, f64)>;
fn push(row: &mut Row, col: Option<usize>, value: f64) {
if let Some(col) = col {
row.push((col, value));
}
}
#[cfg(test)]
fn injection_partials(
net: &SeNetwork,
buses: &[Bus],
p_inj: &[f64],
q_inj: &[f64],
i: usize,
k: usize,
) -> (f64, f64, f64, f64) {
let y_ik = net.ybus.get(i, k);
let (g, b) = (y_ik.re, y_ik.im);
let (v_i, v_k) = (buses[i].voltage_mag, buses[k].voltage_mag);
if i == k {
let v2 = v_i * v_i;
(
-q_inj[i] - b * v2,
p_inj[i] / v_i + g * v_i,
p_inj[i] - g * v2,
q_inj[i] / v_i - b * v_i,
)
} else {
let (s, c) = (buses[i].voltage_ang - buses[k].voltage_ang).sin_cos();
(
v_i * v_k * (g * s - b * c),
v_i * (g * c + b * s),
-v_i * v_k * (g * c + b * s),
v_i * (g * s - b * c),
)
}
}
fn add_functional_row(
row: &mut Row,
layout: &StateLayout,
at: usize,
coefficients: &[(usize, num_complex::Complex<f64>)],
v: &[num_complex::Complex<f64>],
active: bool,
scratch: &mut Vec<Partial>,
) {
scratch.clear();
power_partials_of(at, coefficients, v, scratch);
push_partials(row, layout, scratch, active);
}
fn push_partials(row: &mut Row, layout: &StateLayout, partials: &[Partial], real: bool) {
for p in partials {
let (d_th, d_v) = if real {
(p.d_dtheta.re, p.d_dvmag.re)
} else {
(p.d_dtheta.im, p.d_dvmag.im)
};
push(row, layout.theta(p.bus), d_th);
push(row, Some(layout.vmag(p.bus)), d_v);
}
}
pub fn injection_row(
layout: &StateLayout,
net: &SeNetwork,
v: &[num_complex::Complex<f64>],
bus: usize,
active: bool,
scratch: &mut Vec<Partial>,
) -> Row {
let mut row = Row::new();
add_functional_row(&mut row, layout, bus, net.ybus.row(bus), v, active, scratch);
row
}
pub fn measurement_jacobian(
measurements: &[Measurement],
buses: &[Bus],
net: &SeNetwork,
layout: &StateLayout,
) -> Vec<Row> {
let model = crate::se::MeasurementModel::new(measurements, net);
measurement_jacobian_with(measurements, buses, layout, &model)
}
pub fn measurement_jacobian_with(
measurements: &[Measurement],
buses: &[Bus],
layout: &StateLayout,
model: &crate::se::MeasurementModel,
) -> Vec<Row> {
let v: Vec<num_complex::Complex<f64>> = buses
.iter()
.map(|b| num_complex::Complex::from_polar(b.voltage_mag, b.voltage_ang))
.collect();
let mut scratch = Vec::new();
measurements
.iter()
.enumerate()
.map(|(i, m)| {
let mut row = Row::new();
if !model.is_live(i) {
return row;
}
match (m.kind, m.target) {
(MeasurementKind::VoltageMagnitude, Target::Bus(b)) => {
push(&mut row, Some(layout.vmag(b)), 1.0)
}
(MeasurementKind::VoltageAngle, Target::Bus(b)) => {
push(&mut row, layout.theta(b), 1.0)
}
_ => {
if let Some(f) = model.functional(i) {
scratch.clear();
match m.target {
Target::BranchTerminalCurrent { frame, .. } => match frame {
AngleFrame::Global => {
current_partials_of(&f.coefficients, &v, &mut scratch)
}
AngleFrame::Local => local_current_partials_of(
f.at,
&f.coefficients,
&v,
&mut scratch,
),
},
_ => power_partials_of(f.at, &f.coefficients, &v, &mut scratch),
}
let real = matches!(
m.kind,
MeasurementKind::ActivePower | MeasurementKind::CurrentReal
);
push_partials(&mut row, layout, &scratch, real);
}
}
}
row
})
.collect()
}
pub fn mask_untouched(
triplets: &mut Vec<(usize, usize, f64)>,
rhs: &mut [f64],
row_sets: &[&[Row]],
n: usize,
) -> Vec<usize> {
let mut touched = vec![false; n];
for rows in row_sets {
for row in rows.iter() {
for &(c, _) in row {
touched[c] = true;
}
}
}
let mut untouched = Vec::new();
for (c, &seen) in touched.iter().enumerate() {
if !seen {
triplets.push((c, c, 1.0));
rhs[c] = 0.0;
untouched.push(c);
}
}
untouched
}
pub fn gain_and_rhs(
rows: &[Row],
measurements: &[Measurement],
residuals: &[f64],
) -> (Vec<(usize, usize, f64)>, Vec<f64>, usize) {
let n = rows
.iter()
.flat_map(|r| r.iter().map(|&(c, _)| c + 1))
.max()
.unwrap_or(0);
let mut triplets = Vec::new();
let mut rhs = vec![0.0; n];
for ((row, m), &r) in rows.iter().zip(measurements).zip(residuals) {
let w = m.weight();
let w = if w.is_finite() { w } else { 0.0 };
for &(i, hi) in row {
rhs[i] += w * hi * r;
for &(j, hj) in row {
triplets.push((i, j, w * hi * hj));
}
}
}
(triplets, rhs, n)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::branch_flow::Terminal;
use crate::measurement::Target;
use crate::se::measurement_functions;
pub(super) fn assert_jacobian_matches_finite_differences(
measurements: &[Measurement],
buses: &[Bus],
net: &SeNetwork,
tol: f64,
) {
let layout = StateLayout::new(buses, measurements, net);
let rows = measurement_jacobian(measurements, buses, net, &layout);
let n = layout.n_unknowns();
let h = 1e-7;
let mut dense = vec![vec![0.0; n]; measurements.len()];
for (r, row) in rows.iter().enumerate() {
for &(c, val) in row {
dense[r][c] += val;
}
}
for col in 0..n {
let bump = |delta: f64| {
let mut b = buses.to_vec();
if col < layout.vmag(0) {
let bus = (0..buses.len())
.find(|&i| layout.theta(i) == Some(col))
.expect("angle column maps to a bus");
b[bus].voltage_ang += delta;
} else {
b[col - layout.vmag(0)].voltage_mag += delta;
}
measurement_functions(measurements, &b, net)
};
let plus = bump(h);
let minus = bump(-h);
for (r, m) in measurements.iter().enumerate() {
let numeric = (plus[r] - minus[r]) / (2.0 * h);
let analytic = dense[r][col];
assert!(
(numeric - analytic).abs() < tol,
"row {r} ({:?} on {:?}), column {col}: analytic {analytic}, numeric {numeric}",
m.kind,
m.target,
);
}
}
}
fn measurement(kind: MeasurementKind, target: Target) -> Measurement {
Measurement {
kind,
target,
value: 0.0,
sigma: 1.0,
}
}
#[test]
fn the_unified_row_matches_the_closed_form_it_replaced() {
let (net, mut buses) = super::super::tests::two_bus_net();
buses[0].voltage_mag = 1.037;
buses[0].voltage_ang = 0.147;
buses[1].voltage_mag = 0.962;
buses[1].voltage_ang = -0.083;
let (p_inj, q_inj) = crate::network::power_injections(&buses, &net.ybus);
let v: Vec<num_complex::Complex<f64>> = buses
.iter()
.map(|b| num_complex::Complex::from_polar(b.voltage_mag, b.voltage_ang))
.collect();
let mut compared = 0;
for bus in 0..buses.len() {
let mut partials = Vec::new();
power_partials_of(bus, net.ybus.row(bus), &v, &mut partials);
for p in &partials {
let (dp_dth, dp_dv, dq_dth, dq_dv) =
injection_partials(&net, &buses, &p_inj, &q_inj, bus, p.bus);
assert!(
(p.d_dtheta.re - dp_dth).abs() < 1e-12,
"dP/dθ at ({bus},{})",
p.bus
);
assert!(
(p.d_dvmag.re - dp_dv).abs() < 1e-12,
"dP/dV at ({bus},{})",
p.bus
);
assert!(
(p.d_dtheta.im - dq_dth).abs() < 1e-12,
"dQ/dθ at ({bus},{})",
p.bus
);
assert!(
(p.d_dvmag.im - dq_dv).abs() < 1e-12,
"dQ/dV at ({bus},{})",
p.bus
);
compared += 1;
}
}
assert!(
compared >= 4,
"expected several neighbours to compare, got {compared}"
);
}
#[test]
fn jacobian_matches_finite_differences_for_every_target_kind() {
let (net, buses) = super::super::tests::two_bus_net();
let measurements = vec![
measurement(MeasurementKind::VoltageMagnitude, Target::Bus(1)),
measurement(MeasurementKind::VoltageAngle, Target::Bus(1)),
measurement(MeasurementKind::ActivePower, Target::Bus(1)),
measurement(MeasurementKind::ReactivePower, Target::Bus(1)),
measurement(
MeasurementKind::ActivePower,
Target::BranchTerminal {
branch: 0,
terminal: Terminal::From,
},
),
measurement(
MeasurementKind::ReactivePower,
Target::BranchTerminal {
branch: 0,
terminal: Terminal::To,
},
),
measurement(
MeasurementKind::ActivePower,
Target::SourceInjection { branch: 0 },
),
measurement(
MeasurementKind::ReactivePower,
Target::ShuntInjection { bus: 1 },
),
measurement(MeasurementKind::ActivePower, Target::NodeInjection(1)),
measurement(MeasurementKind::ReactivePower, Target::NodeInjection(1)),
];
assert_jacobian_matches_finite_differences(&measurements, &buses, &net, 1e-5);
}
#[test]
fn state_layout_excludes_only_the_reference_angle() {
let (net, buses) = super::super::tests::two_bus_net();
let layout = StateLayout::new(&buses, &[], &net);
assert_eq!(layout.n_unknowns(), 2 * buses.len() - 1);
let reference = layout
.angle_ref
.expect("no angle measurements, so a reference is pinned");
assert_eq!(layout.theta(reference), None);
for i in 0..buses.len() {
if i != reference {
assert!(
layout.theta(i).is_some(),
"bus {i} should carry an angle unknown"
);
}
}
}
#[test]
fn an_infinite_sigma_changes_values_but_not_the_gain_pattern() {
let (net, buses) = super::super::tests::two_bus_net();
let finite = vec![
measurement(MeasurementKind::VoltageMagnitude, Target::Bus(1)),
measurement(MeasurementKind::ActivePower, Target::Bus(1)),
];
let mut disabled = finite.clone();
disabled[1].sigma = f64::INFINITY;
let layout = StateLayout::new(&buses, &finite, &net);
let residuals = [0.1, 0.2];
let pattern = |ms: &[crate::measurement::Measurement]| {
let rows = measurement_jacobian(ms, &buses, &net, &layout);
let (triplets, _, _) = gain_and_rhs(&rows, ms, &residuals);
triplets
};
let a = pattern(&finite);
let b = pattern(&disabled);
assert_eq!(
a.iter().map(|&(i, j, _)| (i, j)).collect::<Vec<_>>(),
b.iter().map(|&(i, j, _)| (i, j)).collect::<Vec<_>>(),
"disabling a sensor must not move, add or drop a triplet position"
);
assert!(
a.iter().zip(&b).any(|(x, y)| x.2 != y.2),
"the values should differ — otherwise the sigma did nothing at all"
);
let rows = measurement_jacobian(&disabled, &buses, &net, &layout);
let (_, rhs, _) = gain_and_rhs(&rows, &disabled, &residuals);
let only_voltage = vec![finite[0]];
let vrows = measurement_jacobian(&only_voltage, &buses, &net, &layout);
let (_, vrhs, _) = gain_and_rhs(&vrows, &only_voltage, &residuals[..1]);
for (x, y) in rhs.iter().zip(&vrhs) {
assert!(
(x - y).abs() < 1e-15,
"an infinite sigma still moved the rhs: {x} vs {y}"
);
}
}
#[test]
fn gain_matrix_is_symmetric() {
let (net, buses) = super::super::tests::two_bus_net();
let measurements = vec![
measurement(MeasurementKind::VoltageMagnitude, Target::Bus(1)),
measurement(MeasurementKind::ActivePower, Target::Bus(1)),
measurement(
MeasurementKind::ReactivePower,
Target::BranchTerminal {
branch: 0,
terminal: Terminal::From,
},
),
];
let layout = StateLayout::new(&buses, &measurements, &net);
let rows = measurement_jacobian(&measurements, &buses, &net, &layout);
let (triplets, _, n) = gain_and_rhs(&rows, &measurements, &[0.1, 0.2, 0.3]);
let mut dense = vec![vec![0.0; n]; n];
for (i, j, v) in triplets {
dense[i][j] += v;
}
for i in 0..n {
assert!(
dense[i][i] >= -1e-12,
"diagonal {i} is negative: {}",
dense[i][i]
);
for j in 0..n {
assert!(
(dense[i][j] - dense[j][i]).abs() < 1e-9,
"G is not symmetric at ({i},{j}): {} vs {}",
dense[i][j],
dense[j][i]
);
}
}
}
}