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`](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](#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 |
```bash
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-testid`s
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.