Spaces:
Paused
Paused
File size: 7,069 Bytes
1c730d1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | # 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()
|