Replaying recorded events
replay-event is the credibility gate for everything in
counterfactuals: it re-simulates a recorded grid
event from its recorded operating point, with no device, and checks the
simulated RoCoF and nadir against the record. Only a baseline that
reproduces the record unlocks counterfactual claims. Reproduce reality
first.
The event catalogue
An event is a GridEvent: an id, a UTC onset time, the pre-event operating
point (inertia in MW.s, demand in MW), a timed sequence of losses, any
recorded operator actions (e.g. LFDD demand disconnection), the response
services the system held (see below), and the recorded outcome (initial
RoCoF, nadir, settle frequency, MW shed). Every figure carries a provenance
tag (Recorded, DerivedFromRecorded, CuratedPlaceholder, ...).
The recorded outcome can also declare the measurement basis of its RoCoF
figure (RocofMeasurementWindowS). 0 (the default) means an
instantaneous/instrument figure, compared against the simulation's 20 ms
maximum. A positive value means the recorded figure is a windowed average -
e.g. 1.0 for a "max 1-second delta" read off NESO's public 1 Hz trace -
and the simulation is scored on the same sampling grid (median over the
logger's unknown sub-second phase), because a sharp step loss reads very
differently at 20 ms and at 1 s.
Response services. Events after roughly 2021 are not arrested by governor
droop but by procured sub-second response (Dynamic Containment / Moderation /
Regulation). An event record can therefore carry the service volumes the
system actually held (from the EAC auction record), each with its published
delivery shape, and the replay integrates them as proportional responders.
When the record declares the list complete (ResponseServicesComplete, true
for GB events after EAC go-live in November 2023), the generic governor term
is switched off instead of double-counting response the record already lists.
Counterfactuals keep the services too - a device is evaluated on top of
what the real system held, never as a silent replacement for it.
An event can also record the frequency the trip actually landed on
(PreEventFrequencyHz, read off the trace). The replay then starts in
equilibrium at that operating point - response partially deployed per the
published curves, the residual imbalance carried as the balancing mechanism
was carrying it - instead of pretending the system sat at a clean 50.000 Hz.
Recorded balancing-mechanism dispatch (from the BOALF record) enters as
recorded actions, the same mechanism as LFDD.
The replay output also prints a nadir envelope: the same baseline run at the model's documented best/worst corners (its error bars on the unfitted arrest constants). Whether the recorded nadir falls inside that bracket is diagnostic information - the verdict itself is always computed from the nominal run.
Three events are built into the binary:
gb-2019-08-09- the 9 August 2019 GB disturbance (Hornsea 1 + Little Barford). The loss sequence, LFDD action and outcome are sourced from the Ofgem investigation report (January 2020), paragraphs 2.4.1-2.4.16, and each loss cites its paragraph. Operating point: 215 GVA.s pre-event inertia (the NESO recorded outturn for the half-hour) and 21,100 MW demand (the WLS-estimated transmission-net demand for the period). Recorded outcome: -0.17 Hz/s initial RoCoF (E3C interim trace; Ofgem records only "> 0.125 Hz/s"), 48.8 Hz nadir at the LFDD trigger, 892 MW shed, back at 50 Hz within five minutes.gb-2023-12-22- the 22 December 2023 excursion, measured end to end by the apparatus: losses from settlement metering (IFA -1,000 MW at Sellindge with exact time-weighting; Cottam CCGTT_CDCL-1-345 MW at 32% of the period delivered), operating point measured (161 GVA.s recorded outturn inertia, 22,400 MW WLS-estimated demand, 49.879 Hz recorded pre-event frequency), response services measured (DCL 873 / DML 150 / DRL 203 MW, EAC auction 70 cleared volumes, on their published delivery curves), and the BM recovery measured (BOALF: +904 MW by +129 s as recorded actions). Recorded outcome from NESO's public 1-second trace (committed atdata/neso/gb-2023-12-22-frequency.csv): -0.102 Hz/s max 1-second delta (RocofMeasurementWindowS: 1.0), 49.275 Hz nadir, no LFDD. The gate's verdict is MARGINAL (RoCoF 20%, nadir 0.30 Hz), and the recorded nadir sits outside the declared-uncertainty corner envelope - the residual is isolated to the inertia measurement basis (the outturn figure excludes the embedded/demand-side contribution, ~20-40 GVA.s at this operating point; seedocs/evidence-gb-2023-12-22.mdsec. 5). That verdict is the gate working as designed; counterfactuals stay locked on this event.gb-largest-loss-1320- a generic screening case: a single 1,320 MW infeed trip on a nominal operating point. Every figure is aCuratedPlaceholdertagged[VERIFY]; it exists for screening, not as a record of anything.
The measured catalogue data/events/gb-2019-08-09-estimated.json carries
the same 2019 event with the measured operating point (WLS-estimated demand
21,147 MW; built by the GDA pipeline's build_event_catalog.py). Use it via
--catalog when you want the estimator-derived numbers rather than the
rounded curated ones - both reproduce the record.
Honesty note: data/events/gb-events.json in the repo is an older dump
of the built-ins from before the sourcing pass (its demand is 19,600 MW).
The CLI never reads it unless you pass it with --catalog; prefer the
built-ins or the estimated catalogue.
Running a replay
dotnet run --project src/GridSim.Cli replay-event <id> [--catalog <file.json>] [--trace <neso.csv>] [--out <dir>]
| Flag | Default | Meaning |
|---|---|---|
<id> |
required (usage + exit 2 without it) | event id; built-ins listed in the usage text |
--catalog |
built-ins | load events from a catalogue file instead |
--trace |
none | a real 1-second NESO frequency CSV; enables the per-sample RMSE check |
--out |
results/replay-<id> next to the built binaries |
output directory - pass it explicitly to keep artefacts out of bin/ |
--dump-catalog <path> |
- | instead of replaying, write the built-in catalogue to disk for editing (see below) |
The replay strips any device terms, extends the simulation window to cover the whole recorded sequence (last event + 30 s - the 2019 event plays out over minutes, and clipping it would replay a different event), simulates the losses on the recorded operating point, and prints:
RoCoF: sim -0.161 recorded -0.170 Hz/s (5 % error)
Nadir: sim 48.82 recorded 48.80 Hz (0.02 Hz error)
RMSE vs trace: n/a (no 1-sec NESO trace attached - data-gated)
assessment: BELOW STATUTORY (49.5 Hz)
VERDICT: REPRODUCED - baseline reproduces the record; counterfactuals unlocked.
It also prints the inertia implied by the recorded RoCoF (computed from the losses inside the first 5 s only, so a staged sequence's later trips do not pollute the initial-slope cross-check) next to the event's stated inertia.
The verdict ladder
| Verdict | Condition |
|---|---|
| Reproduced | RoCoF relative error <= 15 % and nadir absolute error <= 0.15 Hz |
| Marginal | both errors within twice those tolerances |
| Failed | anything worse |
If a NESO trace is attached (via --trace, or a catalogue entry's trace
path) the validator also computes a per-sample RMSE between the simulated and
recorded trajectories, aligned on the loss instant. An RMSE above 0.05 Hz
demotes Reproduced to Marginal - headline metrics matching while the trace
shape diverges is not reproduction. Without a trace the RMSE prints n/a;
the path is data-gated and changes nothing until a real trace is supplied.
Only Reproduced unlocks the counterfactual verbs. Marginal is not a green light.
Exit codes
| Code | Meaning |
|---|---|
| 0 | verdict Reproduced |
| 2 | missing/unusable arguments, or the event was rejected by the horizon guard |
| 1 | other errors (e.g. unknown event id) |
| 3 | verdict Marginal or Failed - a CI gate cannot pass on a baseline that grounds nothing |
Output artefacts
The output directory contains:
run-metadata.json- commit, backend, host, the run description.baseline-frequency.json- the full baseline swing export (inputs, config, trajectory), so a reviewer can hand-check the RoCoF/nadir arithmetic.summary.md- provenance, verdict, the one-line summary, and the inferred inertia cross-check.
Authoring an event
Dump the built-in catalogue as a starting point:
dotnet run --project src/GridSim.Cli replay-event --dump-catalog my-events.jsonEdit the JSON (schema
gridsim-event-catalog/1): give your event a newid, setwhenUtc, the operating point, thelossessequence (seconds, deltaP MW, description, provenance), anyrecordedActions, and the recordedoutcome. Keep provenance honest - figures you have not sourced stayCuratedPlaceholderwith[VERIFY]in the text.Re-run against your file:
dotnet run --project src/GridSim.Cli replay-event my-event --catalog my-events.jsonIterate on the
[VERIFY]figures until the verdict is Reproduced; only then are counterfactuals on that event meaningful.
The horizon rule: events must be historical. Every timestamp in a
catalogue is checked against the no-forward-inference horizon on load - an
event dated less than 169 hours (7 days + 1 h) in the past is rejected with
REJECTED (forward inference) and exit 2. A catalogue cannot smuggle a
recent or future "event" into the pipeline; GridSim replays history only.
See also
- Counterfactuals -
scenario,fleet,evidence, all gated on a Reproduced baseline - Frequency studies - the swing model these replays run on
- Exports and formats - the event-catalogue and frequency-export schemas
- Troubleshooting - horizon rejections and what to do about them