#!/usr/bin/env python3 # Copyright (c) 2025-2026, RTE (https://www.rte-france.com) # SPDX-License-Identifier: MPL-2.0 """Sample N scenarios of a chosen difficulty from the RTE7000 France THT graded scenario database (``data/rte7000_tht/scenarios.json``). Difficulty is the expert recommender's solvability at the 95 % monitoring factor: easy - a suggested UNITARY action resolves the overloads; medium - no unitary resolves, but a first-identified COMBINATION does; hard - neither a unitary nor a first combination resolves. The output is a ``GameStudy[]`` list (the exact shape the Game Mode UI / backend consume), so it can be dropped straight into a session config. Dates stay hidden: titles carry only month + weekday + hour-period, and grids live under opaque ids. Usage: python3 scripts/game_mode/sample_rte7000.py --difficulty easy --n 5 python3 scripts/game_mode/sample_rte7000.py --difficulty hard --n 3 --seed 7 \ --out /tmp/session_studies.json """ import argparse import json import os import random REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) DB = os.path.join(REPO, "data", "rte7000_tht", "scenarios.json") DIFFICULTIES = ("easy", "medium", "hard") def load_db(path=DB): with open(path, encoding="utf-8") as f: return json.load(f) def to_game_study(s): """Project a DB scenario onto the GameStudy fields the UI/backend use. The ``solution`` field (reference remediation) is intentionally dropped — it is analysis metadata the player must not see. """ return { "id": s["id"], "label": s["label"], "networkPath": s["networkPath"], "actionFilePath": s["actionFilePath"], "layoutPath": s.get("layoutPath"), "contingencyElementId": s["contingencyElementId"], "contingencyLabel": s.get("contingencyLabel"), "baselineMaxLoadingPct": s.get("baselineMaxLoadingPct"), "description": (f"{s['title']}. Loss of {s['contingencyLabel']} drives a worst-case " f"{s.get('baselineMaxLoadingPct')}% line loading. Bring every monitored " f"line back under the 95% monitoring limit."), } def sample(difficulty, n, seed=None, db_path=DB, spread_grids=True): """Return up to ``n`` GameStudy dicts of ``difficulty``, sampled without replacement. With ``spread_grids`` we round-robin over distinct grids first so a small sample covers different operating points rather than one grid.""" if difficulty not in DIFFICULTIES: raise ValueError(f"difficulty must be one of {DIFFICULTIES}, got {difficulty!r}") db = load_db(db_path) pool = [s for s in db["scenarios"] if s["difficulty"] == difficulty] rng = random.Random(seed) rng.shuffle(pool) if spread_grids: by_grid = {} for s in pool: by_grid.setdefault(s["gridId"], []).append(s) ordered, grids = [], list(by_grid.values()) while any(grids): for g in grids: if g: ordered.append(g.pop()) pool = ordered return [to_game_study(s) for s in pool[:n]] def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--difficulty", choices=DIFFICULTIES, required=True) ap.add_argument("--n", type=int, default=5, help="number of scenarios to sample") ap.add_argument("--seed", type=int, default=None, help="RNG seed for reproducible draws") ap.add_argument("--db", default=DB, help="path to scenarios.json") ap.add_argument("--out", default=None, help="write the GameStudy[] JSON here (default: stdout)") args = ap.parse_args() db = load_db(args.db) counts = db.get("counts", {}) studies = sample(args.difficulty, args.n, seed=args.seed, db_path=args.db) payload = json.dumps(studies, indent=2, ensure_ascii=False) if args.out: with open(args.out, "w", encoding="utf-8") as f: f.write(payload) print(f"wrote {len(studies)} '{args.difficulty}' scenario(s) -> {args.out} " f"(pool: {counts.get(args.difficulty, 0)})") else: print(payload) if __name__ == "__main__": main()