13. Asset-dynamics data grounding¶
This chapter is the provenance ledger for the authored asset classes: for each class kind and each parameter in the
bundled asset-classes.json files, it says whether the value is grounded in a recorded source (Recorded or
DerivedFromRecorded) or is a literature-approximated placeholder (CuratedPlaceholder carrying the literal token
[VERIFY]). It states the sources with their exact values (EREC G99, EREC G59, the Grid Code, NESO services, the GDA
lake), how the loader validates the classes and refuses malformed ones (AssetClass.ValidateProvenance,
AssetClassSupport.Problems), and how each parameter reaches a controller in src/GridSim.Core/Assets. It ends with
a worked example that follows one parameter from the file to a captured trip run.
The bundled files are data/gb-full/asset-classes.json (7 classes, generated by GDA), and
data/gb-synthetic/asset-classes.json (8 classes, hand-authored, with data/gb-synthetic/protections.json placing the
two protection classes). No other bundled case carries asset classes. The class schema is gridsim-asset-classes/1
(src/GridSim.Core/IO/Json/AssetClassDto.cs). The dynamics the classes drive are specified in
ch. 5, Frequency dynamics; the protection stages in
ch. 15, Protections; the cascade validation in
ch. 14, Cascade validation and blind prediction.
Model¶
Provenance tags¶
Every parameter of an authored class has one provenance tag (ProvenanceTag, src/GridSim.Core/Diagnostics/Provenance.cs).
Three tags occur in the bundled asset classes, and the loader accepts any ProvenanceTag name. A fourth tag,
UnvalidatedAtScale, tags fleet-level results:
| Tag | Meaning in an asset class | Note must contain [VERIFY] |
|---|---|---|
Recorded |
A published setting or recorded fact, cited by document, table and page, or by GDA source file | no |
DerivedFromRecorded |
Computed from recorded data (a ratio, a median, a coincident block) | no |
CuratedPlaceholder |
Stands in for data that is not available; vendor-proprietary or absent from every source | yes |
UnvalidatedAtScale |
A fleet-level result whose absolute value at GB scale has no reference | not applicable |
Synthetic, Counterfactual and Interpolated are valid tags for evidence series
(ch. 7) but are not used by asset classes. UnvalidatedAtScale tags a result, not a
parameter: the 2019-08-09 cascade reproduction is tagged UnvalidatedAtScale because its depth depends on the
embedded-generation volume that tripped, which no source records.
A class lists its provenance as a block of parameter -> { tag, note } entries. AssetClass.ProvenanceFor(key)
resolves a parameter in this order:
- the entry named for the parameter;
- the entry named
_default; - a synthetic
CuratedPlaceholderwith the note[VERIFY] unspecified provenance.
A parameter with no entry of its own therefore inherits the class _default, which is a CuratedPlaceholder in every
bundled class that has one. The two protection classes in gb-synthetic have no _default: a parameter they do not
list (rocofTripConfirmS of prot-lom-legacy, lfddDwellS of prot-lfdd-stage1) resolves to the synthetic
placeholder.
Files and binding¶
| File | Content | Loader |
|---|---|---|
asset-classes.json |
The class library: name, kind, mode, params (flat name to number), provenance |
JsonGridLoader.BuildLibrary |
asset-bindings.json |
Rules matchType -> class: bind every generator, transformer or line whose type matches |
JsonGridLoader.BuildAssetDynamics |
protections.json |
Placed protection instances (name, class, monitorBus, capacityMw, optional targetBranch) |
JsonGridLoader.BuildProtections |
AssetBindingSet.Resolve binds in this order: the asset's own dynamicsClass; the first exact matchType
(case-insensitive); the first prefix wildcard (wind* matches wind-onshore and wind-offshore). An asset that
matches nothing stays passive. The bundled rules:
| Dataset | Rules |
|---|---|
gb-full |
wind* and tidal -> gfl-wind-2024; solar -> gfl-solar-2024; storage -> gfm-bess-2024; interconnector -> hvdc-2024; transformer -> tx-oltc-2024; line -> cable-thermal-2024 |
gb-synthetic |
onshore, offshore and wind -> gfl-wind-2024; solar -> gfl-solar-2024; storage -> gfm-bess-2024 |
Synchronous plant (in gb-full: ccgt, ocgt, gas, chp, biomass, hydro, nuclear, recip, pumped-storage, grid, other)
matches no rule and stays passive: it carries no inverter loop. In gb-synthetic the ten interconnector units also
match no rule and stay passive. legacy-dg-old-lom, dg-alomcp and gfm-vsm-study-2024 have no
bulk rule; a generator reaches them through its own dynamicsClass, and gfm-vsm-study-2024 is the class the h0-study
verb substitutes.
Class kinds¶
AssetKind (src/GridSim.Core/Assets/AssetKind.cs) selects the controller:
| Kind (JSON spellings) | Controller | Timescale |
|---|---|---|
gridFollowingInverter (grid-following, gfl) |
GridFollowingInverter |
swing substep, active |
gridFormingInverter (grid-forming, gfm) |
GridFormingInverter |
swing substep, active |
protection (relay, protective) |
ProtectionController |
swing substep, active |
transformer |
TransformerDynamics |
steady-state tick, passive |
line (cable) |
LineThermalDynamics |
steady-state tick, passive |
synchronousMachine (machine) |
none | refused at load |
What the loader validates¶
JsonGridLoader.LoadDirectory builds the class library with the checks below. Every check fails loud with
InvalidDataException. Steps 1 and 2 run class by class while the library is built (for each class its provenance
tags are parsed first, then its kind), and a duplicate name (step 3) throws when it is reached; the first such error
stops the load. Then the problems collected in step 3 are thrown together, then the provenance check of step 4.
- Each class kind must be one of the spellings above (
ParseAssetKind): otherwiseasset class '<name>' has unknown kind '<s>'. - Each provenance tag must parse as a
ProvenanceTagname, ignoring case (ParseProvenanceTag): otherwiseasset class '<name>' parameter '<key>' has unknown provenance tag '<s>'. AssetClassLibraryconstruction refuses a duplicate class name (case-insensitive) and collects every problem fromAssetClassSupport.Problemsinto one exception headedUnsupported asset class input: a class of an unsupported kind (synchronousMachine), a refused key, a key no consumer of the kind reads.AssetClassLibrary.ValidateOrThrowrunsAssetClass.ValidateProvenanceon every class and lists every violation in one exception headedInvalid asset-classes.json.ValidateProvenancereports each provenance entry (explicit or_default) that isCuratedPlaceholderand whose note lacks the literal[VERIFY].asset-bindings.jsonis refused when it is present andasset-classes.jsondefines no classes, and when a rule names a class that does not exist.protections.jsonis refused whenasset-classes.jsondefines no classes, when an instance names an unknown class, and when the named class is not of kindprotection.
A class built in code and passed to new AssetClassLibrary(...) meets steps 3 only; ValidateOrThrow is a separate
call. The loader makes both calls.
ValidateProvenance checks the entries a class lists. It cannot require an entry for a parameter, so the rule that a
Recorded or DerivedFromRecorded tag is true is enforced by review, not by code. Two tests pin the citation text
of three values: the legacy vector-shift setting, and the dg-alomcp RoCoF threshold and delay. The legacy
0.125 Hz/s and the 48.8 Hz LFDD citations are pinned by no test.
Accepted and refused keys¶
AssetClassSupport.AcceptedKeys(kind) is the set of params keys a class of that kind may carry. Each key in it is
read by at least one consumer of the kind. An unknown key is refused with the list of accepted keys in the message; a refused key (below) is refused with its
reason and no list.
| Kind | Accepted keys |
|---|---|
gridFollowingInverter |
deadbandHz, droopGainMwPerHzPerMw, rocofGainMwsPerHz, detectionLatencyS, computeLatencyS, actuateTauS, fastSlewUpMwPerSPerMw, fastSlewDownMwPerSPerMw, headroomUpFraction, headroomDownFraction, rocofTripHzPerS, rocofTripConfirmS, localRocofUpliftFactor, vectorShiftTripDeg, lvrtVoltagePu, pllBandwidthHz, pllDampingRatio, currentLoopBandwidthHz, capacityMwh, socInit, socMin, socMax, batteryChemistry, dcVoltageV, converterEfficiency, ambientC |
gridFormingInverter |
deadbandHz, synInertiaMwsPerHz, dampingMwPerHz, actuateTauS, headroomUpFraction, headroomDownFraction, rocofTripHzPerS, rocofTripConfirmS, localRocofUpliftFactor, lvrtVoltagePu, virtualReactancePu, powerLoopBandwidthHz, capacityMwh, socInit, socMin, socMax, batteryChemistry, dcVoltageV, converterEfficiency, ambientC |
protection |
rocofTripHzPerS, rocofTripConfirmS, dfDtTripHzPerS, dfDtTripDwellS, localRocofUpliftFactor, ufTripHz, ufTripDwellS, ofTripHz, ofTripDwellS, uvTripPu, uvTripDwellS, ovTripPu, ovTripDwellS, lvrtVoltagePu, vectorShiftTripDeg, detectionLatencyS, overloadTripFraction, overloadTripDwellS, lfddStageHz, lfddBlockMw, lfddDwellS, autoRecloseDelayS, settingsNominalHz |
transformer |
targetVpu, tapStepPu, tapDeadbandPu, minTap, maxTap, interTapDelayS, initialTapDelayS, ambientLoadingFraction, thermalTauMin, derateAtFullThermal |
line |
ambientLoadingFraction, thermalTauMin, derateAtFullThermal |
Two keys are refused on every kind (AssetClassSupport.RefusedKeys):
| Key | Reason it is refused | Recorded value it carried |
|---|---|---|
dispatchRampMwPerMin |
The inverter controllers model the frequency response about a fixed dispatch, so there is no dispatch setpoint for a ramp limit to act on. The fast response is already rate-limited by fastSlewUpMwPerSPerMw and fastSlewDownMwPerSPerMw. |
An observed fleet ramp band, Outputs/ramp_correlation.json (gas-versus-renewable ramp bands, 24 months): wind 32.1, solar 39.0, storage 60.0 MW/min in gb-full; wind 30.0 in gb-synthetic. A system-level observation, not a unit control limit. |
minOutputFraction |
No controller models a minimum stable output. | none |
The dispatch ramp is a system-level proxy for wind and solar because those technologies submit no per-unit run-up rate: they track weather or curtail, they do not ramp on command. The refusal message says the band is "recorded in docs/technical/13", a path that does not resolve; the band is recorded in this chapter.
The accepted-key table is pinned to the v2 kind specifications (The_accepted_keys_match_the_v2_kind_specs_less_the_refused_keys).
Parameters and defaults¶
Each controller reads its parameters with AssetClass.Param(key, fallback). The fallback is applied when a class does
not set the key. The fallbacks below differ from the bundled values (for example headroom 0.2 against the bundled
0.10), so a class that omits a key gets the fallback, not the bundled value. Symbols are the AssetParams constants
in src/GridSim.Core/Assets/AssetClassLibrary.cs.
Grid-following inverter (GridFollowingInverter)¶
| Key | Source symbol | Fallback | Unit | Meaning |
|---|---|---|---|---|
deadbandHz |
DeadbandHz |
0.015 | Hz | frequency deviation below which the droop leg is zero |
droopGainMwPerHzPerMw |
DroopGainMwPerHzPerMw |
2.0 | MW/Hz per MW rated | droop gain; gain in MW/Hz is this times rated MW |
rocofGainMwsPerHz |
RocofGainMwsPerHz |
0.0 | MW per Hz/s | df/dt leg gain |
detectionLatencyS |
DetectionLatencyS |
0.0 | s | measurement transport delay (PLL latency) |
computeLatencyS |
ComputeLatencyS |
0.0 | s | delay from deadband exit to the droop leg arming |
actuateTauS |
ActuateTauS |
0.25 | s | first-order actuation lag |
fastSlewUpMwPerSPerMw |
FastSlewUpMwPerSPerMw |
0.0 (no limit) | MW/s per MW rated | upward ramp limit |
fastSlewDownMwPerSPerMw |
FastSlewDownMwPerSPerMw |
0.0 (no limit) | MW/s per MW rated | downward ramp limit |
headroomUpFraction |
HeadroomUpFraction |
0.2 | fraction of rated | injection cap |
headroomDownFraction |
HeadroomDownFraction |
0.2 | fraction of rated | absorption cap |
rocofTripHzPerS |
RocofTripHzPerS |
+infinity (never) | Hz/s | loss-of-mains RoCoF threshold |
rocofTripConfirmS |
RocofTripConfirmS |
0.5 | s | definite-time delay before the RoCoF trip |
localRocofUpliftFactor |
LocalRocofUpliftFactor |
1.0 (no uplift) | ratio | local over COI RoCoF multiplier, trip only |
vectorShiftTripDeg |
VectorShiftTripDeg |
+infinity (never) | degrees | phase step over one-cycle window |
lvrtVoltagePu |
LvrtVoltagePu |
0.0 (never) | pu | trip when local voltage falls below it |
capacityMwh and the DC keys |
see "DC source" | 0.0 (unbounded) | MWh | energy store behind the DC link |
Grid-forming inverter (GridFormingInverter)¶
| Key | Source symbol | Fallback | Unit | Meaning |
|---|---|---|---|---|
synInertiaMwsPerHz |
SynInertiaMwsPerHz |
0.0 | MW.s per Hz | power per df/dt (virtual inertia; not stored energy) |
dampingMwPerHz |
DampingMwPerHz |
0.0 | MW/Hz | power per frequency deviation |
deadbandHz |
DeadbandHz |
0.0 | Hz | deviation below which damping is zero |
actuateTauS |
ActuateTauS |
0.05 | s | first-order lag |
headroomUpFraction, headroomDownFraction |
HeadroomUpFraction, HeadroomDownFraction |
0.2 | fraction of rated | injection and absorption caps |
rocofTripHzPerS |
RocofTripHzPerS |
+infinity (never) | Hz/s | RoCoF trip threshold |
rocofTripConfirmS |
RocofTripConfirmS |
0.5 (AssetParamDefaults.RocofTripConfirmS, shared with the grid-following inverter) |
s | definite-time delay |
localRocofUpliftFactor |
LocalRocofUpliftFactor |
1.0 | ratio | multiplier on the RoCoF the trip tests |
lvrtVoltagePu |
LvrtVoltagePu |
0.0 (never) | pu | undervoltage trip |
The grid-forming controller has no detection latency, no droop leg and no vector-shift element.
DC source (DcSources.FromClass, src/GridSim.Core/Assets/DcSource.cs)¶
| Key | Source symbol | Fallback | Unit | Meaning |
|---|---|---|---|---|
capacityMwh |
CapacityMwh |
0.0 | MWh | usable energy; 0 or less gives no store (unbounded) |
socInit |
SocInit |
0.5 | fraction | initial state of charge |
socMin, socMax |
SocMin, SocMax |
0.0, 1.0 | fraction | bucket bounds (no chemistry) |
batteryChemistry |
BatteryChemistry |
0 (bucket) | code | 1 LFP, 2 NMC, 3 LTO, 4 Na-ion, 5 VRFB, 6 lead-acid, after rounding to the nearest integer; any other code gives the bucket |
dcVoltageV |
DcVoltageV |
1000.0 | V | pack DC voltage |
converterEfficiency |
ConverterEfficiency |
0.98 | ratio | clamped to [0.5, 1.0] by BatteryDcSource |
ambientC |
AmbientC |
25.0 | degC | pack ambient temperature |
Protection (ProtectionController.BuildStages)¶
A class produces one stage per key present. Trip stages latch and remove capacityMw of infeed once; in a recloser
class (one that sets autoRecloseDelayS) every trip stage instead opens targetBranch and recloses after the delay.
Three rules apply before any stage is built:
- Dwells are required. A class that sets a threshold must set its dwell (
AssetParamDefaults.ProtectionDwellKeys:rocofTripConfirmS,dfDtTripDwellS,ufTripDwellS,ofTripDwellS,uvTripDwellS,ovTripDwellS,overloadTripDwellS,lfddDwellS).0is a legitimate instantaneous setting; an absent dwell is refused (InvalidDataException) naming every missing key. The LVRT and vector-shift stages are instantaneous by definition and have no dwell key. - Frequency settings move with the nominal.
settingsNominalHz(AssetParams.SettingsNominalHz) is the nominal the class'sufTripHz,ofTripHzandlfddStageHzare written for; absent, it is the GB 50 Hz (AssetParamDefaults.ProtectionSettingsNominalHz). On a system at another nominal (the fleet passes the model's nominal) each of these settings becomessystemNominal + (setting - settingsNominalHz): a GB 48.8 Hz LFDD stage operates at 58.8 Hz on a 60 Hz case. At equal nominals the setting is used exactly as written. - Frequency settings must be plausible. A UF or LFDD setting at or above its settings nominal, an OF setting at or
below it, a setting more than 10 Hz from it on its own side (
AssetClassSupport.MaxFrequencySettingDistanceHz), a non-finite setting, or asettingsNominalHzthat is not a finite frequency above 0, is refused by name, at library load (AssetClassSupport.ProtectionFrequencyProblems) and again when a class built in code reachesProtectionController. A setting written for a 60 Hz system must declaresettingsNominalHz60.
| Key | Source symbol | Stage built | Quantity tested |
|---|---|---|---|
rocofTripHzPerS, rocofTripConfirmS |
RocofTripHzPerS, RocofTripConfirmS |
trip, above threshold, dwell = confirm (required) | abs(RoCoF) x uplift |
dfDtTripHzPerS, dfDtTripDwellS |
DfDtTripHzPerS, DfDtTripDwellS |
trip, above | abs(RoCoF) x uplift |
localRocofUpliftFactor |
LocalRocofUpliftFactor |
multiplier on the RoCoF stages (fallback 1.0) | |
ufTripHz, ufTripDwellS |
UfTripHz, UfTripDwellS |
trip, below (setting moved to the system nominal) | delayed frequency |
ofTripHz, ofTripDwellS |
OfTripHz, OfTripDwellS |
trip, above (setting moved to the system nominal) | delayed frequency |
uvTripPu, uvTripDwellS |
UvTripPu, UvTripDwellS |
trip, below | local voltage |
ovTripPu, ovTripDwellS |
OvTripPu, OvTripDwellS |
trip, above | local voltage |
lvrtVoltagePu |
LvrtVoltagePu |
trip, below, dwell 0.0; only when above 0 | local voltage |
overloadTripFraction, overloadTripDwellS |
OverloadTripFraction, OverloadTripDwellS |
trip, above | loading fraction |
vectorShiftTripDeg |
VectorShiftTripDeg |
trip, above, dwell 0.0; only when finite | abs(phase step) |
lfddStageHz, lfddDwellS, lfddBlockMw |
LfddStageHz, LfddDwellS, LfddBlockMw |
shed lfddBlockMw x demand scale of demand, below (setting moved to the system nominal) |
delayed frequency |
settingsNominalHz |
SettingsNominalHz |
the nominal the frequency settings are written for (default 50) | |
detectionLatencyS |
DetectionLatencyS |
measurement delay for the frequency, RoCoF and phase | |
autoRecloseDelayS |
AutoRecloseDelayS |
turns trip stages into open-branch stages with this restore delay |
An LFDD stage is always a demand shed, whatever the recloser flag says. An LFDD block is a share of the demand on the
feeders it disconnects (Grid Code CC.A.5.5.1): ProtectionController.SetDemandScale(factor) tells the device the
demand now running as a fraction of the base case, and a shed stage then disconnects lfddBlockMw * factor
(remembered per stage, so a reclose reconnects the same MW). The live loop sets the factor every tick from the
load-scaled operating point; every batch study leaves it at 1.0. An infeed trip keeps its capacity. A stage's MW is
its ActionMw when positive, else the instance capacityMw. A class that sets several trip keys loses its capacityMw once, however many stages
fire.
Transformer (TransformerDynamics) and line (LineThermalDynamics)¶
| Key | Source symbol | Fallback (transformer / line) | Unit | Meaning |
|---|---|---|---|---|
targetVpu |
TargetVpu |
1.0 / n.a. | pu | controlled-bus voltage target |
tapStepPu |
TapStepPu |
0.0125 / n.a. | pu | tap step |
tapDeadbandPu |
TapDeadbandPu |
0.01 / n.a. | pu | voltage deadband around target |
minTap, maxTap |
MinTap, MaxTap |
0.9, 1.1 / n.a. | ratio | tap limits |
interTapDelayS |
InterTapDelayS |
5.0 / n.a. | s | time out of band before each later step |
initialTapDelayS |
InitialTapDelayS |
10.0 / n.a. | s | time out of band before the first step |
thermalTauMin |
ThermalTauMin |
20.0 / 15.0 | min | thermal time constant |
ambientLoadingFraction |
AmbientLoadingFraction |
0.0 / 0.0 | fraction | thermal floor (loading the plant starts from) |
derateAtFullThermal |
DerateAtFullThermal |
0.0 / 0.1 | fraction | rating reduction at thermal state 1 |
Algorithm: where each parameter acts¶
All equations are as implemented. rated is the generator MBase in MW, pg its dispatched Pg.
AssetDynamicsFleet.StepCoi passes a local voltage of 1.0 pu and a loading of 0.0 unless its voltageOf and
loadingOf suppliers are given, and the scripted trip --asset-dynamics run gives neither. On that path the
lvrtVoltagePu trips, uvTripPu, ovTripPu and overloadTripFraction therefore cannot fire; LiveSim supplies
both after each power-flow solve (ch. 15).
Grid-following inverter¶
Per substep, with dt the substep and f_meas, rocof_meas the local frequency and RoCoF read from the measurement
delay buffer at age detectionLatencyS:
protect: over = abs(rocof_meas) * localRocofUpliftFactor > rocofTripHzPerS
dwell = over ? dwell + dt : 0
trip = over and dwell >= rocofTripConfirmS
or abs(phaseStep) > vectorShiftTripDeg (phase change over max(dt, 0.02 s))
or (lvrtVoltagePu > 0 and V_local < lvrtVoltagePu)
on trip: output 0, infeed lost = pg, latched
compute: armed when abs(f_meas - f0) > deadbandHz, at t + computeLatencyS
capUp = headroomUpFraction * rated (min with DC source headroom)
capDown = headroomDownFraction * rated (min with DC source headroom)
gain = droopGainMwPerHzPerMw * rated
fullHz = gain > 0 ? max(capUp, capDown) / gain : 0.5
droop = FastResponseTarget(df, max(capUp, capDown), deadbandHz, fullHz)
= clamp(-(max(capUp, capDown) / fullHz) * df, -cap, +cap), 0 inside the deadband
target = clamp(droop - rocofGainMwsPerHz * rocof_meas, -capDown, capUp)
actuate: ramped = clamp(target, p - slewDown*rated*dt, p + slewUp*rated*dt) (a zero slew is no limit)
p += (ramped - p) / max(dt, actuateTauS) * dt
then the DC source draws p and returns what it delivered
The droop leg disarms again once the measured frequency has stayed inside the deadband for computeLatencyS
(counted in whole substeps), so every later excursion meets the compute latency again. The uplift multiplies the RoCoF
the trip tests and nothing else: the droop and df/dt legs act on the delayed, unuplifted value.
Dispatch. The infeed a trip books is the unit's dispatched output. AssetDynamicsFleet.SetDispatch(model) hands
every generator-bound asset its generator's Pg in the operating point now running; the live loop calls it every tick
with the load-scaled, re-balanced model, so a trip books the output actually lost. It refuses an operating point with
a different generator count or a bound generator at another bus (ArgumentException). Placed protection devices keep
their capacity. For the bundled wind class, droopGainMwPerHzPerMw 2.0 and headroomUpFraction 0.10 give
fullHz = 0.10 * rated / (2.0 * rated) = 0.05 Hz: the droop leg reaches full output at 0.05 Hz deviation.
Grid-forming inverter¶
trip: abs(rocof_local) * localRocofUpliftFactor > rocofTripHzPerS for rocofTripConfirmS,
or (lvrtVoltagePu > 0 and V_local < lvrtVoltagePu)
target = clamp(-synInertiaMwsPerHz * rocof_local - dampingMwPerHz * dfActive, -capDown, capUp)
dfActive = df when abs(df) > deadbandHz, else 0
p += (target - p) / max(dt, actuateTauS) * dt
synInertiaMwsPerHz is a power gain on df/dt. It adds no energy to the system's stored rotational energy E
(ch. 5).
DC source¶
With capacityMwh at or below 0 there is no store. With a batteryChemistry code in 1 to 6 the store is a PackModel
sized from capacityMwh and dcVoltageV behind a converter of efficiency converterEfficiency
(ch. 27); a reset restores the pack in place from a snapshot that keeps its
ageing state, so the pack ages across resets. Otherwise it is the SocBucket:
soc -= p * dt / 3600 / capacityMwh (then clamped to [0, 1])
headroom up = 0 when soc <= socMin, else unbounded
headroom down = 0 when soc >= socMax, else unbounded
Protection stage¶
Each stage tests its quantity against its threshold; an excursion that lasts dwell seconds fires the stage once
(ProtectionEvaluator.Step). With localRocofUpliftFactor u and threshold T, a RoCoF stage fires when
abs(RoCoF) * u > T for the dwell, so the effective threshold on the COI RoCoF is T / u.
Transformer and line¶
transformer: err = V_ctrl - targetVpu
out of band when abs(err) > tapDeadbandPu; time accumulates, resets inside the band
step when time >= (first step ? initialTapDelayS : interTapDelayS):
tap = clamp(tap -/+ tapStepPu, minTap, maxTap)
direction: tapped (From) end controlled: high V lowers the tap
untapped (To) end controlled: high V raises the tap
thermal += (max(loading, ambientLoadingFraction) - thermal) / max(dt, 60*thermalTauMin) * dt
derate = derateAtFullThermal * clamp(thermal, 0, 1)
line: thermal += (max(loading^2, ambientLoadingFraction) - thermal) / max(dt, 60*thermalTauMin) * dt
derate = derateAtFullThermal * clamp(thermal, 0, 1)
The tap starts at the bound branch's own TapRatio; a branch tap outside [minTap, maxTap] throws
ArgumentOutOfRangeException at build. PassivePlantFleet.ApplyTo sets the branch TapRatio to the tap and the
branch rating to baseRating * (1 - derate), always from the original rating, so re-applying an update does not
compound. A transformer whose controlled bus is generator-regulated (PV or slack in the tick's solved bus types,
PowerFlowResult.SolvedBusTypes) holds its tap and only derates. The fleet runs per tick when
DynamicsConfig.EnablePassivePlant is set (LiveSim) or gda-replay --passive-plant is given; off, the loops are
bit-identical to a model with nothing bound.
Grounded values per class¶
Tags are those in the file. "gf" is gb-full, "gs" is gb-synthetic. A parameter marked P is a CuratedPlaceholder
(its note carries [VERIFY]), explicit or inherited from _default.
Recorded and derived parameters¶
| Parameter | Value | Tag | Classes (dataset) | Source |
|---|---|---|---|---|
rocofTripHzPerS (modern) |
1.0 Hz/s | Recorded | gfl-wind-2024 (gf, gs), gfl-solar-2024 (gf, gs), hvdc-2024 (gf), dg-alomcp (gs) |
dg-alomcp cites EREC G99 Issue 2 (S124) Table 10.1, p.125; gfl-wind-2024 cites the modern GB LoM setting (FrequencyLimits.RocofLimitModernHzPerS) or ALoMCP; gfl-solar-2024 and hvdc-2024 cite the modern GB LoM setting. The value matches S124 p.125. |
rocofTripConfirmS (modern) |
0.5 s | Recorded | gfl-wind-2024, gfl-solar-2024, hvdc-2024 (gf); dg-alomcp (gs) |
dg-alomcp cites G99 Issue 2 (S124) Table 10.1 and its note, pp.125-126 (LoM time delay 0.5 s, reset if RoCoF falls below the threshold); the gf classes cite GDA Documentation/manual/54 (ALoMCP 500 ms definite-time delay) |
rocofTripHzPerS (legacy) |
0.125 Hz/s | Recorded | legacy-dg-old-lom (gf, gs), prot-lom-legacy (gs) |
EREC G59 Issue 3 Amendment 8 (S135) p.62: not less than K2 x 0.125 Hz/s with K2 = 1.0 (low-impedance network), plant commissioned before 01/02/18; FrequencyLimits.RocofLimitLegacyHzPerS; GDA rocof_events relay_legacy grade |
rocofTripConfirmS (legacy) |
0.0 s | Recorded in gf; inherits P in gs |
legacy-dg-old-lom |
legacy relays have no intentional delay, GDA Documentation/manual/54 |
vectorShiftTripDeg |
6.0 degrees | Recorded | legacy-dg-old-lom (gf, gs) |
G59 Issue 3 Amendment 8 (S135), Historic Vector Shift Settings, p.63: K1 x 6 degrees with K1 = 1.0 (low-impedance network); not allowed on or after 01/09/22 and not acceptable under G99 Issue 2 (S124) clause 10.4.11 |
deadbandHz |
0.015 Hz | Recorded | gfl-wind-2024 (gf, gs) |
NESO frequency-response service deadband; P on the other classes that carry the same value |
localRocofUpliftFactor |
1.16 | DerivedFromRecorded | all gf inverter classes; prot-lom-legacy (gs) |
GDA Derived/subcycle_period_features, rocof_uplift_ratio: instantaneous RoCoF is 1.16x typical, up to 1.35x, the 1 s-averaged value (reconstruction-based, not PMU-measured) |
lfddStageHz |
48.8 Hz | Recorded | prot-lfdd-stage1 (gs) |
Grid Code Issue 6 Revision 16 (S076) CC.A.5.5.1, Table CC.A.5.5.1a (PDF p.264); FrequencyLimits.LfddFirstStageHz |
lfddBlockMw |
632 MW | DerivedFromRecorded | prot-lfdd-stage1 (gs) |
GDA Events/lfdd_risk: 632 MW coincident block, 2019-08-09 |
targetVpu |
1.0 pu | Recorded | tx-oltc-2024 (gf) |
nominal voltage target |
tapStepPu |
0.0125 pu | DerivedFromRecorded | tx-oltc-2024 (gf) |
SCADA-recovered OLTC step, 1.0 to 1.25 % nominal, Derived/tap_changers/tap_changers.parquet step_pct (0.46 % observed) |
minTap, maxTap |
0.85, 1.15 | DerivedFromRecorded | tx-oltc-2024 (gf) |
LTDS tap span, ltds_tap_span_pct median 31 %, and NetworkModel/normalized/<licence>_tx2.parquet tap positions (about 16 steps, -12 to +4) |
derateAtFullThermal (cable) |
0.11 | DerivedFromRecorded | cable-thermal-2024 (gf) |
seasonal derate: summer rating is 0.89 x winter, <licence>_circuits.parquet summer_rating_mva / winter_rating_mva (0.86 to 0.91; NPGN 1.0) |
The legacy and modern relay values are the two ends of the loss-of-mains population that the 2019-08-09 cascade turns
on: legacy-dg-old-lom is the pre-ALoMCP tripping population, dg-alomcp the population upgraded under the
Accelerated Loss-of-Mains Change Programme.
Placeholder parameters (CuratedPlaceholder, [VERIFY])¶
| Class | Placeholder parameters and values |
|---|---|
gfl-wind-2024 |
droopGainMwPerHzPerMw 2.0; rocofGainMwsPerHz 0.0 (gs only); detectionLatencyS 0.10; computeLatencyS 0.02; actuateTauS 0.30; fastSlewUpMwPerSPerMw 1.0; fastSlewDownMwPerSPerMw 1.0; headroomUpFraction 0.10; headroomDownFraction 0.10; lvrtVoltagePu 0.80; rocofTripConfirmS 0.5 in gs |
gfl-solar-2024 |
deadbandHz 0.015; droopGainMwPerHzPerMw 2.0; detectionLatencyS 0.12; computeLatencyS 0.02; actuateTauS 0.30; headroomUpFraction 0.05; headroomDownFraction 0.05; lvrtVoltagePu 0.80; rocofTripConfirmS 0.5 in gs |
gfm-bess-2024 |
synInertiaMwsPerHz 8.0; dampingMwPerHz 15.0; deadbandHz 0.0; actuateTauS 0.05; headroomUpFraction 0.20; headroomDownFraction 0.20; capacityMwh 2.0; socInit 0.55; socMin 0.05; socMax 0.95; rocofTripHzPerS 2.5; rocofTripConfirmS 1.0; lvrtVoltagePu 0.50 |
gfm-vsm-study-2024 (gs) |
synInertiaMwsPerHz 120.0; dampingMwPerHz 40.0; deadbandHz 0.0; actuateTauS 0.05; headroomUpFraction 1.0; headroomDownFraction 1.0; capacityMwh 0.0; rocofTripHzPerS 1000000000.0; lvrtVoltagePu 0.0 (an idealised ride-through former with no SoC cap, for the H=0 counterfactual) |
hvdc-2024 (gf) |
deadbandHz 0.015; droopGainMwPerHzPerMw 3.0; detectionLatencyS 0.05; computeLatencyS 0.02; actuateTauS 0.15; headroomUpFraction 0.10; headroomDownFraction 0.10 |
legacy-dg-old-lom |
deadbandHz 0.015; droopGainMwPerHzPerMw 0.0; headroomUpFraction 0.0; headroomDownFraction 0.0 (the fleet has no frequency response); rocofTripConfirmS 0.0 in gs |
dg-alomcp (gs) |
deadbandHz 0.015; droopGainMwPerHzPerMw 0.0; headroomUpFraction 0.0; headroomDownFraction 0.0 |
prot-lom-legacy (gs) |
rocofTripConfirmS 0.0 (no entry; resolves to the synthetic placeholder) |
prot-lfdd-stage1 (gs) |
lfddDwellS 0.0 (no entry; resolves to the synthetic placeholder) |
tx-oltc-2024 (gf) |
tapDeadbandPu 0.01; interTapDelayS 5.0; initialTapDelayS 10.0; thermalTauMin 30.0; derateAtFullThermal 0.1 |
cable-thermal-2024 (gf) |
thermalTauMin 15.0; ambientLoadingFraction 0.0 |
These placeholders carry their own note beyond the _default: in gb-full, gfm-bess-2024 synInertiaMwsPerHz and
capacityMwh, tx-oltc-2024 interTapDelayS, initialTapDelayS, thermalTauMin and derateAtFullThermal, and
cable-thermal-2024 thermalTauMin; in gb-synthetic, synInertiaMwsPerHz of gfm-bess-2024 and
gfm-vsm-study-2024. Three of them carry substance:
synInertiaMwsPerHz(both grid-forming classes): the gain is power per df/dt, not stored energy. Ingfm-vsm-study-2024it is read asEv = synInertia * f0 / 2in the forming denominator.capacityMwh(gfm-bess-2024, gf): the note reads "BESS duration absent from REPD/TEC/DUKES/BMU; assumed C-rate x MW [VERIFY]".derateAtFullThermal(tx-oltc-2024): emergency headroom is 1.58 to 2.0 x nominal (<licence>_tx2.parquetemergency_rating_mva / nominal_rating_mva, median 1.583), which grounds a steady-state figure, not the 0.1 sustained derate.
Quantities with no source¶
These have no value in any lake product or standard, so they stay placeholders or are swept:
- Inverter internal control loops. Droop and df/dt gains, detection, compute and actuate latencies, PLL and current-loop time constants, fast-slew MW/s, LVRT/FRT behaviour are vendor-proprietary.
- BESS
capacityMwhand duration. The MW sizing is grounded (REPD operational batteries: 171 sites, 4,755 MW, median 23 MW); the duration is not. - The legacy-versus-upgraded embedded-generation fraction. The relay thresholds are recorded; no register says how
much embedded generation was upgraded under ALoMCP. It is the highest-leverage knob of the RoCoF-trip cascade, which is
why a real-fleet claim is a parameter-sweep envelope (
AssetEnvelope,AssetEnvelopeResult.CascadeIsPossible) and not a point prediction. - OLTC mechanical delay and transformer or cable thermal time constants. The lake has no tap-position-against-time
channel and no temperature series (
DataSources/UkPowerNetworks/Processors/detect_tap_changers.py). Grounded ratings give steady-state headroom only. - Fault level and grid stiffness. No fault-level product exists in the lake; the local-stiffness proxy stays a placeholder.
- Transformer MW and MVAr loading. There is no
transformer_flowsproduct, only a BSP power-factor summary (mean 0.95, median 0.98, so MVA is close to MW at most BSPs). - Inter-domain propagation delay.
Derived/propagation_delaysis synthetic (is_synthetic_speed=True, literature band 500 to 1500 km/s, 5 to 8 ms per corridor). - Protection settings with no operational data. Recloser, auto-reclose and breaker operation (only customer-outage counts exist), under- and over-voltage (no measured GB voltage series), thermal and reactive overload, and fault level. The recloser mechanism is modelled in full; its settings are assumed.
- Impedance-screen converter parameters.
pllBandwidthHz,pllDampingRatio,currentLoopBandwidthHz,virtualReactancePuandpowerLoopBandwidthHzare set by no bundled class. The impedance screen takes theConverterDefaultsliterature values (PLL bandwidth 20 Hz, the one default EMT, impedance and small-signal share; damping 0.707) and labels the resultCuratedPlaceholder; it labels itRecordedonly when the bandwidth came from a class whose provenance forpllBandwidthHzisRecordedorDerivedFromRecordedand no override was typed. The same rule applies tovirtualReactancePu.
Recorded context behind the classes¶
These quantities are not class parameters. They are the recorded values the class values and the validation are checked against. The sources are GDA lake files; this repository does not carry the files.
Frequency and inertia¶
| Quantity | Value | Source |
|---|---|---|
| GB system inertia | 101 to 338 GVA.s; typical 150; synchronous floor 107.5 | Derived/inertia_reconstructed/..._validation.json, Derived/weather_inertia_coupling/..._validation.json |
| Inertia against synchronous GW | 7.45 GVA.s per GW synchronous (r^2 = 0.87) | weather_inertia_coupling_validation.json |
| Worst-case RoCoF | 0.42 Hz/s at 1800 MW at the inertia floor | inertia_reconstructed_validation.json |
| Per-domain inertia shares | thermal 98.6 %, hydro 1.4 %, inverter 0 % of spinning inertia; converters 57 % of online MW; LOS-fragmentation flag true 9 % of the time | Derived/speed_domains/... |
| Sub-cycle RoCoF uplift | 1.16 x typical, up to 1.35 x | Derived/subcycle_period_features/... |
| Response-curve deviation grid | 0.2, 0.5, 0.8 Hz | NESO firm_frequency_response_ffr_post_tender_reports schema |
| Arrest-layer held volumes | DC 950 to 1000 MW, DM 500 to 550 MW, DR 480 to 500 MW, each direction; legacy FFR 0.5 GW Primary at 0.5 Hz (207 MW at 0.2 Hz) | NESO EAC auction clears, DM/DR/DC requirement forecasts, FFR post-tender |
| Delivery-curve shape | DC full at 0.5 Hz; DM and DR full at 0.2 Hz; deadband 0.015 Hz | NESO published service terms |
| Reserve held | Positive Balancing Reserve 450 MW, Negative 1450 MW; Quick Reserve 300 to 500 MW | NESO balancing_reserve_..., quick_reserve_... forecasts |
| Validation frequency data | 1 s (NESO), 10 Hz (EirGrid all-island) | Derived/frequency_all_srcs, EirGrid/frequency_10hz |
| GB phasor data | 4 nodes (london, buckingham, manchester, strathclyde), 100 ms free tier, 6 kHz paid tier; schema-registered, not materialised (token-gated) | Gridradar/gb_frequency |
| PMU event signatures | forced-oscillation and ring-down, 30 frames/s, 16 points; US data, not GB: physics validation only; not materialised | GESL/signatures |
| Capacity and bus per technology | grounded per unit (Built 81.6 GW) | TEC register tec_2026-07-10.parquet, REPD, DUKES, BMU crosswalk |
| Ramp absorption by the fleet | 0.40 to 0.55 of a renewable swing | Outputs/flex_stack_response.json |
Passive plant¶
| Quantity | Value | Source |
|---|---|---|
| Transformer emergency headroom | 1.58 to 2.0 x nominal, median 1.583 | <licence>_tx2.parquet |
| Cable share of route length (transmission) | 6.5 % | NetworkModel/resolved/etys_circuits.parquet circuit_type, ohl_km, cable_km |
| Line R, X, B, ratings, length | per circuit | normalised LTDS <licence>_circuits.parquet, feeding transmission.json |
Synchronous plant (not a class: no controller exists)¶
The Elexon dynamic-parameter hives (DataSources/Elexon/Parquet/{rure,rdre,sel,mels,mzt,mnzt,ndz}/...), joined to fuel
by Reference/bmunits_all.json, carry per-unit dispatch primitives. A synchronousMachine class is refused because a
machine's inertia comes from its generator's H and MBase.
| Parameter | Value per fuel |
|---|---|
Run-up ramp (rure rate3) |
CCGT 15 MW/min; pumped storage unconstrained; wind, nuclear and interconnectors submit no run-up rate |
Run-down ramp (rdre) |
CCGT 15 to 25 MW/min |
| Headroom (MELS - SEL) / MELS | CCGT 0.40; nuclear 0.03 |
| Minimum run, minimum off, notice | CCGT 6 h, 6 h, 70 min; pumped storage 30, 15, 2 min |
Protection event streams¶
| Protection | Value | Source |
|---|---|---|
| RoCoF and loss-of-mains grades | 0.0625, 0.125, 0.25, 0.5 Hz/s (and 1 s and 3 s streams); modern ALoMCP 1.0 Hz/s with 500 ms delay | Events/rocof_events (794 events), Random/detect_rocof_events.py |
| LFDD first stage | 48.8 Hz and a 632 MW coincident block, 2019-08-09 | Events/lfdd_risk, Random/find_lfdd_events.py |
| Under and over frequency | statutory 49.5 and 50.5 Hz | FrequencyLimits.StatutoryLowHz, StatutoryHighHz; GDA lfdd_risk stages |
| Infeed loss and interconnector trip | MW, nadir, RoCoF and recovery per event | Events/infeed_loss (8,288), Events/interconnector_trips (755) |
The G99 Issue 2 (S124) Table 10.1, p.125 interface-protection defaults in ProtectionSettings are: under-voltage 0.80 pu
(V - 20 %, 2.5 s), over-voltage stage 1 1.10 pu (V + 10 %, the HV protection column; LV is V + 14 %; 1.0 s), under-frequency stage 1 47.5 Hz (20 s),
over-frequency 52.0 Hz (0.5 s), RoCoF 1.0 Hz/s (0.5 s). The default ProtectionSettings.VectorShiftTripDeg is 12 degrees,
the upper end of the legacy G59 range (K1 x 6 degrees with K1 from 1.66 to 2.0 for a high-impedance network, S135 p.63).
2019-08-09 event targets¶
The GDA post-event replay reports give these targets for the 2019-08-09 event. In this repository only the nadir
48.787 Hz and the 215 GVA.s inertia are used, by Asset2019EventTests; the other values are in the lake only.
AssetValidation.Compare is a generic back-test gate (tolerances 0.05 Hz, 0.05 Hz/s and 2.0 s) called only by
AssetValidationTests.
| Quantity | Recorded value | Source |
|---|---|---|
| Pre-event frequency | 49.992 Hz | reports/rocof_events-20190809T155233-0386/report.json |
| Nadir | 48.787 Hz at 15:53:49Z | same |
| Peak RoCoF | -0.151 Hz/s (1 s window) | same |
| Infeed loss | 1618 MW (detector estimate); the report gives the real-world loss as 1878 MW, with components 800 Hornsea, 640 Little Barford and 430 embedded, which sum to 1870 MW | co-located infeed_loss-... |
| Inertia | NESO outturn 215 GVA.s (swing-equation estimate 267.8) | same |
| LFDD | armed at the 48.787 Hz nadir; no MW disconnected recorded in this report | same |
The GDA report records the aggregate loss only: no per-asset breakdown and no LFDD MW. The repository's
EventCatalog (src/GridSim.Core/Events/EventCatalog.cs, from the Ofgem report) carries both: the per-asset steps
(Hornsea 737 MW, Little Barford 244 MW, 210 and 187 MW, embedded generation 150, 390, 200 and 550 MW) and LFDD stage 1
at 892 MW of net demand disconnected, with a catalogue nadir of 48.8 Hz. The 632 MW LFDD block of prot-lfdd-stage1 is
the GDA coincident block, a different measure from the catalogue's 892 MW net figure. The embedded-generation trip
volume that a relay cascade would produce is swept. The candidate blind-predict event
2026-05-31 (992 MW, nadir 49.806 Hz, reports/infeed_loss-20260531T180031-31869) is disqualified as a step-loss test:
its recorded peak RoCoF is +0.007 Hz/s, 19 times below step physics, so it was a gradual imbalance. Loss-magnitude-only
events (no trajectory) are 601 MW (interconnector trip 2026-06-26) and 1014, 798 and 514 MW (2026-07-03). The 2024-08-14
"19.7 GW" record is a detector artefact and is rejected. None is an embedded-generation cascade
(ch. 14).
Invariants¶
| Invariant | Test |
|---|---|
A CuratedPlaceholder entry without [VERIFY] is a violation and ValidateOrThrow throws |
AssetProvenanceTests.Curated_placeholder_without_verify_is_a_violation |
A CuratedPlaceholder entry with [VERIFY] is clean |
AssetProvenanceTests.Curated_placeholder_with_verify_is_clean |
Recorded and DerivedFromRecorded notes need no [VERIFY] |
AssetProvenanceTests.Recorded_and_derived_tags_do_not_require_verify |
A parameter's provenance falls back to _default (the explicit-entry-first and synthetic fallbacks are untested) |
AssetProvenanceTests.Provenance_for_a_parameter_falls_back_to_default_then_synthetic |
| The same rule holds for protection classes | ProtectionTests.Protection_provenance_verify_rule_is_enforced |
A synchronousMachine class is refused, with "no controller" in the message |
AssetClassSupportTests.A_synchronous_machine_class_is_refused_because_nothing_simulates_it |
dispatchRampMwPerMin and minOutputFraction are refused with their reason |
AssetClassSupportTests.The_two_unread_inverter_keys_are_refused_with_their_reason |
| A key no consumer of the kind reads is refused as "silently ignored" | AssetClassSupportTests.A_key_no_consumer_of_the_kind_reads_is_refused |
| Every problem in a library is listed in one exception | AssetClassSupportTests.Every_problem_in_a_library_is_listed_in_one_exception |
| A class using only keys its kind reads loads | AssetClassSupportTests.A_class_using_only_keys_its_kind_reads_loads |
gb-full and gb-synthetic load with no Problems and no dispatchRampMwPerMin |
AssetClassSupportTests.The_bundled_classes_load_and_no_longer_carry_the_unread_ramp_key |
An unsupported kind in a case folder fails at LoadDirectory, naming the class and the key |
AssetClassSupportTests.Loading_a_case_whose_classes_use_an_unsupported_kind_fails_loud_at_load |
| The accepted-key table equals the v2 kind specifications less the refused keys | AssetClassSupportTests.The_accepted_keys_match_the_v2_kind_specs_less_the_refused_keys |
| The v2 kind files keep the refused keys with a refused description | AssetClassSupportTests.The_refused_keys_are_still_declared_in_the_v2_kind_specs_with_a_refused_description |
A RestoreBranch protection stage is refused |
AssetClassSupportTests.A_restore_branch_protection_stage_is_refused_rather_than_silently_doing_nothing |
legacy-dg-old-lom vector shift is 6.0 degrees, Recorded, cites S135 p.63, no [VERIFY] (both datasets) |
S3ProtectionGroundingTests.Legacy_dg_vector_shift_is_the_cited_G59_setting |
dg-alomcp has 1.0 Hz/s and 0.5 s, both citing Table 10.1 |
S3ProtectionGroundingTests.Alomcp_dg_rocof_and_its_delay_cite_G99_Table_10_1 |
ProtectionSettings default thresholds are the G99 Table 10.1 values (the time delays are not asserted) |
S3ProtectionGroundingTests.Voltage_frequency_and_rocof_defaults_are_the_G99_Issue_2_Table_10_1_settings |
| The local RoCoF uplift trips a relay the COI value would not | InverterTripCascadeTests.Local_rocof_uplift_makes_a_relay_trip_that_the_coi_value_would_not |
| A fleet below its limit does not trip | InverterTripCascadeTests.A_fleet_below_its_limit_does_not_trip |
A RoCoF protection trips its capacityMw and deepens the nadir |
ProtectionTests.Rocof_lom_protection_trips_and_deepens_the_nadir |
An OLTC holds for initialTapDelayS, then steps |
PassivePlantTests.Oltc_does_not_move_within_the_mechanical_delay_and_moves_after |
| Line derate is monotone in loading | PassivePlantTests.Line_thermal_derate_is_monotone_in_loading |
| The tap starts at the branch tap; a tap outside the class range fails loud | PassivePlantWiringTests.Fleet_starts_each_transformer_at_its_branch_tap_not_at_nominal, A_branch_tap_outside_the_class_range_fails_loud_at_build |
| Tap direction follows the controlled-bus side | PassivePlantWiringTests.A_to_side_controlled_bus_that_runs_high_raises_the_tap_and_a_from_side_one_lowers_it |
| A generator-regulated bus holds its tap and still derates | PassivePlantWiringTests.A_transformer_regulating_a_generator_bus_holds_its_tap_and_still_derates |
| Re-applying an update does not compound the derate | PassivePlantWiringTests.ApplyTo_reapplied_to_its_own_output_derates_from_the_original_rating |
| Passive plant on with nothing bound is bit-identical to off | PassivePlantWiringTests.Replay_with_passive_plant_on_but_nothing_bound_is_bit_identical_to_off, Replay_of_gb_synthetic_with_passive_plant_on_is_bit_identical_to_off |
Binding order: inline, exact, then prefix wildcard; synchronous plant stays unbound (the test uses its own five-rule set, not the shipped asset-bindings.json) |
AssetBindingTests.Inline_class_overrides_a_matching_rule, Exact_rule_beats_a_wildcard_regardless_of_order, Gb_full_vocabulary_binds_the_inverter_fleet_and_excludes_synchronous_and_synthetic |
| An unknown bound class fails loud | AssetBindingTests.An_unknown_bound_class_fails_loud_when_the_fleet_is_built |
The gb-synthetic asset branch loads |
Wave6IoTests.JsonGridLoader_loads_the_asset_dynamics_branch_from_gb_synthetic |
| The 2019-08-09 mechanism emerges from the legacy fleet's own protection, and the recorded nadir lies inside the swept band (not a nadir match) | Asset2019EventTests.The_cascade_mechanism_emerges_from_first_principles, The_recorded_nadir_lies_WITHIN_the_embedded_trip_envelope_not_at_a_predicted_point |
| Sweeping the trip threshold brackets cascade and no cascade | AssetEnvelopeTests.Sweeping_the_trip_threshold_brackets_cascade_and_no_cascade |
Worked example¶
The parameter followed here is rocofTripHzPerS = 0.125 of the class prot-lom-legacy in
data/gb-synthetic/asset-classes.json. The captures are in docs/_captures/asset-grounding/. The outputs carry no timings;
every number in them is a simulation result.
1. The file. The class is kind: protection, params rocofTripHzPerS 0.125, rocofTripConfirmS 0.0,
localRocofUpliftFactor 1.16. Its provenance entries are Recorded for rocofTripHzPerS (legacy GB LoM setting, GDA
rocof_events relay_legacy grade) and DerivedFromRecorded for localRocofUpliftFactor (sub-cycle local over COI,
1.16). rocofTripConfirmS has no entry, so it resolves to the synthetic CuratedPlaceholder. protections.json
places the class as embedded-dg-lom at bus 1 with capacityMw 800.
2. The loader. AssetClassSupport.AcceptedKeys(Protection) contains all three keys, so step 3 passes. The class has
no CuratedPlaceholder entry, so step 4 has nothing to check for it. The instance references a class of kind
protection, so step 6 passes. A broken file is refused before any run; for example a dispatchRampMwPerMin key:
$ gridsim trip results/asset-grounding/ramp-key --asset-dynamics on
error: cannot load case folder 'results/asset-grounding/ramp-key': Unsupported asset class input (R073):
asset class 'gfl-wind-2024' sets 'dispatchRampMwPerMin': no controller reads it: the inverter controllers model the frequency response about a fixed dispatch, so there is no dispatch setpoint to ramp (the fast response is already rate-limited by fastSlewUp/DownMwPerSPerMw). The bundled values were an observed fleet ramp band, recorded in docs/technical/0013 Asset-dynamics data grounding.md. Remove the key.
[exit 1]
and a placeholder note stripped of its token:
$ gridsim trip results/asset-grounding/no-verify --asset-dynamics on
error: cannot load case folder 'results/asset-grounding/no-verify': Invalid asset-classes.json:
asset class 'gfl-wind-2024' parameter '_default' is CuratedPlaceholder but its note lacks the literal [VERIFY] token.
[exit 1]
Each broken case is a copy of data/gb-synthetic with one edit: ramp-key adds "dispatchRampMwPerMin": 30 to the
params of gfl-wind-2024; no-verify replaces the note of the _default entry of gfl-wind-2024 with text that
lacks the token. The folders are under the git-ignored results/.
3. The controller. ProtectionController.BuildStages finds rocofTripHzPerS and adds one stage: quantity RoCoF,
sense above, threshold 0.125, dwell rocofTripConfirmS (0.0), action trip infeed, uplift 1.16. The stage fires when
and a zero dwell fires on the first substep over the threshold. The trip removes the instance capacityMw, 800 MW, once.
4. The captured run. gridsim trip data/gb-synthetic --asset-dynamics on trips Hinkley Point Nuclear (2880 MW) at
t = 1 s. Without the fleet the peak RoCoF is -0.453 Hz/s, 4.2 times the effective threshold of 0.1078 Hz/s (indicative: the
relay reads the local RoCoF of the run with the fleet):
$ gridsim trip data/gb-synthetic --asset-dynamics on
Asset dynamics: on (8 class(es), 2 protection(s); 1 unit(s) tripped, 800 MW of infeed lost: 0 inverter(s) 0 MW (Loss-of-Mains or ride-through), 1 protection device(s) 800 MW)
note: 40 asset(s) carry voltage or loading trip stages (LVRT, under- / over-voltage, overload); this lumped run solves no network, so those stages are not evaluated (transient and the live views evaluate them against the solved state).
GridSim - contingency on GB synthetic
================================================================
Tripped: Hinkley Point Nuclear (2880 MW lost, nuclear)
System inertia: 171.7 -> 158.9 GVAs after the trip
Peak RoCoF: -0.576 Hz/s (largest |df/dt| over the run)
Nadir: 49.052 Hz (t+5.5 s)
Settling: 49.525 Hz
50.05 Hz |***=-:,.. ....,,,:::------===========------------:::::| 49.05 Hz (nadir)
...at HALF the inertia (79.5 GVAs): RoCoF -0.906 Hz/s, nadir 47.872 Hz
Frequency protection on this trajectory (G99 Issue 2 Table 10.1 settings; system-wide, not per site):
81U Underfrequency holds: run minimum frequency 49.052 Hz (>47.5 Hz)
81O Overfrequency holds: run maximum frequency 50.000 Hz (<52.0 Hz)
81R RoCoF holds: peak |df/dt| 0.576 Hz/s (<1.00 Hz/s)
78 Vector shift / LoM not evaluated: vector shift / loss-of-mains needs a voltage-angle transient; this run is a frequency (centre-of-inertia) trajectory with no bus voltage angle
[exit 0]
The first line reports 1 unit(s) tripped, 800 MW lost to LoM: the embedded-dg-lom instance, at its capacityMw. The
second protection, system-lfdd-1 (48.8 Hz, 632 MW), does not operate: the nadir is 49.052 Hz, above its stage.
5. The same case with the modern setting. The control run replaces rocofTripHzPerS 0.125 by 1.0 and
rocofTripConfirmS 0.0 by 0.5 in prot-lom-legacy (the dg-alomcp values, G99 Table 10.1), and changes nothing else.
The effective threshold on the COI RoCoF becomes 1.0 / 1.16 = 0.862 Hz/s, above the 0.450 Hz/s peak, so nothing trips:
$ gridsim trip results/asset-grounding/modern-lom --asset-dynamics on
Asset dynamics: on (8 class(es), 2 protection(s), 0 unit(s) tripped, 0 MW lost to LoM)
GridSim - contingency on GB synthetic
================================================================
Tripped: Hinkley Point Nuclear (2880 MW lost, nuclear)
System inertia: 171.7 -> 158.9 GVAs after the trip
Peak RoCoF: -0.450 Hz/s (largest |df/dt| over the run)
Nadir: 49.480 Hz (t+4.2 s)
Settling: 49.762 Hz
50.05 Hz |***-,. ....,,,:::-----==============---------------:::| 49.48 Hz (nadir)
...at HALF the inertia (79.5 GVAs): RoCoF -0.906 Hz/s, nadir 47.872 Hz
[exit 0]
Between the two runs the only difference is that one parameter pair. The legacy setting trips the 800 MW and moves the
peak RoCoF from -0.450 to -0.576 Hz/s and the nadir from 49.480 to 49.052 Hz, a loss of 0.428 Hz. The run without
--asset-dynamics (docs/_captures/asset-grounding/trip-synthetic-assets-off.txt) has no fleet response at all and
reaches 48.334 Hz. With no trip in the modern-setting run, the difference between those two runs is the response of the
bound inverter classes, whose controller constants are all CuratedPlaceholder.
Source map¶
| File | Type or function | Responsibility |
|---|---|---|
src/GridSim.Core/Assets/AssetClassLibrary.cs |
AssetClass, AssetClassProvenance |
class record, Param, ProvenanceFor, ValidateProvenance |
| same | AssetClassLibrary |
name lookup, duplicate refusal, ValidateOrThrow |
| same | AssetClassSupport |
accepted keys per kind, refused keys, Problems |
| same | AssetParams |
canonical key spellings (the source symbols above) |
src/GridSim.Core/Diagnostics/Provenance.cs |
ProvenanceTag |
the tag enumeration |
src/GridSim.Core/IO/Json/JsonGridLoader.cs |
BuildAssetDynamics, BuildLibrary, BuildProtections, ParseAssetKind, ParseProvenanceTag |
file to library, fail-loud steps 1 to 6 |
src/GridSim.Core/IO/Json/AssetClassDto.cs |
DTOs | gridsim-asset-classes/1 carriers |
src/GridSim.Core/Assets/AssetBinding.cs |
AssetBindingSet, AssetControllerFactory |
binding order, controller construction |
src/GridSim.Core/Assets/AssetDynamicsFleet.cs |
AssetDynamicsFleet.Build |
one controller per bound generator and per protection instance |
src/GridSim.Core/Assets/GridFollowingInverter.cs |
GridFollowingInverter, MeasurementDelay |
the grid-following loop and its delay buffer |
src/GridSim.Core/Assets/GridFormingInverter.cs |
GridFormingInverter |
the grid-forming loop |
src/GridSim.Core/Assets/DcSource.cs |
SocBucket, BatteryDcSource, DcSources |
the energy store behind the DC link |
src/GridSim.Core/Assets/Protection/ProtectionController.cs |
ProtectionController.BuildStages |
class keys to stages, trip accounting |
src/GridSim.Core/Assets/Protection/ProtectionEvaluator.cs |
ProtectionEvaluator |
dwell and edge detection |
src/GridSim.Core/Assets/Passive/TransformerDynamics.cs |
TransformerDynamics |
OLTC delay, tap direction, thermal derate |
src/GridSim.Core/Assets/Passive/LineThermalDynamics.cs |
LineThermalDynamics |
line thermal derate |
src/GridSim.Core/Assets/PassivePlantFleet.cs |
PassivePlantFleet |
binding to branches, ApplyTo |
src/GridSim.Core/Assets/AssetEnvelope.cs |
AssetEnvelope |
sweep over unsourced knobs |
src/GridSim.Core/Assets/AssetValidation.cs |
AssetValidation.Compare |
back-test against recorded targets |
src/GridSim.Core/Dynamics/FrequencyLimits.cs |
FrequencyLimits |
0.125 and 1.0 Hz/s, 48.8 Hz, 49.5 and 50.5 Hz |
data/gb-full/asset-classes.json, asset-bindings.json |
data | 7 generated classes and 7 rules |
data/gb-synthetic/asset-classes.json, asset-bindings.json, protections.json |
data | 8 authored classes, 5 rules, 2 protection instances |
docs/_captures/asset-grounding/ |
captures | the refusals and trip runs above |
Limits¶
- The bundled provenance blocks are not checked against the sources:
ValidateProvenanceenforces[VERIFY]on placeholders only, and no test compares aRecordedvalue withFrequencyLimitsor with the GDA lake. - In
gb-syntheticthe grid-following wind and solarrocofTripConfirmS(0.5 s) and the legacyrocofTripConfirmS(0.0 s) carry noRecordedentry, althoughgb-fullanddg-alomcpcite them, andgb-syntheticsets nolocalRocofUpliftFactoron its inverter classes. - The vector-shift trip is a phase-change proxy, not a phase-jump detector: phase is integrated smoothly (
phase += 360 * (f - f0) * dt), so the relay compares the accumulated change overmax(dt, 0.02 s)withvectorShiftTripDegand fires on a sustained frequency offset, not on a discrete angle step. - The depth of the 2019-08-09 cascade is not predicted: the embedded volume that trips is
[VERIFY]and swept, the nadir is a strong function of it, and the result is taggedUnvalidatedAtScale; a blind cross-event test needs a second GB cascade event, and none exists. - Every inverter control constant, the BESS duration, the OLTC delays, the thermal time constants, fault level and the legacy-versus-upgraded embedded-generation fraction are placeholders or swept, not recorded (listed under "Quantities with no source").
- No synchronous-machine class exists: a machine trip must drop
Elive, and no controller does so. - The GDA lake files cited here are outside this repository, so their values cannot be re-derived from it.
See also¶
- Ch. 5, Frequency dynamics: the
pAssetFleetswing term. - Ch. 14, Cascade validation and blind prediction: the 2019-08-09 depth and the envelope argument.
- Ch. 15, Protections: the protection stages and grading.
- Ch. 27, Battery energy storage: the chemistry pack behind
batteryChemistry. - Ch. 9, GDA integration: the GDA lake the class sources come from, and the binding of the
gb-fullfleet. - Extending GridSim: authoring a class and a kind.