Skip to content

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:

  1. the entry named for the parameter;
  2. the entry named _default;
  3. a synthetic CuratedPlaceholder with 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.

  1. Each class kind must be one of the spellings above (ParseAssetKind): otherwise asset class '<name>' has unknown kind '<s>'.
  2. Each provenance tag must parse as a ProvenanceTag name, ignoring case (ParseProvenanceTag): otherwise asset class '<name>' parameter '<key>' has unknown provenance tag '<s>'.
  3. AssetClassLibrary construction refuses a duplicate class name (case-insensitive) and collects every problem from AssetClassSupport.Problems into one exception headed Unsupported asset class input: a class of an unsupported kind (synchronousMachine), a refused key, a key no consumer of the kind reads.
  4. AssetClassLibrary.ValidateOrThrow runs AssetClass.ValidateProvenance on every class and lists every violation in one exception headed Invalid asset-classes.json. ValidateProvenance reports each provenance entry (explicit or _default) that is CuratedPlaceholder and whose note lacks the literal [VERIFY].
  5. asset-bindings.json is refused when it is present and asset-classes.json defines no classes, and when a rule names a class that does not exist.
  6. protections.json is refused when asset-classes.json defines no classes, when an instance names an unknown class, and when the named class is not of kind protection.

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). 0 is 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's ufTripHz, ofTripHz and lfddStageHz are 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 becomes systemNominal + (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 a settingsNominalHz that is not a finite frequency above 0, is refused by name, at library load (AssetClassSupport.ProtectionFrequencyProblems) and again when a class built in code reaches ProtectionController. A setting written for a 60 Hz system must declare settingsNominalHz 60.
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. In gfm-vsm-study-2024 it is read as Ev = synInertia * f0 / 2 in 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.parquet emergency_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 capacityMwh and 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_flows product, 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_delays is 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, virtualReactancePu and powerLoopBandwidthHz are set by no bundled class. The impedance screen takes the ConverterDefaults literature values (PLL bandwidth 20 Hz, the one default EMT, impedance and small-signal share; damping 0.707) and labels the result CuratedPlaceholder; it labels it Recorded only when the bandwidth came from a class whose provenance for pllBandwidthHz is Recorded or DerivedFromRecorded and no override was typed. The same rule applies to virtualReactancePu.

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

abs(RoCoF) * 1.16 > 0.125        that is   abs(RoCoF) > 0.125 / 1.16 = 0.1078 Hz/s

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: ValidateProvenance enforces [VERIFY] on placeholders only, and no test compares a Recorded value with FrequencyLimits or with the GDA lake.
  • In gb-synthetic the grid-following wind and solar rocofTripConfirmS (0.5 s) and the legacy rocofTripConfirmS (0.0 s) carry no Recorded entry, although gb-full and dg-alomcp cite them, and gb-synthetic sets no localRocofUpliftFactor on 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 over max(dt, 0.02 s) with vectorShiftTripDeg and 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 tagged UnvalidatedAtScale; 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 E live, 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