User manual — chapter list

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,assessment
  • risk-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_device
  • summary.md - the gate verdict, LFDD threshold (48.8 Hz) and the risk curve as a table
  • counterfactual-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 --out for all three verbs is results/<verb>-<id> under the application base directory (the CLI's build output), not your current directory. Pass --out explicitly for anything you intend to keep.
  • --inertia-model distributed is 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