use num_complex::Complex;
use crate::measurement::{AngleFrame, Measurement, MeasurementKind, Target};
use crate::types::Bus;
use gridoxide_sparse::sparse::ComplexSparseSystem;
use super::SeNetwork;
use super::nr::{SeOptions, SeReport, SeStatus};
#[derive(Clone, Copy, Debug)]
enum RowKind {
Voltage {
bus: usize,
magnitude: f64,
angle: Option<f64>,
},
Power(Complex<f64>),
GlobalCurrent(Complex<f64>),
LocalCurrent(Complex<f64>),
}
struct LinearRow {
coefficients: Vec<(usize, Complex<f64>)>,
kind: RowKind,
reference_bus: usize,
weight: f64,
}
impl RowKind {
fn measured(&self, v: &[Complex<f64>], reference_bus: usize) -> Option<Complex<f64>> {
match *self {
RowKind::Voltage {
bus,
magnitude,
angle,
} => Some(Complex::from_polar(
magnitude,
angle.unwrap_or_else(|| v[bus].arg()),
)),
RowKind::Power(s) => {
let u = v[reference_bus];
(u.norm() > 0.0).then(|| (s / u).conj())
}
RowKind::GlobalCurrent(i) => Some(i),
RowKind::LocalCurrent(i) => {
let u = v[reference_bus];
(u.norm() > 0.0).then(|| i.conj() * u / u.norm())
}
}
}
fn weight_scale(&self, buses: &[Bus], reference_bus: usize) -> f64 {
match self {
RowKind::Power(_) => buses[reference_bus].voltage_mag.powi(2),
_ => 1.0,
}
}
}
fn build_rows(measurements: &[Measurement], net: &SeNetwork) -> Vec<LinearRow> {
use std::collections::HashMap;
let mut by_target: HashMap<Target, Vec<usize>> = HashMap::new();
let mut order: Vec<Target> = Vec::new();
for (i, m) in measurements.iter().enumerate() {
let entry = by_target.entry(m.target).or_default();
if entry.is_empty() {
order.push(m.target);
}
entry.push(i);
}
let mut rows = Vec::new();
for target in order {
let indices = &by_target[&target];
let find = |kind: MeasurementKind| {
indices
.iter()
.copied()
.find(|&i| measurements[i].kind == kind && measurements[i].weight() > 0.0)
};
let magnitude = find(MeasurementKind::VoltageMagnitude);
let angle = find(MeasurementKind::VoltageAngle);
if let (Some(mag), Target::Bus(bus)) = (magnitude, target) {
if !net.energized[bus] {
continue;
}
let m = &measurements[mag];
rows.push(LinearRow {
coefficients: vec![(bus, Complex::new(1.0, 0.0))],
kind: RowKind::Voltage {
bus,
magnitude: m.value,
angle: angle.map(|a| measurements[a].value),
},
reference_bus: bus,
weight: m.weight(),
});
}
let (real, imag) = match target {
Target::BranchTerminalCurrent { .. } => (
find(MeasurementKind::CurrentReal),
find(MeasurementKind::CurrentImag),
),
_ => (
find(MeasurementKind::ActivePower),
find(MeasurementKind::ReactivePower),
),
};
let (Some(p), Some(q)) = (real, imag) else {
continue;
};
let Some(functional) = net.functional(target) else {
continue;
};
if !net.energized[functional.at] {
continue;
}
let variance = measurements[p].sigma.powi(2) + measurements[q].sigma.powi(2);
if variance <= 0.0 || !variance.is_finite() {
continue;
}
let value = Complex::new(measurements[p].value, measurements[q].value);
rows.push(LinearRow {
coefficients: functional.coefficients,
kind: match target {
Target::BranchTerminalCurrent {
frame: AngleFrame::Global,
..
} => RowKind::GlobalCurrent(value),
Target::BranchTerminalCurrent {
frame: AngleFrame::Local,
..
} => RowKind::LocalCurrent(value),
_ => RowKind::Power(value),
},
reference_bus: functional.at,
weight: 1.0 / variance,
});
}
rows
}
pub fn estimate(
measurements: &[Measurement],
buses: &mut [Bus],
net: &SeNetwork,
options: &SeOptions,
) -> SeReport {
let n = buses.len();
let rows = build_rows(measurements, net);
if rows.is_empty() {
return SeReport {
status: SeStatus::Singular,
iterations: 0,
objective: f64::NAN,
last_step: f64::INFINITY,
residuals: vec![0.0; measurements.len()],
unconstrained: Vec::new(),
};
}
let constrained: Vec<usize> = net
.constrained_buses()
.iter()
.enumerate()
.filter(|&(_, &z)| z)
.map(|(i, _)| i)
.collect();
let n_aug = n + constrained.len();
let mut triplets: Vec<(usize, usize, Complex<f64>)> = Vec::new();
let mut row_weights = Vec::with_capacity(rows.len());
for row in &rows {
let w = row.weight * row.kind.weight_scale(buses, row.reference_bus);
row_weights.push(w);
for &(i, ci) in &row.coefficients {
for &(j, cj) in &row.coefficients {
triplets.push((i, j, ci.conj() * cj * w));
}
}
}
for (k, &bus) in constrained.iter().enumerate() {
let lambda = n + k;
for &(j, y) in net.ybus.row(bus) {
triplets.push((lambda, j, y));
triplets.push((j, lambda, y.conj()));
}
}
let mut touched = vec![false; n];
for row in &rows {
for &(i, _) in &row.coefficients {
touched[i] = true;
}
}
for &bus in &constrained {
for &(j, _) in net.ybus.row(bus) {
touched[j] = true;
}
}
let unconstrained: Vec<usize> = (0..n).filter(|&i| !touched[i]).collect();
for &i in &unconstrained {
triplets.push((i, i, Complex::new(1.0, 0.0)));
}
let Some(system) = ComplexSparseSystem::new(n_aug, &triplets) else {
return SeReport {
status: SeStatus::Singular,
iterations: 0,
objective: f64::NAN,
last_step: f64::INFINITY,
residuals: vec![0.0; measurements.len()],
unconstrained,
};
};
let phase_is_measured = rows
.iter()
.any(|r| matches!(r.kind, RowKind::Voltage { angle: Some(_), .. }));
let reference = net.reference_bus(buses);
let mut v: Vec<Complex<f64>> = buses
.iter()
.map(|b| Complex::from_polar(b.voltage_mag, b.voltage_ang))
.collect();
let mut last_step = f64::INFINITY;
let mut status = SeStatus::MaxIterations;
let mut iterations = options.max_iter;
let mut relaxation = 1.0f64;
let mut previous_step = f64::INFINITY;
for iteration in 1..=options.max_iter {
let mut rhs = vec![Complex::new(0.0, 0.0); n_aug];
for (row, &w) in rows.iter().zip(&row_weights) {
let Some(measured) = row.kind.measured(&v, row.reference_bus) else {
continue;
};
for &(i, ci) in &row.coefficients {
rhs[i] += ci.conj() * w * measured;
}
}
let Some(solution) = system.solve(&rhs) else {
return SeReport {
status: SeStatus::Singular,
iterations: iteration,
objective: f64::NAN,
last_step,
residuals: vec![0.0; measurements.len()],
unconstrained,
};
};
let mut next: Vec<Complex<f64>> = solution[..n].to_vec();
if !phase_is_measured {
let r = next[reference];
if r.norm() > 0.0 {
let rotation = Complex::from_polar(1.0, -r.arg());
for u in next.iter_mut() {
*u *= rotation;
}
}
}
let raw_step = next
.iter()
.zip(&v)
.map(|(a, b)| (a - b).norm())
.fold(0.0f64, f64::max);
if iteration > 2 {
if raw_step > previous_step * 0.9 {
relaxation = (relaxation * 0.5).max(1.0 / 64.0);
} else if raw_step < previous_step * 0.75 {
relaxation = (relaxation * 1.2).min(1.0);
}
}
previous_step = raw_step;
for (u, n) in v.iter_mut().zip(&next) {
*u += (n - *u) * relaxation;
}
last_step = raw_step * relaxation;
if last_step < options.tol {
status = SeStatus::Converged;
iterations = iteration;
break;
}
}
for (bus, u) in buses.iter_mut().zip(&v) {
bus.voltage_mag = u.norm();
bus.voltage_ang = u.arg();
}
let h = super::measurement_functions(measurements, buses, net);
let residuals: Vec<f64> = measurements
.iter()
.zip(&h)
.map(|(m, &hi)| m.value - hi)
.collect();
let objective = measurements
.iter()
.zip(&residuals)
.filter(|(m, _)| m.weight().is_finite() && m.weight() > 0.0)
.map(|(m, &r)| m.weight() * r * r)
.sum::<f64>()
/ 2.0;
SeReport {
status,
iterations,
objective,
last_step,
residuals,
unconstrained,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::branch_flow::Terminal;
use crate::se::measurement_functions;
use crate::se::nr::flat_start;
fn m(kind: MeasurementKind, target: Target, sigma: f64) -> Measurement {
Measurement {
kind,
target,
value: 0.0,
sigma,
}
}
#[test]
fn unpaired_power_measurements_are_dropped() {
let (net, _) = crate::se::tests::two_bus_net();
let measurements = vec![m(MeasurementKind::ActivePower, Target::Bus(1), 0.01)];
assert!(build_rows(&measurements, &net).is_empty());
}
#[test]
fn a_magnitude_and_angle_pair_into_one_row() {
let (net, _) = crate::se::tests::two_bus_net();
let measurements = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(1), 0.01),
m(MeasurementKind::VoltageAngle, Target::Bus(1), 0.01),
];
let rows = build_rows(&measurements, &net);
assert_eq!(rows.len(), 1);
assert!(
matches!(rows[0].kind, RowKind::Voltage { angle: Some(_), .. }),
"the phasor row should carry the measured angle"
);
}
#[test]
fn the_true_state_is_a_fixed_point() {
let (net, truth) = crate::se::tests::two_bus_net();
let probe = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 0.001),
m(MeasurementKind::VoltageMagnitude, Target::Bus(1), 0.001),
m(MeasurementKind::ActivePower, Target::Bus(1), 0.001),
m(MeasurementKind::ReactivePower, Target::Bus(1), 0.001),
];
let exact = measurement_functions(&probe, &truth, &net);
let measurements: Vec<Measurement> = probe
.iter()
.zip(&exact)
.map(|(p, &v)| Measurement { value: v, ..*p })
.collect();
let mut buses = truth.clone();
let report = estimate(
&measurements,
&mut buses,
&net,
&SeOptions {
max_iter: 1,
..SeOptions::default()
},
);
for (i, r) in report.residuals.iter().enumerate() {
assert!(
r.abs() < 1e-9,
"measurement {i} residual {r} should stay at zero"
);
}
for (i, (got, want)) in buses.iter().zip(&truth).enumerate() {
assert!(
(got.voltage_mag - want.voltage_mag).abs() < 1e-9,
"bus {i} magnitude moved: {} vs {}",
got.voltage_mag,
want.voltage_mag
);
}
let offsets: Vec<f64> = buses
.iter()
.zip(&truth)
.map(|(g, w)| g.voltage_ang - w.voltage_ang)
.collect();
for (i, offset) in offsets.iter().enumerate() {
assert!(
(offset - offsets[0]).abs() < 1e-9,
"bus {i} angle offset {offset} is not the shared rotation {}",
offsets[0]
);
}
}
#[test]
fn recovers_the_state_its_measurements_came_from() {
let (net, truth) = crate::se::tests::two_bus_net();
let probe = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 0.001),
m(MeasurementKind::VoltageMagnitude, Target::Bus(1), 0.001),
m(MeasurementKind::ActivePower, Target::Bus(1), 0.001),
m(MeasurementKind::ReactivePower, Target::Bus(1), 0.001),
m(
MeasurementKind::ActivePower,
Target::BranchTerminal {
branch: 0,
terminal: Terminal::From,
},
0.001,
),
m(
MeasurementKind::ReactivePower,
Target::BranchTerminal {
branch: 0,
terminal: Terminal::From,
},
0.001,
),
];
let exact = measurement_functions(&probe, &truth, &net);
let measurements: Vec<Measurement> = probe
.iter()
.zip(&exact)
.map(|(p, &v)| Measurement { value: v, ..*p })
.collect();
let mut buses = truth.clone();
flat_start(&mut buses, &measurements);
let report = estimate(&measurements, &mut buses, &net, &SeOptions::default());
assert_eq!(report.status, SeStatus::Converged, "{report:?}");
for (i, (got, want)) in buses.iter().zip(&truth).enumerate() {
assert!(
(got.voltage_mag - want.voltage_mag).abs() < 1e-6,
"bus {i}: {} vs {}",
got.voltage_mag,
want.voltage_mag
);
}
}
}