State Estimation
This page summarizes the state-estimation (SE) functionality in Sparlectra and shows how it connects to regular network studies.
Release status: State Estimation is currently experimental. The current implementation is intended as a first practical WLS workflow for studies, examples, and early application feedback.
Theory (compact)
Sparlectra currently provides a classical weighted least-squares (WLS) formulation:
- State vector:
x = [θ(non-slack); Vm(all buses)] - Measurement model:
z = h(x) + e - Objective:
J(x) = (z - h(x))' * W * (z - h(x))
Where:
zis the measurement vector,h(x)is the nonlinear prediction of each measurement from the network model,W = diag(1/σ²)is the inverse-variance weighting matrix.
The algorithm linearizes h(x) and iterates Newton-style until the update norm or residual criteria satisfy tolerance.
Why the FD measurement Jacobian works
The internal helper _measurement_jacobian_fd approximates the Jacobian of the measurement model h(x) by finite differences. The key idea is the same as in power-flow FD Newton methods, but now the nonlinear map is the SE prediction function rather than the PF mismatch function.
If h(x) is differentiable, then for a small perturbation δx:
\[h(x + \delta x) \approx h(x) + H(x)\,\delta x,\]
where H(x) = \partial h / \partial x is the measurement Jacobian. Each Jacobian column can therefore be approximated numerically via
\[\frac{\partial h}{\partial x_k}(x) \approx \frac{h(x + \varepsilon e_k) - h(x)}{\varepsilon}.\]
This works because WLS only needs the local first-order sensitivity of the measurements with respect to the state in order to build the linearized normal equations. The underlying estimation model does not change; only the derivative evaluation is numerical instead of analytic.
Conceptually:
- PF FD Jacobian approximates derivatives of the residual map
F(x). - SE FD Jacobian approximates derivatives of the measurement map
h(x).
In both cases, the finite-difference step is justified by the same first-order Taylor approximation.
Measurement model
The current implementation supports these measurement types:
VmMeas(bus voltage magnitude)PinjMeas,QinjMeas(bus injections)PflowMeas,QflowMeas(branch flows with direction)
Passive / transit buses
For buses without load, generation, or shunt contribution, Sparlectra does not currently introduce a separate hard equality-constraint block in the WLS solver. Instead, the recommended modeling approach is to add zero-injection pseudo-measurements
Pinj = 0Qinj = 0
for those buses. In other words, the physical equality constraint is embedded through very small-variance measurements in the standard WLS formulation.
Helper functions:
findPassiveBuses(net)detects passive / transit buses from the bus power aggregates.addZeroInjectionMeasurements!(meas; net, sigma=...)appends the matching zero-injection pseudo-measurements automatically.
This is especially useful in sparse measurement scenarios, where a passive node may otherwise leave the estimator merely critical or weakly redundant.
At the moment, this is the supported way to model ZIB behavior in Sparlectra. There is not yet a separate hard-constraint solver block for zero-injection buses.
Typical synthetic-data workflow (for studies/tests):
- Solve a power flow to get a physically consistent reference state.
- Create synthetic measurements using
generateMeasurementsFromPF. - Configure standard deviations via
measurementStdDevs. - Optionally add Gaussian noise.
In real operation, SE uses field measurements directly and does not require a preceding power-flow run to create data.
Observability
SE quality depends strongly on observability.
Global observability
Use evaluate_global_observability(net; ...) to assess if the complete state can be estimated from the active measurements stored in net.measurements.
Typical metrics:
- Number of measurements
m - Number of states
n - Redundancy
r = m - n - Redundancy ratio
ρ = m / n - Numerical/structural observability flags
- Quality label (e.g.
:observable,:critical,:not_observable)
Local observability
Use evaluate_local_observability(net, cols; ...) to assess a selected subset of state columns (for example one bus angle and one bus magnitude).
This is useful for sensor-placement studies and for identifying vulnerable areas.
Integration with the Net workflow
SE is designed to run on the same Net data model used for power flow:
- Build/import
Net - Build measurements (SCADA/PMU/custom)
- Optional for synthetic studies: run
runpf!+generateMeasurementsFromPF - Check observability (global/local)
- Run estimator (
runse!) - Optionally write estimates back into the network (
updateNet = true)
Conceptually, SE is the measurement-driven counterpart of power flow:
- Power flow computes states from setpoints.
- SE computes states from measured values.
- Measurement redundancy improves robustness and enables bad-data detection using residual statistics.
At present, Sparlectra does not yet expose a full public bad-data-detection API (for example a dedicated chi-square / normalized-residual workflow). The current result object and examples support residual inspection, while a more complete diagnostics API remains future work.
Minimal example
using Sparlectra
using Random
net = run_acpflow(casefile = "case9.m")
std = measurementStdDevs(vm = 1e-3, pinj = 1.0, qinj = 1.0, pflow = 0.7, qflow = 0.7)
setMeasurementsFromPF!(
net;
includeVm = true,
includePinj = true,
includeQinj = true,
includePflow = true,
includeQflow = true,
noise = true,
stddev = std,
rng = MersenneTwister(42),
)
gobs = evaluate_global_observability(net; flatstart = true, jacEps = 1e-6)
println("Global observability quality: ", gobs.quality)
se = runse!(net; maxIte = 12, tol = 1e-6, flatstart = true, jacEps = 1e-6, updateNet = true)
println("Converged: ", se.converged, ", iterations: ", se.iterations)Example without PF pre-step (measurement-driven)
using Sparlectra
net = Net(name = "se_measurement_driven", baseMVA = 100.0)
addBus!(net = net, busName = "B1", busType = "Slack", vn_kV = 110.0)
addBus!(net = net, busName = "B2", busType = "PQ", vn_kV = 110.0)
addBus!(net = net, busName = "B3", busType = "PQ", vn_kV = 110.0)
addPIModelACLine!(net = net, fromBus = "B1", toBus = "B2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B2", toBus = "B3", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B3", toBus = "B1", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0)
ok, msg = validate!(net = net)
ok || error("Validation failed: \$msg")
empty!(net.measurements)
append!(net.measurements, Measurement[
Measurement(typ = VmMeas, value = 1.01, sigma = 0.002, busIdx = 1, id = "VM_B1"),
Measurement(typ = VmMeas, value = 0.99, sigma = 0.004, busIdx = 2, id = "VM_B2"),
Measurement(typ = PinjMeas, value = -25.0, sigma = 1.0, busIdx = 2, id = "PINJ_B2"),
Measurement(typ = QinjMeas, value = -8.0, sigma = 1.0, busIdx = 2, id = "QINJ_B2"),
Measurement(typ = PflowMeas, value = 24.0, sigma = 0.8, branchIdx = 1, direction = :from, id = "PF_12"),
Measurement(typ = PflowMeas, value = 23.5, sigma = 0.8, branchIdx = 1, direction = :from, id = "PF_12_REDUNDANT"),
])
obs = evaluate_global_observability(net; flatstart = true, jacEps = 1e-6)
println("Observable quality: ", obs.quality)
se = runse!(net; maxIte = 12, tol = 1e-6, flatstart = true, jacEps = 1e-6, updateNet = true)
println("Converged: ", se.converged)Adding measurements with helper functions
Instead of constructing each Measurement(...) manually, you can build the measurement vector with helper functions that resolve bus names and branch references for you:
using Sparlectra
net = Net(name = "se_helpers", baseMVA = 100.0)
addBus!(net = net, busName = "B1", busType = "Slack", vn_kV = 110.0)
addBus!(net = net, busName = "B2", busType = "PQ", vn_kV = 110.0)
addBus!(net = net, busName = "B3", busType = "PQ", vn_kV = 110.0)
addPIModelACLine!(net = net, fromBus = "B1", toBus = "B2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B2", toBus = "B3", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B3", toBus = "B1", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0)
empty!(net.measurements)
addVmMeasurement!(net; busName = "B1", value = 1.01, sigma = 0.002)
addPinjMeasurement!(net; busName = "B2", value = -25.0, sigma = 1.0)
addQinjMeasurement!(net; busName = "B2", value = -8.0, sigma = 1.0)
addPflowMeasurement!(net; fromBus = "B1", toBus = "B2", value = 24.0, sigma = 0.8, direction = :from)
addQflowMeasurement!(net; branchNr = 1, value = 6.5, sigma = 0.8, direction = :to)
obs = evaluate_global_observability(net; flatstart = true, jacEps = 1e-6)
println("Observable quality: ", obs.quality)Further examples and workshop material
- Extended tutorial and a simple 7-bus setup: Workshop
- Detailed WLS reporting example script:
src/examples/state_estimation_wls.jl - Observability-focused scenario script:
src/examples/state_estimation_observability.jl - Passive-bus ZIB comparison example:
src/examples/state_estimation_passive_bus_zib_comparison.jl - Matrix-based observability/redundancy demo:
src/examples/h_matrix_observability_demo.jl
H-matrix observability demo (A..E)
If you want to study observability directly on Jacobian-like matrices H without building a full network first, use:
julia --project=. src/examples/h_matrix_observability_demo.jlThe script evaluates each matrix with:
- Structural observability via sparsity matching
- Numerical observability via rank test
- Per-row redundancy classification (critical vs. redundant)
- Local observability on selected state-column subsets
Included demo matrices:
H_A: fully observable with duplicate information (clear redundancy).H_B: minimal square observable case (m = n), therefore every row is critical.H_C: structurally observable but numerically fragile (near linear dependence).H_D: sparse case highlighting matching behavior and extra measurements.H_E: incidence-like matrix paired with a toy graph/spanning-tree interpretation.
This is intended as a compact didactic companion to evaluate_global_observability / evaluate_local_observability.