# FlowTwin — what to fix next Written at the end of the build, from what actually broke. Ordered by value per hour, not by size. Everything here is a known gap, not a wish list. --- ## P0 — before the final submission ### 1. The optimizer stops separating once the queue exists **The bug.** Intervene at T+450 s and the winner is clearly better (peak density 2.45 → 1.74, queue 2,185 → 1,534). Intervene at T+900 s and *every* strategy returns identical peak density and critical time; the winner is chosen only because staggering reroutes nobody and so scores lowest on the reroute-cost term. **Why.** Once a 4,000-person queue exists it drains at the gate rate (380/min) no matter where new arrivals are sent. Peak density over the window is already locked in, so `J` cannot distinguish the candidates. This is physically correct and it only appeared after the queue-extent fix landed. **Fix.** Add an end-of-window term to `J` — a strategy that leaves the queue smaller at T+horizon is better even when the peak is identical. Concretely, in `strategy/counterfactual.py` the metrics already carry `final_density` and `final_queue`; add them to `OBJECTIVES` in `optimizer.py` with weights around 0.10–0.15 and re-run `scripts/run_benchmarks.py` to confirm the early case does not regress. Also consider scoring the *integral* of density over the window rather than its maximum. **Test to add.** Assert separation at both an early and a late intervention time, so this cannot silently return. ### 2. Verify the Hugging Face path against real weights Nothing in the perception chain has ever run against downloaded weights — this sandbox had no access to `huggingface.co`. On a networked machine: ```bash pip install -r backend/requirements.txt python scripts/fetch_hf_model.py ``` Expect the CSRNet candidate to be the risky one: the repo may ship a full module rather than a `state_dict`, or use different layer names. `perception/huggingface.py` already handles both shapes and falls through to YOLOv8n and then to YOLOS-tiny, but **the fallthrough has not been exercised**. Budget an hour. If CSRNet resists, take the detector path and say so in the pitch — the architecture is the point, not which checkpoint won. Then drop two or three crowd photographs into `data/perception/` with an `index.json` giving each one a `zone_id` and `zone_area_m2`, so the Perception panel has something to click rather than requiring an upload. ### 3. Commit and push ```bash git init && git add -A && git commit -m "FlowTwin: AI crowd digital twin for F1 venues" git branch -M main && git remote add origin && git push -u origin main ``` `data/fallback/*.json` is gitignored (16 MB of generated frames). Anyone cloning regenerates it with `python scripts/record_fallback.py`. The trained model *is* committed (3.8 MB) so a fresh clone predicts properly without retraining. If the repo must stay under a size limit, move the model to a release asset and have `DensityPredictor` fall back to the analytic baseline — it already does that cleanly. --- ## P1 — makes the submission materially stronger ### 4. Safety mode (compression risk) as a visible mode The compression-risk proxy is **already implemented** — density growth, velocity drop, queue growth and opposing flow all feed the risk score, and `risk_contributions()` exposes the per-term breakdown. What is missing is the proposal's in-event scenario and the framing. Cheapest useful version: a third scenario on `circuit_alpha` where the crowd is already inside and a large fraction converges on one zone (a fan-zone stage). That is a new JSON in `data/scenarios/` plus demand groups pointing at `CONC_NORTH` — no engine changes. Add a "SAFETY" toggle to the header that switches the alert panel to rank by compression risk instead of flow risk. Keep the Astroworld framing exactly as the proposal states it: a stress-test reference for the *type* of scenario, never a claim of prevention. ### 5. Personnel dispatch The largest genuinely-missing feature from the master proposal. Scope it small: - A `resources` block in the venue JSON: id, type (security/medical), position, response speed. - `DispatchScore = expected_risk_reduction − λ·response_time − µ·cost`, computed against the primary bottleneck. - A map layer for unit positions and a dashed line to the recommended assignment. - A `dispatch` intervention family so it appears in the strategy table alongside the flow levers. Half a day. High pitch value because it answers "who should act", which no crowd-monitoring product does. ### 6. Ablation study The proposal asks for it and it is nearly free — the benchmark harness already supports arms. Add: routing-only, prediction + routing, and the full prediction + counterfactual loop. This is the cleanest possible answer to "which part is actually doing the work", and right now that question has no data behind it. ### 7. Barcelona's static baseline is identical to shortest path Both baselines produce byte-identical results because most origin–destination pairs in the reconstructed topology have exactly one sensible route. That is a legitimate finding and the README says so, but the benchmark column looks broken at a glance. Either add a second rail approach so route choice exists, or annotate the table in-place explaining why the two columns match. --- ## P2 — polish, if time remains - **Pitch deck.** `docs/DEMO.md` has the narrative and every real number; it needs to become slides. Slide 7 (results) should be a screenshot of the strategy table, not retyped figures. - **Deploy.** One box running `uvicorn` behind a reverse proxy is enough. Do not containerise for the sake of it; the demo runs from one command today. - **Record a screen capture** of both demos as the true last-resort fallback, above the replay recordings. - **Agent groups.** Families walk together and comply together; a `group_id` already exists in the schema but nothing uses it. - **Uncertainty on predictions.** Quantile regression would give the alert a confidence band, which is honest and cheap with the existing training script. - **Mobile/tablet layout.** Breakpoints exist down to 900 px but have not been exercised on a real device. --- ## Things not to change Learned the hard way; each of these was a bug once. 1. **Do not average density over a whole corridor.** Per-cell evaluation is why a queue at a gate does not freeze people 200 m back. Reverting collapses throughput to about a tenth. 2. **Do not let a link accept people until it is physically full.** The backward-wave receiving function is what makes congestion spill back instead of a corridor silently absorbing an impossible crowd. 3. **Do not measure the queue only at the stop line.** Measuring it from everyone who has stopped is what lets a gate discharge at its real rate. 4. **Do not let routes transit grandstands.** They are seating bowls. Allowing it deadlocked Barcelona with 18,000 people stranded. 5. **Do not remove the U-turn guard or the penalty decay.** Without them, repeated interventions leave a residue bouncing between two nodes forever. 6. **Do not put a language model in the decision path.** It is the answer to "where is the AI" that judges respect, and the whole explainability story depends on the reasoning being the same arithmetic that produced the score. 7. **Do not let panels re-render every frame.** Frames arrive 5×/second; rebuilding a card restarts its entry animation and the panel goes invisible. This bug shipped once already. 8. **Do not unpin the thread limits** in `flowtwin/__init__.py`. A 66-row inference takes 1,000 ms on two threads and 9 ms on one. --- ## Pre-demo checklist ```bash python scripts/build_venues.py # venues regenerate cleanly cd backend && python -m pytest -q # 70 tests cd .. && python scripts/ui_check.py # full acceptance path in a browser python scripts/record_fallback.py # refresh replay insurance ``` Then, in the dashboard: run Simulation 1, wait for the prediction to read "critical in N seconds" (roughly **T+07:30–T+10:00**), and press *Simulate strategies* **then** — not after the alert has been red for minutes. See P0-1 for why that timing matters until the optimizer is fixed.