use crate::measurement::Measurement;
use crate::solver::JacobianBackend;
use crate::types::Bus;
use gridoxide_sparse::lu::SparseLu;
use super::constraints::Constraints;
use super::jacobian::{StateLayout, gain_and_rhs, mask_untouched, measurement_jacobian_with};
use super::{MeasurementModel, SeNetwork, measurement_functions, measurement_functions_with};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SeStatus {
Converged,
MaxIterations,
Singular,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum SeMethod {
#[default]
NewtonRaphson,
IterativeLinear,
}
#[derive(Clone, Copy, Debug)]
pub struct SeOptions {
pub tol: f64,
pub max_iter: usize,
pub backend: JacobianBackend,
pub method: SeMethod,
}
impl Default for SeOptions {
fn default() -> Self {
Self {
tol: 1e-8,
max_iter: 20,
backend: JacobianBackend::default(),
method: SeMethod::default(),
}
}
}
#[derive(Clone, Debug)]
pub struct SeReport {
pub status: SeStatus,
pub iterations: usize,
pub objective: f64,
pub last_step: f64,
pub residuals: Vec<f64>,
pub unconstrained: Vec<usize>,
}
pub fn flat_start(buses: &mut [Bus], measurements: &[Measurement]) {
for b in buses.iter_mut() {
b.voltage_mag = 1.0;
b.voltage_ang = 0.0;
}
apply_measured_magnitudes(buses, measurements);
}
pub fn linear_start(buses: &mut [Bus], net: &SeNetwork, measurements: &[Measurement]) {
for b in buses.iter_mut() {
if !matches!(b.bus_type, crate::types::BusType::Slack) {
b.voltage_mag = 1.0;
b.voltage_ang = 0.0;
}
}
crate::network::linear_initial_guess(buses, &net.ybus);
apply_measured_magnitudes(buses, measurements);
for (b, &live) in buses.iter_mut().zip(&net.energized) {
if !live {
b.voltage_mag = 0.0;
b.voltage_ang = 0.0;
}
}
}
fn apply_measured_magnitudes(buses: &mut [Bus], measurements: &[Measurement]) {
for m in measurements {
if let (
crate::measurement::MeasurementKind::VoltageMagnitude,
crate::measurement::Target::Bus(bus),
) = (m.kind, m.target)
{
if m.value.is_finite() && m.value > 0.0 {
buses[bus].voltage_mag = m.value;
}
}
}
}
fn estimate_with(
measurements: &[Measurement],
buses: &mut [Bus],
net: &SeNetwork,
layout: &StateLayout,
constraints: &Constraints,
model: &MeasurementModel,
options: &SeOptions,
lu: &mut SparseLu,
) -> SeReport {
let mut residuals = Vec::new();
let mut last_step = f64::INFINITY;
let mut last_unconstrained = Vec::new();
for iteration in 1..=options.max_iter {
let h = measurement_functions_with(measurements, buses, model);
residuals = measurements
.iter()
.zip(&h)
.map(|(m, &hi)| m.value - hi)
.collect();
let rows = measurement_jacobian_with(measurements, buses, layout, model);
let (mut triplets, rhs, _) = gain_and_rhs(&rows, measurements, &residuals);
let n = layout.n_unknowns();
let mut rhs = rhs;
rhs.resize(n, 0.0);
let (c_values, c_rows) = constraints.evaluate(buses, net, layout);
let unconstrained = mask_untouched(&mut triplets, &mut rhs, &[&rows, &c_rows], n);
let (triplets, rhs) = super::constraints::augment(triplets, rhs, n, &c_values, &c_rows);
let n_aug = n + constraints.len();
if !lu.is_analyzed() && !lu.analyze(n_aug, &triplets) {
return SeReport {
status: SeStatus::Singular,
iterations: iteration,
objective: objective(measurements, &residuals),
last_step,
residuals,
unconstrained,
};
}
let Some(dx) = lu.factor_and_solve(&triplets, &rhs) else {
return SeReport {
status: SeStatus::Singular,
iterations: iteration,
objective: objective(measurements, &residuals),
last_step,
residuals,
unconstrained,
};
};
for bus in 0..layout.n_buses {
if let Some(col) = layout.theta(bus) {
buses[bus].voltage_ang += dx[col];
}
buses[bus].voltage_mag += dx[layout.vmag(bus)];
}
last_unconstrained = unconstrained.clone();
last_step = dx[..n].iter().fold(0.0f64, |a, &b| a.max(b.abs()));
if last_step < options.tol {
let h = measurement_functions(measurements, buses, net);
let residuals: Vec<f64> = measurements
.iter()
.zip(&h)
.map(|(m, &hi)| m.value - hi)
.collect();
return SeReport {
status: SeStatus::Converged,
iterations: iteration,
objective: objective(measurements, &residuals),
last_step,
residuals,
unconstrained,
};
}
}
SeReport {
status: SeStatus::MaxIterations,
iterations: options.max_iter,
objective: objective(measurements, &residuals),
last_step,
residuals,
unconstrained: last_unconstrained,
}
}
fn objective(measurements: &[Measurement], residuals: &[f64]) -> f64 {
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
}
pub struct PersistentEstimator {
options: SeOptions,
layout: Option<StateLayout>,
constraints: Option<Constraints>,
model: Option<MeasurementModel>,
lu: SparseLu,
}
impl PersistentEstimator {
pub fn new(options: SeOptions) -> Self {
let lu = SparseLu::new(options.backend.lu_backend());
Self {
options,
layout: None,
constraints: None,
model: None,
lu,
}
}
pub fn reset(&mut self) {
self.layout = None;
self.constraints = None;
self.model = None;
self.lu.reset();
}
pub fn estimate(
&mut self,
measurements: &[Measurement],
buses: &mut [Bus],
net: &SeNetwork,
) -> SeReport {
if self.options.method == SeMethod::IterativeLinear {
return super::iterative::estimate(measurements, buses, net, &self.options);
}
if self.layout.is_none() {
self.layout = Some(StateLayout::new(buses, measurements, net));
}
if self.constraints.is_none() {
self.constraints = Some(Constraints::new(net));
}
if self.model.is_none() {
self.model = Some(MeasurementModel::new(measurements, net));
}
let layout = self.layout.as_ref().expect("just populated");
let constraints = self.constraints.as_ref().expect("just populated");
let model = self.model.as_ref().expect("just populated");
estimate_with(
measurements,
buses,
net,
layout,
constraints,
model,
&self.options,
&mut self.lu,
)
}
}
pub fn estimate(
measurements: &[Measurement],
buses: &mut [Bus],
net: &SeNetwork,
options: &SeOptions,
) -> SeReport {
if options.method == SeMethod::IterativeLinear {
return super::iterative::estimate(measurements, buses, net, options);
}
let layout = StateLayout::new(buses, measurements, net);
let constraints = Constraints::new(net);
let model = MeasurementModel::new(measurements, net);
estimate_with(
measurements,
buses,
net,
&layout,
&constraints,
&model,
options,
&mut SparseLu::new(options.backend.lu_backend()),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::measurement::{MeasurementKind, Target};
fn m(kind: MeasurementKind, target: Target, value: f64, sigma: f64) -> Measurement {
Measurement {
kind,
target,
value,
sigma,
}
}
#[test]
fn recovers_the_state_its_measurements_came_from() {
let (net, truth) = crate::se::tests::two_bus_net();
let layout = StateLayout::new(&truth, &[], &net);
let h_targets = vec![
(MeasurementKind::VoltageMagnitude, Target::Bus(0)),
(MeasurementKind::VoltageMagnitude, Target::Bus(1)),
(MeasurementKind::ActivePower, Target::Bus(1)),
(MeasurementKind::ReactivePower, Target::Bus(1)),
(
MeasurementKind::ActivePower,
Target::BranchTerminal {
branch: 0,
terminal: crate::branch_flow::Terminal::From,
},
),
(
MeasurementKind::ReactivePower,
Target::BranchTerminal {
branch: 0,
terminal: crate::branch_flow::Terminal::From,
},
),
];
let probe: Vec<Measurement> = h_targets
.iter()
.map(|&(kind, target)| m(kind, target, 0.0, 1.0))
.collect();
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={report:?}");
assert!(report.objective < 1e-16, "objective={}", report.objective);
for (i, (got, want)) in buses.iter().zip(&truth).enumerate() {
assert!(
(got.voltage_mag - want.voltage_mag).abs() < 1e-8,
"bus {i} magnitude: {} vs {}",
got.voltage_mag,
want.voltage_mag
);
let reference = layout
.angle_ref
.expect("no angle measurements here, so one is pinned");
let d_ang = (got.voltage_ang - want.voltage_ang)
- (buses[reference].voltage_ang - truth[reference].voltage_ang);
assert!(d_ang.abs() < 1e-8, "bus {i} angle off by {d_ang}");
}
}
#[test]
fn unobservable_unknowns_are_pinned_and_reported() {
let (net, truth) = crate::se::tests::two_bus_net();
let measurements = vec![m(
MeasurementKind::VoltageMagnitude,
Target::Bus(0),
1.03,
0.01,
)];
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={report:?}");
assert!(
!report.unconstrained.is_empty(),
"a single measurement cannot constrain the whole state: {report:?}"
);
assert!(
(buses[0].voltage_mag - 1.03).abs() < 1e-9,
"got {}",
buses[0].voltage_mag
);
assert!(
(buses[1].voltage_mag - 1.0).abs() < 1e-12,
"got {}",
buses[1].voltage_mag
);
}
#[test]
fn reuse_reproduces_a_fresh_estimate_exactly() {
let (net, truth) = crate::se::tests::two_bus_net();
let probe = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 0.0, 0.01),
m(MeasurementKind::VoltageMagnitude, Target::Bus(1), 0.0, 0.01),
m(MeasurementKind::ActivePower, Target::Bus(1), 0.0, 0.01),
m(MeasurementKind::ReactivePower, Target::Bus(1), 0.0, 0.01),
];
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 fresh = truth.clone();
flat_start(&mut fresh, &measurements);
let one_shot = estimate(&measurements, &mut fresh, &net, &SeOptions::default());
let mut persistent = PersistentEstimator::new(SeOptions::default());
let mut reused = Vec::new();
for _ in 0..3 {
let mut buses = truth.clone();
flat_start(&mut buses, &measurements);
let report = persistent.estimate(&measurements, &mut buses, &net);
reused.push((report, buses));
}
for (i, (report, buses)) in reused.iter().enumerate() {
assert_eq!(report.status, one_shot.status, "solve {i} status");
assert_eq!(
report.iterations, one_shot.iterations,
"solve {i} iterations"
);
for (bus, (got, want)) in buses.iter().zip(&fresh).enumerate() {
assert_eq!(
got.voltage_mag, want.voltage_mag,
"solve {i} bus {bus}: reuse changed the answer"
);
assert_eq!(
got.voltage_ang, want.voltage_ang,
"solve {i} bus {bus} angle"
);
}
}
}
#[test]
fn reuse_tracks_changed_measurement_values() {
let (net, truth) = crate::se::tests::two_bus_net();
let probe = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 0.0, 0.01),
m(MeasurementKind::VoltageMagnitude, Target::Bus(1), 0.0, 0.01),
m(MeasurementKind::ActivePower, Target::Bus(1), 0.0, 0.01),
m(MeasurementKind::ReactivePower, Target::Bus(1), 0.0, 0.01),
];
let exact = measurement_functions(&probe, &truth, &net);
let first: Vec<Measurement> = probe
.iter()
.zip(&exact)
.map(|(p, &v)| Measurement { value: v, ..*p })
.collect();
let second: Vec<Measurement> = first
.iter()
.map(|m| Measurement {
value: m.value * 0.98,
..*m
})
.collect();
let mut persistent = PersistentEstimator::new(SeOptions::default());
let mut a = truth.clone();
flat_start(&mut a, &first);
persistent.estimate(&first, &mut a, &net);
let mut b = truth.clone();
flat_start(&mut b, &second);
persistent.estimate(&second, &mut b, &net);
let mut reference = truth.clone();
flat_start(&mut reference, &second);
estimate(&second, &mut reference, &net, &SeOptions::default());
for (bus, (got, want)) in b.iter().zip(&reference).enumerate() {
assert!(
(got.voltage_mag - want.voltage_mag).abs() < 1e-12,
"bus {bus}: cached run gave {} against {}",
got.voltage_mag,
want.voltage_mag
);
}
assert!(
(a[1].voltage_mag - b[1].voltage_mag).abs() > 1e-6,
"the two measurement sets should give visibly different answers, \
or this test proves nothing"
);
}
#[test]
fn converges_with_inconsistent_measurements() {
let (net, truth) = crate::se::tests::two_bus_net();
let probe = vec![
m(MeasurementKind::VoltageMagnitude, Target::Bus(0), 0.0, 0.01),
m(MeasurementKind::VoltageMagnitude, Target::Bus(1), 0.0, 0.01),
m(MeasurementKind::ActivePower, Target::Bus(1), 0.0, 0.01),
m(MeasurementKind::ReactivePower, Target::Bus(1), 0.0, 0.01),
m(
MeasurementKind::ActivePower,
Target::BranchTerminal {
branch: 0,
terminal: crate::branch_flow::Terminal::From,
},
0.0,
0.01,
),
];
let exact = measurement_functions(&probe, &truth, &net);
let measurements: Vec<Measurement> = probe
.iter()
.zip(&exact)
.enumerate()
.map(|(i, (p, &v))| Measurement {
value: v + 0.002 * (i as f64 - 2.0),
..*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={report:?}");
assert!(
report.objective > 0.0,
"inconsistent data should leave residual disagreement"
);
assert!(report.last_step < SeOptions::default().tol);
}
}