Co-Study4Grid / scripts /game_mode /matpower /build_network.py
github-actions[bot]
Deploy f7b8bf6
1c730d1
Raw
History Blame Contribute Delete
7.07 kB
# Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
# SPDX-License-Identifier: MPL-2.0
"""Stage 1 of the France RTE Matpower game dataset: assemble one game grid per
MATPOWER case from a snapshot **already in detailed topology**.
This stage performs NO grid transformation. Every reconstruction step —
MATPOWER conversion, current limits, Q calibration, AC settling, Rosetta
re-identification, real RTE substation structure, node/breaker detailed
topology (libTOPO) — lives in the ``grid_snapshot_reconstruct`` repo, which
produces per case:
<detailed>/<caseName>/network_detailed.xiidm NODE_BREAKER, converged
<detailed>/<caseName>/grid_layout.json voltage level -> [x, y]
<detailed>/<caseName>/rte_substation_map.json bus -> real substation
<detailed>/<caseName>/detailed_report.json invariants + flow fidelity
Produce them with::
python -m grid_snapshot_reconstruct.matpower_detailed all [--map <v7.json>]
This stage then only does what is specific to the GAME: opaque grid identity
(the real date stays private), the action space offered to players, and the
base-relative N-1 screen that feeds scenario building. Persists, RESUMABLY:
data/rte_matpower/grids/<opaqueId>/network.xiidm
data/rte_matpower/grids/<opaqueId>/grid_layout.json
data/rte_matpower/grids/<opaqueId>/actions.json
data/rte_matpower/grids/<opaqueId>/n1_contingencies.json
data/rte_matpower/mapping_private.json (opaqueId -> real date, private)
Run: python scripts/game_mode/matpower/build_network.py [caseName|all]
"""
from __future__ import annotations
import datetime
import hashlib
import json
import os
import shutil
import sys
import time
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from actions import build_action_space # noqa: E402
RECON_DIR = Path(os.environ.get(
"RECON_DIR", "/Users/antoine/Dev/Grid_snapshot_reconstruct"))
if str(RECON_DIR) not in sys.path:
sys.path.insert(0, str(RECON_DIR))
from grid_snapshot_reconstruct import tht_dataset as T # noqa: E402
import pypowsybl as pp # noqa: E402
REPO = Path(__file__).resolve().parents[3]
DATA = REPO / "data" / "rte_matpower"
GRIDS = DATA / "grids"
#: Where ``grid_snapshot_reconstruct.matpower_detailed`` wrote its artifacts.
DETAILED_DIR = Path(os.environ.get(
"MATPOWER_DETAILED_DIR", RECON_DIR / "data" / "matpower_detailed"))
#: Re-identified 2013 timestamps (kept here for the private mapping only).
INSTANTS = {
"case6468rte": "2013-01-14T06:30",
"case6470rte": "2013-01-18T00:30",
"case6495rte": "2013-01-15T19:00",
"case6515rte": "2013-01-18T19:00",
}
_PERIODS = [(6, "night"), (12, "morning"), (18, "afternoon"), (24, "evening")]
#: Artifacts copied as-is from the detailed snapshot into the grid folder.
_COPIED = {"network_detailed.xiidm": "network.xiidm",
"grid_layout.json": "grid_layout.json",
"rte_substation_map.json": "rte_substation_map.json"}
def opaque_id(case_name: str) -> str:
return "grid_" + hashlib.sha1(case_name.encode()).hexdigest()[:8]
def period_title(iso: str) -> str:
"""month + weekday + hour-period, NO year (hidden-date convention)."""
dt = datetime.datetime.fromisoformat(iso)
hour_period = next(name for bound, name in _PERIODS if dt.hour < bound)
return f"{dt.strftime('%B')}{dt.strftime('%A')} {hour_period}"
def detailed_dir(case_name: str) -> Path:
"""Directory holding the detailed snapshot of ``case_name``, or exit with
the command that produces it."""
d = DETAILED_DIR / case_name
if (d / "network_detailed.xiidm").exists():
return d
raise SystemExit(
f"Instantané en topologie détaillée absent : {d}\n"
f"Produisez-le côté grid_snapshot_reconstruct :\n"
f" python -m grid_snapshot_reconstruct.matpower_detailed "
f"{case_name} --out {d}\n"
f"(ou pointez MATPOWER_DETAILED_DIR sur le répertoire de sortie)")
def load_detailed(case_name: str):
"""The detailed snapshot as ``(network, report, source_dir)``."""
d = detailed_dir(case_name)
net = pp.network.load(str(d / "network_detailed.xiidm"))
rp = d / "detailed_report.json"
return net, (json.loads(rp.read_text()) if rp.exists() else {}), d
def build_case(case_name: str) -> str:
gid = opaque_id(case_name)
out = GRIDS / gid
out.mkdir(parents=True, exist_ok=True)
net_path = out / "network.xiidm"
layout_path = out / "grid_layout.json"
n1_path = out / "n1_contingencies.json"
actions_path = out / "actions.json"
if all(p.exists() for p in (net_path, layout_path, n1_path, actions_path)):
print(f"[{case_name} -> {gid}] already built; skipping")
return gid
t0 = time.time()
print(f"[{case_name} -> {gid}] assembling ...")
src_dir = detailed_dir(case_name)
for src_name, dst_name in _COPIED.items():
src, dst = src_dir / src_name, out / dst_name
if src.exists() and not dst.exists():
shutil.copyfile(src, dst)
rp = src_dir / "detailed_report.json"
report = json.loads(rp.read_text()) if rp.exists() else {}
det = report.get("detailed", {})
print(f" detailed snapshot: buses={det.get('buses')} "
f"switches={det.get('switches')} "
f"RTE-structured VLs={det.get('rte_structured_vls')} "
f"converged={det.get('converged')} "
f"flow-fidelity median={report.get('flow_bench', {}).get('median_MW')} MW")
net = pp.network.load(str(net_path))
if not actions_path.exists():
actions_path.write_text(json.dumps(build_action_space(net)))
print(f" actions: {actions_path.name}")
if not n1_path.exists():
ts = time.time()
settle = T.settle_state(net, "eps0.01")
ant = T.antenna_analysis(net)
res = T.screen_n1(net, settle.get("stage", "eps0.01"), antennas=ant,
chunk_size=250)
n1_path.write_text(json.dumps(res))
conts = res.get("contingencies", [])
non_ant = [c for c in conts if not c.get("antenna")]
print(f" scan: {len(non_ant)} non-antenna constraining "
f"(of {res.get('total_contingencies_tested')} tested) "
f"in {time.time()-ts:.0f}s")
_update_mapping(case_name, gid)
print(f"[{case_name} -> {gid}] done in {time.time()-t0:.0f}s")
return gid
def _update_mapping(case_name: str, gid: str):
mp = DATA / "mapping_private.json"
data = json.loads(mp.read_text()) if mp.exists() else {}
iso = INSTANTS.get(case_name, "")
data[gid] = {
"case_label": case_name,
"case_date": iso,
"period_title": period_title(iso) if iso else case_name,
"network_path": f"data/rte_matpower/grids/{gid}/network.xiidm",
}
mp.write_text(json.dumps(data, indent=2))
def main():
which = sys.argv[1] if len(sys.argv) > 1 else "all"
names = list(INSTANTS) if which == "all" else [which]
for name in names:
build_case(name)
if __name__ == "__main__":
main()