Spaces:
Runtime error
A newer version of the Gradio SDK is available: 6.24.0
FlowTwin — Architecture
This document explains how the system is put together and, where a decision was not obvious, why it was made that way.
┌──────────────── OBSERVATION ────────────────┐
│ │
synthetic agents camera frame
(simulator, exact ground truth) (Hugging Face crowd model)
│ │
└──────────────────┬──────────────────────────┘
▼
CROWD STATE ENGINE
occupancy · density · inflow · outflow · velocity
utilisation · density growth · queue growth · risk
│
┌──────────────┴──────────────┐
▼ ▼
CURRENT STATE PREDICTED STATE
(+30 / +60 / +90 / +120 s)
└──────────────┬──────────────┘
▼
BOTTLENECK DETECTION
▼
STRATEGY ENGINE
candidates generated from venue topology
▼
COUNTERFACTUAL SIMULATOR
each candidate applied to an identical clone of state
▼
OPTIMIZER
J = Σ wᵢ · (metricᵢ / no-action metricᵢ)
▼
RECOMMENDATION + EXPLANATION
▼
RACE CONTROL UI
▼
operator applies → NEW STATE ─┐
▲ │
└────────────────┘
1. Venue digital twin
A venue is a directed weighted graph (venue/models.py). Nodes are places a
spectator can be; edges are the pedestrian links between them.
An edge carries length_m, width_m and capacity_ppm. A node may carry
area_m2 (so it can hold a crowd) and service_rate_ppm (how many people per
minute it can process).
A perimeter exit is not a sink. It is a throughput constraint on the way to somewhere else — a station, a car park. Modelling it as a destination would hide exactly the queue this project exists to predict. Sinks are transport interfaces and car parks; exits are gates in between.
CompiledVenue is the array-oriented view built once per venue: node and edge
attributes as numpy arrays, a CSR-style adjacency, polyline geometry with
cumulative arc length, and the pairing between the two directions of a two-way
corridor. The hot loop never touches a Python object.
Both venues are generated by scripts/build_venues.py rather than hand-written
JSON, so edge lengths are always derived from the drawn geometry and the map can
never disagree with the physics.
2. Simulation
simulation/engine.py. A mesoscopic, capacity-constrained pedestrian network
model. Agents are individuals — own walking speed, destination, route, reroute
compliance — but they travel along graph edges rather than in free 2-D space.
Why not a full social-force model? A microscopic 2-D simulation of 40,000 agents cannot run five alternative futures while an operator waits. The counterfactual comparison is the product, so the movement model was chosen to make it affordable: a step costs ~2–4 ms for 40,000 agents, which makes an eight-strategy sweep over a 300-second horizon about six seconds.
Four pieces of physics do the work:
Speed depends on local density. Weidmann's (1993) exponential fundamental diagram. Free walking at low density, speed collapse approaching jam density.
Density is evaluated per cell, not per edge. Every corridor is divided into ~12 m cells. This matters more than it sounds: with edge-average density, a queue at a gate slows everybody in the corridor, including someone 200 m back with clear space in front of them. The result was a corridor that filled uniformly to jam and delivered a tenth of its real throughput. With cells, the congested region grows upstream one cell at a time, as a queue does.
Throughput is bounded twice, and admission is FIFO. Moving from one link to the next requires passing a node budget (the gate's people-per-minute) and an edge budget (what the next corridor accepts). Fractional capacity is carried across steps so a 90/minute gate really passes 90 per minute. Whoever has been waiting longest goes first.
A link stops accepting people before it is physically full. Receiving capacity falls as a link fills, at the backward wave speed. Without this, a corridor quietly absorbs an impossible number of people instead of pushing the congestion upstream — spillback is what turns one degraded gate into a network-wide event, and it has to be in the model.
Agents that reach the head of a queue and cannot pass are marked blocked and spread across the length the queue physically occupies, so the map shows the queue backing up the corridor and approaching walkers meet it where it really is.
Reproducibility and branching
snapshot() captures everything: agent arrays, budgets with their fractional
carry, cost model, routing tables, crowd-state history, counters, fired events,
and the state of both random generators. branch() produces a detached copy.
This is the foundation of the counterfactual: every candidate strategy starts from a byte-identical state with an identical random stream, so the only difference between two results is the intervention. Tests assert it directly.
3. Crowd State Engine
crowd/state.py. Converts agent positions into the aggregates everything
downstream reasons about, and keeps a rolling history so it can talk about
trajectories, not just instants.
A corridor at 2.1 p/m² filling at 0.4 p/m² per minute is a different operational situation from one sitting at 2.1 p/m² in steady state, and only the first needs an intervention. That distinction is the reason for the history buffers.
The composite risk score combines density, capacity utilisation, density growth, queue growth, velocity drop and opposing flow — deliberately not a threshold on raw density, which cannot tell a busy concourse from a compressing queue. Weights are configurable and the per-term contributions are exposed, so an alert can say why it fired.
Density is reported two ways: the mean over the corridor (the headline number, which moves continuously as a queue lengthens) and the peak in any single cell. The dashboard labels which is which.
4. Prediction
prediction/. Features come straight from the Crowd State Engine — the model
sees exactly what the operator sees, with no privileged knowledge of the scenario
script.
Two predictors exist:
- Analytic mass-balance projection.
density(t+h) = density + (inflow − outflow)·h/(60·area), damped as the corridor approaches jam. Always available. - Gradient-boosted regressor, one per horizon, trained by
scripts/train_predictor.pyon data the simulator generates.
Because the simulator provides exact ground truth, the model can be validated
honestly. Training and test use disjoint seeds, and the report records the
model's mean absolute error alongside the baseline's. The trained model is only
used if it beat the baseline on held-out seeds; otherwise DensityPredictor
refuses to load it. The dashboard shows which predictor is active and its
accuracy.
Density is a property of the physical corridor, so a projection that differs by direction is an artefact of direction-specific features, not a real disagreement — the carrying direction's projection is mirrored to its pair so the alert list, the prediction panel and the strategy engine cannot quote different futures for the same piece of concrete.
Projections are memoised per (simulation, step): one dashboard frame asks for them several times and they must all agree.
5. Routing
routing/. FlowTwin stores, for each policy and destination, the best next
edge from every node, rather than a route per agent. A 40,000-agent population
then routes with one fancy-index lookup, and a change in crowd state re-routes
everyone who has not committed, in one Dijkstra per destination.
Three policies exist so the benchmark can compare like with like:
shortest_path— minimise distance.static_assignment— a real method-of-successive-averages traffic assignment with BPR-style congestion costs, computed once before the event from expected demand. Capacity-aware, but blind to what actually happens.flowtwin_adaptive—C_e = α·distance + β·travel time at current speed + γ·congestion + δ·risk, plus expected waiting time at each node from its live queue and service rate.
That node term is what makes rerouting more than cosmetic: an exit with 2,400 people waiting and a 750/minute service rate is a 192-second delay, and the router has to know it.
Oscillation control. A node only abandons its incumbent next hop when the challenger is meaningfully cheaper (hysteresis), and agents that adopt the adaptive plan keep it. Hysteresis can in principle retain a hop that closes a loop, so the merged table is checked for termination and any node that fails is reverted to the pure shortest-path hop. A test asserts the tables stay acyclic and that repeated refreshes on an unchanged state change nothing.
6. Strategy Engine
strategy/. The candidate set is not a fixed list — it is derived from the
bottleneck that was detected and what the surrounding network makes possible. A
reroute is only offered when an alternative path exists; an alternate exit only
when one has measured spare throughput; a destination split only when two
interchangeable destinations exist.
Families: no action, reroute (20/30/40%), staggered release, open an alternate exit, destination split, and a combined response.
Each intervention knows how to apply itself to a simulator. That is the whole contract, and it matters: the counterfactual applies it to a clone, the operator applies it to the live run, and both go through the same code path — what the operator gets is what was measured.
Compliance is modelled per agent. An instruction reaches everyone selected; only those whose personal compliance clears a random draw act on it.
Counterfactual and optimizer
For each candidate: clone, apply, roll forward, measure. Metrics are scoped to the asset under threat — a network-wide maximum set by some unrelated corridor would make every strategy look identical.
J = w₁·peak density + w₂·critical duration + w₃·travel time
+ w₄·risk + w₅·queue + w₆·(1/throughput) + w₇·reroute cost
Every term is normalised against the no action counterfactual, so weights
express relative importance rather than doing unit conversion, and a score reads
directly as "fraction of the do-nothing outcome". The optimum is argmin J.
The explanation is generated from the same normalised terms that produced the score. There is no separate narrative layer that could drift away from the arithmetic, and no language model anywhere in this path — a numerical safety-adjacent decision should be measurable and reproducible, which an LLM is not.
7. Runtime
runtime/session.py. A session owns one simulator, advances it on a wall-clock
timer at the requested speed multiplier, and publishes frames to connected
dashboards. Stepping and counterfactual sweeps run off the event loop so the
WebSocket never stalls; a slow client has its oldest frame dropped rather than
slowing the venue down.
A session with no subscribers does no work, and idle sessions are reaped. A refreshed browser tab would otherwise leave an orphaned simulation stepping forever, and enough of those starve the event loop.
ReplaySession implements the same interface from a precomputed recording. It is
demo insurance only — the live simulation is always the primary path.
8. Frontend
frontend/. A single-page Race Control console served by the backend: no build
step, no package install, one process to start.
That is a deliberate trade against the framework named in the specification. On demo day, one command that serves both the API and the UI removes an entire class of failure — dependency install, build output, port and CORS configuration — and none of what the dashboard has to do (a canvas map, a WebSocket, a few panels) needs a framework. Everything is vanilla ES modules and hand-written CSS, and it works offline.
The map is Canvas 2D, layered: circuit geometry, corridors coloured by measured density, predicted congestion as a dashed overlay, animated flow direction, reroute overlay, agents, nodes with queue rings, labels, and a pulsing halo on the primary bottleneck. Nothing on it is decorative state — if a corridor is orange, its measured density put it there.
Panels re-render only when their content would actually differ. Frames arrive five times a second, and rewriting a panel on every one of them restarts its entry animation and leaves it permanently mid-fade.
9. Configuration
Everything a deployment might reasonably want to change lives in config.py and
is overridable by environment variable: movement physics, risk weights, routing
costs, optimizer weights, prediction horizons, server behaviour, perception model.
No tuning constant is hard-coded inside an algorithm module.
One non-obvious setting is applied at package import: the BLAS/OpenMP thread pools are pinned to one thread. FlowTwin's numeric work is many small operations, and on a small container the thread pools spend far longer coordinating than computing — one edge-density inference measured 1,000 ms across two threads and 9 ms on one.
10. Deliberate omissions
- Redis — the state that would live there is owned by a single process, and adding a network hop between a simulation and its own state buys nothing for a single-node demo while adding a thing that can be down.
- PostgreSQL — nothing in the demo path needs durable storage. Venues and scenarios are JSON; benchmark results and recordings are files.
- A language model in the decision loop — excluded on purpose. It may be added as an interface layer that reads structured engine output; it must never determine an intervention.