Skip to content

Repository files navigation

GridSim

A grounded, physics-first power-grid simulator in C# / .NET 10. A small, correct core - per-unit network model, complex Y-bus, Newton-Raphson load flow - grown deliberately into a full grid stack: generation mix, frame-by-frame inertia and frequency dynamics, reactive power and tap changers, contingency analysis, the real GB network from published data, a classical WLS state estimator fitted to real settlement metering, and a swappable CPU/GPU solve backend. Every step is validated against published reference cases - and cross-checked against pandapower to machine epsilon.

What this is for. GridSim is the core engine of a self-made evidence apparatus for the Twin Scroll Grid Balancer (TSGB) case. Its endpoint is a historical (offline, NON-live) GB grid state estimator: feed public data through validated solvers to reconstruct what the grid state actually was, then run grounded counterfactuals on that reconstructed truth. That endpoint now exists: per-settlement-period WLS estimates of the real GB grid fitted to settlement metering, reaching back to March 2016 - and the 9 August 2019 disturbance reproduced from them (docs/evidence-gb-2019-08-09.md). It never predicts the future - that "no forward inference" rule is a hard code invariant, not a policy.

Responsible disclosure & lifecycle. GridSim and its data companion (GDA) demonstrate the mosaic effect: fusing thousands of individually-harmless public sources reconstructs a near-operational picture of GB grid infrastructure with no privileged or SCADA access. That is a disclosure of capability, not an incident - responsibly disclosed to the UK NCSC and recognised with a Challenge Coin. The guards in these tools (the 169 h (7 d + 1 h) no-forward-inference horizon, data kept out of the repo, provenance on every figure) are a deliberately constrained public build the author enforces voluntarily, as mitigation. Ofgem's "Securing Open Data" consultation closes 14 July 2026; if its recommendations are actioned, the public data these tools depend on is secured - and they retire with no viable update path.


What makes GridSim different

Most grid simulators bolt frequency dynamics on as a single lumped number. GridSim doesn't.

  1. Inertia is measured frame-by-frame, not assumed as one scalar. The inertia model is a runtime seam (IInertiaModel: lumped-scalar COI <-> distributed multi-domain), the same clean-abstraction pattern as the CPU/GPU solver seam. Every machine is classified by technology into a synchronous swing mass, a synthetic-inertia (grid-following storage) domain, or a pure inverter (PLL) - so system inertia and its spatial spread track the live mix as it shifts, instead of a frozen E = sumH*MBase. The distributed model couples per-domain masses by physically-derived (Kron-reduced) synchronising power with propagation delay, and reports a dominance read-out that catches "inertia too evenly spread, no domain holds waveform authority"

    • a failure a single lumped total is blind to. It collapses exactly to the validated lumped model in the single-domain limit.
  2. The steady state needs no swing equation - so a 100% renewable grid still solves. The Newton-Raphson power flow has no inertia term at all; it solves voltages, angles and flows purely from the admittance matrix and the P/Q injections. Rotational inertia is irrelevant to whether an operating point exists. A 100%-inverter / zero-rotational-inertia grid therefore solves for a valid state exactly like any conventional mix - the swing equation is a separate, opt-in layer for transients, never a prerequisite. Inverter domains carry apparent-power stiffness, not spinning mass, so a converter-dominated system is reasoned about on its own terms rather than by a swing model dividing by an inertia that is heading to zero.

  3. A grounded evidence engine, not a crystal ball. On top of the validated solver sits a historical state estimator + counterfactuals: reconstruct what the grid actually did from public data, then ask what a device would have done - gated behind a baseline that must first reproduce the recorded event, with a 169-hour (7-day + 1 h) no-forward-inference horizon wired into every dated ingester in code. The estimator is the classical article - WLS z = h(x) + e with a chi-square consistency gate and bad-data identification (manual ch. 78) - fitted per half-hour settlement period to real Elexon settlement metering on the real GB network (316-bus spine to 3,539-bus full model), and the gate has closed: the 9 Aug 2019 disturbance is reproduced from a fully sourced record.

  4. Validated, and built for the hardware that suits the maths. Power flow is double end-to-end and cross-checked against pandapower/scipy to machine epsilon; the compute stack is AMD-first because FP64 is where AMD Instinct wins outright.


Contents


Quick start

Requires the .NET 10 SDK.

dotnet build GridSim.slnx
dotnet test  GridSim.slnx                                   # 927 tests, incl. case9 vs published MATPOWER
dotnet run --project src/GridSim.Cli                         # UK reduced model, live
dotnet run --project src/GridSim.Cli case9                   # IEEE 9-bus benchmark (single solve)
dotnet run --project src/GridSim.Cli case118                 # any built-in case: case14/30/57/118/300/145/case24/cigre
dotnet run --project src/GridSim.Cli data/matpower/pegase/case9241pegase.m   # any MATPOWER .m file
dotnet run --project src/GridSim.Cli bench                   # dense-vs-sparse convergence table
dotnet run --project src/GridSim.Cli bench --gpu             # ...on the GPU backend (falls back to CPU)

The evidence engine - reproduce a real event, then run the counterfactual, then the dossier:

dotnet run --project src/GridSim.Cli replay-event gb-largest-loss-1320   # credibility gate: reproduce with NO device
dotnet run --project src/GridSim.Cli scenario --event gb-largest-loss-1320 --device-inertia 40 --device-ffr 1.5
dotnet run --project src/GridSim.Cli fleet    --event gb-largest-loss-1320 --units 1,100 --stagger 4
dotnet run --project src/GridSim.Cli evidence --event gb-largest-loss-1320 --device-inertia 40 --device-ffr 2
dotnet run --project src/GridSim.Cli gda-replay --synth --case case118    # the historical state estimator (demand replay)
dotnet run --project src/GridSim.Cli estimate case14 --synth              # the WLS state estimator: fit, chi-square, bad data
dotnet run --project src/GridSim.Cli crossval case118                     # diff vs pandapower (needs Python)

Full command reference: docs/cli-reference.md.


Status

Steady-state power flow is in and validated; the analysis, data, GPU and dashboard layers are built on top.

  • Network model - buses, generators, branches (pi-model lines & transformers), per-unit on a system MVA base; immutable value types.
  • Y-bus - complex admittance with line charging, shunts, and tap/phase-shift support.
  • Newton-Raphson load flow - full polar Jacobian, quadratic convergence; generator Q-limits (PV->PQ) and OLTC voltage regulation. Numerically hardened: near-singular Jacobians and non-finite states are flagged (a distinct NonFinite status, relative-magnitude pivot guards) rather than silently returning NaN.
  • Dense and sparse backends behind one interface; the sparse path scales to tens of thousands of buses.
  • Two inertia/frequency models behind IInertiaModel - the validated lumped-scalar COI swing and an opt-in distributed multi-domain model (see below).
  • Authorable per-asset dynamics (--asset-dynamics) - grid-following/forming inverters (with RoCoF/LoM trips), passive transformer/cable plant, and a generic protections layer (RoCoF/LoM, U/O-frequency, dynamic LFDD, U/O-voltage, thermal, and a full topology-aware auto-recloser with islanding + per-island slack election). Authored in JSON, data-grounded where the GDA event record supports it and [VERIFY]-tagged elsewhere; opt-in, byte-identical when off. Technical ch. 13-15.
  • Validated against the WSCC / MATPOWER IEEE 9-bus and the published 14/57/118/300/145 solutions - and cross-checked against pandapower to machine epsilon (case9/14/30/57/118).
  • Evidence engine - a historical grid state estimator + grounded counterfactuals on top of the solver, with a hard no-forward-inference invariant (169 h (7 d + 1 h) horizon, enforced in code).
  • State estimation - a classical WLS estimator (estimate, manual ch. 78) with methods behind one seam (--method wls|constrained|huber|lav|schweppegm|qr), cross-validated against pandapower.estimation to machine epsilon, fitting the real GB grid per half-hour settlement period from Elexon settlement metering.
  • Real-time, distribution & dynamic estimation (technical ch. 18) - a resident streaming estimator on permitted-age / replayed data (CLI serve, a Web hosted service); recursive EKF/UKF/EnKF and a frequency/RoCoF Kalman filter (gated); a full unbalanced three-phase DSSE (node-voltage + branch-current, sequence-network faults); an IEEE C37.118 synchrophasor pipeline + linear PMU estimator; an FDIA screen + hash-chained audit log; CIM/CGMES import/export and IEC 60870-5-104 / DNP3 / IEC 61850-GOOSE frame codecs; continuation power flow and a loss-aware AC-OPF.
  • A full frequency-domain harmonic subsystem (gridsim harmonics <case>): Y(h) with skin/long-line, penetration, resonance/impedance scan, THD + IEEE 519 / G5-5 compliance, three-phase/sequence, coupled harmonic power flow, and harmonic state estimation - grounded physics with the source spectra as the only assumed (provenance-tagged, measurement-ready) input; the feed schema (gridsim-harmonics/1) lives in GDA, and the output cross-checks a NumPy reference to machine epsilon.
  • 927 tests pass (steady-state, both inertia models, the 7-phase evidence pipeline, the estimation estate, and the cross-val contract). The whole solution builds 0-warning under a pedantic analyzer gate (TreatWarningsAsErrors).
GridSim  -  IEEE 9-bus (WSCC)   (9 buses, 9 branches, base 100 MVA)
================================================================
Converged in 4 iterations (max mismatch 1.84E-014 p.u.)
Bus    Vm(pu)   Va(deg)     P(MW)    Q(MVAr)
  1    1.0400     0.000     71.64     27.05
  ...
Total losses: 4.641 MW

Frequency, inertia & stability - the distributed model

Inertia is where GridSim departs from the textbook single-number model. The whole frequency stack lives in GridSim.Core.Dynamics, and the inertia model is a runtime-selectable seam - InertiaModelSelector.Create(InertiaModel.LumpedScalar | .Distributed) - so the CLI, WPF and Web surfaces all pick the model explicitly and every consumer takes either drop-in.

Lumped scalar (COI) - the validated reference. One system inertia number E = sumH*MBase and one swing equation df/dt = f*deltaP / (2E). After a loss deltaP, frequency falls at a rate set entirely by the inertia; governor droop and load damping arrest it into a nadir and settle. Halve the inertia and the RoCoF doubles - the whole "as GB inertia falls, RoCoF worsens" argument, runnable, on the same 20 ms timeline as the NESO data.

Distributed multi-domain - inertia resolved in space and time. Every generator is classified by technology (PoleMap -> DomainAssignment) into a speed domain:

Domain kind Example plant Contributes
Synchronous swing mass nuclear, gas, hydro, biomass real rotational inertia E = sumH*MBase
Synthetic-inertia grid-following battery / BESS df/dt-emulated response, no spinning mass
PLL inverter wind, solar, HVDC, interconnectors apparent-power stiffness, zero rotational inertia

The synchronous domains become coherent masses, each with its own frequency and rotor angle, coupled across the network by physically-derived (Kron-reduced) synchronising power with inter-domain propagation delay (MultiDomainSwing, DomainCoupling, PropagationDelay). Two properties are built in and tested: the tie power is antisymmetric so the centre-of-inertia trajectory equals the lumped model to rounding, and the single-domain limit reduces exactly to the lumped scalar. On top of that sits a dominance read-out (GVad): rank the domains by apparent-power stiffness and flag when no single domain holds "waveform authority" - the low-inertia failure mode a lumped total physically cannot express.

Why this matters for a renewable grid. Because the steady-state solve is inertia-agnostic (sec. What makes GridSim different), GridSim will solve and display a converter-dominated or 100%-inverter network's operating point directly. The distributed model then lets you watch intrinsic inertia fall toward zero, the dominance gap close, and RoCoF steepen - the transition to a converter-led system, told honestly rather than hidden inside one averaged number.

The situational-awareness helpers built on this (StabilityMetrics) are what the control-room panels draw: purchased time (seconds a system rides a credible loss inside its frequency band on real stored energy), the dynamic MIF intrinsic-energy floor, RoCoF for a loss, largest single and largest correlated infeed. Supporting tooling:

  • RocofUpsampler - NESO's 1 s frequency -> a 20 ms timeline, the resolution the dynamics need.
  • InertiaEstimator - E = f*|P| / (2*|RoCoF|): infer spinning energy from a known loss and the measured RoCoF (the inverse swing equation; round-trips the simulator exactly).
  • FrequencyLimits - the GB thresholds (operational 49.8, statutory 49.5, LFDD 48.8, RoCoF 0.125) and breach assessment for a simulated or measured trace.
  • TsgbSizer / RocofScreening - the synthetic inertia + fast response to hold a given loss inside the limits; and N-1 frequency security (lose each infeed's output and rotating mass, rank by RoCoF).
dotnet run --project src/GridSim.Cli freq                 # NESO frequency stats + GB limit scan
dotnet run --project src/GridSim.Cli trip "Sizewell"      # trip a unit: RoCoF, nadir, recovery
dotnet run --project src/GridSim.Cli screen               # rank single losses by RoCoF
dotnet run --project src/GridSim.Cli gda --fleet          # real GB fleet: secure the largest loss + screen it

The evidence engine - historical state estimation & grounded counterfactuals

The reason GridSim exists. On top of the validated solver sits a grounded-replay + counterfactual evidence engine: reconstruct what the GB grid actually did from public data, then ask what a device would have done - never what the grid will do. It is built to be self-evidently rigorous, because the TSGB case has no peer-review path and the tools have to make the argument themselves.

The hard invariant: no forward inference, enforced in code

GridSim is a historical state estimator. It refuses to process any timestamp newer than DateTime.UtcNow.AddHours(-25) - a single constant (ForwardInferenceGuard.HorizonHours) wired into every dated ingester (NESO frequency CSV, event catalog, GDA time-series, the replay loop). The engine can only ever lag real time by >=169 h, so it is structurally incapable of nowcasting or prediction. This is not a policy that can be toggled; a load throws a ForwardInferenceException if any row is past the horizon.

Two more invariants ride alongside it: intrinsic (1/2)J*omega^2 rotational inertia is never blurred with fast frequency response (separate, separately-labelled columns everywhere - the device's real spinning energy adds to E; its FFR is a distinct power injection, never merged); and every counterfactual is gated behind a baseline that reproduces the recorded event - reproduce reality first, or no counterfactual is shown.

The pipeline (each a CLI verb; each independently tested)

verb what it delivers
replay-event <id> The credibility gate. Reproduces a recorded event's RoCoF/nadir with no device, scores it Reproduced / Marginal / Failed, cross-checks the implied inertia, and (with a 1 s NESO trace attached) computes a per-sample RMSE. A counterfactual is only credible once this says Reproduced.
scenario --event <id> The counterfactual sweep: inertia x device x best/worst corners, gated behind the baseline. Emits the comparison matrix and the inertia risk curve (260 / 155 / 120 / 102 / 50 GVA.s, with and without the device).
fleet --event <id> Single vs fleet emergent behaviour. Intrinsic inertia is N-additive (RoCoF unchanged), but fast response is non-additive - a staggered fleet has less FFR online at the nadir, so the nadir is deeper.
evidence --event <id> The umbrella dossier (gridsim-evidence/1): recorded vs simulated vs counterfactual columns, the risk curve, single-vs-fleet, and avoided cost from the recorded LFDD shed x Value of Lost Load - grounded in the record, never a marketing figure.
gda-replay (--series ‹ts.json› | --synth) The historical grid state estimator (demand replay). Replays a demand time-series through the validated solver, reconstructing the grid state (voltages, losses, convergence) at each past timestamp - offline, horizon-guarded.
estimate ‹case› (--measurements ‹m.json› | --synth) The measurement-based WLS state estimator (manual ch. 78, detail below). Fits the state to redundant, noisy telemetry z = h(x) + e - Gauss-Newton with trust-capped steps, observability analysis, chi-square consistency, largest-normalized-residual bad-data removal with critical-measurement protection. Methods behind one seam (--method wls|constrained|huber|lav|schweppegm|qr); runs the real GB cases (gb-spine / gb-full, sparse backend auto). Cross-validated against pandapower.estimation to ~1e-13 pu. Still strictly historical: measurement sets pass the same horizon guard.

A worked run - the risk curve is the headline (scenario, synthetic screening event):

   inertia | RoCoF no-dev    nadir  LFDD |  RoCoF +dev    nadir  LFDD | avoided
  ---------------------------------------------------------------------------------
     260GV |     -0.127Hz/s  49.31Hz    no |    -0.110Hz/s  49.71Hz    no | -
     155GV |     -0.213Hz/s  49.17Hz    no |    -0.169Hz/s  49.69Hz    no | -
     120GV |     -0.275Hz/s  49.09Hz    no |    -0.206Hz/s  49.67Hz    no | -
     102GV |     -0.324Hz/s  49.04Hz    no |    -0.232Hz/s  49.67Hz    no | -
      50GV |     -0.660Hz/s  48.77Hz   YES |    -0.367Hz/s  49.63Hz    no | *** YES ***

The WLS state estimator on the real GB grid

estimate is the classical estimator done properly (user manual ch. 10, technical manual ch. 6): z = h(x) + e solved by Gauss-Newton with trust-capped steps, a chi-square consistency gate, largest-normalized-residual bad-data identification with critical-measurement protection, and observability analysis (island sweep + gain-rank). Estimation methods sit behind one seam - --method wls|constrained|huber|lav|schweppegm|qr (constrained = KKT-enforced hard zero-injections; huber / lav / schweppegm = IRLS robust; qr = orthogonal, avoids the normal equations) - and cross-agree on clean data; the dense and sparse backends share one Jacobian path (bitwise-identical injections).

It runs on the real GB grid: gb-spine (316 GSP buses) and gb-full (3,539 buses, sparse backend), fitted to real Elexon B1610 settlement metering plus interconnector flows per half-hour settlement period, with estimable history reaching back to March 2016 (Physical Notification fallback). The spine branch set carries real ETYS Appendix B circuits (impedances and winter ratings) wherever the register resolves, replacing synthetic per-km guesses.

The triad pipeline: GDA measured injections -> a measured-dispatch power-flow solve (the prior) -> WLS fusion with the telemetry. The two reconstruction routes - deterministic solve and measurement-fitted estimate - backfill history in parallel, and their divergence is a first-class evidence product (GDA Derived/state_divergence). Per-period ETYS boundary transfers are evaluated at the estimated state, and estimated states stream over the GDA bridge as source="estimated" with a per-frame chi-square quality block - a SRC picker + quality pill in the WPF dashboard, bridge:<case>@estimated on the web.

The measurement apparatus audits itself: the chi-square gate and the outlier league found - and drove fixes for - real data defects (a BMU-to-GSP crosswalk fallback mis-siting whole fleets; offshore farms bound to the farm centroid instead of the cable landing; ~23 GVAr of switched shunt compensation missing from the model, now ingested from ETYS Appendix B as declared uncertainty). The flagship 2019 week estimates 46/46 chi-square-consistent, with zero removals, on every day.

And the evidence headline: the 9 August 2019 GB disturbance (Hornsea + Little Barford) is reproduced by the replay credibility gate from a fully sourced record - the loss timeline from the Ofgem investigation report, the operating point measured (NESO's recorded 215 GVA.s inertia outturn plus the estimator's own transmission-net demand). RoCoF agrees within 5 %, the nadir within 0.02 Hz - and the estimator reconstructed the event from settlement metering alone (Hornsea 1,566 -> 1,372 -> 755 MW; Little Barford flipping to -77 MW net offtake at the trip). The dossier: docs/evidence-gb-2019-08-09.md.

Data provenance & what's still gated

Everything ships with a ProvenanceTag - Recorded, DerivedFromRecorded, CuratedPlaceholder, Synthetic, Counterfactual, Interpolated - so a reviewer can trust the source of every number. The 9 Aug 2019 event has graduated from CuratedPlaceholder to a fully sourced record (Ofgem loss timeline, measured operating point, estimator-reconstructed injections), and the credibility gate scores it Reproduced - the full account is docs/evidence-gb-2019-08-09.md. The 9 Aug 2019 1 s NESO trace is now committed (data/neso/gb-2019-08-09-frequency.csv; it corroborates the record and exposes the single-mass model's shape limit - RMSE 0.153 Hz, documented in the dossier) and the VoLL is cited (£6,000/MWh regulatory, Ofgem EBSCR/BSC P305). A second sourced event ships too: gb-2023-12-22 (IFA + Cottam, the entire loss inventory settlement-measured), operating point, response services and BM recovery all measured, verdict an honest Marginal with the residual isolated to the public inertia feed's measurement basis - docs/evidence-gb-2023-12-22.md. Any figure still unsourced stays tagged [VERIFY] and must not be cited until replaced.

The gate earned that verdict honestly: the earlier placeholder version of the same event scored Failed (its compressed timeline produced a 33 % RoCoF error), and scenario/evidence refused to run a counterfactual off the non-reproduced baseline until the sourced record replaced it. That is the gate working, not a bug.


Cross-validation vs pandapower & scipy

"Validated solver" is demonstrable, not asserted. The sidecars in tools/crossval/ do not trust GridSim's math - they rebuild the network from the raw inputs GridSim records in its export JSON and re-solve independently, then diff.

Power flow vs pandapower 3.4 (bus voltages/angles, angle reference aligned to the slack, Q-limits enforced on both sides):

case buses max |delta Vm| (pu) max |delta Va| ( deg) verdict
case9 9 4.4e-16 0.0000 machine epsilon
case14 14 5.5e-12 0.0000 machine epsilon
case30 30 1.7e-15 0.0000 machine epsilon
case57 57 2.0e-12 0.0000 machine epsilon
case118 118 6.7e-16 0.0000 machine epsilon

State estimation vs pandapower.estimation (tools/crossval/crossval_estimation.py): identical measurement sets fed to both estimators agree to machine epsilon (case14: ~2.3e-13 pu), and the four --method variants cross-agree on clean data. A second, independent instrument - a NumPy residual/optimality check (tools/crossval/se_residual_check.py) - re-verifies the WLS first-order conditions without trusting either implementation.

Swing dynamics vs scipy (solve_ivp, RK45, re-integrating the identical ODE): initial RoCoF and nadir agree to <2 x 10^-^4 Hz. CrossvalContractTests lock the export-JSON field contract in CI (no Python needed); the live diff is env-gated (pip install pandapower scipy) and degrades gracefully when they're absent. Full write-up: docs/benchmarking.md sec. 2.


Solvers

Power flow is solved by two interchangeable backends behind IPowerFlowSolver:

  • NewtonRaphsonPowerFlow - a dense full-polar-Jacobian Newton-Raphson; the validated reference. Dense LU (Gaussian elimination, partial pivoting) behind ILinearSolver.
  • SparseNewtonRaphsonPowerFlow - sparse complex Y-bus + sparse Jacobian, solved by direct sparse LU (reverse Cuthill-McKee ordering, partial pivoting) with an ILU0/BiCGSTAB fallback, behind ISparseLinearSolver. Exists because a dense Jacobian is infeasible at scale (the 9241-bus PEGASE dense Jacobian alone is ~2.7 GB).
  • BackwardForwardSweep - a bidirectional sweep for radial distribution feeders the transmission Newton-Raphson can't converge (--bfs).

SteadyStateSolver wraps any of them with the OLTC outer loop. The CLI auto-selects (radial -> sweep, large -> sparse, else dense), or you force a backend. Internals: docs/architecture.md.


Compute backends - AMD-first

Power-flow Newton-Raphson is double-precision all the way down, which inverts the usual GPU pecking order - this is the workload where FP64 throughput decides everything, and where AMD Instinct (MI200/MI250/MI300, FP64 at near-full rate) wins outright over consumer/workstation NVIDIA cards that throttle FP64 to 1:64. Both solve paths sit behind narrow seams (ILinearSolver / ISparseLinearSolver) with a SolverBackend factory selecting the implementation at runtime; the physics never changes.

Backend Status
CPU yes shipping - the validated reference every result is checked against
AMD ROCm / HIP (--gpu=rocm) yes implemented - native hipSOLVER dense LU + device-resident rocSPARSE/rocBLAS BiCGSTAB; ABI matched to ROCm 6.x, awaiting first MI-series runtime run
Open drivers (OpenCL, --gpu=opencl) yes implemented - the same ILGPU kernels via OpenCL, FP64-gated, portable across AMD/Intel/NVIDIA
NVIDIA CUDA (ILGPU, --gpu=cuda) yes works - correct but FP64-throttled on consumer cards

Bare --gpu auto-detects AMD-first: rocm -> opencl -> cuda -> cpu. A missing accelerator (or an OpenCL device without FP64) warns and falls back to CPU, and every report stamps the backend it used. Rented-box driver caveats (the CUDA-on-A100 Xid 119 story, the gpucheck preflight) and the full CPU/GPU scaling numbers live in docs/gpu-rental-runbook.md and docs/benchmarking.md sec. 4.


Test-case libraries & the MATPOWER importer

GridSim reads cases in MATPOWER .m format - the lingua franca every standard test system ships in - via GridSim.Core.IO.Matpower.MatpowerParser. One parser unlocks the PEGASE, RTS, NESTA and wider MATPOWER libraries and backs the built-in IeeeCases factories, producing a GridModel with exact per-unit values (it preserves mpc.gen Pg/Qg/Vg and the stored bus voltages, so a case reproduces its published operating point). It handles Inf limits, transformer-by-tap-ratio, isolated/out-of-service dropping, and non-contiguous bus ids.

Folder (data/matpower/) Cases Source
lib/ case9/14/30/57/118/145/300, case24_ieee_rts MATPOWER 8.0
pegase/ case1354/2869/9241pegase MATPOWER 8.0 (RTE/PEGASE)
rts/ case73_ieee_rts (RTS-96, 73-bus 3-area) PGLib-OPF
nesta/ NESTA-lineage subset PGLib-OPF (NESTA successor)
benchmarks/ cigre_mv (15-node MV feeder) CIGRE TF C6.04.02

Small classic cases are embedded in GridSim.Core (so they work with no files present); larger libraries are discovered from data/matpower/. tools/fetch-cases.sh pulls the full PGLib-OPF + MATPOWER archive (~161 systems, up to ~78,000 buses). Full detail: docs/case-libraries.md.


Benchmarking & results

gridsim bench runs both backends on every case and reports iterations-to-convergence, final mismatch and timing (--runs, --parallel, --out -> markdown + CSV + run-metadata.json, stamped with machine, runtime, git commit and backend). Analysis verbs - nminus1-sweep, stress, chain - share the harness. See docs/benchmarking.md.

Dense == sparse agreement (the trust signal that the sparse path used at scale is faithful):

Case Buses Dense: iters / mismatch / ms Sparse: iters / mismatch / ms
case9 9 4 / 1.8E-14 / 1.2 4 / 1.7E-14 / 2.1
case118 118 3 / 1.5E-12 / 17 3 / 1.5E-12 / 5.5
case300 300 5 / 1.4E-12 / 893 5 / 1.4E-12 / 34
case1354pegase 1354 4 / 1.9E-12 / 35,571 4 / 3.3E-12 / 155

Sparse at scale (AMD EPYC 7763, 28 cores) - machine-tight mismatches in a handful of Newton iterations, scaling to 25,000 buses:

Case Buses iters mismatch mean ms
case2869pegase 2,869 6 2.2E-09 735
case9241pegase 9,241 6 2.1E-09 3,832
case_ACTIVSg10k 10,000 4 4.5E-09 16,662
case_ACTIVSg25k 25,000 4 9.1E-11 94,560

Non-convergences are expected and informative, not solver bugs: PGLib OPF operating points don't solve as a flat-start power flow while their non-OPF twins do, and radial distribution feeders need the backward/forward-sweep solver (--bfs).


The GB models: reduced, GDA (real), and synthetic

Reduced 10-zone model (data/uk/)

The default case: a reduced GB transmission model as editable JSON - each zone a 400 kV bus and a 132 kV demand bus joined by a GSP transformer with an OLTC. Generation dispatches from capacities (merit order, gas as marginal slack), so it arrives balanced and settles into a natural steady state.

GridSim  -  GB reduced (10-zone)   20 buses . 24 branches . 22 generators . base 10000 MVA
Converged (3 NR iters, 3 OLTC passes, mismatch 1.29E-011)
Generation 39.56 GW . Demand 39.50 GW . Losses 56.3 MW . System inertia 171.7 GVA.s

The real grid from the GDA data lake (GridSim.Gda)

GridSim.Gda reads the read-only GB grid-data archive (NESO + the DNO Long Term Development Statements) and assembles a faithful, node-level GridModel - every node, cable, transformer, demand point and embedded generator in a licence area. The data stays out of the repo: the converter embeds only the schema mapping and materialises models into a git-ignored directory.

gridsim gda --root /gda --gsp "Coventry 132kV" --solve   # one GSP distribution network from the archive
gridsim gda --root /gda --out ./gda-out                  # whole NGED footprint -> git-ignored JSON
gridsim --dir ./gda-out                                  # run it like any other case

It collapses ~22,590 raw LTDS nodes to ~6,000 electrical buses (union-find on zero-impedance ties), refers every impedance to its GSP-group voltage, attaches demand/generation by topology, and adds OLTCs on step-down transformers. The full NGED footprint (~6,000 buses, 15.5 GW) materialises in ~0.5 s; solving GSP-area by GSP-area reaches steady state across the board. DNO-agnostic (NGED CSV and NPg Parquet ingest today) - details in docs/architecture.md.

At transmission level the same lake assembles gb-spine (316 GSP buses, real ETYS Appendix B circuit impedances and winter ratings where the register resolves) and gb-full (3,539 buses) - the cases the WLS state estimator fits to settlement metering, per half-hour period, back to March 2016 (sec. the evidence engine).

Synthetic GB at scale (data/gb-synthetic)

A procedurally generated, entirely synthetic GB-shaped network down the full voltage ladder (400->275->132->33->11->6.6 kV), ~2,200 buses, no licensed data - for exercising the UI and solvers at scale. Regenerate/rescale with gridsim synth --scale 1.5.


Control-room dashboard (WPF)

src/GridSim.Wpf runs the GB model live and draws it like a London-Underground map - substations as roundels (ring coloured by live voltage), corridors as tube lines with real power flowing as marching dashes (speed = MW, direction = sign), running hot as they load. Around it: a frequency dial, demand/generation/inertia/losses, transmission-boundary loadings, a status lamp and event log, re-solving each tick.

dotnet run --project src/GridSim.Wpf        # Windows + .NET 10 desktop

The map is a real renderer over the live model (smooth at ~6,000 buses / ~8,000 branches with viewport culling + LOD): scroll-zoom, drag-pan, click for detail fly-outs, double-click to drill in, right-click a generator to trip it and watch the frequency dip play through the dial. A SOLVER dropdown exposes all three engines (Auto / Newton dense / Newton sparse / backward-forward sweep); a SRC picker switches between the reduced GB model, GDA-materialised cases, the MATPOWER libraries, and - over the GDA bridge - estimated states (source="estimated", each frame carrying its chi-square quality block behind a quality pill; the web dashboard reaches the same feed via bridge:<case>@estimated). Imported cases carry no coordinates, so the topology is synthesised - a force-directed layout with a graph-derived voltage hierarchy - while the true nominal kV and the solve stay untouched.

A Generation & Inertia tab reframes the same solve as the decarbonisation-and-stability story: fuel mix split synchronous vs converter-connected, live system inertia (GVA.s), and the TSGB device - dial in synthetic inertia + fast response, trip the largest unit, and watch the RoCoF cut and nadir lift. Control-room panels (NESO RP1, MECHANICAL) draw the StabilityMetrics / distributed-inertia read-outs directly; provenance of every number is explicit (real vs synthetic flagged). See docs/neso-rp1-and-mechanical.md.

A CYCLE tab (both dashboards) exposes the prioritised parallel analysis cycle the live rooms run on. The Newton-Raphson base solve is single-core, but everything a control room reads after it converges - N-1 contingencies, harmonics, RoCoF, physics conformance, the watch list - works off the same immutable solved frame and is embarrassingly parallel. So each cycle does one single-core base solve, then a thread-pool fan-out of every analysis with High (must complete each cycle) and Low (best-effort, rotating to full coverage) tiers under a core-millisecond budget (workers x window, so it scales with core count). High always completes; deferred Low work keeps its place and is covered by a later cycle - no starvation. The tab shows High-tier-met, utilization, the budget accounting and a colour-coded job table; both the WPF and Blazor rooms drive it from one shared engine, so their telemetry can't drift.

Windows-only at run time, but it builds everywhere (EnableWindowsTargeting) so CI compiles it.

The same room in the browser (GridSim.Web)

src/GridSim.Web is a Blazor twin of the desktop app - the same map, tabs, transport bar and control-room theme, in a browser, over the same GridSim.Core solve. It runs at full parity with the WPF room: the Canvas GB map (LOD + viewport culling, four colour modes, marching-dash flow, right-click trip, coastline + DNO/GSP/asset overlays), the navigable Assets explorer, and every analysis tab - THE MACHINE's instrument banks, NESO RP1, MECHANICAL (P-Q capability envelope), the Generation TSGB what-if, plus a HUMAN SCALE tab and an on-demand HARMONICS study (penetration, THD, resonance scan, IEEE-519 / G5-5). The presentation constants it shares with WPF live in GridSim.Core.Presentation, so the two twins cannot drift apart.

ASPNETCORE_ENVIRONMENT=Development dotnet run --project src/GridSim.Web   # then browse http://localhost:5217

Cross-platform at run time (no Windows dependency). See user manual ch. 4.


Architecture

GridSim.Core   Model/ (Bus*Generator*Branch*GridModel, per-unit)
               PowerFlow/ (YBus * NewtonRaphson dense * SparseNewtonRaphson * BackwardForwardSweep * SteadyState * DenseLuSolver)
               Numerics/ (CsrMatrix * SparseLu * ILU0 * BiCGSTAB)
               Dynamics/ (FrequencyDynamics lumped-COI * InertiaModelSelector * StabilityMetrics * TsgbSizer * RocofScreening)
               Dynamics/Distributed/ (PoleMap * DomainAssignment * MultiDomainSwing * DomainCoupling * PropagationDelay * DominanceReport)
               Abstractions/ (ILinearSolver * ISparseLinearSolver * IPowerFlowSolver * IInertiaModel <- the seams)
               IO/ (IeeeCases * Matpower/ parser * Json/ loaders * SolveExport * FrequencyExport)
               Diagnostics/ (ForwardInferenceGuard <- the 169 h (7 d + 1 h) horizon * ProvenanceTag * SolveTrace)
               Estimation/ (WLS * Gauss-Newton * chi-square/bad-data * observability * the --method seam)
               Events/ * Replay/ * Scenarios/ * Fleet/ * Evidence/ * Gda/ (the evidence engine)
GridSim.Gpu    ILGPU: GpuContext * GpuDenseLinearSolver * GpuSparseLinearSolver   (behind the seams)
GridSim.Gda    GDA/LTDS -> GridModel converter (schema mapping only; reads /gda at runtime)
GridSim.Cli    run a case, bench/N-1/stress/chain, replay-event/scenario/fleet/evidence/gda-replay/estimate/crossval
GridSim.Wpf    the live control-room dashboard (Windows desktop)
GridSim.Web    the same control room in the browser (Blazor; full parity twin over GridSim.Core)
GridSim.Tests  validated vs published IEEE solutions + cross-val contract + evidence + both inertia models + the estimation estate (927)
tools/crossval crossval.py (pandapower) * crossval_freq.py (scipy) * crossval_estimation.py (pandapower.estimation) * se_residual_check.py (numpy) - independent re-solve + diff

The expensive kernel of a load flow is the linear solve inside each Newton step - that's the seam we build behind, the same "clean abstraction, swappable backend" pattern as OpenMPPT, and the same pattern the inertia model follows. Full walkthrough: docs/architecture.md.


Roadmap

  1. Steady state yes - Q-limits (PV->PQ) + OLTC regulation; numerically hardened.
  2. Live simulation yes - day/night curve, re-dispatch and re-solve each tick.
  3. Frequency dynamics yes - lumped-COI swing (governor droop, load damping) + the distributed multi-domain inertia model behind IInertiaModel.
  4. Scale yes - sparse solver (to ~25k+ buses), parallel benchmarking & N-1 sweeps.
  5. MATPOWER libraries yes - PEGASE / RTS / NESTA / PGLib / CIGRE via the .m importer.
  6. Distribution solver yes - backward/forward-sweep (--bfs) for radial feeders; validated on CIGRE MV and the Baran & Wu 33-bus feeder.
  7. GPU backend yes three backends behind one seam: ROCm/HIP for AMD Instinct (native, AMD-first) + OpenCL (portable, FP64-gated) + CUDA via ILGPU; auto = rocm -> opencl -> cuda -> cpu.
  8. Evidence engine yes - historical grid state estimator + grounded counterfactuals, the no-forward-inference 169 h (7 d + 1 h) horizon as a hard code invariant, intrinsic-inertia-vs-FFR never blurred.
  9. Cross-validation yes - solver diffed against pandapower to machine epsilon; swing vs scipy to <2e-4 Hz; estimator vs pandapower.estimation to ~1e-13 pu + an independent NumPy optimality check.
  10. Measurement-based state estimation yes - classical WLS (estimate, four methods behind one seam) fitting the real GB grid to Elexon settlement metering per half-hour period, history to March 2016; the 9 Aug 2019 record reproduced from it.
  11. Real data - broaden GDA DNO coverage; UK demand / generation-mix profiles; the TSGB evidence data still to source (the 9 Aug 2019 1 s NESO trace for a per-sample RMSE, a cited VoLL).
  12. Distributed model, next - per-machine classical model + RK4 first-swing accuracy; grid-forming inverter dynamics for the fully converter-led case.

Documentation & screenshots

The dashboard on the real GB fleet - the inertia story front and centre - plus the network map and imported cases (full set in docs/screenshots/):

THE MACHINE - every generator locked together as one rotating mass; wind/solar/imports tagged "no inertia", 242 GVA.s of stored spin MECHANICAL - per-machine state, inertia-per-unit, and the RoCoF relay margin, most-stressed first OPERATOR - the live generation stack split synchronous (26.2 GW) vs converter (23.7 GW), 242 GVA.s inertia NESO RP1 - purchased time, intrinsic vs market inertia, inertia by source, correlated-trip risk the live network drawn like a tube map - power flowing as marching dashes any MATPOWER case, topology synthesised - IEEE 118-bus, meshed with graph-derived voltage tiers PEGASE 2869 at scale - concentric-ring layout


License

From V1.3.0, GridSim and GDA are source-available, proprietary software - see LICENSE.md for the full terms. Licensing is measured on buses only: no tier limits generators, demand, lines, transformers, studies or compute.

Use Cost Bus limit
Personal, non-commercial Free 1,500
Academic research & teaching Free - published research must cite GridSim/GDA 1,500 - larger on request
Commercial - Code Licence One-time fee per major version (all 1.x updates included); product manuals must credit GridSim/GDA None
Commercial - Supported Deployment From £50,000 incl. hardware & setup, then £1,750/yr (5 seats included, +£50/seat/yr) None
TSO / ESO / DNO Site licence for a nominal fee, subject to regulatory approval None

Bundled sample networks (the embedded IEEE/CIGRE cases and the synthetic GB network) don't count toward the bus limit. Releases before V1.3.0 were published under GPL-3.0-or-later and remain available under it.

Licensing contact: mark@twinscrollgridbalancer.co.uk

Releases

Packages

Used by

Contributors

Languages