Protections
A generic protective-device layer: RoCoF / Loss-of-Mains, under/over-frequency, df/dt, low-frequency
demand disconnection (LFDD), under/over-voltage, thermal-overload, and topology auto-reclosers. It is
deliberately not a protection simulator — real relay settings are unobtainable — so settings are generic
and [VERIFY]-tagged, grounded only where the GDA event-detector data supports it. See
docs/manual/18-extending-gridsim.md for the authoring recipe and
docs/technical/13-asset-dynamics-data-grounding.md for the grounding ledger.
The abstraction (src/GridSim.Core/Assets/Protection/)
A protection is a generalisation of the inverter self-protection block in GridFollowingInverter (RoCoF
dwell + sub-cycle uplift, vector-shift, LVRT). It reuses the asset-dynamics seam wholesale.
ProtectionStage— one definite-time stage: aMonitoredQuantity(Frequency,Rocof,Voltage,LoadingFraction,PhaseShift) crossing aThresholdin aThresholdSense(Above/Below), held forDwellSeconds, drives aProtectionAction. RoCoF-LoM is{Rocof, Above, rocofTripHzPerS, rocofTripConfirmS, TripInfeed}.ProtectionAction— maps onto the channelsAssetActuationalready exposes, so no integrator change:TripInfeed→TrippedInfeedMw(lost infeed folded into the swing deficit);ShedDemand→ a positiveActivePowerMwstep (LFDD: load removed reduces the deficit);InjectStep→ a signed swing-power step;OpenBranch/RestoreBranch→ a topology request (the recloser; §Topology).ProtectionEvaluator— the extracted, quantity-agnostic dwell/edge state machine: accumulate time over threshold, emitFiredonce when the dwell is met, and (if a finiteAutoRestoreDelayS)Restoredafter it. Reuses the inverter'sMeasurementDelayfor relay latency and the phase-window vector-shift proxy.ProtectionController : IActiveAssetDynamics— builds a stage per present class parameter, drives one evaluator each, and aggregates the result. Contributes exactly0 MWuntil a stage operates.Trippedreflects an infeed-trip or branch-open only (a demand-shed operates but is not a "tripped unit").
Placement and JSON
Device description is an AssetClass of kind: "protection" in asset-classes.json (reusing the
[VERIFY] provenance validation). Placement is a separate protections.json — {name, class, monitorBus, capacityMw, targetBranch?} — because a protection needs an explicit location and the MW it acts on, which
fuel-type binding cannot express. Both are optional and null-gated via GridModel.Protections
(ProtectionModel = library + placed ProtectionInstances). The fleet builds a ProtectionController per
instance in AssetDynamicsFleet.Build.
Canonical stage parameters (AssetParams): rocofTripHzPerS+rocofTripConfirmS+localRocofUpliftFactor,
ufTripHz/ofTripHz (+dwell), dfDtTripHzPerS, uvTripPu/ovTripPu (+dwell), overloadTripFraction,
lfddStageHz+lfddBlockMw+lfddDwellS, vectorShiftTripDeg, lvrtVoltagePu, autoRecloseDelayS.
Two integration tiers
Tier A — frequency/injection domain (grounded). RoCoF/LoM, U/O-frequency, df/dt, and dynamic LFDD
plug into the existing AssetDynamicsFleet as extra active controllers — the integrators
(AssetFrequencyDynamics, AssetAwareFrequencyIntegrator) are untouched. Dynamic LFDD is the headline:
a ShedDemand stage fires when the simulated frequency crosses 48.8 Hz and injects +lfddBlockMw,
turning the previously-scripted LFDD RecordedAction into an endogenous, closed-loop protection (it must
replace the scripted step, not run alongside it — the ExogenousLossEvents split avoids double-counting).
Tier B — voltage/loading domain. U/O-voltage and thermal-overload read AssetStepContext.LocalVoltagePu
/ LoadingFraction, which the pure lumped COI model does not carry (defaults 1.0 / 0.0) — so they are
inert without a network solve (a guard test codifies this). LiveSim feeds the solved per-bus voltage
and per-bus max-incident-branch loading via AssetAwareFrequencyIntegrator.SetNetworkState (held constant
across the fast sub-steps within a tick). StepCoi's optional voltageOf/loadingOf suppliers default to
null, preserving the batch reduction gate.
Topology recloser and islanding (the network work)
A recloser class (has autoRecloseDelayS) turns its trip stages into OpenBranch actions targeting
targetBranch; the controller tracks its open branches and AssetDynamicsFleet.OpenBranches() aggregates
them. The network layer:
Branch.InService(defaulttrue) —YBus.Build/BuildSparse,BackwardForwardSweep, andNewtonShell.ComputeLineFlowsskip an out-of-service branch. With every branch in service the admittance accumulation order is unchanged, so the solve is bit-identical to before (pinned by the power-flow suite before any topology feature is exposed).IslandPartition.Prepare— union-find over in-service branches finds the connected components; per island: keep it if it has a slack; elect the largest-generator bus as reference if it has generation but no slack; black it out (drop its buses/branches, book the lost load/gen) if it has no generation. A single connected component with a slack (everything in service) is a pure no-op.TopologyOps.Apply— marks the recloser-opened branches out of service and runs the partition.LiveSim— applies the fleet's open branches before solving, feeds the blackout net-load (LostLoadMw − LostGenMw) to the swing model as a one-off perturbation (load lost = surplus = frequency up), and reclose reverses it. Auto-reclose restores the branch after the delay and the next tick re-solves.
Grounding
Grounded from the GDA event-detector layer (Events/{rocof_events,lfdd_risk}): RoCoF/LoM grades
(0.0625/0.125/0.25/0.5 Hz/s; modern ALoMCP 1.0 Hz/s + 500 ms delay) DerivedFromRecorded/Recorded; LFDD
first stage 48.8 Hz (Recorded) and the 632 MW 2019 block (DerivedFromRecorded); statutory U/O-frequency
band. Ungrounded → [VERIFY]: recloser/auto-reclose/breaker operation, measured voltage, thermal
loading, and fault level — none exist in the lake. Emitter: GDA NetworkModel/build_protections.py, wired
into assemble/run.py.
Invariants (pinned by tests)
- Frequency reduction gate — no
protections.json(or a never-operating fleet) ⇒ byte-identical swing trajectory (ProtectionTests). - Power-flow reduction gate — all branches in service ⇒ bit-identical solve (
IslandPartitionno-op; the power-flow suite). - Voltage/thermal inertness — provably inert in the pure lumped model until the context feed supplies
the quantity (
ProtectionVoltageTests). - [VERIFY] discipline — every unmeasurable setting is
CuratedPlaceholderwith the literal token.
Key files
Assets/Protection/{ProtectionElement,ProtectionEvaluator,ProtectionController,ProtectionModel}.cs,
IO/Json/ProtectionDto.cs, PowerFlow/{IslandPartition,TopologyOps}.cs, Model/Model.cs (Branch.InService,
GridModel.Protections), Simulation/LiveSim.cs (context feed + topology), PowerFlow/YBus.cs +
NewtonShell.cs + BackwardForwardSweep.cs (InService skip). Tests: ProtectionTests,
ProtectionEvaluatorTests, ProtectionVoltageTests, IslandPartitionTests, ProtectionRecloserTests.