NESO RP1 + MECHANICAL panels, drill-down, and the bridge map fix¶
Two situational-awareness control-room tabs added to GridSim (2026-07-09), streamed frame-by-frame
from the GDA lake over the bridge, plus a reusable drill-down inspector and a fix for the
bridge/replay map geolocation. Both panels are grounded in the TSGB corpus
(W:\Code\TSGB-2026\Proposals\MIF, Policy/0021 - The 1,800 Megawatt Fiction.md).
NESO RP1 - "where am I now / where am I going / how long do I have"¶
A situational-awareness page, not a dashboard. Hero quantity is purchased time - seconds of frequency ride-through on real intrinsic mass.
- Physics - new pure
src/GridSim.Core/Dynamics/StabilityMetrics.cs: PurchasedTimeSeconds(intrinsicGvaS, credibleLossMw, currentHz, thresholdHz=49.2)-t = deltaf/RoCoF,RoCoF = f0*deltaP/(2E). Threshold 49.2 Hz (deltaf 0.8 Hz) is the corpus working set-point (Policy 0021 sec. 6).DynamicMifGvaS(loss, rocofLimit)=f0*deltaP/(2*RoCoF_limit)(S_base cancels).RocofFor(...)wrapsInertiaEstimator.PredictRocof;LargestCorrelatedGroup(groups, flows).- Tests (
tests/GridSim.Tests/StabilityMetricsTests.cs) reproduce the corpus tables: 3,600 MW -> 720->6.4 s, 360->3.2 s, 155->1.4 s, 102->0.9 s; and the MIF 1,800 MW table. - Inertia split (fuel gauge, MIF 0001 sec. 10) - GVA.s total = (i) + (p) (additive, never petrol>tank).
(i) intrinsic = synchronous mass (lake
synchronous_inertia_gvas, else computed from the model's synchronous generatorssum H*MBase/1000); (p) = NESO "Market Provided" (labelled market/contracted, not pure proxy - per MIF 0004 sec. 8 it can include real sync condensers). - Credible loss - shows both: the live largest-possible-now (biggest online synchronous unit +
largest correlated HVDC group: Sellindge IFA+ElecLink, North-Sea NSL+Viking; the corridor table and the
link-name matching live in
GridSim.Core.Presentation.InterconnectorCorridors, shared by web and WPF, and recognise every source's spelling - gb-full's "IFA Interconnector", "NS Link interconnector", "Viking Link Denmark Interconnector", the reduced cases' "North Sea Link" / "Viking Link", and the bridge raw ids) and the hard 3,600 MW Dogger Bank design basis, with the 1,800 MW convention struck through (Policy 0021). - Also: inertia-by-source (seconds), 30-min stability trend, auto-generated handover narrative,
BM Stability Delta (each acceptance green/red by inertia direction - needs the lake's BMU->fuel tag),
interconnector summary with correlated-trip dots, LFDD traffic light (49.5/49.2/48.8 ladder from
find_lfdd_events.py), and coherence-cost GBP/h by constraint (RIGHT column; gb-full has no boundary model so boundary bars are flagged absent, cost still shown).
MECHANICAL / Plant Engineer - "what the MACHINES are doing"¶
Per-machine, asset-level (Views/MachineEngineerVm.cs + MachineEngineerView.xaml +
Visuals/PqDiagram.cs, a custom P-Q capability control). Five sections: per-machine state
(risk-sorted by loading), shaft/torsional + thermal, protection envelope (P-Q dot + dual RoCoF-relay
margin: current vs the original 0.125 AND relaxed 1.0 limit - Policy 0021 sec. 10), inertia-per-unit
(sum asset KE vs reported (i), synthetic gap), fleet trajectory (MW/min + projected inertia +15 min).
Ramp identity and period: each machine's ramp is its own frame-to-frame change, tracked by generator
index (checked against bus and name, GridSim.Core.Presentation.UnitRampHistory) because gb-full unit names
are not unique; it is divided by the frame interval - the WPF view derives it from the frame clock, the web
view follows the transport STEP picker (SimSession.SetStepMinutes).
Provenance is explicit and honest: MW / loading / inertia (H*MBase) / ramp / RoCoF-margin are REAL. MVAr, power factor, excitation, AVR/governor mode, shaft torsional stress, stator/rotor temps, certified-J + type-test dates, committed starts are shown "-" or flagged synthetic until a plant feed is wired. Do NOT reconstruct MVAr from per-bus net QInj - it misattributes shunt/line-charging reactive to a small co-located generator (an 11 MW CHP reads +368 MVAr). Filters to synchronous units
=10 MW to drop sub-MW embedded plant noise.
Drill-down (click a paused frame to explore it)¶
Views/InspectorVm.cs + InspectorView.xaml - a reusable overlay: headline stat tiles + constituent
rows + a back-stack (< back) + > chevron (shown when a row is drillable) + x close.
- RP1: dispatch-stack / inertia-by-source rows -> fuel breakdown (constituent units + combined totals/averages); BM row -> acceptance detail; interconnector row -> flow + correlated-trip corridor with the co-members drillable. Nested: a fuel's unit rows drill to a unit detail (whose Fuel row drills back up); interconnector members hop between each other.
- MECHANICAL: machine row -> full per-machine card.
- Wiring idiom (important): wrap the
ItemsControlrow DataTemplate in<Button Style="{StaticResource RowLinkBtn}" Command="{Binding DataContext.InspectXCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding [Fuel|/row]}">. A bareMouseBindingon the row Grid does not fire reliably inside aScrollViewer. Per-row nested drill usesInspectRow.Drill(anICommandset per row vianew RelayCommand(() => ...)).
Bridge / lake wiring¶
- Protocol (
Core/IO/BridgeProtocol.cs):OperatorBlockgainedSynchronousInertiaGvaS,OutturnInertiaGvaS,MarketInertiaGvaS;BalancingActionDtogainedFuelType,Synchronous;BusDtogainedX,Y. Fed via the existingMarketDataReceivedside-channel + per-frameUpdate. - GDA builder (
NetworkModel/build_operator_market_frames.py): enrichesbalancing_actions_jsonfromDataSources/Elexon/Reference/bmunits_all.json(elexonBmUnit->fuelType; synchronous = {CCGT,OCGT,COAL, BIOMASS,NUCLEAR,GAS,OIL,PS,NPSHYD}) and as-of joins the inertia columns. The server'sopmkt_load_scalar_tablereads an EXPLICIT column list (bridge_server.py~line 340) - new columns must be added there. Rebuild the table after editing the builder.
Map fix (bridge/replay geolocation)¶
Symptom: under the live bridge, gb-full stacked every node at the Cornwall corner. Cause: the
presolved topology.json carried no per-bus x/y (gridsim_export.to_solve_export dropped the
geolocated coords that out/<case>/buses.json already holds). Fix: to_solve_export now emits x/y;
C# BusDto/BuildModel read them; existing topology.json for all 7 cases were patched in place by
merging x/y from buses.json (no re-solve). Restart the bridge to reload topology.json. NB the
bridge bus names are LTDS tower ids ("Tower 48CM13"), so C#-side GeoLocator.Apply name-matching is a
dead end (0% match) - the coords must ride in the stream.
Run it¶
python W:\Code\GDA\v1\Applications\GridSim-Bridge\bridge_server.py # :8770; restart to reload topology
dotnet build src/GridSim.Wpf/GridSim.Wpf.csproj -c Release # close the app first (file locks)
dotnet run --project src/GridSim.Wpf -c Release -- bridge --zone gb-full --back 30 --tab "NESO RP1"
--zone = gb-full or a DNO zone; --back 30 lands in the presolved 2026-06-10 window; --tab boots a tab.
- Tests: dotnet test tests/GridSim.Tests (451). Bridge acceptance:
python W:\Code\GDA\v1\Applications\GridSim-Bridge\test_bridge_stream.py gb-full (asserts the inertia
split + BM fuel tags).
- Screenshot to iterate - capture with PowerShell PrintWindow (flag 2, PW_RENDERFULLCONTENT); it
grabs the app window without stealing focus. Do NOT script mouse_event clicks while the user is at
the keyboard - clicks need real foreground and will fight them (and miss).
Known follow-ups¶
- Asset-sum-vs-reported "synthetic gap" (MECHANICAL sec. 4) shows 0 when the lake intrinsic side-channel is null for that period (SetIntrinsic timing) - wire more robustly.
- Wire real plant SCADA to fill the flagged MVAr/PF/excitation/torsional/temps/certified-J fields.
- RP1 boundary bars are empty for gb-full (no boundary model in the streamed case) - carry ETYS boundaries in the model or compute them.