Physics conformance and the constrained estimator
This chapter documents the physics-law audit engine, the ten laws it scores and which of them gate a state, the physics-constrained state estimator built on that gate, and the two instruments (a C# verb and an independent NumPy re-derivation) that keep the audit honest.
The motivating problem: the WLS state estimator could produce physically impossible states. On real GB data the undamped Gauss-Newton solve "walked Vm through zero", and a VAr-blind measurement set drags Vm to collapse. Those were patched reactively (trust-region damping, a voltage-schedule prior, the zero-injection KKT). This subsystem generalises those patches into one idea: the physical laws define the admissible region, and the estimator is constrained to live inside it.
The audit engine (Core/Physics/)
PhysicsLaws.Audit(model, result, tol, synchronism?) re-derives the network
laws from the raw branch data by an independent per-branch pi-model path -
it accumulates each branch's S_from/S_to and the bus shunt draw, and never
calls IPowerFlowSolver. So a pass is genuine corroboration that the solved
voltages satisfy the network equations, not a check of the solver against
itself. The per-branch loop lives once in PhysicsAuditContext; the laws read
its shared arrays.
Audit returns a PhysicsLawReport (Physics/PhysicsLawReport.cs): a
LawResult per law (number, name, statement, LawStatus, residual, unit,
tolerance, detail) plus Gate(hardLaws) - the acceptance verdict.
The ten laws, and what is checkable
GridSim is a positive-sequence phasor model plus an electromechanical frequency layer. That makes the circuit laws and AC synchronism directly checkable; it is not an electromagnetic-field model, so the field laws are embodied constitutively rather than verified. The audit states this honestly rather than fabricating a residual.
| # | Law | Status class | What is checked |
|---|---|---|---|
| 1 | Conservation of energy | hard | independent total dissipation == reported loss |
| 2 | Ohm's law | hard | branch flow y·dV matches the reported line flow |
| 3 | Joule heating | hard | branch-loss sum consistent with the reported total |
| 4 | Faraday induction | constitutive | transformer tap / gen EMF; no field to check |
| 5 | Ampere's law (passivity) | reported | passive branches dissipate: I²R >= 0 |
| 6 | Kirchhoff current | hard | injection balance at constrained buses |
| 7 | Kirchhoff voltage | structural | nodal potentials are single-valued (loops vanish) |
| 8 | Complex power S=VI* |
hard | the reactive component of the same balance |
| 9 | AC synchronism | dynamics | rotor-angle spread / frequency excursion margin |
| 10 | Gauss / Coulomb | not modelled | field/geometry, outside a phasor model |
PhysicsCheckRegistry.HardGateLaws = {1, 2, 3, 6, 8} - the laws whose
violation is physically impossible. A breach of any of these means the state is
not a real network state, so the constrained estimator rejects it. Laws 1, 6
and 8 are three views of the single power-balance residual and are reported as
such - no fabricated independence.
The Joule-vs-Ampere passivity split (a real finding)
The corpus sweep flagged case145, case588 and case9241pegase on Joule
heating with large residuals. They converge and conserve energy exactly, but
carry a branch with negative real loss - their data has a
negative-resistance branch, a network-reduction/equivalencing artefact. A
negative I²R is a model passivity property, not a bad state, and the
estimator cannot fix the model. So Joule (hard) checks only loss consistency,
and passivity moved to Ampere (law 5), reported when violated but not
hard-gated. The gate then correctly passes these states while surfacing the
non-passive branch. This is the audit doing its job across the corpus.
The constrained estimator (EstimationMethod.Feasible)
Three rings around the existing Gauss-Newton loop keep the estimate inside the
admissible region. Every ring is inert for the other methods, so Wls,
Huber, Lav and Constrained are byte-for-byte unchanged.
- Ring A - interior guard (
WlsStateEstimator.ApplyStep). Extends the existing trust-region damping with a fraction-to-boundary scale and a hard clamp so a voltage magnitude can never reach zero. This is the direct fix for "walked Vm through zero";ApplyStepreceives the feasibility spec only for the feasible method (nullotherwise). - Ring B - active-set KKT (
FeasibleWlsStep). ComposesConstrainedDenseWlsStep(reusing its zero-injection equalities, observability and covariance) and adds, each iteration, a linearised constraint row for every currently-violated state-coupled inequality: generator Q-capabilityQg(x) in [Qmin,Qmax](Jacobian = the injection-Q partialsMeasurementModel.InjectionQRow), across-branch angle difference|theta_f - theta_t| <= limit(the two +-1 angle entries), and, opt-in, branch thermal rating|S_end| <= RateA. The thermal limit is nonlinear, so it is enforced through its squared formS^2 = P^2 + Q^2with the row2P*dP + 2Q*dQfromMeasurementModel.BranchFlowRow(the branch's own pi-model stamps, no copy-drift); only the more-loaded end is held (the two ends are near-collinear, so both would singularise the KKT), a small hysteresis band keeps the constraint active at the boundary, and its RHS is clamped so it can only hold or pull an over-rating flow down, never push a slack branch up onto its limit. Thermal enforcement is off by default - unlike a generator's reactive capability (a device limit), a thermal rating is operational and a real grid legitimately runs over it, so forcing the estimate onto the rating would distort a true over-rating state rather than report it (the overload is always surfaced onLimitViolations). The active set re-detects each iteration and converges with the state. Load-bearing invariant: with no equality and no active inequality the augmented system is the free normal equations, so the step matchesDenseWlsStepto solver tolerance - the feasible method costs nothing on an already-feasible state. Dense only, under the same 3,000-bus fail-loud guard asConstrained. - Ring C - acceptance gate (
WlsStateEstimator, post-convergence). Audits the converged state (evaluated at x-hat viaToPowerFlowResult- pure evaluation, no forward inference) and, on a hard-law breach, demotes it toEstimationStatus.Infeasiblerather than shipping a fantasy. Operational limits (PhysicsFeasibility: generator Q-capability slack-exempt, thermal ratings, voltage band) are always reported onEstimationResult.LimitViolationsbut demote only underFeasibilityConstraints.RejectOnLimitViolation(a real grid legitimately runs branches over rating and buses outside the nominal band).
Constraints, spec and gate live in Estimation/FeasibilityConstraints.cs;
EstimationStatus.Infeasible is the distinct loud terminal state.
Instruments
gridsim physics-check <case>- audit one solved state; prints the per-law table and the gate verdict (exit 1 on gate fail).gridsim conformance- sweep the audit across the whole case corpus, writing acase × lawmatrix (results/conformance/conformance.{md,csv}+run-metadata.json, RunInfo-stamped, git-commit per CSV row). Every converged benchmark case is physically admissible.- Additive
physicsblock ingridsim-solve-export/1andgridsim-estimate-export/1(schema unchanged; readers ignore unknown fields): the per-law report and gate verdict, so an estimated or presolved state carries its own conformance evidence. tools/crossval/physics_audit.py- the primary instrument. Rebuilds Ybus and recomputes laws 1/3/6/8 in NumPy (reusingresidual_check.build_ybus), independent of the C# math, and checks the exported gate verdict agrees with the independent recomputation. Verified agreeing to machine epsilon on real exports, including the negative-resistancecase145(both gates PASS; the passivity artefact is reported, non-gating).PhysicsExportContractTestslocks the export block shape in CI.
Reproducing
gridsim physics-check case118 # one state
gridsim conformance # the whole corpus -> a matrix
gridsim crossval case145 --tol 1e-4 # produce a solve-export
python tools/crossval/physics_audit.py results/crossval-case145/solve-export.json --tol 1e-4
gridsim estimate <case> --method feasible # the physics-constrained estimate
Experimental models (gridsim experimental, Physics/Experimental/)
Opt-in, provenance-tagged, unvalidated-at-scale models live behind the experimental verb and off
every validated path. Each follows the PropagationDelay discipline: a PROVENANCE block, off by
default, and a degenerate limit that collapses to established physics (pinned by a test).
experimental reciprocity <case>(ReciprocityCheck, law 11) - passive-network reciprocityY = Y^T. Phase-shifting transformers are the deliberate non-reciprocal exception (excluded); an asymmetry over the rest is a model/build defect, not an impossible state, so it never gates. Collapses to a trivial pass on a phase-shifter-free network.experimental wave <case>(ContinuumWaveModel) - the grid as a damped-wave medium. The linearised swing on the network is a wave equationM d2(delta)/dt2 + D d(delta)/dt + K_L delta = P(inertia M = the 1/2 J w^2 density, damping D = the viscosity analogue, synchronising graph-Laplacian K_L = the elastic coupling). Its checkable content is the MODAL structure: the eigenvalues ofK_L phi = w^2 M phi(via the self-containedSymmetricEigenJacobi solver) are the network's inter-area electromechanical normal modes (~0.1-2 Hz), and the null mode is the centre of inertia. It is a linearisation of the validated nonlinearClassicalMultiMachineswing - oscillation frequencies and mode shapes, not large-disturbance transients or turbulence - and collapses to the SMIB analytic frequencyw_n = sqrt(w_s P_s / 2H)in its degenerate limit (test-pinned against the same oracleClassicalMultiMachineTestsuses). The dominant-mode damping ratio is the Reynolds analogue:< 1underdamped (wave-like),>= 1overdamped (diffusive). Needs per-machine inertia (fails loud throughClassicalIniton a case with no H).
On real GB data
The experimental and analysis models run end-to-end on the real GB spine (gb-spine, 316 real
transmission buses assembled from NESO/DNO data, with the real ETYS boundary set). Headline results
from one operating point:
- Boundary attribution (
experimental boundaries): B6 Scotland-England (SCOTEX) carries ~3,780 MW (limit 4,900), B8 North-to-Midlands (FLOWSTH) ~8,620 MW, SEIMP importing ~2,790 MW - each decomposed into the specific generators supplying the transfer. This is the north-to-south story, quantified per generator. - Carbon tracing (
experimental carbon): a flow-based system intensity of ~181 gCO2/kWh with per-load intensities to ~335 gCO2/kWh, from the real per-generator fuel types. - DC-OPF (
dcopf): ~53 GW of demand met at ~£257k/h as the provably cost-optimal security-constrained dispatch - a bounded-variable LP (balance + a two-sided thermal limit per rated line) solved to its global optimum by the self-containedBoundedSimplex. Cheapest (wind) plant is dispatched first, then backed off exactly where the network requires costlier local generation to hold every line inside its rating (the earlier marginal one-line-at-a-time redispatch under-secured, so this correct full-LP figure is higher than the old heuristic's). - Lake conformance (
tools/crossval/lake_conformance.py): the physical-law audit over the stored parquet lake found 93/98 sampled gb-spine periods physically admissible to machine epsilon (~1e-9 MW), and flagged 5 anomalous periods - the sweep doing its job across years of real GB history. NOTE: the lake states are demand-scaled per period, so the audit comparesS = V conj(Y V)to the STORED injection, not the static case's Pg-Pd.
The full gb-full case (3,539 real transmission buses) also loads and audits end-to-end: it converges
and passes the hard-gate laws to machine epsilon (energy/KCL/S=VI* residuals ~4e-9, ~524 MW total
losses). An earlier stale export carried two generator names with unescaped inner quotes that broke the
JSON loader; both the C# GdaModelWriter and the Python writer.py emit through a JSON serializer that
escapes correctly, so a regeneration is clean and the stale file was repaired in place.
Invariants a change here must preserve
- Hard-gate laws
{1,2,3,6,8}are the definition of "physically possible"; do not add operational preferences (band, ratings) to that set - they are reported, not gated. - The feasible method must remain inert for already-feasible states
(
FeasibleWlsStep==DenseWlsStepto1e-12with nothing binding) and for every non-feasible method (ApplyStepwithfeas == nullis the old path). - The audit stays an independent recompute (per-branch pi-model, never the solver); the Python instrument is the cross-language backstop.
- No forward inference: the gate consumes a
ToPowerFlowResultprojection, never a solver call.