Co-Study4Grid / docs /features /game-mode-matpower.md
github-actions[bot]
Deploy f7b8bf6
1c730d1
|
Raw
History Blame Contribute Delete
8.28 kB

Game Mode: France RTE Matpower — offline dataset pipeline

Offline pipeline that turns the public MATPOWER RTE cases (case6468rte, case6470rte, case6495rte, case6515rte — real 2013 French EHV operating points, ~6500 buses, © Josz/Fliscounakis/Maeght/Panciatici, CC-BY-4.0) into a Game Mode scenario family alongside game-mode-rte7000-tht.md.

Status: complete chain. Offline tooling under scripts/game_mode/matpower/, the packaged scenario database (data/rte_matpower/scenarios.json), the generated frontend presets and the third Game Mode mode are all in place. The database grows as grading progresses — see Grading.

Why a node-breaker rebuild is needed

MATPOWER cases import as BUS_BREAKER with zero switches, so the expert recommender has no topological levers (no coupler opening, no node splitting) — only redispatch and load shedding. On these heavily loaded states that grades almost everything hard and gives players nothing to manoeuvre.

node_breaker.rebuild_node_breaker rebuilds the same electrical network as NODE_BREAKER: every voltage level gets busbar sections and every feeder a bay (breaker + disconnectors), and multi-feeder VLs get a closed coupler named *_COUPL.* — the name the recommender keys on — so opening it splits the node into an open_coupling action.

Two invariants make the rebuild faithful; both were regressions found the hard way:

  • Each substation keeps its loaded electrical node count. The import leaves 208 VLs holding more than one bus (up to 9). Collapsing them onto a single busbar rewires the grid — ~610 MW of extra losses, 26° angle shifts, 3 GW flow errors, base peak 324 % vs 199 %. Each source bus therefore gets its own busbar, and couplers between genuinely distinct nodes are created open.
  • Out-of-service elements stay out. ~700 of 1389 generators carry MATPOWER STATUS = 0; pypowsybl's bay helpers create every feeder connected, and the phantom generation alone stops the load flow converging.

Also copied, because each is individually required for convergence or fidelity: shunt compensators, phase tap changers, generator reactive limits, the slack terminal, and the solved (VM, VA) warm start (these cases are stiff and only converge from their own operating point).

Result on case6515rte: 6515/6515 buses, base peak 199.2 % / 10 overloads — identical to the bus-branch source, converged, 1591 coupler breakers (266 open / 1325 closed).

Positioning on a France map

The cases are anonymised (integer buses, no names, no coordinates). geo.py recovers a France layout by matching each case's 400 kV postes to a named THT reference snapshot (the committed grid_5384e039, whose VL ids are real RTE names) through the grid_snapshot_reconstruct Rosetta electrical-distance percolation, then chaining matched substations to grid_layout_rte.json.

This yields a genuine identity mapping for 520 of 6515 buses → 125 real RTE substations (all at 380 kV — Rosetta only matches the 400 kV backbone), persisted as rte_substation_map.json. Everything below 380 kV is placed at plausible real 225 kV positions or propagated along the graph: positional only, no identity claimed.

Where a bus is identified, geo.reference_vl_structure() supplies that substation's real RTE busbar count, which the rebuild replicates — 430 VLs on case6515rte, giving real 4-, 6- and 9-busbar substations instead of a uniform double busbar.

Modules

Module Role
current_limits.py APPARENT_POWER (MVA) → CURRENT (A) permanent limits; without them a matpower network reports zero loadings
geo.py Rosetta identity match + France layout + real RTE busbar structure
node_breaker.py The NODE_BREAKER rebuild, fidelity copies and BusView validation
actions.py Curated action space — open_coupler_* in the Co-Study4Grid schema
build_network.py Stage 1 per case, resumable, into an opaque grid_<sha1[:8]> folder
grade.py Stage 2 — difficulty grading (easy / medium / hard), resumable
build_scenarios.py Stage 3 — fold every graded.jsonl into scenarios.json
gen_matpower_presets.py Stage 4 — emit the frontend presets from that database
python scripts/game_mode/matpower/build_network.py all           # ~225 s per case
python scripts/game_mode/matpower/grade.py all                   # ~14 s per contingency
python scripts/game_mode/matpower/build_scenarios.py             # -> data/rte_matpower/scenarios.json
python scripts/game_mode/matpower/gen_matpower_presets.py        # -> frontend/src/game/matpower*
python scripts/game_mode/gen_network_previews.py                 # -> public/game/preview-matpower.svg
python scripts/game_mode/pack_grids.py data/rte_matpower         # network.xiidm -> .gz.b64 for commit

Stages 3 and 4 are cheap and idempotent: re-run them any time grading advances. Scenario ids are derived from (gridId, contingency), so a rebuild keeps the ids stable and does not orphan recorded sessions or retained solutions.

Transport and packaging

The 4 networks are ~20 MB of XIIDM each — too large to commit raw, and a binary .zip would need Git-LFS (whose object endpoint is blocked in some CI egress policies). They ship as network.xiidm.gz.b64 (gzip + base64, 8.7×: 20.5 MB → 2.4 MB), exactly like the THT family. pack_grids.py encodes, decode_tht_grids.py decodes both families (it is what the Dockerfile calls), and the decoded network.xiidm is gitignored as the build artifact it is.

Dates are hidden exactly as in the THT family: opaque grid folders, and titles carrying only month + weekday + hour-period. mapping_private.json and rte_substation_map.json keep the real identity recoverable for analysis and are never surfaced to players.

Grading

Difficulty mirrors the THT rule at monitoring_factor = 0.95: easy if a suggested unitary action resolves every contingency-attributable overload, medium if a first-identified superposition pair does, hard otherwise. Resolution is base-relative — pre-existing overloads the contingency does not worsen are not counted.

The grader must reset the recommender before every contingency. run_analysis_step2 mutates network state, so grading in a plain loop silently poisons every subsequent contingency. configure() is therefore re-run per contingency (grade_all(..., reset_each=True), the default).

That is measured, not defensive: on grid_6be3a179 (case6515rte), grading the same 12 contingencies both ways gives 9 divergent verdicts. The first three agree, then every remaining case collapses to trivial with zero overloads — and the error is silent in the worst direction, since a real hard scenario disappears from the database as "nothing to solve" rather than failing loudly. Reloading the 20 MB network per contingency costs ~4× (2.4 s → 10.6 s on that grid; ~14 s/contingency across the family). --no-reset exists for raw timing only and is documented as producing wrong verdicts.

The four cases together yield 901 non-antenna constraining contingencies (165 / 270 / 265 / 201 for case6468 / 6515 / 6495 / 6470), out of ~7420 tested each — about 3.5 h of grading, resumable via graded.jsonl.

An earlier note in this file predicted a hard-skewed distribution (86 % medium/hard). That measurement came from the poisoned loop; with the reset the early distribution is far more balanced (24 % easy / ~68 % medium / ~8 % hard over the first 38 graded), which is what makes three playable tiers viable.

The third Game Mode

GameConfigScreen carries one set of branches parameterised by graded family (GRADED in that module) rather than one branch per family, so France THT and France EHV share the level picker, case count, summary and preview. data-testids are derived from the family key (game-mode-matpower, game-matpower-count, …), which keeps the THT ids — and the tests that assert them — unchanged.

The seeded sampler is shared too: frontend/src/game/sampleScenarios.ts, used by both generated preset modules instead of being emitted twice.