use crate::error::{OxiGridError, Result};
use nalgebra::{DMatrix, DVector};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum MeasurementType {
PowerInjection,
ReactiveInjection,
VoltageMagnitude,
BranchActivePower,
BranchReactivePower,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Measurement {
pub mtype: MeasurementType,
pub bus: usize,
pub to_bus: Option<usize>,
pub value: f64,
pub sigma: f64,
}
impl Measurement {
pub fn weight(&self) -> f64 {
1.0 / (self.sigma * self.sigma)
}
pub fn power_injection(bus: usize, value: f64, sigma: f64) -> Self {
Self {
mtype: MeasurementType::PowerInjection,
bus,
to_bus: None,
value,
sigma,
}
}
pub fn voltage(bus: usize, value: f64, sigma: f64) -> Self {
Self {
mtype: MeasurementType::VoltageMagnitude,
bus,
to_bus: None,
value,
sigma,
}
}
pub fn branch_flow(from_bus: usize, to_bus: usize, value: f64, sigma: f64) -> Self {
Self {
mtype: MeasurementType::BranchActivePower,
bus: from_bus,
to_bus: Some(to_bus),
value,
sigma,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DcSeResult {
pub theta: Vec<f64>,
pub residuals: Vec<f64>,
pub chi2: f64,
pub dof: usize,
pub converged: bool,
}
impl DcSeResult {
pub fn normalised_chi2(&self) -> f64 {
if self.dof == 0 {
return 0.0;
}
self.chi2 / self.dof as f64
}
}
pub struct DcStateEstimator {
pub n_bus: usize,
pub slack_idx: usize,
pub b_bus: Vec<Vec<f64>>,
pub branch_from: Vec<usize>,
pub branch_to: Vec<usize>,
pub branch_x: Vec<f64>,
}
impl DcStateEstimator {
pub fn new(
n_bus: usize,
slack_idx: usize,
b_bus: Vec<Vec<f64>>,
branch_from: Vec<usize>,
branch_to: Vec<usize>,
branch_x: Vec<f64>,
) -> Self {
Self {
n_bus,
slack_idx,
b_bus,
branch_from,
branch_to,
branch_x,
}
}
pub fn estimate(&self, measurements: &[Measurement]) -> Result<DcSeResult> {
let n_red = self.n_bus - 1;
let nm = measurements.len();
if nm < n_red {
return Err(OxiGridError::InvalidNetwork(format!(
"Under-determined: {nm} measurements for {n_red} states"
)));
}
let bus_map: Vec<usize> = (0..self.n_bus).filter(|&i| i != self.slack_idx).collect();
let mut h_mat = DMatrix::<f64>::zeros(nm, n_red);
let mut z_vec = DVector::<f64>::zeros(nm);
let mut w_diag = DVector::<f64>::zeros(nm);
for (mi, meas) in measurements.iter().enumerate() {
z_vec[mi] = meas.value;
w_diag[mi] = meas.weight();
match meas.mtype {
MeasurementType::PowerInjection => {
let bus = meas.bus;
for (ri, &j) in bus_map.iter().enumerate() {
h_mat[(mi, ri)] = self.b_bus[bus][j];
}
}
MeasurementType::BranchActivePower => {
let from = meas.bus;
let to = meas.to_bus.unwrap_or(0);
let br_idx = self
.branch_from
.iter()
.zip(self.branch_to.iter())
.position(|(&f, &t)| f == from && t == to)
.ok_or_else(|| {
OxiGridError::InvalidNetwork(format!("Branch {from}→{to} not found"))
})?;
let x_l = self.branch_x[br_idx];
if let Some(fi) = bus_map.iter().position(|&b| b == from) {
h_mat[(mi, fi)] = 1.0 / x_l;
}
if let Some(ti) = bus_map.iter().position(|&b| b == to) {
h_mat[(mi, ti)] = -1.0 / x_l;
}
}
_ => {}
}
}
let w_mat = DMatrix::<f64>::from_diagonal(&w_diag);
let ht = h_mat.transpose();
let g = &ht * &w_mat * &h_mat;
let rhs = &ht * &w_mat * &z_vec;
let lu = g.clone().lu();
let theta_red = lu
.solve(&rhs)
.ok_or_else(|| OxiGridError::LinearAlgebra("Gain matrix G is singular".into()))?;
let mut theta = vec![0.0_f64; self.n_bus];
for (ri, &i) in bus_map.iter().enumerate() {
theta[i] = theta_red[ri];
}
let h_theta = &h_mat * &theta_red;
let residuals: Vec<f64> = (0..nm).map(|i| z_vec[i] - h_theta[i]).collect();
let chi2: f64 = residuals
.iter()
.zip(w_diag.iter())
.map(|(r, &w)| r * r * w)
.sum();
let dof = nm.saturating_sub(n_red);
Ok(DcSeResult {
theta,
residuals,
chi2,
dof,
converged: true,
})
}
}
pub fn detect_bad_data(
residuals: &[f64],
measurements: &[Measurement],
threshold: f64,
) -> Vec<usize> {
residuals
.iter()
.zip(measurements.iter())
.enumerate()
.filter_map(|(i, (r, m))| {
let normalised = r.abs() / m.sigma;
if normalised > threshold {
Some(i)
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_3bus_estimator() -> DcStateEstimator {
let b_bus = vec![
vec![10.0, -10.0, 0.0],
vec![-10.0, 15.0, -5.0],
vec![0.0, -5.0, 5.0],
];
DcStateEstimator::new(3, 0, b_bus, vec![0, 1], vec![1, 2], vec![0.1, 0.2])
}
#[test]
fn test_dc_se_exact_measurements() {
let est = simple_3bus_estimator();
let meas = vec![
Measurement::branch_flow(0, 1, 0.5, 0.01),
Measurement::branch_flow(1, 2, 0.5, 0.01),
Measurement::power_injection(1, 0.0, 0.01), ];
let result = est.estimate(&meas).unwrap();
assert!(result.converged);
assert!(
result.theta[0].abs() < 1e-9,
"slack angle ≠ 0: {}",
result.theta[0]
);
assert!(
(result.theta[1] - (-0.05)).abs() < 1e-4,
"θ1 = {:.4}, expected -0.05",
result.theta[1]
);
assert!(
(result.theta[2] - (-0.15)).abs() < 1e-4,
"θ2 = {:.4}, expected -0.15",
result.theta[2]
);
}
#[test]
fn test_dc_se_redundant_measurements() {
let est = simple_3bus_estimator();
let meas = vec![
Measurement::branch_flow(0, 1, 0.5, 0.01),
Measurement::branch_flow(1, 2, 0.5, 0.01),
Measurement::power_injection(1, 0.0, 0.01),
Measurement::power_injection(2, -0.5, 0.01),
];
let result = est.estimate(&meas).unwrap();
assert!(result.converged);
assert!(result.dof == 2); }
#[test]
fn test_bad_data_detection() {
let meas = vec![
Measurement::power_injection(0, 0.5, 0.01),
Measurement::power_injection(1, 100.0, 0.01), ];
let residuals = [0.001, 1.0]; let bad = detect_bad_data(&residuals, &meas, 3.0);
assert_eq!(bad, vec![1]);
}
#[test]
fn test_measurement_weight() {
let m = Measurement::power_injection(0, 0.5, 0.02);
assert!((m.weight() - 2500.0).abs() < 1e-6); }
#[test]
fn test_under_determined_returns_error() {
let est = simple_3bus_estimator();
let meas = vec![Measurement::branch_flow(0, 1, 0.5, 0.01)];
assert!(est.estimate(&meas).is_err());
}
#[test]
fn test_normalised_chi2_zero_dof() {
let r = DcSeResult {
theta: vec![0.0, 0.0],
residuals: vec![0.5],
chi2: 99.0,
dof: 0,
converged: true,
};
assert_eq!(r.normalised_chi2(), 0.0);
}
#[test]
fn test_normalised_chi2_nonzero_dof() {
let r = DcSeResult {
theta: vec![0.0, -0.05, -0.15],
residuals: vec![0.001, -0.001],
chi2: 4.0,
dof: 2,
converged: true,
};
assert!((r.normalised_chi2() - 2.0).abs() < 1e-12);
}
#[test]
fn test_voltage_measurement_constructor() {
let m = Measurement::voltage(3, 1.02, 0.005);
assert_eq!(m.mtype, MeasurementType::VoltageMagnitude);
assert_eq!(m.bus, 3);
assert!(m.to_bus.is_none());
assert!((m.value - 1.02).abs() < 1e-12);
assert!((m.sigma - 0.005).abs() < 1e-12);
assert!((m.weight() - 40000.0).abs() < 1e-6);
}
#[test]
fn test_branch_flow_measurement_to_bus_set() {
let m = Measurement::branch_flow(1, 4, 0.3, 0.02);
assert_eq!(m.mtype, MeasurementType::BranchActivePower);
assert_eq!(m.bus, 1);
assert_eq!(m.to_bus, Some(4));
assert!((m.value - 0.3).abs() < 1e-12);
assert!((m.weight() - 2500.0).abs() < 1e-9);
}
#[test]
fn test_detect_bad_data_empty() {
let bad = detect_bad_data(&[], &[], 3.0);
assert!(bad.is_empty());
}
#[test]
fn test_detect_bad_data_all_good() {
let meas = vec![
Measurement::power_injection(0, 0.5, 0.01),
Measurement::power_injection(1, -0.5, 0.01),
];
let residuals = [0.005, -0.003];
let bad = detect_bad_data(&residuals, &meas, 3.0);
assert!(bad.is_empty());
}
#[test]
fn test_dc_se_slack_at_last_bus() {
let b_bus = vec![
vec![10.0, -10.0, 0.0],
vec![-10.0, 15.0, -5.0],
vec![0.0, -5.0, 5.0],
];
let est = DcStateEstimator::new(3, 2, b_bus, vec![0, 1], vec![1, 2], vec![0.1, 0.2]);
let meas = vec![
Measurement::branch_flow(0, 1, 1.0, 0.01),
Measurement::branch_flow(1, 2, 0.25, 0.01),
Measurement::power_injection(0, 1.0, 0.01),
];
let result = est
.estimate(&meas)
.expect("DC SE with last-bus slack should succeed");
assert!(result.converged);
assert!(
result.theta[2].abs() < 1e-9,
"slack θ[2] = {}",
result.theta[2]
);
assert!(
(result.theta[0] - 0.15).abs() < 1e-4,
"θ[0] = {:.6}, expected 0.15",
result.theta[0]
);
assert!(
(result.theta[1] - 0.05).abs() < 1e-4,
"θ[1] = {:.6}, expected 0.05",
result.theta[1]
);
}
}