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.
Most grid simulators bolt frequency dynamics on as a single lumped number. GridSim doesn't.
-
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 frozenE = 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.
-
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.
-
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) + ewith 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. -
Validated, and built for the hardware that suits the maths. Power flow is
doubleend-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.
- Quick start
- Status
- Frequency, inertia & stability - the distributed model
- The evidence engine - historical state estimation & grounded counterfactuals
- Cross-validation vs pandapower & scipy
- Solvers
- Compute backends - AMD-first
- Test-case libraries & the MATPOWER importer
- Benchmarking & results
- The GB models: reduced, GDA (real), and synthetic
- Control-room dashboard (WPF)
- Architecture
- Roadmap
- Documentation & screenshots
- License
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.
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
NonFinitestatus, 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 againstpandapower.estimationto 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
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 itThe 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.
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.
| 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 ***
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.
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), andscenario/evidencerefused to run a counterfactual off the non-reproduced baseline until the sourced record replaced it. That is the gate working, not a bug.
"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.
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) behindILinearSolver.SparseNewtonRaphsonPowerFlow- sparse complex Y-bus + sparse Jacobian, solved by direct sparse LU (reverse Cuthill-McKee ordering, partial pivoting) with an ILU0/BiCGSTAB fallback, behindISparseLinearSolver. 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.
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.
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.
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 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
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 caseIt 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).
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.
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 desktopThe 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.
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:5217Cross-platform at run time (no Windows dependency). See user manual ch. 4.
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.
- Steady state yes - Q-limits (PV->PQ) + OLTC regulation; numerically hardened.
- Live simulation yes - day/night curve, re-dispatch and re-solve each tick.
- Frequency dynamics yes - lumped-COI swing (governor droop, load damping) + the distributed
multi-domain inertia model behind
IInertiaModel. - Scale yes - sparse solver (to ~25k+ buses), parallel benchmarking & N-1 sweeps.
- MATPOWER libraries yes - PEGASE / RTS / NESTA / PGLib / CIGRE via the
.mimporter. - Distribution solver yes - backward/forward-sweep (
--bfs) for radial feeders; validated on CIGRE MV and the Baran & Wu 33-bus feeder. - 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.
- 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.
- Cross-validation yes - solver diffed against pandapower to machine epsilon; swing vs scipy
to <2e-4 Hz; estimator vs
pandapower.estimationto ~1e-13 pu + an independent NumPy optimality check. - 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. - 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).
- Distributed model, next - per-machine classical model + RK4 first-swing accuracy; grid-forming inverter dynamics for the fully converter-led case.
docs/architecture.md- projects, data model, solver internals, the seams.docs/cli-reference.md- every command and flag.docs/distributed-inertia.md- the multi-domain inertia model in depth.docs/manual/10-state-estimation.md+docs/technical/06-state-estimation.md- the WLS state estimator in depth (manual ch. 78).docs/evidence-gb-2019-08-09.md- the 9 Aug 2019 estimated-state dossier: the credibility gate closed on sourced data end to end.docs/evidence-gb-2023-12-22.md- the 22 Dec 2023 dossier: the measured EAC response services, the RoCoF measurement-basis fix, and why MARGINAL is the honest verdict.docs/case-libraries.md- the MATPOWER parser and case archive.docs/benchmarking.md- the bench/regime harness, solver correctness, CPU/GPU scaling.docs/neso-rp1-and-mechanical.md- the control-room panels.docs/gpu-rental-runbook.md- CPU vs CUDA vs MI300 on rented boxes.CHANGELOG.md- version history.
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/):
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






