Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """Record a scenario to a replayable file. | |
| Demo insurance. The primary demonstration is always the live simulation; this | |
| exists so that if a live run cannot be created on the day — a broken | |
| dependency, a machine that cannot carry the agent count — the dashboard can | |
| still show the complete result rather than an error. | |
| A recording is a sequence of the same frames the WebSocket would have sent, | |
| plus the strategy comparison captured at the decision point, so a replay is | |
| visually and numerically identical to the live run it was made from. It is a | |
| recording of a real run, never a hand-written script. | |
| Run: python scripts/record_fallback.py [--scenario ...] [--interval 10] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import datetime as dt | |
| import json | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT / "backend")) | |
| from flowtwin.config import FALLBACK_DIR, SETTINGS # noqa: E402 | |
| from flowtwin.runtime.session import SessionConfig, SimulationSession # noqa: E402 | |
| from flowtwin.venue import load_scenario # noqa: E402 | |
| async def record(scenario_id: str, interval_s: float, decision_t_s: float | None, | |
| seed: int | None) -> Path: | |
| scenario = load_scenario(scenario_id) | |
| cfg = SessionConfig( | |
| venue_id=scenario.venue_id, | |
| scenario_id=scenario_id, | |
| seed=seed if seed is not None else scenario.default_seed, | |
| speed=10, | |
| ) | |
| session = SimulationSession(cfg, SETTINGS) | |
| steps_per_frame = max(1, int(round(interval_s / session.sim.dt))) | |
| frames: list[dict] = [] | |
| strategy_run: dict | None = None | |
| applied_branches: dict[str, int] = {} | |
| agents_affected = 0 | |
| # Pick the decision point automatically: the first moment the primary | |
| # bottleneck is projected to go critical. | |
| auto_decision = decision_t_s is None | |
| target = decision_t_s | |
| while session.sim.time < scenario.duration_s and not session.sim.is_complete: | |
| frame = session.frame() | |
| frames.append(frame) | |
| if strategy_run is None: | |
| fire = False | |
| if auto_decision: | |
| alerts = frame.get("alerts") or [] | |
| fire = any(a["severity"] == "critical" and a["time_to_critical_s"] is not None | |
| for a in alerts) | |
| elif target is not None and session.sim.time >= target: | |
| fire = True | |
| if fire: | |
| print(f" decision point at T+{session.sim.time:.0f}s — evaluating strategies") | |
| strategy_run = await session.evaluate_strategies(horizon_s=300) | |
| if strategy_run.get("available"): | |
| rec = strategy_run["recommendation"]["strategy_id"] | |
| applied_branches[rec] = len(frames) | |
| result = await session.apply_strategy(rec) | |
| agents_affected = result.get("agents_affected", 0) | |
| print(f" applied {rec} · {agents_affected:,} people rerouted") | |
| session._advance(steps_per_frame) | |
| if session.finished: | |
| break | |
| frames.append(session.frame()) | |
| FALLBACK_DIR.mkdir(parents=True, exist_ok=True) | |
| path = FALLBACK_DIR / f"{scenario_id}.json" | |
| payload = { | |
| "meta": { | |
| "venue_id": scenario.venue_id, | |
| "scenario_id": scenario_id, | |
| "seed": session.sim.seed, | |
| "crowd_size": session.sim.n_agents, | |
| "speed": 10, | |
| "interval_s": interval_s, | |
| "frames": len(frames), | |
| "applied_branches": applied_branches, | |
| "agents_affected": agents_affected, | |
| "recorded_utc": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"), | |
| "note": ("Recording of a real seeded run of this scenario. Used only " | |
| "if a live simulation cannot be created."), | |
| }, | |
| "frames": frames, | |
| "strategy_run": strategy_run, | |
| } | |
| path.write_text(json.dumps(payload), encoding="utf-8") | |
| await session.close() | |
| return path | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--scenario", action="append", default=None) | |
| ap.add_argument("--interval", type=float, default=10.0, | |
| help="simulated seconds between recorded frames") | |
| ap.add_argument("--decision", type=float, default=None, | |
| help="force the strategy decision at this sim time") | |
| ap.add_argument("--seed", type=int, default=None) | |
| args = ap.parse_args() | |
| scenarios = args.scenario or ["circuit_alpha_post_race", "barcelona_2022_egress"] | |
| for scenario_id in scenarios: | |
| print(f"recording {scenario_id} …") | |
| path = asyncio.run(record(scenario_id, args.interval, args.decision, args.seed)) | |
| size_mb = path.stat().st_size / 1e6 | |
| print(f" saved {path} ({size_mb:.1f} MB)") | |
| if __name__ == "__main__": | |
| main() | |