goatifi / scripts /record_fallback.py
KonoDioDaa's picture
Initial FlowTwin deployment
e7a9f02
Raw
History Blame Contribute Delete
5.01 kB
#!/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()