Legacy WLS state estimation
Kind: Problem specification · Maturity: prototype · Direction: inverse · Temporal: single-snapshot
This page documents the original JuMP/Ipopt WLS prototype. For the compiled four-wire constrained-NLLS estimator, exact device equations, branch telemetry, and time series, use Constrained NLLS state estimation.
solve_state_estimation is a different problem specification over the same network physics: given noisy measurements of an energised network, find the bus voltage state that best fits them in a weighted-least-squares (WLS) sense. It is the maximum-likelihood estimate under independent, zero-mean, Gaussian measurement errors with a diagonal covariance (weight $1/\sigma_i^2$).
It reuses the BMOPFTools device model but with three changes, all expressed through the public seams:
- No operational bounds. The estimation network is a physics-only model — buses, lines, transformers, shunts, and a voltage source. It carries no loads, generators, IBRs, or operational limits.
- Free injections instead of fixed loads. A
model_hook!adds a free injection current at each measured bus (so KCL closes with the voltages free to fit the data). - A residual objective instead of generation cost. The hook sets the WLS objective $\sum_i (z_i - h_i(\text{state}))^2 / \sigma_i^2$.
The estimation network is a contract
An operational net biases the estimate: a retained load adds its fixed injection on top of the estimated one, and a retained voltage/thermal limit constrains the state to a feasible region that has nothing to do with the measurements. On the worked example below, retaining the loads moves the estimate by ~20 V and inflates the objective from ≈0 to ≈355; a 950 V bus limit pins the estimate to 950 V — both while still reporting LOCALLY_SOLVED.
solve_state_estimation therefore rejects a net that carries injecting devices (load, generator, ibr, dc_source) or operational limits (v_min/v_max/…, line i_max/s_max/s_rating). Pass allow_operational=true to override with a warning when you deliberately want to estimate against such a model.
Injection coverage is explicit
Absence of telemetry is not evidence of zero injection. Every non-source phase terminal must be either a measured injection or explicitly declared zero-injection:
- A measured injection needs both
:pinjand:qinj(an injection is a complex quantity; a lonePorQis ill-posed). That pair gives the bus a free injection current. - Everything else must be listed in
zero_injection(a bus id, expanded over its phase terminals, or a(bus, terminal)pair) — the classic zero-injection pseudo-measurement.
An un-declared, un-measured bus is an error, not a silent zero injection.
Measurements and reference convention
Each Measurement is an SI scalar with a standard deviation sigma (WLS weight $1/\sigma^2$), validated at construction (finite value, sigma>0, supported kind):
:vmag— voltage magnitude across(bus, terminal)→reference, in volts.:pinj— active power injected into the network at that terminal pair, watts.:qinj— reactive power injection, vars.
All three share one reference terminal per measurement (reference, defaulting to the solve's neutral): a smart-meter reading is phase-to-neutral for voltage and power. Pass reference=nothing to reference a terminal to ground, or an explicit terminal name for a bespoke return path. This resolves the earlier inconsistency where :vmag was terminal-to-ground while :pinj/:qinj were phase-to-neutral — masked only because the test feeders perfectly ground every neutral.
Observability — convergence is not uniqueness
The WLS fit is nonconvex: the squared P/Q residuals are quartic and the voltage-magnitude equality is nonconvex, so Ipopt returns a local stationary point (and low-voltage solutions exist). A converged solve does not prove the state is unique.
The result carries an observability diagnostic: the Jacobian of the measurement + zero-injection equations with respect to the rectangular node voltages is formed at the returned point (reusing BMOPFTools' ybus_passive for I = Y·V), and observable = rank == n_states. It reports redundancy (surplus equations), the smallest singular value, and the condition number, and solve_state_estimation warns on rank deficiency. This is a local numerical identifiability check (grounded-neutral networks; it does not model floating neutral displacement), not a global uniqueness proof.
Like the IVQ battery solve, this is a nonconvex problem solved to a local stationary point. For a promotion-grade estimator, add multistart or a Gauss–Newton/normal-equations method, and check observability.observable and primal_status before trusting a result.
Residuals
residuals[i].standardized = residual/σ is the σ-normalised raw residual — a scale-free residual, not the classical leverage-adjusted normalised residual $r^N_i = r_i/\sqrt{S_{ii}}$ (with $S$ the residual covariance) used for bad-data identification. A χ² goodness-of-fit test and largest-normalised-residual bad-data processing are not yet implemented.
Worked example
using PowerOptLab
using BMOPFTools: parse_bmopf
# A physics-only net: buses, lines, source; no loads or operational limits.
net = parse_bmopf("""
{"bus":{
"src": {"terminal_names":["1","n"],"perfectly_grounded_terminals":["n"]},
"bus1":{"terminal_names":["1","n"],"perfectly_grounded_terminals":["n"]},
"bus2":{"terminal_names":["1","n"],"perfectly_grounded_terminals":["n"]}},
"voltage_source":{"vs":{"bus":"src","terminal_map":["1"],
"v_magnitude":[1000.0],"v_angle":[0.0]}},
"linecode":{"lc":{"R_series_1_1":0.5}},
"line":{
"l1":{"bus_from":"src","bus_to":"bus1","terminal_map_from":["1"],"terminal_map_to":["1"],"linecode":"lc","length":1.0},
"l2":{"bus_from":"bus1","bus_to":"bus2","terminal_map_from":["1"],"terminal_map_to":["1"],"linecode":"lc","length":1.0}}}
"""; from_string=true)
# Both load buses are measured (a :pinj+:qinj pair each), so both get a free
# injection; no bus is left to a silent zero-injection assumption.
meas = [
Measurement(kind=:vmag, bus="bus1", value=979.5, sigma=2.0),
Measurement(kind=:pinj, bus="bus1", value=-20_000.0, sigma=400.0),
Measurement(kind=:qinj, bus="bus1", value=0.0, sigma=400.0),
Measurement(kind=:vmag, bus="bus2", value=969.2, sigma=2.0),
Measurement(kind=:pinj, bus="bus2", value=-20_000.0, sigma=400.0),
Measurement(kind=:qinj, bus="bus2", value=0.0, sigma=400.0),
]
se = solve_state_estimation(net, meas)
se.primal_status # "FEASIBLE_POINT" — trust the estimate only then
se.bus["bus1"]["1"]["vm"] # estimated |V| at bus1 (V); NaN if not FEASIBLE_POINT
se.residuals # per-measurement measured/estimated/residual/standardized
se.observability # (observable, n_states, rank, redundancy, min_singular, cond)
se.objective # optimal weighted-residual sumHere the six measurements exceed the four voltage components, and — because they are placed so the state is observable (se.observability.observable == true, redundancy == 2) — the fused estimate filters measurement noise. Redundancy alone is not observability: placement and Jacobian rank decide whether the state is determined.
Not yet supported
Branch-flow / branch-current / phasor (PMU) / source measurements; bad-data detection and identification (χ², largest-rᴺ); floating/displaced-neutral observability; multistart / global search.
Literature
- Schweppe & Wildes, Power system static-state estimation, Part I (1970) — the foundational exact WLS model.
- Baran & Kelley, State estimation for real-time monitoring of distribution systems (1994) — node-voltage DSSE with power/voltage/current measurements.
- Monticelli & Garcia, Reliable bad data processing for real-time state estimation (1983) — classical normalised-residual bad-data processing.
- Dehghanpour et al., A survey on state estimation techniques and challenges in smart distribution systems (2019) — pseudo-measurements, observability, topology, and meter placement in modern DSSE.
See the API reference for Measurement, solve_state_estimation, and StateEstimationResult.