Counterfactuals
Three verbs answer "what would have happened with different inertia, or with
a device on the system": scenario (the inertia x device x corner sweep),
fleet (one device budget split across N staggered units), and evidence
(the umbrella dossier combining everything, with an avoided-cost figure).
All three run the same swing model as
replay-event, on the same recorded events.
The reproduced-baseline gate
Every counterfactual verb first re-validates the event's no-device
baseline. If replay-event <id> does not report Reproduced, the verb
refuses to run:
GATE FAILED: baseline verdict is Marginal, not Reproduced - ...
Fix the event's [VERIFY] figures (or tolerances) until 'replay-event' reports Reproduced, then re-run.
and exits with code 3. evidence goes one step further: it always
produces a report, but withholds the counterfactual, risk-curve, fleet and
avoided-cost sections (and still exits 3) when the gate fails. There is no
flag to bypass the gate - a counterfactual on a baseline that does not
reproduce reality is exactly the ungrounded claim the pipeline exists to
prevent.
Shared exit codes across all three verbs: 0 success; 2 missing --event or a
horizon (forward-inference) rejection; 1 other errors; 3 gate failed.
Specifying the device
All three verbs model the device as two separate terms - real rotational inertia ((1/2)J*omega^2) and fast frequency response - which are never merged into one "equivalent inertia" number. Either:
--device <file.json>- a device-config JSON (camelCase):{ "name": "tsgb", "rotationalInertiaMwS": 30000, "fastResponseMw": 1000, "fastResponseTimeConstantS": 0.5, "fastResponseDeadbandHz": 0.015 }(the last two default as shown if omitted), or
--device-inertia <GVA.s>and--device-ffr <GW>- build one from flags. Defaults are 30 GVA.s and 1 GW, a placeholder TSGB; treat results run on the defaults as illustrative until you supply your device's real figures.
scenario - the counterfactual sweep
dotnet run --project src/GridSim.Cli scenario --event <id> [--catalog <f>]
[--inertia 260,155,120,102,50] [--device <f.json> | --device-inertia <GVA> --device-ffr <GW>]
[--corners best,worst] [--out <dir>]
| Flag | Default | Meaning |
|---|---|---|
--event |
required | event id (built-in or from --catalog) |
--inertia |
260,155,120,102,50 |
system-inertia axis in GVA.s - the GB trajectory from high-inertia past to projected floor (the constants are marked [VERIFY] in source; override freely) |
--corners |
best,worst |
which model corners to sweep (nominal also accepted) |
--out |
results/scenario-<id> next to the built binaries |
output directory - pass explicitly to keep artefacts out of bin/ |
Each cell of the sweep replays the event's exogenous losses only - never the recorded LFDD disconnection, since the point of the device is to make LFDD unnecessary - at one (inertia, device, corner) triple.
Corners are the model's own error bars: Best/Worst scale the documented- but-unfitted constants (load damping, responsive fraction, governor lag) optimistically/pessimistically. Corners do not touch the initial RoCoF - that is set purely by the inertia at t = 0 - so the RoCoF story is corner-independent.
The console headline is the inertia risk curve: at each inertia level (nominal corner), RoCoF / nadir / LFDD-at-risk without the device and with it, and whether the device avoided LFDD at that level, ending with "device avoids LFDD at N/M inertia levels".
Artefacts in the output directory:
matrix.csv- every cell:inertia_gvas,device,corner,rocof_hz_s,nadir_hz,settle_hz,lfdd_at_risk,assessmentrisk-curve.csv- the paired curve:inertia_gvas,rocof_no_device,nadir_no_device,lfdd_no_device,rocof_with_device,nadir_with_device,lfdd_with_device,lfdd_avoided_by_devicesummary.md- the gate verdict, LFDD threshold (48.8 Hz) and the risk curve as a tablecounterfactual-frequency.json- the full swing export of the hardest case (device at the lowest swept inertia, worst corner)run-metadata.json
fleet - single device vs a staggered fleet
dotnet run --project src/GridSim.Cli fleet --event <id> [--catalog <f>] [--units 1,100]
[--total-inertia 40] [--total-ffr 1.5] [--stagger 10] [--tau 0.5] [--deadband 0.015]
[--inertia <GVA>] [--out <dir>]
| Flag | Default | Meaning |
|---|---|---|
--units |
1,100 |
unit counts to compare (comma list); the first count is the comparison baseline |
--total-inertia |
40 GVA.s | the fleet's total rotational inertia budget |
--total-ffr |
1.5 GW | the fleet's total FFR budget |
--stagger |
10 s | spread of response onsets across the fleet (GB primary response is fully delivered by 10 s; a sub-second stagger would hide the effect being measured) |
--tau |
0.5 s | per-unit response time constant |
--deadband |
0.015 Hz | per-unit frequency deadband |
--inertia |
the event's pre-event inertia | system inertia in GVA.s |
--out |
results/fleet-<id> next to the built binaries |
output directory |
The same total budget is split N ways. Per unit count, the table shows
RoCoF, nadir, FFR actually delivered at the nadir, peak fleet FFR, and LFDD
risk, followed by the conclusion in three lines: RoCoF is identical across
unit counts (intrinsic inertia is N-additive); at the nadir the staggered
fleet has less FFR online than the aggregate unit (FFR is not additive
when it counts); and the resulting nadir difference. Output CSV:
fleet.csv with units,rocof_hz_s,nadir_hz,ffr_at_nadir_mw,peak_fleet_ffr_mw,lfdd_at_risk.
evidence - the umbrella dossier
dotnet run --project src/GridSim.Cli evidence --event <id> [--catalog <f>]
[--device <f.json> | --device-inertia <GVA> --device-ffr <GW>]
[--inertia 260,155,120,102,50] [--cf-inertia <GVA>] [--units 100] [--stagger 10]
[--voll 6000] [--duration 0.5] [--out <dir>]
| Flag | Default | Meaning |
|---|---|---|
--cf-inertia |
the event's pre-event inertia | inertia level for the headline counterfactual column |
--units / --stagger |
100 / 10 s | the fleet section's split |
--voll |
6000 | Value of Lost Load, £/MWh - a placeholder; see below |
--duration |
0.5 | assumed outage duration in hours |
| others | as for scenario |
The dossier assembles, in one place: the recorded column, the simulated (no device) column, the counterfactual (device) column - each with RoCoF, nadir, LFDD risk, the device's inertia and FFR in separate labelled fields, and a provenance tag - plus the inertia risk curve, the single-vs-fleet comparison, and the avoided cost.
Avoided cost is grounded in the record, not marketing: it is claimed only when the device keeps the counterfactual nadir above the 48.8 Hz LFDD threshold and the record actually shed load, and it is computed as
recorded LFDD MW shed x duration (h) x VoLL (£/MWh)
VoLL is a policy number, not physics. The £6,000/MWh default is a
CuratedPlaceholder (GB estimates span roughly £6k-£17k/MWh) and the result
carries that provenance tag until you supply a cited figure - override it
with --voll (and the outage duration with --duration) whenever you intend
to quote the output.
Artefacts: evidence.json (schema gridsim-evidence/1 - the full report:
columns, risk curve, fleet, avoided cost, every provenance tag),
summary.md (the same as a table, with the gate verdict and whether the
counterfactual was unlocked or withheld), and run-metadata.json. Exit code
3 whenever the counterfactual was withheld.
Notes
- The default
--outfor all three verbs isresults/<verb>-<id>under the application base directory (the CLI's build output), not your current directory. Pass--outexplicitly for anything you intend to keep. --inertia-model distributedis not honoured by these verbs - they run the lumped scalar model and warn if you pass it.- The inertia risk curve is always evaluated at the nominal corner, so
its numbers sit between the best/worst cells in
matrix.csv.
See also
- Replaying recorded events - the gate these verbs stand on, verdicts and exit codes
- Frequency studies - the swing model and GB limits
- Exports and formats -
evidence.jsonand the CSV schemas - CLI reference - every flag in one table