User manual — chapter list

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 CCGT T_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 at data/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; see docs/evidence-gb-2023-12-22.md sec. 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 a CuratedPlaceholder tagged [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

  1. Dump the built-in catalogue as a starting point:

    dotnet run --project src/GridSim.Cli replay-event --dump-catalog my-events.json
    
  2. Edit the JSON (schema gridsim-event-catalog/1): give your event a new id, set whenUtc, the operating point, the losses sequence (seconds, deltaP MW, description, provenance), any recordedActions, and the recorded outcome. Keep provenance honest - figures you have not sourced stay CuratedPlaceholder with [VERIFY] in the text.

  3. Re-run against your file:

    dotnet run --project src/GridSim.Cli replay-event my-event --catalog my-events.json
    

    Iterate 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