22. Estimating GB history¶
GridSim reconstructs the state of the Great Britain grid for a past settlement period from the GDA data lake. Three
steps make that happen, and each has its own tool. gridsim gda reads a distribution network operator's Long Term
Development Statement (LTDS) tables from the lake and materialises a network. gridsim gda-replay runs a demand
history through a network, one power flow per timestamp. gridsim estimate, driven day by day by the GDA repository's
Python builders, fits the network to that period's real telemetry and stores the result in the estimated-state lake
that the bridge streams (ch. 24) and the apps replay
(ch. 23). Use this chapter to build a network from the lake, to replay a
demand history, or to produce estimated states for a day of your own. Every step refuses data newer than 169 hours,
so the pipeline reconstructs the past and never nowcasts.
Before you start¶
- The lake.
$GDA_ROOTstands for the GDA root: the lake directory that holdsNationalGrid/,NESO/andDerived/(in the GDA repository it isv1/DataSources).gridsim gdatakes the root from--root, or from theGDA_ROOTenvironment variable when--rootis absent. The lake is read-only to GridSim. The LTDS tables the verb reads sit underNationalGrid/RAWData/LTDS Tabular Model/in the lake. - The GDA repository. The estimated-state builders are Python scripts in the
v1directory of the GDA repository (NetworkModel/andScripts/). Run them fromv1, with Python and thepandasandpyarrowpackages installed. Each builder finds the lake atDataSources/besideNetworkModel/, from its own location; none of them readsGDA_ROOTor takes a--rootflag. The builders call the GridSim command line, so set theGRIDSIM_CLIenvironment variable to the full path of your ReleaseGridSim.Cliexecutable. The default path inside the script points at a build location that exists only on the GDA host. - Time. Every timestamp is UTC. A settlement period is 30 minutes; a settlement day has 46, 48 or 50 periods depending on the clock change. The builders read the day's actual period list from the measurement file, so a day with a gap in the lake has fewer.
- Every flag of the three verbs is in ch. 19, CLI reference,
gda-replayandestimate. This chapter shows the flags in use.
Timings in the captures differ by machine and run. The counts and numbers hold on the reference lake and change as a lake is refreshed.
The pipeline at a glance¶
| Stage | Tool | Reads | Writes |
|---|---|---|---|
| 1. Network | gridsim gda |
LTDS tables in the lake | a case directory (system.json, buses.json, ...) |
| 2. Demand replay | gridsim gda-replay |
a network and a demand series | state-estimate.csv, exports/state-NNNNNN.json |
| 3. Measurements | NetworkModel/build_gsp_measurements.py |
the day's B1610 (or PN) generation | per-GSP generation, group demand and interconnector flows |
| 4. Measurement set | NetworkModel/build_estimator_measurements.py |
stage 3 output, the national demand series and group demand | a measurement set for one settlement period |
| 5. Estimate | NetworkModel/build_estimated_state.py |
stage 4, once per settlement period | Derived/estimated_state/<case>/... |
| 6. Comparison | build_state_divergence.py, report_estimation_closure.py |
divergence: the estimated and presolved lakes; closure: the estimated lake and demand_all_srcs |
Derived/state_divergence, Derived/estimation_closure |
Stages 1 and 2 run entirely inside GridSim. Stages 3 to 6 are the GDA repository's, stage 5 imports stage 4 and calls
gridsim estimate once per settlement period. The estimated lake is built on gb-spine, the 316-bus fold of the
national gb-full case; the assembler that builds both cases is described in
ch. 9, GDA integration. Stage 1 is a separate route: it builds one
network operator's own network (NGED, UKPN or NPg) from its LTDS tables.
There are two reconstruction routes for any day. The solve route (the presolved lake) scales the demand of the static network to the national demand shape and re-solves. The estimate route (the estimated lake) fits the network to that period's measurements. The divergence product compares them.
Worked example¶
The example reconstructs 1 September 2026. The four parts use the lake held on the reference machine.
1. Materialise a network from the lake¶
$ gridsim gda --root $GDA_ROOT --licence EMIDS --out results/gda-emids --solve
GridSim - GDA ingestion from $GDA_ROOT (NGED)
==========================================================================
Built in 1433 ms - GB/NGED ex-GDA
Raw LTDS nodes 22,590
Electrical buses 2,253 (busbar/disconnector ties merged: 4,790)
Series circuits 2,027 (cables & lines)
Transformers (2w/3w) 940 / 2 (+2 star nodes)
Total branches 2,973
Demand points 824 matched 824, total 8.90 GW
Generators 2 (0.00 GW capacity, 894 unmatched)
System inertia 0.0 GVAs
OLTCs 939
Connected islands 1 (largest 2,253 buses)
Buses by voltage:
132 kV 280
66 kV 1
33 kV 1,511
25 kV 5
11 kV 438
7 kV 18
15 GSP infeed(s) modelled as slack buses; primary = bus 3 (Warwick 132kV Main 2, 132 kV).
WARNING OLTC: 1 transformer(s) publish an invalid tap range and got NO OLTC: 'TX SWIB3/SWIB5 33/11' (-16 %/-1 %: range excludes the nominal 0 % tap).
...
Licence = EMIDS.
Geolocated 606/2253 buses from substations.csv.
validation: warning: 15 slack (reference) buses - treating as a multi-island system.
Materialised replica -> results\gda-emids (git-ignored). Run it: gridsim --dir "results/gda-emids"
Solving with the sparse (ILU0/BiCGSTAB) backend...
Converged in 530 ms (4 NR, 11 OLTC, mismatch 7.31E-009)
Generation 9.01 GW Demand 8.90 GW Losses 111.8 MW V 0.850-1.010 p.u.
[exit 0]
The 22,590 LTDS nodes of the East Midlands licence collapse to 2,253 electrical buses once busbar and disconnector ties
are merged. The model is one connected island, it carries 8.90 GW of matched demand, and it solves in 4 Newton
iterations. The case directory written to results/gda-emids opens like any other case: gridsim --dir results/gda-emids.
2. Replay a day of demand through a network¶
gda-replay takes a demand series: a JSON file of timestamps and a system load factor for each. The script below
builds one for 1 September 2026 from the lake's NESO national demand (nd_mw), dividing each value by the total
demand of the gb-full case so that the factor is a plain multiplier on the case's stated demand. Run it from the GridSim
repository root, where data/gb-full/ sits, with GDA_ROOT set.
import json, os, pandas as pd
root = os.environ["GDA_ROOT"]
base_mw = sum(r["pMw"] for r in json.load(open("data/gb-full/demand.json")))
nd = pd.read_parquet(f"{root}/Derived/demand_all_srcs/year=2026/week=36/demand_all_srcs_2026w36.parquet")
nd = nd[(nd.source == "neso_demand") & (nd.timestamp_utc >= "2026-09-01") & (nd.timestamp_utc < "2026-09-02")]
nd = nd.sort_values("timestamp_utc")
steps = [{"timeUtc": t.strftime("%Y-%m-%dT%H:%M:%SZ"), "systemLoadFactor": round(float(v) / base_mw, 4)}
for t, v in zip(nd.timestamp_utc, nd.nd_mw)]
os.makedirs("results", exist_ok=True)
json.dump({"name": "nd-2026-09-01", "steps": steps}, open("results/nd-2026-09-01.json", "w"), indent=1)
print(f"case demand {base_mw:.0f} MW, {len(steps)} steps, first {steps[0]}, last {steps[-1]}")
$ python make_series.py
case demand 53285 MW, 48 steps, first {'timeUtc': '2026-09-01T00:00:00Z', 'systemLoadFactor': 0.403}, last {'timeUtc': '2026-09-01T23:30:00Z', 'systemLoadFactor': 0.4193}
[exit 0]
Replay it on the national case:
gridsim gda-replay --series results/nd-2026-09-01.json --case gb-full --export-every 24 --out results/gda-replay-nd
$ gridsim gda-replay --series results/nd-2026-09-01.json --case gb-full --export-every 24 --out results/gda-replay-nd
=== Historical state estimate: gb-full over nd-2026-09-01 ===
reconstructed 48 timestamps (2026-09-01T00:00:00.0000000Z .. 2026-09-01T23:30:00.0000000Z)
non-converged: 0
system loss over the series: 71.2 .. 174.2 MW
-> results\gda-replay-nd
[exit 0]
All 48 half-hours converged, and the system loss moves between 71.2 MW (overnight) and 174.2 MW (the evening peak) as the load factor moves. The per-step record is the CSV:
$ Get-Content results/gda-replay-nd/state-estimate.csv -TotalCount 14
# GridSim - 2026-10-03 12:33:43 UTC
# code: git afedfedf | GridSim.Core v1.95.99.0 | build Release
# runtime: .NET 10.0.12 X64 GC=Server
# machine: LAPTOP Microsoft Windows 10.0.26200
# cpu: X64 - 8 logical cores - 8 GB RAM
# simd: Avx256 (4 doubles/step) FMA=True
# backend: cpu
# run: gda-replay case=gb-full series=nd-2026-09-01 steps=48
index,time_utc,load_factor,converged,iterations,max_mismatch,total_loss_mw,vmin_pu,vmax_pu
0,2026-09-01T00:00:00.0000000Z,0.4030,True,6,3.715E-011,87.714,0.99442,1.04576
1,2026-09-01T00:30:00.0000000Z,0.4021,True,2,2.598E-011,87.338,0.99449,1.04576
2,2026-09-01T01:00:00.0000000Z,0.3914,True,2,7.533E-011,82.936,0.99525,1.04577
3,2026-09-01T01:30:00.0000000Z,0.3915,True,2,4.453E-011,82.976,0.99524,1.04577
4,2026-09-01T02:00:00.0000000Z,0.3754,True,2,3.783E-010,76.576,0.99637,1.04577
[exit 0]
The first step takes 6 iterations from the flat start; every later step warm-starts from the previous converged state and needs 2.
3. Estimate one settlement period from telemetry¶
The estimate route does not scale demand. It fits the network to a measurement set built from that period's metered
generation, interconnector flows and demand pseudo-measurements. Run from v1 in the GDA repository, the first command
writes the measurement set for settlement period 25 (11:00 UTC on 1 September 2026) on gb-spine, and the second is
the gridsim estimate call the day builder makes for every period:
python NetworkModel/build_estimator_measurements.py --date 2026-09-01 --period 25 --out results/gda-estimate/m-sp25.json
$ python NetworkModel/build_estimator_measurements.py --date 2026-09-01 --period 25 --out results/gda-estimate/m-sp25.json
{
"date": "2026-09-01",
"period": 25,
"takenUtc": "2026-09-01T11:00:00Z",
"measurements": 948,
"buses_with_metered_gen": 42,
"metered_gen_mw": 11057.6,
"ic_buses": 0,
"ic_net_mw": 0,
"national_demand_mw": 19885.0,
"demand_scale": 0.3732,
"groups_used": 0,
"group_demand_total_mw": 0.0,
"unallocated_gen_mw": 6038.8,
"output": "results\\gda-estimate\\m-sp25.json"
}
[exit 0]
The set holds 948 measurements. Metered B1610 generation binds to 42 spine buses (11,057.6 MW), 6,038.8 MW of metered
generation could not be bound to a Grid Supply Point and is reported as unallocated rather than spread across the
network, and the demand pseudo-measurements are scaled by 0.3732 to the day's 19,885 MW national demand. ic_buses is
0 because the lake holds no interconnector-flow partition for that week.
gridsim estimate NetworkModel/out/gb-spine --measurements results/gda-estimate/m-sp25.json --prior solve --warm-start --export-solve --max-iter 100 --lean --backfill-removals --out results/gda-estimate/sp25
$ gridsim estimate NetworkModel/out/gb-spine --measurements results/gda-estimate/m-sp25.json --prior solve --warm-start --export-solve --max-iter 100 --lean --backfill-removals --out results/gda-estimate/sp25
error loading case 'NetworkModel/out/gb-spine': unknown case 'NetworkModel/out/gb-spine' (built-in names: case118, case14, case145, case24, case30, case300, case57, case9, cigre, gb, gb-full, gb-network-2224, gb-reduced, gb-reduced-29, gb-synthetic, ieee9, pypsa-gb-reduced, pypsa-gb-zonal, rts, synthetic, uk; or a case folder, or a MATPOWER .m file, or a JSON v2 case manifest .json)
[exit 2]
Chapter 21 explains every line. Here, 948 measurements fit 631 unknowns (316 buses give
2 * 316 - 1 states), leaving 317 degrees of freedom. The fit statistic J is 330.37 against a 95 per cent chi-square
threshold of 359.52 (the estimator's Wilson-Hilferty approximation of the quantile, 359.5207; the exact quantile,
359.5218, also rounds to 359.52), so the fit is consistent with the sigmas the measurements declared.
This capture is from an earlier build. The current estimator leaves the prior-solved rows that --prior solve adds
out of J and out of the degrees of freedom (ch. 21), so on this run the dof, the
threshold, J and possibly the verdict differ from the figures above: the degrees of freedom fall by the number of
prior-solved rows (315 on the --prior solve set of chapter 21), which leaves almost no redundancy. The capture has
not been retaken.
4. Estimate a whole day and store it¶
The builder runs that estimate for every settlement period in the day's stage 3 measurement file (stage 3 must have run for the date), collects each period's quality columns and writes them to the estimated lake:
$ python NetworkModel/build_estimated_state.py --date 2026-09-01
SP02 ok iters=23 J=301.6 chi=PASS removed=1
SP03 ok iters=23 J=288.8 chi=PASS removed=1
SP04 ok iters=22 J=278.4 chi=PASS removed=1
...
SP45 ok iters=18 J=300.8 chi=PASS removed=0
SP46 ok iters=158 J=357.5 chi=PASS removed=12
SP47 ok iters=29 J=202.8 chi=PASS removed=1
SP48 ok iters=29 J=220.9 chi=PASS removed=1
{
"date": "2026-09-01",
"case": "gb-spine",
"periods": 47,
"converged": 47,
"chi_passed": 47,
"removed_total": 96,
...
}
[exit 0]
The day has 47 periods because the day's measurement file carries periods 2 to 48, one fewer than the 48 the clock
allows. All 47 converged and passed the chi-square test, with 96 measurements removed as bad data across the day. The
builder merges the rows into one weekly partition,
Derived/estimated_state/gb-spine/year=2026/week=36/estimated_state.parquet under the lake. In the captured run the
elided output line names a scratch tree, results/estimated_state/gb-spine/year=2026/week=36/estimated_state.parquet,
so the reference lake keeps its single stored period.
Reading the output¶
gridsim gda¶
The report is a build summary, a list of notes, the written location and, with --solve, a power-flow result.
- Header.
GDA ingestion from ... (NGED)names the lake and the source reader (--dno,ngedby default).Built in N msis the build time and the name that follows is the model name; a carve appends[<GSP group>]or>=132kV. Raw LTDS nodes. Rows in the LTDS node table.Electrical busesis the bus count after the builder merges every zero-impedance circuit: a busbar or disconnector tie or, with a warning, any other equipment. The bracket counts the ties merged.Series circuitsare cables and lines.Transformers (2w/3w)counts two- and three-winding units. Each three-winding unit becomes a new star node with one branch per winding it has, so+2 star nodesis two units. Both have three windings, andTotal branchesis2,027 + 940 + 6 = 2,973.Demand points. The count of demand rows joined to a bus, and the total of their maximum demand in GW.matchedcounts those that found a bus.Generators. The embedded generation placed on the network: the count, the export capacity in GW, and the register rows that found no bus (unmatched). By default only plant whose status isConnectedenters the model;--all-genadmits every row.System inertiain GVAs, summed from the placed generators.OLTCsis one on-load tap changer per step-down transformer with a valid published tap range; each regulates its low-voltage bus to 1.0 per unit.Connected islandsand the size of the largest.Buses by voltagelists the bus count at each nominal kV.- Notes. Every row the builder drops or repairs is counted, and the notes that carry examples name the first five. In the example: 15 Grid Supply Point infeeds are modelled as slack buses (one per GSP group, with the largest island's primary slack named); one transformer published a tap range that excludes 0 per cent, so it has no OLTC; 37 zero-impedance circuits that are not busbars or switches were merged as ties (a sign of missing R and X cells); and 1,294 demand rows lie on nodes outside the carve.
- Solve block.
--solvesolves the model with the sparse backend (ILU0 with BiCGSTAB) when the largest island exceeds 200 buses and the dense backend otherwise.Converged in 530 ms (4 NR, 11 OLTC, mismatch 7.31E-009)gives Newton iterations, OLTC passes and the largest remaining mismatch in per unit.Generation 9.01 GWis the sum of the positive bus injections, which here the slack infeeds supply because the model carries 0.00 GW of embedded generation;Demandis the sum of the buses' demand. A solve that does not converge printsDID NOT CONVERGEand exits 3.
gridsim gda-replay¶
reconstructed 48 timestamps (first .. last) echoes the series; non-converged counts steps whose power flow failed
(any non-zero count exits 3); system loss over the series is the lowest and highest total_loss_mw. The CSV has
eight comment lines naming the build and machine, then one row per timestamp:
| Column | Meaning |
|---|---|
index |
Step number from 0. |
time_utc |
The series timestamp. |
load_factor |
The multiplier applied to the case. Every bus's real and reactive demand is scaled by it, every generator not on the first slack bus is scaled by it, and the slack bus re-balances. |
converged, iterations, max_mismatch |
The solve's outcome; the mismatch is per unit. |
total_loss_mw |
Network loss at that step. |
vmin_pu, vmax_pu |
Lowest and highest bus voltage. |
run-metadata.json repeats the build and machine header. With --export-every N, step 0 and every N-th step also
write exports/state-NNNNNN.json, a gridsim-solve-export/1 document holding the model, the solve and the full
result at that step (state-000024.json is the 12:00 UTC step). The warm start is only ever taken from a converged
state.
The stored estimate and its lake row¶
Each settlement period becomes one row of estimated_state.parquet. This is the stored row for 11:00 UTC on 1
September 2026 in the reference lake:
$ python -c "import os, pandas as pd; d = pd.read_parquet(os.environ['GDA_ROOT'] + '/Derived/estimated_state/gb-spine/year=2026/week=36/estimated_state.parquet').drop(columns=['removed_json', 'boundary_json', 'result_json']); print(d.T.to_string(header=False))"
datetime_utc 2026-09-01 11:00:00+00:00
case gb-spine
status Converged
converged True
iterations 8
max_state_update 0.0
objective_j 225.414917
chi_threshold 359.520725
chi_passed True
dof 317
measurements_n 948
removed_n 0
vm_min 0.999362
vm_max 1.051805
[exit 0]
The transposed print shows max_state_update as 0.0 because pandas rounds the row to six decimals; the stored value is
1.4822059173716063e-09.
| Column | Meaning |
|---|---|
datetime_utc |
The instant the state describes. |
status, converged, iterations, max_state_update |
The estimator's outcome, as in ch. 21. A period that runs and fails to converge is stored with converged false and no result block. |
objective_j, chi_threshold, chi_passed, dof |
The fit statistic, its 95 per cent chi-square threshold, the verdict and the degrees of freedom. The pill in the apps shows these. |
measurements_n |
Measurements the fit consumed. |
removed_n, removed_json |
Bad-data removals: the count and, per removal, the measurement kind, bus and normalised residual. |
vm_min, vm_max |
Lowest and highest estimated voltage, per unit. |
boundary_json |
When the case carries boundaries.json, the ETYS boundary transfers (flow and limit in MW) evaluated at the estimated state. |
result_json |
The slim state block the bridge streams: each bus's vm, vaDeg, pInjMw, qInjMvar and each line flow. |
The stored row is a separate run from the re-run in step 3: its J is 225.41 after 8 iterations, the re-run's is
330.37 after 10, on the same 948 measurements and 317 degrees of freedom. Both were made on an earlier build, before
the prior-solved rows were left out of J and dof (see step 3). The apps and the bridge show the stored row.
The day summary, divergence and closure¶
The builder's final JSON counts the periods, how many converged and passed chi-square, and the removals. Its exit code
is 0 when every period converged and 3 when any did not. Periods that ran and did not converge are stored and counted,
so exit code 3 means "stored with failures", not "nothing stored". A period whose gridsim estimate call writes no
export (an invalid measurement set, or a horizon rejection) aborts the whole day: nothing is stored and the exit code
is 1. A missing GridSim CLI and a day with no stage 3 measurement file also exit 1.
Divergence and closure then compare the day against the other route and against national records:
$ python NetworkModel/report_estimation_closure.py --case gb-spine --date 2026-09-01
{
"date": "2026-09-01",
"case": "gb-spine",
"periods": 47,
"converged_n": 47,
"chi_passed_n": 47,
"removed_total": 96,
"est_gen_gwh": 471.94,
"est_load_gwh": 459.9,
"nd_gwh": 586.47,
"load_vs_nd_pct": 78.42,
"outlier_league": "PInjMw@bus89 x11; PInjMw@bus20 x10; PInjMw@bus40 x9; PInjMw@bus255 x7; PInjMw@bus176 x7",
...
}
[exit 0]
est_gen_gwhandest_load_gwhare the day's energy from the positive and negative estimated bus injections of the stored periods in the window;nd_gwhis the NESO national demand over the same window, andload_vs_nd_pctisest_load_gwhas a percentage ofnd_gwh. The window runs from one hour before 00:00 UTC on the date to 00:00 UTC the next day (25 hours). Herend_gwhsums 50 half-hours and the estimate totals sum the 47 stored periods, so 78.42 per cent compares unequal windows; the report applies no pass or fail threshold to it.outlier_leagueranks the measurements removed most often across the day. A measurement that is removed period after period at the same bus is a measurement or model defect, not noise.
$ python NetworkModel/build_state_divergence.py --case gb-spine --date 2026-09-01
{
"case": "gb-spine",
"date": "2026-09-01",
"periods_compared": 47,
"dvm_max_pu": 0.26789,
"dva_max_deg": 33.314,
"dflow_max_mw": 3816.9,
"worst_vm_bus_mode": 11,
...
}
[exit 0]
Divergence joins the estimate and presolved routes per period and reports the largest voltage-magnitude difference
(dvm_max_pu), angle difference (dva_max_deg) and flow difference (dflow_max_mw), plus the bus that is the worst
voltage offender most often. Persistent disagreement localises model or dispatch error. Treat it as evidence.
Variations¶
A voltage cut. --minkv 132 builds only the 132 kV network and folds the demand below the cut onto the nearest
retained bus. The notes report how much was folded:
$ gridsim gda --root $GDA_ROOT --licence EMIDS --minkv 132 --solve
...
Electrical buses 474 (busbar/disconnector ties merged: 770)
...
Connected islands 3 (largest 418 buses)
...
Demand: 814 row(s), 8699.8 MW below the 132 kV cut were folded onto their nearest boundary bus (losses and voltage drop below the cut are not modelled).
...
Converged in 126 ms (4 NR, 10 OLTC, mismatch 1.76E-009)
Generation 8.97 GW Demand 8.90 GW Losses 84.9 MW V 0.921-1.007 p.u.
[exit 0]
The 132 kV carve has 474 buses instead of 2,253. The losses fall from 111.8 MW to 84.9 MW because the 33 kV and 11 kV networks, and the losses in them, are no longer modelled.
One GSP supply area. --gsp carves one Grid Supply Point group. Use the names --all-gsps prints:
Every GSP area in turn. --all-gsps builds and solves each GSP group independently and prints one line per group:
$ gridsim gda --root $GDA_ROOT --all-gsps
GridSim - solving every GSP supply area of GB/NGED ex-GDA (60 groups, 61 spellings)
==============================================================================
GSP group Buses Demand Loss Vmin Vmax Status
------------------------------------------------------------------------------
Abham 132 kV 141 0.22GW 2MW 0.975 1.010 converged
Alverdiscott 132 kV 184 0.20GW 4MW 0.942 1.009 converged
...
Feckenham 66kV 82 0.30GW 18MW 0.578 1.000 DIV 3E-003
...
------------------------------------------------------------------------------
50/51 GSP areas converged in 4.6s
'Iron Acton 132 kV' also covers 'Iron Acton 132kV' (same group, built once)
9 group name(s) select no live element and were skipped (not counted):
Alverdiscott 132 (spelling variant of 'Alverdiscott 132 kV')
Axminster 132 (spelling variant of 'Axminster 132 kV')
...
Seabank 132 (spelling variant of 'Seabank 132 kV')
Demand: 19.26 GW solved of 19.55 GW total losses 247 MW (1.28%)
[exit 3]
The 61 group spellings in the circuit table are 60 groups: Iron Acton 132 kV and Iron Acton 132kV have the same
canonical key, so they select the same elements and the area is built once. Nine names, such as
Alverdiscott 132 beside Alverdiscott 132 kV, select no live element; they are listed after the table and not
counted. Of the 51 areas that build, Feckenham 66kV diverged
(DIV 3E-003, the remaining mismatch in per unit), so the run still exits 3: any area that builds and does not
converge fails the run, and so does a run in which no area builds.
Another operator. --dno ukpn and --dno npg read the UK Power Networks and Northern Powergrid tables; --licence
carves one licence area. --neg-gen folds embedded generation into negative demand, --no-oltc builds without tap
changers, and --no-geo skips locating buses from the substation table.
A register vintage. --as-of 2026-05-01 picks the newest generation and capacity registers published on or before
that date. Without it, the as-of instant is now minus 169 hours. Files whose names carry no publication date are never
excluded.
Inspect a table. --schema <path> prints a CSV or Parquet table's columns and row count and exits:
$ gridsim gda --root $GDA_ROOT --schema "$GDA_ROOT/NationalGrid/RAWData/LTDS Tabular Model/ltds-table-2a-two-winding-transformer.csv"
$GDA_ROOT\NationalGrid\RAWData\LTDS Tabular Model\ltds-table-2a-two-winding-transformer.csv
17 columns 2,705 rows
Licence
GSP Group
Node 1
...
[exit 0]
Replay variations. gda-replay --synth replaces the series with a synthetic day (load factor 0.60 to 1.00, trough at
04:00 and peak at 16:00): gridsim gda-replay --synth --steps 6. --passive-plant runs the delayed tap changers and
thermal derating between steps. --bess adds hypothetical storage and tags the result as a counterfactual
(ch. 20, Counterfactuals).
Estimate variations. Pass a single settlement period with --periods 25 or another case with
--case-dir NetworkModel/out/gb-full. For a day before the settlement-metering (B1610) record, --source pn in
build_gsp_measurements.py builds the measurements from Physical Notification levels, which are submitted intent
rather than metering; they carry looser sigmas (the larger of 10 MW and 10 per cent of the value, against 2 MW and 2
per cent for B1610, each divided by the weakest crosswalk confidence in the aggregate, at least 0.6) and no group-demand
product. The estimable history therefore starts at 2016-03-01, the first PN day. With --source auto, a day uses
B1610 when its file has at least 40 periods and PN otherwise.
Keep the lake current. The GDA host runs Scripts/refresh_estimated_state.py daily. Each run builds the newest
settlement day that is 8 or more days old (searching back at most 30 days), has a B1610 file and has no estimate,
through the full route
(measurements, presolve, estimate, divergence, closure). --backfill N also estimates up to N earlier days, walking a
cursor in Scripts/state/estimated_backfill.json backwards towards the 2016-03-01 floor; a day that fails is recorded
and retried on later runs, and manual and scheduled runs share the cursor:
Calibrate the sigmas. python NetworkModel/calibrate_sigmas.py runs the estimator with full residual output over
a spread of periods and writes NetworkModel/resolved/sigma_calibration.json. Read rms_lev, the leverage-corrected
residual RMS per measurement class: 1 means the declared sigma matches the noise, well below 1 means the sigma
overstates the noise, well above 1 means the class is overconfident. It reports; it never changes the constants.
The 169-hour horizon¶
The pipeline reconstructs the past only. The rule is one number, 169 hours (7 days and 1 hour), enforced independently at every stage:
gridsim gdapicks registers published on or before now minus 169 hours unless you pass--as-of.gridsim gda-replayrejects a series with any timestamp newer than now minus 169 hours, both when the file is loaded and again when the loop starts. The exit code is 2.gridsim estimaterefuses a measurement set dated newer than the horizon (exit 2).- The GDA builders reject a day unless the whole day, to the following midnight, is older than the horizon.
- The bridge rejects a start newer than the horizon and sends no tick newer than it, and the GridSim client refuses any tick newer than it (ch. 24).
$ gridsim gda-replay --series results/nd-too-recent.json --case gb-full --out results/gda-replay-recent
REJECTED (forward inference): GDA time-series timestamp 2026-10-02T12:00:00.0000000Z is newer than the historical horizon 2026-09-26T11:33:47.1181361Z (data must lag real time by >=169 h). GridSim replays history only - it never processes recent or future data, so it cannot forward-infer.
[exit 2]
$ python NetworkModel/build_gsp_measurements.py --date 2026-10-02
Traceback (most recent call last):
...
horizon.ForwardInferenceError: 2026-10-03T00:00:00+00:00 is newer than the no-forward-inference horizon (2026-09-26T11:38:11.703146+00:00, now - 7.041666666666667d). This is a replay system: it may only reconstruct the settled past.
[exit 1]
The horizon moves with the clock, so the newest estimable day is always at least 8 days old. The daily refresh uses an 8-day lag for that reason, which also clears the settlement publication lag.
Pitfalls¶
- Wrong or unreadable lake.
GDA root error: GDA root not found: no-such-lake(exit 1) means--rootandGDA_ROOTpoint at a directory that does not exist. A root that exists but lacks a table fails at load withLoad failed: GDA dataset not found: <path>(exit 1).--fleetneeds the NESO Transmission Entry Capacity register folder and fails when the lake lacks it (here a root,GDA_ROOT_NO_TEC, that has no TEC register at all):
$ gridsim gda --root $GDA_ROOT_NO_TEC --fleet
Fleet build failed: GDA dataset folder not found: $GDA_ROOT_NO_TEC\NESO\RAWData\Transmission Entry Capacity (TEC) register
[exit 1]
A lake that holds the register only as its Parquet conversion (NESO/Parquet/transmission_entry_capacity_tec_register)
fails the same way (exit 1), and the message says so: GridSim reads the published tec-register-*.csv files under
NESO/RAWData, not the Parquet copy (capture docs/_captures/gda/gda-fleet.txt).
- No root at all.
gridsim gdawith neither--rootnorGDA_ROOTprints its usage and exits 2. - A mistyped option value.
--dno fooexits 2 witherror: --dno expected 'nged', 'ukpn' or 'npg' but got 'foo'. An--as-ofthat is not an ISO-8601 UTC timestamp (yyyy-MM-dd[THH:mm[:ss]]) exits 2 the same way. - A mistyped GSP name.
--gspis checked against the groups in the data (by the same canonical key the carve uses, so case, spacing andkVspelling do not matter) before anything is built. An unknown name exits 2 with the nearest names:
$ gridsim gda --root $GDA_ROOT --gsp "Exter 132 kV" --solve
error: --gsp 'Exter 132 kV' is not a GSP group of GB/NGED ex-GDA. Nearest: 'Exeter 132 kV', 'Enderby 132kV', 'Exeter 132'. gda --all-gsps lists every group.
[exit 2]
- A carve that keeps nothing. A known name whose elements are all out of service or planned, or a
--licence/--minkvcombination that keeps no element, prints the build summary and thenerror: the carve selected no live element (0 buses) ... No model was written.and exits 3. Nothing is written to--outand nothing is solved.--gsp "Exeter 132"is an example on the reference lake: useExeter 132 kV. - A materialised case goes into
gda-replayas a directory.gda-replay --casetakes a built-in name, a file underdata/matpower/, or a directory holdingsystem.json, such as one written bygda --out:
$ gridsim gda --root $GDA_ROOT --gsp "Exeter 132 kV" --out results/gda-exeter
$ gridsim gda-replay --synth --steps 4 --case results/gda-exeter
=== Historical state estimate: results/gda-exeter over synthetic-gda-exeter ===
reconstructed 4 timestamps (2024-01-01T00:00:00.0000000Z .. 2024-01-01T01:30:00.0000000Z)
non-converged: 0
system loss over the series: 1.3 .. 1.5 MW
-> ...\results\gda-replay-gda-exeter
[exit 0]
The default output folder is named after the directory's last component.
- Exit 3 is not exit 0. gda-replay exits 3 when any step fails to converge and gda --all-gsps when any group does
not. The day builder exits 3 when any period runs and fails to converge, and the wrappers treat that as "stored". Exit 1
from the day builder means the day aborted and nothing was stored.
- Chi-square pass is not plausibility. chi_passed says the fit agrees with the declared sigmas. It does not say
the state is physical. The re-run above passed every one of its 47 periods while its first period carries a
vm_max of 1.267891 per unit and a worst-case divergence from the presolved route of 0.26789 per unit. Read
vm_min, vm_max, the removals and the divergence product beside the verdict. The cause of that PASS is known
(October 2026): the re-run had no interconnector-flow partition for week 36 (ic_buses 0) and 6,567 MW of
unallocated generation, the --prior solve power flow of those injections settled at up to 1.2639 pu, and its
voltages entered the fit as 315 prior-solved rows at sigma 0.02. Those rows agree with the injections by
construction, so the fit passed (J = 301.59; the 315 voltage rows contribute 32.2 and the 305 reactive pseudo rows
30.8, about a tenth of their expected share each). The same set with --prior flat fails (J = 557.40, 20
removals at the buses that sat at 1.27 pu). gridsim estimate now warns when the prior leaves the 0.9 to 1.1 pu band
(ch. 21), and --fdia-screen reads suspected on that
period.
- Missing builder path. error: GridSim CLI not found: GRIDSIM_CLI is not set and 'gridsim' is not on PATH. means
the builders cannot find the executable; set GRIDSIM_CLI or put gridsim on PATH. When GRIDSIM_CLI is set to a file
that does not exist, the message reads error: the GridSim CLI set by GRIDSIM_CLI does not exist: <path>.
- A day that is too recent. The builders raise ForwardInferenceError. Choose a day at least 8 days old.
- Embedded generation joins. The generation register joins to the demand table by GSP group and primary-substation
name, compared by a canonical key that drops voltage tokens (33 11 kV, 6.6) and station words (S Stn) so the two
tables' spellings of one primary meet. A row whose group does not list the primary joins by name alone when the name
is unique to one group; at a site whose demand sits on several buses it joins the bus of its own voltage. A row whose
primary or voltage picks no bus, or several, is left unplaced with a WARNING generation join: note naming it and
its export capacity. The Generation join notes count each route; rows that find no bus add nothing to the model,
and the slack infeeds supply the demand.
Limits¶
- The estimated lake is built on
gb-spine, a fold ofgb-fullthat drops impedances below the Grid Supply Point; the other zone cases have no estimated lake. gda --all-gspssets aside a group name that selects no live element; it does not tell a spelling variant from a genuinely missing area except by thespelling variant ofhint, which only matches a name that another group's name begins with.- Generation rows whose primary-substation name matches no bus of their GSP group, by the canonical key, are not placed; the notes count them.
- A period whose
gridsim estimatecall writes no export ends the day builder with exit 1 and stores none of that day's periods.
See also¶
- ch. 9, GDA integration - the model builder, the assembler, the lake products
- ch. 21, State estimation - the
estimateverb and the chi-square test - ch. 23, Replaying estimated states - viewing the estimated lake in the apps
- ch. 24, The GDA bridge - streaming the lake into GridSim
- ch. 19, CLI reference: gda, gda-replay, estimate
- ch. 30, Troubleshooting - non-convergence and pipeline faults