14. Frequency studies¶
A frequency study answers four questions about a loss of generation or infeed. How fast does frequency fall
straight after the loss (the RoCoF, in Hz/s)? How low does it go before the response arrests it (the nadir)? Where
does it settle? Which services (governor droop, Dynamic Containment, Dynamic Moderation, Dynamic Regulation) did the
arresting? GridSim answers them with five verbs and one live view. freq and replay read a recorded frequency trace.
transient runs a case through a time-domain simulation with an optional infeed trip. domains reports how the
generator fleet divides into speed domains for the distributed inertia model. h0-study sweeps a recorded event
toward zero synchronous inertia with a growing grid-forming converter fleet. The desktop app plays the same
physics live. This chapter shows each verb on real output, explains every line it prints, and states what each
verb does with the inertia model and the response services.
Which verb answers which question¶
| Question | Use | Chapter |
|---|---|---|
| Did a recorded NESO trace breach the GB limits, and how steep was its worst RoCoF? | freq |
this chapter |
| Play a recorded trace back at 20 ms, slice by slice. | replay |
this chapter |
| What RoCoF and nadir does a case give when an infeed of N MW is lost at time T, with a full time series and solve exports? | transient |
this chapter |
| One generator trips: RoCoF, nadir and settling frequency, plus the same trip at half the inertia. | trip |
contingency analysis |
| How does the fleet divide into speed domains, and does one domain dominate? | domains |
this chapter |
| Does the model reproduce a recorded event? | replay-event |
replaying recorded events |
| What happens to a recorded event with a device, or with less inertia? | scenario, fleet, evidence |
counterfactuals |
| What if the synchronous inertia goes to zero and converters form the grid? | h0-study |
this chapter, then counterfactuals |
| Trip a generator by hand and watch frequency live. | the desktop app | the desktop app |
| Rotor-angle stability, machine by machine. | stability |
ch. 19, CLI reference |
| Sub-cycle waveforms, not frequency. | emt |
electromagnetic transients |
The frequency verbs model the electromechanical timescale: seconds to minutes, one frequency for the system (or one per speed domain), with the network held at its steady-state solution. Waveform behaviour inside a cycle is outside them and belongs to the EMT chapter. The equations are in the frequency dynamics technical chapter.
Before you start¶
freqandreplayread a NESO frequency CSV with onetimestamp,hertzrow per line and adtm,fheader. Blank lines are skipped, and so is the first non-blank line when it is not a data row and its first field holds no digit (the header). Every other line must be a timestamp, a comma and a finite frequency above 0 Hz; any other line stops the read with exit 1 and a message naming the file, the line number and the line. Timestamps are read as UTC. The bundled files aredata/neso/frequency-sample.csv(300 s of ambient frequency on 2024-08-01, the default),data/neso/gb-2019-08-09-frequency.csvanddata/neso/gb-2023-12-22-frequency.csv.- Every timestamp is checked against the forward-inference guard: GridSim reads history only, so a row newer than 169 hours before now is refused (see Pitfalls).
- The relative file name
data/neso/...resolves against the directory that holds the executable for the default input, and against the current directory for a path you type. transient,h0-studyandreplay-eventwrite intoresults/under the current directory unless--outnames a directory (see Run directories).- Timings, stamps and run-directory names in the captured output differ by machine and by run.
- The CLI prints a run directory as an absolute path. The captures in this chapter are written by
tools/docs/capture.ps1, which removes the repository root from every path, so a run directory appears relative to the repository (results\transient-uk-...,results\h0-study-...). - A capture is the standard output of the run followed by its standard error. On a terminal the two streams
interleave in the order written, so a
transientrun shows its progress line (standard error) before itsdone inline (standard output); the captured blocks list thedone inline first.
Worked example¶
Score a recorded excursion¶
On 22 December 2023 an interconnector trip and a power-station trip pulled GB frequency down to 49.275 Hz. NESO's
one-second trace for that window is bundled. freq scores it against the GB limits:
$ gridsim freq data/neso/gb-2023-12-22-frequency.csv
NESO frequency - gb-2023-12-22-frequency.csv
================================================================
Samples 601 (1 s cadence)
Frequency min 49.275 Hz mean 49.778 Hz max 49.930 Hz
Max |RoCoF| 0.1020 Hz/s at 2023-12-22 13:09:53
vs GB limits:
RoCoF 0.125 Hz/s: within (worst 0.1020 Hz/s)
operational 49.8 Hz: 299 s below statutory 49.5 Hz: 60 s below LFDD 48.8 Hz: clear
Upsample demo - 1 s frames expanded to 20 ms by RoCoF interpolation:
time Hz RoCoF (Hz/s)
13:07:00.000 49.93000 -0.0080
13:07:00.020 49.92984 -0.0080
13:07:00.040 49.92968 -0.0080
13:07:00.060 49.92952 -0.0080
13:07:00.080 49.92936 -0.0080
13:07:00.100 49.92920 -0.0080
13:07:00.120 49.92904 -0.0080
13:07:00.140 49.92888 -0.0080
[exit 0]
The trace spent 299 samples below the 49.8 Hz operational floor and 60 below the 49.5 Hz statutory floor, never
reached the 48.8 Hz demand-disconnection stage, and its steepest one-second fall was 0.1020 Hz/s, inside the 0.125
Hz/s limit. The minimum, 49.275 Hz, and the 0.1020 Hz/s are the two numbers the event record holds as the
outcome that replay-event and the counterfactual verbs try to reproduce.
Simulate a loss of infeed¶
transient runs a case forward in time at a fixed slice, tripping an infeed part-way through. This run takes the
reduced GB model (uk, 171.7 GVA.s of inertia), runs it for one simulated minute in 100 ms slices, and trips
1,000 MW at 10 s:
$ gridsim transient uk --minutes 1 --dt-ms 100 --trip 1000 --trip-at 10
[transient] uk: done in 1.4s nadir 49.448Hz maxRoCoF -0.147Hz/s exports 42 -> results\transient-uk-20261019-183826
[transient] uk: 600 slices @ 100ms (1 min) on Newton-Raphson (dense)...
[exit 0]
The run lost 1,000 MW of a 39,500 MW system carrying 171.7 GVA.s. The frequency fell at 0.147 Hz/s, reached 49.448 Hz
at t=17 s and was recovering at the end of the minute (49.769 Hz at t=60 s). The first line of the block is
written to standard output when the run ends; the second is the progress line on standard error, which the run writes
first, so a terminal shows the progress line before the done line (see Before you start). All
600 slices converged, so the exit code is 0.
Reading the output¶
freq¶
- Title line.
NESO frequency - <file name>names the file read, without its directory. - Samples. The number of parsed rows. The text
(1 s cadence)is fixed;freqdoes not measure the cadence. - Frequency. The minimum, mean and maximum over every sample, to three decimals.
- Max |RoCoF|. The largest absolute value of
(f2 - f1) / dtover consecutive sample pairs, and the timestamp of the later sample of that pair. A trace with one-second rows gives the one-second average slope, not an instantaneous one. - RoCoF 0.125 Hz/s.
withinorEXCEEDED, with the worst figure repeated. 0.125 Hz/s is the legacy GB loss-of-mains relay setting (FrequencyLimits.RocofLimitLegacyHzPerS). The modern 1.0 Hz/s setting is not printed by this verb. - operational 49.8 Hz, statutory 49.5 Hz.
heldwhen no sample is below the floor, otherwiseN s below, where N is the count of samples strictly below it. N equals seconds only at a one-second cadence. The upper limits are on theover-frequencyline below. - LFDD 48.8 Hz.
clear, orREACHEDwhen any sample is at or below the first low-frequency demand-disconnection stage. - over-frequency. Printed only when some sample is above 50.2 Hz:
operational 50.2 Hz: N s aboveandstatutory 50.5 Hz: heldorN s above, counted as for the low side. A trace that stays at or below 50.2 Hz has no such line. - Upsample demo. The first three samples expanded to 20 ms: 50 sub-samples per second. Each interval's RoCoF is
(f2 - f1) / dtand the sub-samples followf(t) = f1 + RoCoF * t, so the RoCoF column holds one value for each 1 s interval. In the capture the first interval falls at -0.0080 Hz/s, and 49.93000 - 0.0080 * 0.02 gives the 49.92984 of the second row.replayplays these frames.
replay¶
replay prints no statistics headlessly; it plays frames. A five-frame headless run:
$ gridsim replay --frames 5
Loading frequency-sample.csv (600 s) and upsampling to 20 ms...
[ 0/14951] 00:00:00.000 50.1050 Hz RoCoF 0.0010 @50 sps
[ 1/14951] 00:00:00.020 50.1050 Hz RoCoF 0.0010 @50 sps
[ 2/14951] 00:00:00.040 50.1050 Hz RoCoF 0.0010 @50 sps
[ 3/14951] 00:00:00.060 50.1051 Hz RoCoF 0.0010 @50 sps
[ 4/14951] 00:00:00.080 50.1051 Hz RoCoF 0.0010 @50 sps
[exit 0]
- Loading line. The file name and the
--secondsvalue (default 600). It reads--seconds+ 1 samples, or all of them when the file is shorter, so the(600 s)is the limit and not the file length. The sample file holds 300 samples, which is 299 intervals of 50 frames plus one closing frame: 14,951 frames. [index/count]. The zero-based frame index and the total frame count.- Time, Hz, RoCoF. The frame's timestamp, its frequency to four decimals, and the RoCoF of its source interval in Hz/s.
@50 sps. The playback rate in slices per second, after clamping to the range 0.25 to 2,000. Headless mode does not pace itself, so the rate is a label there.
Headless mode stops after the requested number of frames or at the end of the data, whichever is first, and exits 0.
The interactive form (no --frames) is a console display: a title line, the timestamp and slice counter, a gauge from
49.5 Hz to 50.5 Hz with | at 50 Hz and # at the current frequency, the frequency with its deviation from 50 Hz
and the RoCoF, and a progress bar. The gauge is green inside 0.10 Hz of nominal, dark yellow inside 0.20 Hz,
yellow inside 0.50 Hz and red beyond. Keys: space plays or pauses, up and down (or + and -) double or halve
the speed, left and right step one frame and pause, Home restarts, and Q or Esc quits. At the end of the data the
display pauses.
transient¶
Console summary. done in 1.4s is wall-clock time. nadir 49.448Hz is the lowest frequency over the run.
maxRoCoF -0.147Hz/s is the RoCoF of largest magnitude, with its sign (negative is falling). exports 42 is the
number of full solve exports written, and the path is the run directory. The progress line on standard error names
the case, slice count, step, duration and engine, and repeats every 100,000 slices on long runs.
What each slice does. The case is solved by power flow, warm-started from the last converged state, and the frequency integrator then advances one step against the net imbalance. Loads are held constant (no daily demand curve). Three things move the frequency:
- The trip. At
--trip-atthe lost MW enters the integrator as a deficit. The deficit decays with a 300 s time constant of simulated time, the stand-in for reserve and redispatch picking up the loss (SecondaryTauSinsrc/GridSim.Core/Simulation/LiveSim.cs). The inertia of the tripped unit stays in the system energy, and the power flow does not see the trip:gen_mwanddemand_mwdo not change. - Ambient jitter. A small random load imbalance of up to 0.08 % of base demand (
AmbientNoiseFraction) is added each slice so the dial moves in steady state. The generator is seeded with a fixed value (LiveSimSeed), so a rerun gives the same sequence. A run with no trip therefore shows a nadir at the case's nominal frequency (50.000 Hz on a 50 Hz case;case9is a 60 Hz system and runs at 60 Hz). The largest RoCoF is the amplitudeAmbientNoiseFraction * demand * f0 / (2E); the captured runs print -0.001 Hz/s (case9, 10 ms slices) and -0.005 Hz/s (uk, 100 ms slices). - Response. The integrator carries governor droop (5 % droop on 20 % of demand with an 8 s lag) and load damping (1 % of demand per Hz). It carries no Dynamic Containment, Moderation or Regulation. See Response services and How far to trust a nadir.
- Grid-forming and asset dynamics.
--forming(with--forming-inertiaand--forming-damping) runs the integrator's grid-forming kernel, and--asset-dynamicsruns the asset-aware integrator on the case's asset bundle: inverter and protection trips are taken from the running dispatch, and the balance they remove is restored over the same 300 s secondary time constant as the trip.--asset-dynamicson a case with no asset bundle exits 1. The run description inrun-metadata.jsonand the file headers records both switches. - Machine data.
--dyn-data <file>gives the machines the file binds the file's inertiaH; its reactances, exciters and governors are not part of this model, and a[transient]line on standard error says so. A batch run (--cases) refuses it with exit 2.
Inertia. The system energy is the sum of H x MBase over the rotating machines (a converter-type unit adds nothing,
whatever H its data carries). A case that carries no inertia data (every
MATPOWER case) has less than 100 MW.s, and transient then substitutes 4 s on the generators' MVA base
(LiveSolveSettings.FallbackInertiaH, MinInertiaMwS); read the frequency of such a case as a demonstration. The
reduced GB model carries its own data. A step longer than 20 ms is integrated in sub-steps of at most 20 ms (capped at
4,000 sub-steps per slice), so a 100 ms slice is five sub-steps. The RoCoF logged for a slice is the one at its last
sub-step.
timeseries.csv has a # header block (commit, machine, run description) and then one row per logged slice:
| Column | Meaning |
|---|---|
tick |
Slice number, from 1. |
t_seconds |
tick x dt, in seconds. |
freq_hz |
Frequency after the slice. |
rocof_hz_s |
RoCoF at the slice, Hz/s; negative is falling. |
gen_mw |
Sum of the positive bus net injections of the solved state. |
demand_mw |
Total real demand of the solved state. |
iters |
Newton iterations the slice needed. 0 means the warm start already met the tolerance. |
max_mismatch |
Largest power mismatch at the end of the slice, per-unit on the system base. |
vmin_pu, vmax_pu |
Lowest and highest bus voltage magnitude. |
solve_ms |
Wall-clock time of the power-flow solve, in milliseconds. Differs on every run. |
converged |
True or False. A slice that does not converge is never used as the next warm start. |
Values from this run's file show the shape of the event. Before the trip the frequency sits at 50.000 Hz plus jitter
(50.00015 Hz and 0.00146 Hz/s at slice 1). At slice 100 (t=10.0 s, the trip) the RoCoF is -0.14693 Hz/s, which is
the swing-equation figure plus jitter. The deficit decays by exp(-0.1 / 300) before the step, so it is
999.667 MW and the figure is f0 x 999.667 / (2 x 171,700) = 0.14555 Hz/s; the jitter is the remaining -0.00138 Hz/s. By t=11.0 s frequency is 49.846 Hz
and RoCoF -0.130 Hz/s as governor and damping act. At t=15.0 s frequency is 49.490 Hz with the RoCoF down to
-0.038 Hz/s, and the nadir of 49.4475 Hz falls at t=17.0 s, where RoCoF crosses zero. After that the frequency recovers
as the deficit decays and the governor holds the response.
summary.md repeats the headline: slices, wall time, engine, nadir and maximum |RoCoF| to four
decimals, each with its time to three decimals, the count of non-converged slices (and the first one), and the export count.
exports/ holds slice-NNNNNNNNN.json solve exports: slice 1, every slice within --trip-window seconds (default
2.0) of the trip, and every --export-everyth slice. This run wrote 42: slice 1 and the 41 slices from 8.0 s to
12.0 s. The format is in exports and formats.
A quiet run, with no trip, shows the ambient floor:
$ gridsim transient case9 --minutes 0.1 --dt-ms 10
[transient] case9: done in 0.2s nadir 60.000Hz maxRoCoF -0.001Hz/s exports 1 -> results\transient-case9-20261019-183822
[transient] case9: 600 slices @ 10ms (0.1 min) on Newton-Raphson (dense)...
[exit 0]
domains¶
domains groups the case's generators into speed domains and ranks them by stiffness. It loads the positional case or
the folder named by --dir, and otherwise data/uk, the reduced GB model.
$ gridsim domains
GridSim - SPEED DOMAINS on GB reduced (10-zone)
==================================================================
3 speed domains * deltaGVa 56 % * dominant = Domain 1
Domain Speed Kind Mach GVA.s GVad GVA
Domain 1 2p/3000rpm Synchronous 12 167.80 24.34 *
Domain 0 inverter Pll 16 0.00 15.61
Domain 2 8p/750rpm Synchronous 1 3.90 0.65
GVad = apparent-power stiffness sum(S*C); GVA.s = real spinning inertia. A domain is dominant
when deltaGVa >= 10% (LOS sec. 5.5). Distributed model: spatial spread & propagation are unvalidated at scale.
[exit 0]
- Header. The model name, then the count of speed domains,
deltaGVaand the dominant domain. - Grouping. Each generator is mapped from its type to a machine speed: nuclear, coal, biomass, CCGT, OCGT, CHP and gas
are 2-pole turbo-alternators at 3,000 rpm; reciprocating engines are 4-pole at 1,500 rpm; hydro and pumped storage
are 8-pole at 750 rpm. Storage, battery and BESS units form a synthetic-inertia domain. Every other
inverter-based type (wind, solar, converter, interconnector, tidal, unknown) falls into a PLL domain. A machine of
unknown type that carries inertia (
Habove zero) is placed in the 2-pole synchronous domain. Domains are numbered by pole count and kind, so inverter domains take the lowest numbers. - Domain, Speed, Kind. The domain id, its speed (
2p/3000rpm, orinverter), andSynchronous,SyntheticInertiaorPll. - Mach. The count of generators in the domain.
- GVA.s. Real stored kinetic energy, the sum of
H x MBaseover the members, in GVA.s. Inverter domains show 0.00. - GVad GVA. The apparent-power stiffness: the sum of
MBase x Cover the members, where the availability coefficientCis|Pg| / MBaseclamped to 0 to 1. Rows are ordered by this column, largest first, and*marks the dominant domain. - deltaGVa. The gap between the largest and second-largest GVad as a fraction of the second:
(top - second) / second. The top domain is dominant when this is at least 10 %. When the runner-up carries no dispatch, the verb printssole domainand the top domain dominates. When no domain dominates it printsno dominant domain (inertia too evenly spread - the LOS fragmentation mode).
The 56 % in the capture means the 2-pole synchronous domain is 56 % stiffer than the next (the PLL converters at
15.61 GVA). --dir selects a JSON case directory; the synthetic GB model gives a different split:
$ gridsim domains --dir data/gb-synthetic
GridSim - SPEED DOMAINS on GB synthetic
==================================================================
4 speed domains * deltaGVa 52 % * dominant = Domain 2
Domain Speed Kind Mach GVA.s GVad GVA
Domain 2 2p/3000rpm Synchronous 23 167.80 23.92 *
...
The full output of that run is in docs/_captures/frequency/domains-synthetic.txt; its second row is Domain 1
inverter Pll, with 35 machines, 0.00 GVA.s and 15.76 GVad GVA, and its fourth is Domain 0 inverter
SyntheticInertia with 15 machines and 0.05 GVad GVA.
h0-study¶
The H=0 study asks how a recorded loss would have played out if the real synchronous inertia were scaled toward zero
while an increasing share of the converter fleet were grid-forming. It first re-simulates the recorded event with no
device and refuses to continue unless that baseline reproduces the record (the gate described in
replaying recorded events). It needs a case directory that ships an
asset-dynamics bundle (asset-classes.json and asset-bindings.json); the default is data/gb-synthetic.
$ gridsim h0-study --event gb-largest-loss-1320
=== H=0 counterfactual: GB normal infeed loss risk screening (SQSS): single 1320 MW infeed trip [VERIFY] ===
input provenance: [CuratedPlaceholder] (event 'gb-largest-loss-1320' is effectively CuratedPlaceholder, the weakest of its parts): a design-basis study, not a reproduction of history; every figure below inherits it and none is Recorded.
baseline gate: Reproduced dP=1320 MW (first 5 s; 1320 MW over the whole event) LFDD stage: 48.8 Hz
GFM class: gfm-vsm-study-2024 GFL class: gfl-wind-2024 (Ev is virtual control inertia, NEVER in real E)
GFM fraction realised by whole units: 0.25 -> 0.392, 0.50 -> 0.588, 0.75 -> 0.833
corner Best: cell = nadir Hz / verdict (HELD=formed & clear of LFDD, LFDD=below stage, CASC=Loss-of-Mains cascade, NaN=un-formable)
scale\gfm | 0.00 0.25 0.50 0.75 1.00
-------------------------------------------------------------------
1.00 | 49.76 HELD 49.67 HELD 49.56 HELD 49.41 HELD 49.51 HELD
0.50 | 49.58 HELD 49.48 HELD 49.40 HELD 49.25 HELD 49.47 HELD
0.25 | 49.31 HELD 49.21 HELD 49.16 HELD 49.06 HELD 49.44 HELD
0.00 | - NaN 41.09 CASC 44.66 CASC 48.70 CASC 49.40 HELD
corner Worst: cell = nadir Hz / verdict (HELD=formed & clear of LFDD, LFDD=below stage, CASC=Loss-of-Mains cascade, NaN=un-formable)
scale\gfm | 0.00 0.25 0.50 0.75 1.00
-------------------------------------------------------------------
1.00 | 49.74 HELD 49.40 HELD 49.09 HELD 48.80 CASC 49.21 HELD
0.50 | 49.51 HELD 49.19 HELD 48.84 HELD 48.79 CASC 49.17 HELD
0.25 | 49.13 HELD 48.85 HELD 48.80 CASC 48.70 CASC 49.14 HELD
0.00 | - NaN 28.42 CASC 37.11 CASC 44.52 CASC 49.10 HELD
28/40 cells HELD (formed, clear of LFDD); 2 forming-dependent (H=0, no grid-forming source).
Note: forming-dependent cells are expected DATA, not an error - the run still exits 0.
-> results\h0-study-gb-largest-loss-1320
[exit 0]
- Title and gate line. The event title and
baseline gate: Reproduced, the size of the exogenous loss in MW (dP) and the LFDD stage (48.8 Hz). The study runs only when the gate saysReproduced. - Class line. The asset classes used for the grid-forming (
--gfm-class) and grid-following (--gfl-class) converters. The virtual inertiaEvof the grid-forming units enters only the forming denominator; it is never added to the real inertia. - One table per corner.
BestandWorstare the two bracketing sets of the unfitted constants (see How far to trust a nadir). Rows are the synchronous-inertia scale, from 1.00 (the case as it is) to 0.00 (no real inertia). Columns are the fraction of the converter fleet made grid-forming, from 0.00 to 1.00. - A cell is
nadir Hzthen a verdict. HELD: the frequency was formed and its nadir stayed above the 48.8 Hz LFDD stage. It does not mean the nadir stayed above the 49.5 Hz statutory floor: 49.25 Hz readsHELD.CASC: grid-following converters tripped on Loss-of-Mains protection and the nadir reached 48.8 Hz or lower. The cascade removes infeed as the frequency falls; a nadir far below 48.8 Hz (the 28.42 Hz cell) ranks severity and is not a forecast, because the model sheds no load and trips no further plant to arrest it.LFDD: the nadir reached the LFDD stage with no converter trips.NaN, with-in place of a nadir: zero real inertia and no grid-forming source, so nothing forms the frequency. RoCoF, nadir and settling are undefined, and these cells count as a collapse of every grid-following unit. In the capture they are the scale 0.00, fraction 0.00 cells of both corners.- Summary line. The number of cells that were
HELD, out of all cells (here 2 corners x 4 scales x 5 fractions = 40), and the count of forming-dependent cells. Those cells are data, so the run exits 0. - Path line. The run directory.
The run directory holds h0-study.json (every cell with its separate real and virtual inertia, RoCoF, nadir,
settling frequency, trip counts and assessment), matrix.csv (one row per cell), summary.md and run-metadata.json.
Variations¶
Replay a trace interactively¶
This loads the first 121 samples of the 2023 trace and plays them at twice real time. Press Q to quit. Never start
the interactive form from a script: it waits for keys. Use --frames N there.
Batch and sweep runs¶
transient runs several cases in one command. --cases suite (or all) runs case9, case14, case30, case57,
case118, case300 and pegase/case1354pegase.m; --cases a,b,c takes a list. Each case gets a subdirectory of
results/transient-<stamp>/, named after the case with path separators replaced by _.
--log-every N thins the time series (the first slice, the last, and every slice within the trip window are always
logged). --export-every N adds full solve exports. --solver dense|sparse|bfs forces a power-flow engine. A run
directory written by transient loads in the desktop app through LOAD RUN and plays back frame by frame.
Try different sweeps in h0-study¶
gridsim h0-study --event gb-largest-loss-1320 --sync-inertia-scale 1,0.5,0 --gfm-fraction 0,1 --corners best
--sync-inertia-scale and --gfm-fraction take comma lists and --corners takes best, nominal, worst. The
verbs scenario, fleet and evidence use the same gate and the same event file; see
counterfactuals.
Fit the unfitted constants to a measured trace¶
fit-frequency fits load damping, the responsive fraction and the governor time constant of the lumped swing model to
one measured loss-of-infeed trace, by least squares on the trajectory for the given loss, inertia and demand. The form
is gridsim fit-frequency <trace.csv> --inertia <GVA.s> --demand <MW> --loss <MW> [--loss-at <s>] [--fit damping,...].
No trace in this form ships with GridSim, so bring your own measured window. The trace is a two-column seconds,Hz CSV on the model's clock. The report gives each constant's seed (the shipped
default), its fitted value, the RMS error and the nadir error. It is a calibration check: the shipped defaults do not
change. The nominal frequency comes from the trace (50 or 60 Hz) or --nominal-hz. Damping alone is well identified
from one trace; fitting all three is weakly identified and a Note: line says so. Every flag and exit code is in
ch. 19, fit-frequency.
Bracket a trip's nadir¶
trip --band adds the nadir band over the same three unfitted constants, each moved by +/- 20 % one at a time and over
every corner of the joint box; trip --gb-services adds the GB Dynamic Containment, Moderation and Regulation layer at
the typical volumes; and trip --asset-dynamics --envelope <class>:<param>=<low>..<high> sweeps asset-class control
parameters around the trip:
gridsim trip --band
gridsim trip --dir data/gb-synthetic --asset-dynamics --envelope dg-alomcp:rocofTripHzPerS=0.5..2
See ch. 19, trip and contingency analysis.
The live frequency view in the desktop app¶
The desktop app integrates frequency on every tick of the live sim. To run an event by hand, load a case, pick a fine
step in the transport bar (1 ms to 20 min), and right-click a bus on the network map, or press T to trip the
largest online generator. The lost MW enters the same integrator that transient uses, with the same 300 s decay and
the same ambient jitter, at the case's own nominal frequency. The OPERATOR tab shows the frequency and RoCoF, the NESO
RP1 tab shows the RoCoF (on a 50 Hz case only), the MECHANICAL tab shows the RoCoF margin against the 0.125 and
1.0 Hz/s relay settings, and SCADA raises its P2 frequency alarm when the excursion leaves the band 0.1 Hz either side
of nominal (49.9 Hz to 50.1 Hz on a 50 Hz case). The event log records TRIP <unit> (<MW> MW) RoCoF <x> Hz/s nadir <y> Hz; those two
figures are an analytical estimate, not the live reading: a batch FrequencyDynamics.Simulate of the same loss at
t = 1 s, on the inertia with the tripped unit removed and a default DynamicsConfig, so they differ from the live
trace. The GENERATION & INERTIA tab runs an analytical comparison of the same trip with
the configured TSGB device; that analysis removes the tripped unit's inertia from the system, where the live integrator
leaves it in. In replay mode and on a bridge stream there is no live integrator, and a right-click logs TRIP is not available on
this feed (replay or bridge stream). When the case ships an
asset-dynamics bundle, the live integrator runs the asset-aware variant: asset protection trips are taken from the
running dispatch, and the imbalance they leave is restored over the same 300 s secondary time constant as a manual
trip; the tripped units stay tripped. Details are in the desktop app.
Inertia models¶
Frequency studies run on one of two inertia models, chosen with the global switch --inertia-model lumped|distributed
(see ch. 19, CLI reference).
Lumped (the default). The whole system is one mass with energy E = sum(H x MBase), one frequency, and
RoCoF = f0 x deltaP / (2E) at the instant of the loss. Every verb in this chapter that integrates frequency does this
unless told otherwise.
Distributed (experimental). Generators are grouped into speed domains (the grouping domains prints). Each
synchronous domain is a coherent mass with its own frequency and rotor angle, coupled to the others by
synchronising power derived from the network. The system-level forcing (the loss, governor, damping) is computed from
the inertia-weighted centre-of-inertia frequency exactly as in the lumped model, so the centre-of-inertia frequency
equals the lumped trajectory to rounding. What the distributed model adds is per-domain traces and a per-bus
RoCoF map. With one synchronous domain it delegates to the lumped model, so the reduction is exact.
What each verb does with the switch:
| Verb | With --inertia-model distributed |
|---|---|
trip |
Integrates the multi-domain model on the post-trip network and prints the centre-of-inertia result, preceded by an Inertia model: line. The half-inertia comparison line runs lumped. |
transient |
Prints the speed-domain split on standard error and integrates lumped. |
domains |
Unaffected: it always reports the domain split. |
replay-event, scenario, fleet, evidence, h0-study |
Print a warning on standard error and run lumped. |
freq, replay |
Not applicable; they read recorded data. |
| the desktop app | Lumped. |
The same trip on the reduced GB model under each model gives the same centre-of-inertia numbers. Under the distributed
model the verb adds an Inertia model: line naming the swing masses at the trip and a Loss placed: line stating where
the loss lands: on the speed domain whose machines remain at the tripped unit's bus, or, when the unit was the only
machine there, shared among the domains by their synchronising coupling to that bus. When the coupling cannot be derived
from the network a Coupling: line says so and why:
$ gridsim trip --inertia-model distributed
Inertia model: distributed (2 speed domain(s), dGVa 0.37)
Loss placed: bus 10 hosts the machines of speed domain 1, which takes the whole step
GridSim - contingency on GB reduced (10-zone)
================================================================
Tripped: Hinkley Point Nuclear (2880 MW lost, nuclear)
System inertia: 171.7 -> 158.9 GVAs after the trip
Peak RoCoF: -0.453 Hz/s (largest |df/dt| over the run)
Nadir: 48.340 Hz (t+6.9 s)
Settling: 49.144 Hz
...
The full output for both models is in docs/_captures/frequency/trip-lumped.txt and trip-distributed.txt; the lines
from Peak RoCoF to the half-inertia line are the same in both. dGVa 0.37 is the dominance ratio of the post-trip
network (37 %); the 56 % that domains prints is for the network before the trip, which still has Hinkley Point in its
dominant domain. On trip, Settling is the frequency at the end of the 30 s window, not a separately detected
steady state.
The 2 in that line is the number of swing masses (synchronous domains that carry inertia) at the trip. domains
counts every speed domain, including the Pll domain, so it prints 3, and so does the transient line below.
transient under the distributed switch reports the split and does not use it:
$ gridsim transient uk --minutes 0.1 --dt-ms 100 --inertia-model distributed
[transient] uk: done in 0.3s nadir 50.000Hz maxRoCoF -0.005Hz/s exports 1 -> results\transient-uk-20261019-183828
[transient] uk: 60 slices @ 100ms (0.1 min) on Newton-Raphson (dense)...
[transient] distributed inertia: 3 speed domains, deltaGVa 56 % (per-tick integration remains the lumped live model; a distributed live integrator is a follow-up)
[exit 0]
The other verbs print this warning, taken from an h0-study run:
warning: --inertia-model distributed is not honored by 'h0-study' - it runs the lumped scalar model. Only 'trip' integrates the distributed multi-domain model (and 'transient' reports the speed-domain split but still integrates lumped).
Spatial spread between domains and propagation delay are unvalidated at scale. Propagation is off by default
(DistributedConfig.EnablePropagation).
Response services¶
After a loss, three layers arrest the fall.
- Stored energy sets the initial slope (the RoCoF) and does nothing to arrest it.
- Load damping (demand falls as frequency falls, 1 % of demand per Hz) and governor droop (5 % droop on a responsive share of demand, 20 %, with an 8 s lag) are the generic model of the conventional system's response.
- Procured frequency response: Dynamic Containment (DC), Dynamic Moderation (DM) and Dynamic Regulation (DR). Each is a block of MW with a published delivery curve and a response lag.
A service delivers nothing inside its deadband around 50 Hz. Past the deadband the delivery rises along the published curve until the full-delivery deviation, then stays at its MW cap. A knee is a point where the slope changes: below it the service delivers only a small fraction of its volume, past it the slope steepens. The delivered power then follows its target through a first-order lag, with the service's time constant. Low frequency makes a service inject; high frequency makes it absorb.
| Service | Deadband | Knee | Full delivery | Shape |
|---|---|---|---|---|
| DC | 0.015 Hz | 0.2 Hz, at 5 % of volume | 0.5 Hz | Low-gain segment to the knee, steep segment to full. |
| DM | 0.1 Hz | at the deadband, 0 % | 0.2 Hz | Ramp from the deadband to full. |
| DR | 0.015 Hz | at the deadband, 0 % | 0.2 Hz | Ramp from the deadband to full. |
For the 22 December 2023 event the event record holds the volumes the system had cleared in the EAC auction for that
delivery block: DC 873 MW (lag 0.3 s), DM 150 MW (0.3 s) and DR 203 MW (4 s), 1,226 MW in all. At a deviation of 0.3 Hz,
DC's target is 873 x (0.05 + 0.95 x (0.3 - 0.2) / (0.5 - 0.2)) = 320.1 MW. An event that lists its services as
complete (responseServicesComplete, true for events from the start of EAC on 2 November 2023) switches the generic
governor off, so the response is not counted twice; load damping always stays. The event also records the frequency
the trip landed on (49.879 Hz for 22 December 2023), and the simulation starts in equilibrium there, with services
partly deployed.
Where the services apply:
- Applied:
replay-event,scenario,fleet,evidenceandh0-study, from the event record, in the baseline and in every counterfactual (a device is additional to what the system held). - Not applied:
transientand the live view in the apps use the generic governor and damping only, and so doestripwithout--gb-services. - The library also holds a typical GB fleet at DC 950 MW, DM 520 MW and DR 490 MW (
GbResponseServices.Typical), with lags of 0.5 s, 2 s and 10 s.trip --gb-services[=<scale>]adds it, times the scale, on top of the generic governor.
Services are changed through the event file: gridsim replay-event <id> --dump-catalog <file> writes the built-in
events as JSON, the responseServices array holds mw, deadbandHz, fullDeliveryDeviationHz, timeConstantS,
kneeDeviationHz and kneeFraction for each, and --catalog <file> loads an edited copy (see
replaying recorded events).
How far to trust a nadir¶
The RoCoF is set by the inertia and the size of the loss. For a step loss from nominal the swing equation fixes it
exactly. The ambient NESO sample is a consistency check on the scale only: its 0.0080 Hz/s implies a 64 MW imbalance
at 200 GVA.s, and the calibration test round-trips that figure through the same equation. The nadir and the recovery also depend on three constants that no
measured loss-of-infeed trace has yet fitted: load damping (1 % of demand per Hz), the responsive fraction (0.20 of
demand) and the governor time constant (8 s). They are literature-typical GB values, and the code documents their
uncertainty as a factor of two. The governor droop (5 %) is the GB Grid Code figure and is firm. The delivery curves
of DC, DM and DR are the published service shapes; their time constants are literature-typical. fit-frequency fits
the three constants to a measured trace and trip --band shows how far each moves a nadir (see
Variations); neither changes the shipped values.
Read a nadir as an indicative figure bracketed by the Best and Worst corners, never as a validated prediction. The
corners scale the three constants (Best: damping x1.5, responsive fraction x1.5, governor lag x0.7; Worst:
damping x0.6, responsive fraction x0.6, governor lag x1.4). On the 22 December 2023 event the corners give a nadir
envelope of 48.65 Hz to 49.20 Hz against a recorded 49.275 Hz, and replay-event reports the record as outside the
envelope.
The tag [VERIFY] on an event figure means a curated placeholder awaiting a source. The event
gb-largest-loss-1320, the SQSS normal infeed loss risk of 1,320 MW, is such a screening case. Its recorded outcome is a
placeholder, so Reproduced on its gate says only that the model agrees with the placeholder. replay-event refuses
it, because a replay presents its output as a reproduction of history; h0-study, scenario, fleet and evidence
study it, print an input provenance: [CuratedPlaceholder] line and tag every figure they write with it.
Run directories¶
| Verb | Default location | Contents |
|---|---|---|
transient |
results/transient-<case>-<stamp>/ in the current directory; a batch uses results/transient-<stamp>/<case>/ |
timeseries.csv, summary.md, run-metadata.json, exports/slice-*.json |
h0-study |
results/h0-study-<event>/ in the current directory |
h0-study.json, matrix.csv, summary.md, run-metadata.json |
replay-event |
results/replay-<event>/ in the current directory |
baseline-frequency.json, summary.md, run-metadata.json |
freq, replay, domains |
none | They print only. |
--out <dir> names the directory for transient and h0-study. run-metadata.json holds the commit, backend,
machine, runtime and the run description, so a result traces to the build that produced it. Run directories under a
results/ folder are discovered by the desktop app and can be stepped frame by frame
(exports and formats).
Exit codes¶
The general table is in ch. 19, CLI reference. For this chapter's verbs:
| Verb | Code | Meaning |
|---|---|---|
freq |
0 | Printed. |
| 1 | The CSV does not exist. | |
replay |
0 | Played, or printed --frames frames. |
| 1 | The CSV does not exist, or it holds no data. | |
transient |
0 | Every slice converged. |
| 1 | No case loaded, or some cases of a batch failed to load (the rest still ran). | |
| 2 | --dt-ms or --minutes is zero or negative, the run is shorter than one slice, --trip-at falls past the end of the run, --solver is not one of auto, dense, sparse, bfs, sweep, or a numeric flag is malformed. |
|
| 3 | One or more slices did not converge. The run completes and summary.md records the count. |
|
domains |
0 | Printed. |
| 2 | --dir has no value. |
|
h0-study |
0 | The study ran, including cells that read NaN. |
| 1 | The event id is not a built-in event or in the --catalog file, the case fails to load, the case ships no asset-dynamics bundle, or the engine fails. |
|
| 2 | No --event (usage is printed), a malformed list or --corners value, or an event whose timestamp is newer than 169 hours before now. |
|
| 3 | The no-device baseline did not reproduce the record. |
Why h0-study exits 3 on gb-2023-12-22¶
$ gridsim h0-study --event gb-2023-12-22 --sync-inertia-scale 1,0 --gfm-fraction 0,1
GATE FAILED: Refusing to run counterfactuals: the no-device baseline is 'Marginal', not 'Reproduced'. Reproduce reality first. Marginal: RoCoF sim -0.123 vs rec -0.102 Hz/s (20 % err); nadir sim 48.98 vs rec 49.27 Hz (0.30 Hz err); RMSE n/a (no trace)
Fix the event's [VERIFY] figures (or tolerances) until 'replay-event' reports Reproduced, then re-run.
[exit 3]
h0-study first calls the replay validator (ReplayValidator.Validate) with its default tolerances. The validator
re-simulates the recorded event with no device and scores two figures. The RoCoF must agree within 15 % of the
recorded value, and the nadir within 0.15 Hz. The verdict is Reproduced when both are inside those bounds,
Marginal when both are inside twice those bounds (30 % and 0.30 Hz) but not both inside the first, and Failed
otherwise. A per-sample RMSE against a trace above 0.05 Hz also demotes a Reproduced result to Marginal; h0-study
attaches no trace, which the message states as RMSE n/a (no trace).
Here the simulated RoCoF is -0.123 Hz/s against a recorded -0.102 Hz/s, an error of 20 %: outside 15 %, inside 30 %.
The simulated nadir is 48.98 Hz against 49.275 Hz recorded, an error of 0.295 Hz: outside 0.15 Hz, inside 0.30 Hz. Neither
figure is inside the first bound, both are inside the second, so the verdict is Marginal. Only Reproduced unlocks a
counterfactual, so the engine throws BaselineNotReproducedException, the CLI prints a GATE FAILED line and a one-line hint, and
the exit code is 3. This is the only condition that gives h0-study exit 3. replay-event on the same event, with the
real 1 s trace attached, reports the same verdict and a trace RMSE of 0.281 Hz. Its full output is in
docs/_captures/frequency/replay-event-gb-2023-12-22.txt. The gate message points at the cause: this event is blocked
by the model's own figures and tolerances, not by anything in the study.
The h0-study with no arguments prints its usage and exits 2:
$ gridsim h0-study
usage: gridsim h0-study --event <id> [--catalog f] [--dir case]
[--sync-inertia-scale 1,0.5,0.25,0] [--gfm-fraction 0,0.25,0.5,0.75,1]
[--gfm-class gfm-vsm-study-2024] [--gfl-class gfl-wind-2024] [--corners best,worst]
[--trace <neso.csv>] [--out <dir>]
Built-in events: gb-2019-08-09, gb-2023-12-22, gb-largest-loss-1320
Scales REAL synchronous inertia toward 0 x makes the converter fleet grid-forming. REQUIRES a reproduced baseline.
[exit 2]
Pitfalls¶
domainstakes a case or--dir, not both.gridsim domains case9reportsSPEED DOMAINS on IEEE 9-bus (WSCC); with no case it loadsdata/uk. A positional case and a--dirfolder that name different networks exit 2, and a--dirfolder that does not exist exits 1 (ch. 19, Case arguments).transientwith no flags is a long run. The defaults are 60 minutes at 1 ms, which is 3.6 million power-flow solves. Theukrun in this chapter solved 600 slices in 1.4 s, about 429 slices per second, so 3.6 million slices take3,600,000 x 1.4 / 600 = 8,400 s(2.3 hours) at that rate. Set--minutesand--dt-msfor the question you are asking.--trip-atneeds--trip. Without--tripMW above zero no trip is scheduled, so--trip-ator--trip-windowwithout it is refused (exit 2) rather than ignored. A--trip-atof 0 trips on the first slice. A--trip-atpast the end of the run is refused:
$ gridsim transient case9 --minutes 0.1 --dt-ms 100 --trip 100 --trip-at 30
[transient] case9: --trip-at 30s is past the run end (0.1 min = 60 slices @ 100ms) - the contingency never fires. Aborting.
[exit 2]
- A trip changes the frequency and nothing else. In
transientand the live view the trip enters the swing integrator only. The power flow keeps the unit, sogen_mw, voltages and flows do not respond. Usetripfor the contingency that removes the unit and its inertia. - A
CASCnadir is not a Hz forecast. A cell far below 48.8 Hz tells you a Loss-of-Mains cascade ran away in the model. Compare the verdict, not the figure. HELDis relative to LFDD. It means the nadir stayed above 48.8 Hz.- A recent CSV is refused. A row dated within 169 hours of now stops
freqandreplaywithREJECTED (forward inference): ...and exit 2, as on every event verb. Use data that is at least a week old. - A damaged or empty CSV is refused. A line after the header that is not a data row exits 1 naming the line.
freqon a file with no data rows exits 1 witherror: frequency file '<file>' holds no data rows ...;replayprintsNo data.and exits 1. - The interactive
replaynever ends by itself. It waits for Q or Esc. Always pass--frames Nfrom a script. - Run-directory names and timings change on every run. The stamp in a
transientdirectory name,done in ...andsolve_msdiffer by machine and by run. The frequencies, RoCoF and nadir repeat on the same machine and build with the same case and flags, because the ambient jitter is seeded. No test compares machines.
Limits¶
transientintegrates the lumped model under--inertia-model distributed; it prints the speed-domain split only.- The live view and
transientcarry no response services (DC, DM, DR); the event-driven verbs apply the event's recorded services, and onlytrip --gb-servicesapplies the typical GB fleet. - The shipped damping, responsive-fraction and governor-lag constants are not fitted to a measured loss-of-infeed trace (
fit-frequencyfits them to one trace but does not change them), so a nadir is indicative within the Best and Worst corners. - Spatial spread and propagation between speed domains are unvalidated at scale, and the distributed model has no grid-forming path (a forming run falls back to the lumped forming integrator).
- A
transienttrip is a deficit in the swing integrator; the power-flow state does not respond to it. h0-studyruns only on events whose no-device baseline isReproduced;gb-2023-12-22isMarginaland cannot be studied.
See also¶
- The frequency dynamics technical chapter - the swing equation,
DynamicsConfig, the distributed model and the response-service kernel - ch. 19, CLI reference -
freq - ch. 19, CLI reference -
replay - ch. 19, CLI reference - every
transientflag - ch. 19, CLI reference -
domains - ch. 19, CLI reference -
fit-frequency - ch. 19, CLI reference - every
h0-studyflag - ch. 19, CLI reference -
--inertia-model,--forming,--asset-dynamics - ch. 19, CLI reference - the exit-code table
- 18. Replaying recorded events - the credibility gate and the event file
- 20. Counterfactuals -
scenario,fleet,evidenceand the H=0 study in context - 16. Electromagnetic transients - waveform-level studies
- 11. Contingency analysis -
trip,screenand the right-click workflows - 8. The desktop app - the live frequency view, transport bar, LOAD RUN
- Exports and formats - run-directory and solve-export layouts