Spaces:
Runtime error
Runtime error
Commit ·
0847253
1
Parent(s): 3e2e16c
Deploy FlowTwin to Hugging Face Spaces
Browse files- .gitattributes +1 -1
- PROJECT_MASTERFILE.md +1579 -0
- README.md +11 -0
- app.py +171 -0
- backend/flowtwin/prediction/inference.py +1 -1
- backend/requirements.txt +3 -1
- models/perception_manifest.json +8 -0
- requirements.txt +2 -0
- run.sh +0 -0
.gitattributes
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
* text=auto eol=lf
|
| 2 |
*.bat text eol=crlf
|
| 3 |
-
*.joblib
|
| 4 |
*.png binary
|
| 5 |
data/venues/*.json linguist-generated=true
|
| 6 |
data/scenarios/*.json linguist-generated=true
|
|
|
|
| 1 |
* text=auto eol=lf
|
| 2 |
*.bat text eol=crlf
|
| 3 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.png binary
|
| 5 |
data/venues/*.json linguist-generated=true
|
| 6 |
data/scenarios/*.json linguist-generated=true
|
PROJECT_MASTERFILE.md
ADDED
|
@@ -0,0 +1,1579 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FlowTwin — Project Masterfile
|
| 2 |
+
|
| 3 |
+
*Everything about this project in one place: what it is, why it exists, how every
|
| 4 |
+
part works, what was measured, how to pitch it, and how to defend it.*
|
| 5 |
+
|
| 6 |
+
Written to be read cold. If you have never seen this project before, start at
|
| 7 |
+
§1 and keep going — nothing later assumes anything earlier than what you have
|
| 8 |
+
already read.
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## Table of contents
|
| 13 |
+
|
| 14 |
+
**Part I — Understanding the project**
|
| 15 |
+
1. [The one-paragraph version](#1-the-one-paragraph-version)
|
| 16 |
+
2. [The problem, properly explained](#2-the-problem-properly-explained)
|
| 17 |
+
3. [Why existing tools do not solve it](#3-why-existing-tools-do-not-solve-it)
|
| 18 |
+
4. [The core idea: the decision loop](#4-the-core-idea-the-decision-loop)
|
| 19 |
+
5. [A worked example, end to end](#5-a-worked-example-end-to-end)
|
| 20 |
+
|
| 21 |
+
**Part II — How it actually works**
|
| 22 |
+
6. [The venue model](#6-the-venue-model)
|
| 23 |
+
7. [The simulation engine](#7-the-simulation-engine)
|
| 24 |
+
8. [The Crowd State Engine](#8-the-crowd-state-engine)
|
| 25 |
+
9. [Prediction](#9-prediction)
|
| 26 |
+
10. [The Strategy Engine](#10-the-strategy-engine)
|
| 27 |
+
11. [Counterfactual simulation](#11-counterfactual-simulation)
|
| 28 |
+
12. [Multi-objective optimisation and the decisiveness verdict](#12-multi-objective-optimisation-and-the-decisiveness-verdict)
|
| 29 |
+
13. [Dynamic routing](#13-dynamic-routing)
|
| 30 |
+
14. [Perception — the Hugging Face path](#14-perception--the-hugging-face-path)
|
| 31 |
+
|
| 32 |
+
**Part III — The system as software**
|
| 33 |
+
15. [Architecture and module map](#15-architecture-and-module-map)
|
| 34 |
+
16. [Data flow and real-time transport](#16-data-flow-and-real-time-transport)
|
| 35 |
+
17. [The frontend](#17-the-frontend)
|
| 36 |
+
18. [Reproducibility and determinism](#18-reproducibility-and-determinism)
|
| 37 |
+
19. [The three venues](#19-the-three-venues)
|
| 38 |
+
20. [Testing and verification](#20-testing-and-verification)
|
| 39 |
+
|
| 40 |
+
**Part IV — Evidence**
|
| 41 |
+
21. [Measured results](#21-measured-results)
|
| 42 |
+
22. [Every defect found and fixed](#22-every-defect-found-and-fixed)
|
| 43 |
+
23. [What is deliberately not built](#23-what-is-deliberately-not-built)
|
| 44 |
+
|
| 45 |
+
**Part V — The hackathon**
|
| 46 |
+
24. [Mapping to the evaluation criteria](#24-mapping-to-the-evaluation-criteria)
|
| 47 |
+
25. [The pitch](#25-the-pitch)
|
| 48 |
+
26. [The demo, minute by minute](#26-the-demo-minute-by-minute)
|
| 49 |
+
27. [Q&A defence](#27-qa-defence)
|
| 50 |
+
28. [Failure drills](#28-failure-drills)
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
# Part I — Understanding the project
|
| 56 |
+
|
| 57 |
+
## 1. The one-paragraph version
|
| 58 |
+
|
| 59 |
+
FlowTwin is a **digital twin of a crowd**. You give it a venue — where the gates,
|
| 60 |
+
walkways, concessions, exits and transport links are, and how much each can
|
| 61 |
+
handle — plus how many people are coming and when. It then simulates tens of
|
| 62 |
+
thousands of individual people walking through that venue, second by second. As
|
| 63 |
+
it runs, it continuously asks three questions: *where is flow about to break
|
| 64 |
+
down?*, *what could an operator do about it?*, and *which of those options
|
| 65 |
+
actually works?* To answer the third question it does something unusual: it takes
|
| 66 |
+
a perfect copy of the crowd's current state, applies each candidate action to its
|
| 67 |
+
own copy, runs each copy forward four minutes, and **measures** what happened.
|
| 68 |
+
Then it recommends the option that measured best, and shows you the arithmetic.
|
| 69 |
+
If no option measurably beats doing nothing, it says so instead of inventing a
|
| 70 |
+
recommendation.
|
| 71 |
+
|
| 72 |
+
That last sentence is the project in miniature. Most systems in this space are
|
| 73 |
+
dashboards that tell you what is happening. FlowTwin tells you **what to do**,
|
| 74 |
+
and it earns the right to say it by simulating the alternatives rather than
|
| 75 |
+
applying a rule of thumb.
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## 2. The problem, properly explained
|
| 80 |
+
|
| 81 |
+
### 2.1 Crowd disasters are not headcount problems
|
| 82 |
+
|
| 83 |
+
The intuitive model of crowd danger is "too many people in the building". That
|
| 84 |
+
model is wrong, and the wrongness matters.
|
| 85 |
+
|
| 86 |
+
A venue can sell out completely, admit exactly the number of people it is
|
| 87 |
+
licensed for, and still kill someone — because danger is not a property of the
|
| 88 |
+
total, it is a property of the **local density and the local flow**. Five people
|
| 89 |
+
per square metre in one corridor is dangerous whether the rest of the venue is
|
| 90 |
+
empty or full. Crowd crush injuries happen at pinch points: a gate that closed, a
|
| 91 |
+
staircase that narrowed, two streams of people trying to cross.
|
| 92 |
+
|
| 93 |
+
So the quantity that matters is not *how many people are here* but *how many
|
| 94 |
+
people are in this twelve metres of corridor, how fast are they moving, and is
|
| 95 |
+
that number rising*.
|
| 96 |
+
|
| 97 |
+
### 2.2 Flow failures are non-local and delayed
|
| 98 |
+
|
| 99 |
+
Here is what makes it genuinely hard. Suppose an exit loses half its capacity.
|
| 100 |
+
The people at that exit notice immediately. But the *consequence* is not local:
|
| 101 |
+
|
| 102 |
+
- The queue at that exit grows backwards up the corridor.
|
| 103 |
+
- When it reaches the concourse behind it, that concourse starts filling.
|
| 104 |
+
- People arriving at the concourse from an entirely different direction —
|
| 105 |
+
who have nothing to do with that exit — now find their route blocked.
|
| 106 |
+
- The pressure propagates outward, several minutes after the original event, in
|
| 107 |
+
places nobody was watching.
|
| 108 |
+
|
| 109 |
+
This is the same mathematics as a traffic jam. The shockwave travels *backwards*
|
| 110 |
+
through the crowd, slower than the people are walking, and it arrives somewhere
|
| 111 |
+
unexpected several minutes later.
|
| 112 |
+
|
| 113 |
+
Two consequences follow, and both shaped this project:
|
| 114 |
+
|
| 115 |
+
1. **You cannot reason about it locally.** A camera on the failing exit tells you
|
| 116 |
+
about the failing exit. It does not tell you that the west concourse will be
|
| 117 |
+
dangerous in six minutes.
|
| 118 |
+
2. **By the time you can see it, it may be too late to fix by rerouting.** Once a
|
| 119 |
+
queue of four thousand people exists, it drains at the gate's service rate no
|
| 120 |
+
matter where you send new arrivals. The people you would need to move are
|
| 121 |
+
already in the queue and physically cannot move.
|
| 122 |
+
|
| 123 |
+
FlowTwin models both of these explicitly, and — importantly — it *tells you* when
|
| 124 |
+
you have hit the second one, rather than pretending it can still help.
|
| 125 |
+
|
| 126 |
+
### 2.3 The operator's actual problem
|
| 127 |
+
|
| 128 |
+
Put yourself in the control room. You have:
|
| 129 |
+
|
| 130 |
+
- Cameras and counters, so you know roughly where people are.
|
| 131 |
+
- A handful of levers: reroute a percentage of people, hold back departures from
|
| 132 |
+
a section, open contingency lanes, unlock an emergency gate, change where a
|
| 133 |
+
shuttle picks up.
|
| 134 |
+
- Minutes, not hours.
|
| 135 |
+
- No way to test a decision before making it.
|
| 136 |
+
|
| 137 |
+
That last one is the gap. Every lever has a cost and a side effect. Rerouting
|
| 138 |
+
40% of a stand relieves one corridor and loads another. Holding back departures
|
| 139 |
+
keeps people safe but makes their evening longer, and if you hold too long the
|
| 140 |
+
release is worse than the original problem. Opening an emergency gate means
|
| 141 |
+
staffing it, breaking a perimeter, and explaining it afterwards.
|
| 142 |
+
|
| 143 |
+
**An operator has to choose between options whose consequences are separated
|
| 144 |
+
from the decision by five minutes and half a venue.** That is exactly the kind of
|
| 145 |
+
decision a simulation should make for you, and nobody does it.
|
| 146 |
+
|
| 147 |
+
### 2.4 The specific case this project is built around
|
| 148 |
+
|
| 149 |
+
The 2022 Spanish Grand Prix at the Circuit de Barcelona-Catalunya reported a
|
| 150 |
+
weekend attendance of 277,836, with over 120,000 on race day. Contemporary
|
| 151 |
+
reporting described severe road and public-transport congestion leaving the
|
| 152 |
+
circuit, heavy pressure on the Montmeló rail infrastructure, long concession
|
| 153 |
+
queues and water shortages. Formula 1 publicly told the promoter the fan
|
| 154 |
+
experience was not acceptable.
|
| 155 |
+
|
| 156 |
+
Nobody was hurt. That is the point: this is the *ordinary* failure mode, the one
|
| 157 |
+
that happens dozens of times a year at venues that are competently run, and the
|
| 158 |
+
one that becomes a disaster when the geometry is slightly worse or the crowd is
|
| 159 |
+
slightly bigger.
|
| 160 |
+
|
| 161 |
+
The same shape of failure covers the applications the problem statement names —
|
| 162 |
+
railway station design, IPL match egress, airport terminals, Kumbh-scale
|
| 163 |
+
gatherings. It is one problem, and it is not a motorsport problem.
|
| 164 |
+
|
| 165 |
+
---
|
| 166 |
+
|
| 167 |
+
## 3. Why existing tools do not solve it
|
| 168 |
+
|
| 169 |
+
There are three categories of existing tool, and each stops short in a different
|
| 170 |
+
place.
|
| 171 |
+
|
| 172 |
+
**Crowd monitoring / people counting.** Cameras plus a counting model, feeding a
|
| 173 |
+
dashboard with occupancy numbers and threshold alarms. This tells an operator
|
| 174 |
+
*where people are*. It is reactive by construction: the alarm fires when the
|
| 175 |
+
density is already high, which is after the point at which rerouting could have
|
| 176 |
+
helped. It also has no notion of *why*, so it cannot suggest an action.
|
| 177 |
+
|
| 178 |
+
**Offline crowd simulation.** Professional pedestrian modelling packages are
|
| 179 |
+
excellent, and they are used at design time: you model the venue, run scenarios,
|
| 180 |
+
and change the architecture or the plan. They are not real-time decision tools —
|
| 181 |
+
a run takes minutes to hours, the model is not connected to live conditions, and
|
| 182 |
+
the output is a report rather than an instruction.
|
| 183 |
+
|
| 184 |
+
**Traffic-style routing.** Shortest-path or capacity-aware assignment can tell
|
| 185 |
+
people where to go. But a pre-computed plan is blind to what actually happens on
|
| 186 |
+
the day, and a purely reactive router chases congestion around the venue,
|
| 187 |
+
producing oscillation: send people east, the east fills, send them west, the west
|
| 188 |
+
fills.
|
| 189 |
+
|
| 190 |
+
FlowTwin sits in the hole between these three. It is a **real-time simulation
|
| 191 |
+
that is fast enough to run its own hypotheticals while an operator waits**. The
|
| 192 |
+
architectural decision that makes this possible is described in §7.1, and it is
|
| 193 |
+
the single most important engineering choice in the project.
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## 4. The core idea: the decision loop
|
| 198 |
+
|
| 199 |
+
```
|
| 200 |
+
┌─────────────────────────────────────────────────────────┐
|
| 201 |
+
│ │
|
| 202 |
+
▼ │
|
| 203 |
+
┌──────┐ ┌─────────┐ ┌──────────┐ ┌──────┐ │
|
| 204 |
+
│ SEE │ ───► │ PREDICT │ ───► │ SIMULATE │ ───► │ ACT │ ───┘
|
| 205 |
+
└──────┘ └─────���───┘ └──────────┘ └──────┘
|
| 206 |
+
where are where will it what would apply the
|
| 207 |
+
people, and break down, each option one that
|
| 208 |
+
how fast are and when? actually do? measured best
|
| 209 |
+
they moving?
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
**SEE.** Turn raw positions into the quantities that predict failure: density per
|
| 213 |
+
short segment of corridor, walking speed against free speed, inflow and outflow
|
| 214 |
+
per minute, queue length, how fast density is *changing*, and whether two streams
|
| 215 |
+
are fighting for the same floor.
|
| 216 |
+
|
| 217 |
+
**PREDICT.** Project each of those forward 30, 60, 90 and 120 seconds, and
|
| 218 |
+
convert that into the only number an operator can act on: **how long until this
|
| 219 |
+
corridor is critical**.
|
| 220 |
+
|
| 221 |
+
**SIMULATE.** Generate the candidate actions that this venue's topology actually
|
| 222 |
+
permits, then clone the entire crowd state once per candidate, apply the
|
| 223 |
+
candidate to its clone, and run each clone forward four simulated minutes.
|
| 224 |
+
|
| 225 |
+
**ACT.** Score the outcomes on a weighted objective, recommend the best — or
|
| 226 |
+
refuse to recommend if nothing beat doing nothing — and show the arithmetic. When
|
| 227 |
+
the operator applies it, the intervention enters the live simulation through the
|
| 228 |
+
exact same code path that was measured, and the loop starts again.
|
| 229 |
+
|
| 230 |
+
The loop is what makes this a decision-support system rather than a dashboard.
|
| 231 |
+
Each stage exists because the stage after it needs something the stage before
|
| 232 |
+
could not provide.
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## 5. A worked example, end to end
|
| 237 |
+
|
| 238 |
+
Concrete, from the flagship scenario, with real numbers from a real seeded run.
|
| 239 |
+
|
| 240 |
+
**T+00:15.** The chequered flag. 40,000 spectators begin leaving six seating
|
| 241 |
+
areas on an eighteen-minute departure curve. Everyone routes by shortest path
|
| 242 |
+
towards one of four destinations: the rail interchange, the coach interchange,
|
| 243 |
+
or one of two car parks.
|
| 244 |
+
|
| 245 |
+
**T+04:00.** A scripted infrastructure failure fires: **Exit B loses half its
|
| 246 |
+
throughput**, dropping from 760 people/minute to 380. This is a real change to
|
| 247 |
+
the simulated network — the exit's service budget is halved — not a label on a
|
| 248 |
+
map.
|
| 249 |
+
|
| 250 |
+
**T+05:30.** *SEE.* The corridor feeding Exit B (`X_E_EXITB`, 114 m long, 11 m
|
| 251 |
+
wide) is now taking more people per minute than it can pass. Measured: inflow 556
|
| 252 |
+
p/min, outflow 380 p/min. Density is rising at 0.14 p/m² per minute. Walking
|
| 253 |
+
speed has fallen to 0.13 m/s against a free speed of 1.34. A queue is forming.
|
| 254 |
+
|
| 255 |
+
**T+05:30.** *PREDICT.* The gradient-boosted model, fed seventeen features from
|
| 256 |
+
the Crowd State Engine, projects density at +30/60/90/120 s. Crossing the venue's
|
| 257 |
+
critical threshold of 2.8 p/m² happens inside the horizon, so the alert reads
|
| 258 |
+
**"critical in 96 seconds"** — and it explains itself: *density rising, velocity
|
| 259 |
+
collapsed, queue growing, downstream service constrained*.
|
| 260 |
+
|
| 261 |
+
**T+07:30.** *SIMULATE.* The operator presses **Simulate strategies**. The engine
|
| 262 |
+
inspects the topology around the bottleneck and generates eight candidates,
|
| 263 |
+
including: do nothing; redirect 20/30/40% of the affected flow; stagger the
|
| 264 |
+
release from the three stands feeding it; open contingency lanes at another exit
|
| 265 |
+
and divert 30%; unlock the north-east emergency gate and divert 35%; move 30% of
|
| 266 |
+
coach demand to the south apron; and a combined redirect-plus-stagger.
|
| 267 |
+
|
| 268 |
+
Eight complete copies of the crowd — every agent's position, route, destination,
|
| 269 |
+
compliance and the random number generator's internal state — are made. Each
|
| 270 |
+
candidate is applied to its own copy. Each copy runs forward 240 simulated
|
| 271 |
+
seconds. About nine seconds of wall-clock later, eight measured futures exist.
|
| 272 |
+
|
| 273 |
+
**T+07:31.** *ACT.* Scored against the do-nothing arm on nine weighted terms.
|
| 274 |
+
**Redirect 40%** wins by 17.1%. The panel says why, in measured deltas: peak
|
| 275 |
+
density 2.19 → 1.58 (−28%), queue at end of window 1,636 → 1,245 (−24%), critical
|
| 276 |
+
duration to zero, average journey time essentially unchanged, 834 people
|
| 277 |
+
rerouted. The verdict reads **Decisive**.
|
| 278 |
+
|
| 279 |
+
**T+07:45.** The operator applies it. 1,700 people are instructed; per-person
|
| 280 |
+
compliance means roughly 70% actually change route. Green rerouting paths animate
|
| 281 |
+
on the map. Over the next three minutes the queue metric falls and the alert
|
| 282 |
+
drops from critical to warning.
|
| 283 |
+
|
| 284 |
+
**And the counter-example, which is the more interesting demo.** Do nothing until
|
| 285 |
+
**T+15:00** and press the button then. All eight candidates now return an
|
| 286 |
+
*identical* peak density of 3.31 p/m². The engine does not pick a winner. It
|
| 287 |
+
returns:
|
| 288 |
+
|
| 289 |
+
> **Not decisive.** Every candidate landed within 0.0% of doing nothing.
|
| 290 |
+
> `E CONCOURSE → EXIT B` is already discharging at its service limit (380
|
| 291 |
+
> people/min) with 3,275 people held, so it needs about 9 minutes to clear on
|
| 292 |
+
> throughput alone. Rerouting only reaches people who have not yet committed to
|
| 293 |
+
> this asset, and there are too few of them left for any routing change to
|
| 294 |
+
> register. The remaining levers are capacity and staffing, not routing.
|
| 295 |
+
|
| 296 |
+
Every number in that paragraph is read from the measured state. That is the
|
| 297 |
+
system telling you the decision window closed — which is more useful, and far
|
| 298 |
+
more credible, than a confident recommendation that would not have worked.
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
---
|
| 302 |
+
|
| 303 |
+
# Part II — How it actually works
|
| 304 |
+
|
| 305 |
+
## 6. The venue model
|
| 306 |
+
|
| 307 |
+
### 6.1 A venue is a graph
|
| 308 |
+
|
| 309 |
+
`backend/flowtwin/venue/models.py`
|
| 310 |
+
|
| 311 |
+
A venue is a **directed, weighted graph**. Nodes are places a person can be;
|
| 312 |
+
edges are the walkable links between them.
|
| 313 |
+
|
| 314 |
+
**Node types**, and what each means to the engine:
|
| 315 |
+
|
| 316 |
+
| Type | Role |
|
| 317 |
+
|---|---|
|
| 318 |
+
| `gate` | Entry point with a service rate in people/minute. An origin in arrival scenarios. |
|
| 319 |
+
| `grandstand`, `general_admission` | Seating/standing areas. Origins; a route may *end* at one but never pass *through* one. |
|
| 320 |
+
| `platform` | Railway platform. Same semantics as a grandstand — you leave from it, you do not walk across it. |
|
| 321 |
+
| `concourse`, `junction` | Circulation space. Optionally rate-limited (a foot-over-bridge is a junction with a service rate set by stair width). |
|
| 322 |
+
| `concession` | A dwell point. People passing through stop here for a while. |
|
| 323 |
+
| `exit` | A perimeter throughput constraint. **Deliberately not a destination** — see §6.3. |
|
| 324 |
+
| `emergency_exit` | A route that physically exists but is locked. **Absent from routing until opened** — see §6.4. |
|
| 325 |
+
| `transport`, `parking` | Destinations. These absorb people, at a rate. |
|
| 326 |
+
|
| 327 |
+
**Edges** carry `length_m`, `width_m` and `capacity_ppm` (people per minute that
|
| 328 |
+
may *enter*). Capacity follows Fruin-style pedestrian flow: about 70 people per
|
| 329 |
+
minute per metre of effective width in one direction. A bidirectional venue edge
|
| 330 |
+
compiles into two directed edges that share the same physical floor, which is how
|
| 331 |
+
opposing-flow conflict is measured.
|
| 332 |
+
|
| 333 |
+
Edge lengths are **derived from node geometry** by `scripts/build_venues.py`
|
| 334 |
+
rather than hand-written, so the map you see and the physics that runs can never
|
| 335 |
+
drift apart.
|
| 336 |
+
|
| 337 |
+
### 6.2 Compilation and cells
|
| 338 |
+
|
| 339 |
+
`CompiledVenue` turns the pydantic model into flat numpy arrays indexed by node
|
| 340 |
+
or directed-edge index, so the simulation's inner loop never touches a Python
|
| 341 |
+
object.
|
| 342 |
+
|
| 343 |
+
Then every edge is split into **cells of about 12 metres**. Density and walking
|
| 344 |
+
speed are evaluated per cell, not per edge.
|
| 345 |
+
|
| 346 |
+
This is not a detail. It is the difference between a model that works and one
|
| 347 |
+
that does not:
|
| 348 |
+
|
| 349 |
+
> With edge-average density, a queue at a gate slows down *everyone* on that
|
| 350 |
+
> corridor — including a person 200 metres back with completely clear space in
|
| 351 |
+
> front of them. Measured effect when this was wrong: network throughput
|
| 352 |
+
> collapsed to roughly **one tenth** of its correct value.
|
| 353 |
+
|
| 354 |
+
Cells on a two-way corridor are mirrored to their opposite-direction twin
|
| 355 |
+
(`cell_pair`), so two people walking towards each other in the same twelve metres
|
| 356 |
+
are counted as sharing that floor.
|
| 357 |
+
|
| 358 |
+
### 6.3 The decision that an exit is not a destination
|
| 359 |
+
|
| 360 |
+
A perimeter exit is modelled as a **throughput constraint on the way to somewhere
|
| 361 |
+
else** — a station, a car park, a coach apron — not as a place journeys end.
|
| 362 |
+
|
| 363 |
+
If an exit were a sink, everyone reaching it would vanish, and the queue *behind*
|
| 364 |
+
it would never form. That queue is the single most important phenomenon this
|
| 365 |
+
project exists to predict. Modelling exits as sinks would have made the demo
|
| 366 |
+
easier and the model useless.
|
| 367 |
+
|
| 368 |
+
### 6.4 The decision that a locked gate is absent, not expensive
|
| 369 |
+
|
| 370 |
+
An emergency exit is not modelled as an available-but-costly route. It is
|
| 371 |
+
**excluded from every routing table for every policy and every destination**.
|
| 372 |
+
|
| 373 |
+
The reason is precise. If a locked gate were merely expensive, the optimiser
|
| 374 |
+
would quietly have access to capacity that nobody has unlocked; under enough
|
| 375 |
+
congestion the crowd would start using it on its own, and the recommendation
|
| 376 |
+
*"open the north gate"* would never appear, because the crowd would already be
|
| 377 |
+
going there. Modelling it as absent makes opening it a real decision with a real
|
| 378 |
+
consequence — and it makes `open_emergency_exit` the only candidate in the whole
|
| 379 |
+
strategy set that **adds** network capacity rather than redistributing capacity
|
| 380 |
+
already in service.
|
| 381 |
+
|
| 382 |
+
### 6.5 Concessions as dwell points
|
| 383 |
+
|
| 384 |
+
A concession node carries `dwell_s` (mean stop time) and `dwell_share` (the
|
| 385 |
+
fraction of passers-by who stop). A person who stops:
|
| 386 |
+
|
| 387 |
+
- still occupies the floor they are standing on, and counts in the queue extent;
|
| 388 |
+
- does **not** consume the downstream node's service budget, because they are not
|
| 389 |
+
trying to go anywhere.
|
| 390 |
+
|
| 391 |
+
That is what makes a food court a crowd feature rather than a label. It also
|
| 392 |
+
requires the concession to be **on** a route — a dead-end spur is never on
|
| 393 |
+
anybody's path, so nobody ever visits it. Both the fan zone at Circuit Alpha and
|
| 394 |
+
the food court at Sangam Junction sit on the main circulation route, with a
|
| 395 |
+
longer bypass available, which is what gives the strategy engine something to
|
| 396 |
+
reroute people *onto*.
|
| 397 |
+
|
| 398 |
+
The randomness lives in the agent population, sampled once at creation, not in a
|
| 399 |
+
live random stream. That is deliberate: it means a counterfactual branch
|
| 400 |
+
reproduces the same dwell decisions exactly, so two branches of one state stay
|
| 401 |
+
byte-identical.
|
| 402 |
+
|
| 403 |
+
---
|
| 404 |
+
|
| 405 |
+
## 7. The simulation engine
|
| 406 |
+
|
| 407 |
+
`backend/flowtwin/simulation/engine.py`
|
| 408 |
+
|
| 409 |
+
### 7.1 The critical architectural choice: mesoscopic, not microscopic
|
| 410 |
+
|
| 411 |
+
A microscopic pedestrian model (social forces, agents in free 2-D space) is more
|
| 412 |
+
physically detailed and completely unusable here: it is far too slow to run eight
|
| 413 |
+
alternative futures while an operator waits.
|
| 414 |
+
|
| 415 |
+
FlowTwin is **mesoscopic**. Agents are individuals — each has a personal walking
|
| 416 |
+
speed, an origin, a destination, a route, a compliance probability and a position
|
| 417 |
+
— but they move **along graph edges**, not across open floor. Agent state is
|
| 418 |
+
stored as a **structure of arrays** (numpy), so a step is a handful of vectorised
|
| 419 |
+
operations over the whole population rather than a loop over 40,000 objects.
|
| 420 |
+
|
| 421 |
+
Measured: **2–4 ms per simulated second at 40,000 agents.**
|
| 422 |
+
|
| 423 |
+
That number is the enabling fact for the entire project. Because a step is
|
| 424 |
+
milliseconds, four minutes of simulation is about a second, and eight
|
| 425 |
+
counterfactual futures are about nine seconds — short enough that an operator
|
| 426 |
+
will actually press the button. Every other capability in this document is
|
| 427 |
+
downstream of that choice.
|
| 428 |
+
|
| 429 |
+
### 7.2 The walking model
|
| 430 |
+
|
| 431 |
+
Speed as a function of density uses **Weidmann's (1993) exponential fundamental
|
| 432 |
+
diagram**, the standard empirical pedestrian relation:
|
| 433 |
+
|
| 434 |
+
```
|
| 435 |
+
v(ρ) = v_free · (1 − exp(−γ · (1/ρ − 1/ρ_jam)))
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
with `v_free = 1.34 m/s`, `γ = 1.913`, `ρ_jam = 5.4 p/m²`. Each agent has a
|
| 439 |
+
personal multiplier drawn from a clipped normal (σ = 0.16), so a crowd contains
|
| 440 |
+
fast and slow walkers.
|
| 441 |
+
|
| 442 |
+
This reproduces the two behaviours everything else depends on: unimpeded walking
|
| 443 |
+
at low density, and speed collapse as density approaches jam.
|
| 444 |
+
|
| 445 |
+
### 7.3 The step, in order
|
| 446 |
+
|
| 447 |
+
Each simulated second:
|
| 448 |
+
|
| 449 |
+
1. **Timeline events** fire (capacity changes, phase transitions).
|
| 450 |
+
2. **Cell density and speed** are computed, including the mirrored opposite
|
| 451 |
+
direction.
|
| 452 |
+
3. **Queue extent** is derived (§7.4).
|
| 453 |
+
4. **Agents advance** at their cell's speed × personal factor. A walker cannot
|
| 454 |
+
step into a cell that is already at 90% of jam density, and stops when it
|
| 455 |
+
reaches the back of a standing queue.
|
| 456 |
+
5. **Transition candidates** are gathered: everyone released and waiting, plus
|
| 457 |
+
everyone standing at the head of an edge who is not currently dwelling.
|
| 458 |
+
6. **Node service budget** admits people first-come-first-served by how long they
|
| 459 |
+
have been queueing.
|
| 460 |
+
7. **Edge admission** is limited by three separate constraints (§7.5).
|
| 461 |
+
8. **Moves and absorptions** apply.
|
| 462 |
+
9. **Measurement** updates the Crowd State Engine.
|
| 463 |
+
10. **Routing tables** refresh on a 5-second cadence.
|
| 464 |
+
|
| 465 |
+
### 7.4 Queue extent — a queue is a length, not a point
|
| 466 |
+
|
| 467 |
+
A queue occupies corridor. If you measure it only at the stop line, the standing
|
| 468 |
+
queue has zero physical extent, and the model then makes everyone behind it *walk
|
| 469 |
+
through* a near-jammed corridor at a few centimetres per second to reach the back
|
| 470 |
+
of it.
|
| 471 |
+
|
| 472 |
+
Measured consequence when this was wrong: a gate rated at 500 people/minute
|
| 473 |
+
discharged at **under 200**.
|
| 474 |
+
|
| 475 |
+
The fix: queue extent is derived from everyone who has actually stopped —
|
| 476 |
+
`queue_len = queued_count / (queue_pack_density × width)` — with a packing
|
| 477 |
+
density of 4.6 p/m², lower than jam because a queue that has stopped moving is
|
| 478 |
+
not yet a crush. Walkers then join the *back* of the queue where the back
|
| 479 |
+
actually is.
|
| 480 |
+
|
| 481 |
+
### 7.5 Three admission constraints, and why each is needed
|
| 482 |
+
|
| 483 |
+
An edge accepts people this second up to the minimum of:
|
| 484 |
+
|
| 485 |
+
**(a) Nominal capacity.** `capacity_ppm × dt`, with fractional carry so a 70/min
|
| 486 |
+
link does not admit zero people every second and then seventy at once.
|
| 487 |
+
|
| 488 |
+
**(b) The backward-wave receiving function.** As a link fills, the rate at which
|
| 489 |
+
it can accept anyone new falls towards zero. Congestion propagates *backwards* at
|
| 490 |
+
`backward_wave_mps = 0.36 m/s`:
|
| 491 |
+
|
| 492 |
+
```
|
| 493 |
+
receiving_ppm = 0.36 × 60 × free_space / length
|
| 494 |
+
```
|
| 495 |
+
|
| 496 |
+
This is the cell-transmission idea from traffic flow. Without it a corridor
|
| 497 |
+
silently absorbs an impossible crowd instead of pushing congestion upstream — and
|
| 498 |
+
"congestion spills back" is the entire non-local behaviour described in §2.2.
|
| 499 |
+
|
| 500 |
+
**(c) Entry-cell headroom.** People enter a corridor **at its mouth**, and the
|
| 501 |
+
mouth is one cell wide. A 400 m corridor with room for 2,000 people cannot take
|
| 502 |
+
2,000 people this second, because they would all have to stand in the first
|
| 503 |
+
twelve metres.
|
| 504 |
+
|
| 505 |
+
Constraint (c) was added late, after an existing test caught a peak local density
|
| 506 |
+
of **8.0 p/m²** against a jam density of 5.4 on a corridor whose mean was 1.3.
|
| 507 |
+
Whole-edge headroom had been passing that traffic; the entrance had not.
|
| 508 |
+
|
| 509 |
+
### 7.6 Routing rules that had to be added
|
| 510 |
+
|
| 511 |
+
- **No transit through seating areas or platforms.** A shortest path was
|
| 512 |
+
otherwise happy to cut through a grandstand as a shortcut, misrouting the crowd
|
| 513 |
+
and deadlocking against the people trying to leave. Barcelona gridlocked with
|
| 514 |
+
18,000 people stranded before this rule existed.
|
| 515 |
+
- **No U-turns.** A routing table that has just been re-weighted can briefly make
|
| 516 |
+
the corridor an agent is standing in look like the cheapest way onward. After
|
| 517 |
+
repeated interventions this left 262 agents bouncing between two nodes forever.
|
| 518 |
+
Reversing is refused unless it is genuinely the only option; the residue fell
|
| 519 |
+
to 10.
|
| 520 |
+
- **Penalty clamping and decay.** Intervention penalties are capped and relax
|
| 521 |
+
towards neutral each refresh, so repeated operator action cannot permanently
|
| 522 |
+
distort the cost surface.
|
| 523 |
+
|
| 524 |
+
### 7.7 Compliance
|
| 525 |
+
|
| 526 |
+
Rerouting instructs people; it does not teleport them. Each agent carries a
|
| 527 |
+
compliance probability sampled per scenario (typically 0.40–0.97). An instruction
|
| 528 |
+
to reroute 40% reaches the agents whose route uses the bottleneck, and roughly
|
| 529 |
+
70% of those actually change. The measured improvement is therefore an
|
| 530 |
+
improvement *net of people ignoring you*, which is why it is believable.
|
| 531 |
+
|
| 532 |
+
---
|
| 533 |
+
|
| 534 |
+
## 8. The Crowd State Engine
|
| 535 |
+
|
| 536 |
+
`backend/flowtwin/crowd/`
|
| 537 |
+
|
| 538 |
+
Turns raw agent positions into the quantities that predict failure. Per directed
|
| 539 |
+
edge and per node, every second:
|
| 540 |
+
|
| 541 |
+
| Quantity | Why it is measured |
|
| 542 |
+
|---|---|
|
| 543 |
+
| Occupancy, density | Density, not headcount, is the danger |
|
| 544 |
+
| Peak **local** density | The worst 12 m, not the average |
|
| 545 |
+
| Velocity, and velocity ratio vs free speed | Speed collapse precedes compression |
|
| 546 |
+
| Inflow / outflow (people per minute) | The imbalance *is* the queue growth |
|
| 547 |
+
| Capacity utilisation | How close to the design limit |
|
| 548 |
+
| Density growth (per minute) | Rate of change is the leading indicator |
|
| 549 |
+
| Queue growth (net people/minute) | Same, in people rather than density |
|
| 550 |
+
| Opposing-flow conflict | Two streams on one floor is a distinct hazard |
|
| 551 |
+
| Composite risk score (0–1) | One number for ranking |
|
| 552 |
+
|
| 553 |
+
The **risk score** is a weighted sum, not a density threshold, because a single
|
| 554 |
+
density number cannot distinguish a busy concourse from a compressing queue:
|
| 555 |
+
|
| 556 |
+
```
|
| 557 |
+
risk = 0.30·density + 0.18·utilisation + 0.18·density_growth
|
| 558 |
+
+ 0.12·queue_growth + 0.12·velocity_drop + 0.10·flow_conflict
|
| 559 |
+
```
|
| 560 |
+
|
| 561 |
+
Crucially, `risk_contributions()` exposes the per-term breakdown, so an alert
|
| 562 |
+
does not just say "risk 0.81" — it says **why**: *density rising fast, velocity
|
| 563 |
+
collapsed, queue growing, opposing flow*. A test asserts the contributions sum
|
| 564 |
+
to the score, so the explanation can never drift from the number.
|
| 565 |
+
|
| 566 |
+
Alerts are raised at 0.42 (watch), 0.58 (warning) and 0.74 (critical), and are
|
| 567 |
+
de-duplicated so a two-way corridor produces one alert, not two.
|
| 568 |
+
|
| 569 |
+
---
|
| 570 |
+
|
| 571 |
+
## 9. Prediction
|
| 572 |
+
|
| 573 |
+
`backend/flowtwin/prediction/`
|
| 574 |
+
|
| 575 |
+
### 9.1 The honest-baseline design
|
| 576 |
+
|
| 577 |
+
The predictor is a **gradient-boosted regressor** (`HistGradientBoostingRegressor`),
|
| 578 |
+
one model per horizon (+30, +60, +90, +120 s), predicting density on each edge.
|
| 579 |
+
|
| 580 |
+
The important design decision is what it is measured against. There is an
|
| 581 |
+
**analytic mass-balance baseline** — project density forward from current inflow,
|
| 582 |
+
outflow and free storage — which is genuinely good, because pedestrian flow is
|
| 583 |
+
substantially conservation of people. The trained model is used at inference time
|
| 584 |
+
**only if it beats that baseline on held-out seeds.** Otherwise the system falls
|
| 585 |
+
back to the baseline and says so in the UI.
|
| 586 |
+
|
| 587 |
+
This is what stops "we used ML" from being decoration.
|
| 588 |
+
|
| 589 |
+
### 9.2 Features
|
| 590 |
+
|
| 591 |
+
Seventeen, all from the Crowd State Engine, all quantities an operator would
|
| 592 |
+
recognise:
|
| 593 |
+
|
| 594 |
+
`density`, `density_growth_per_min`, `velocity_ratio`, `inflow_per_capacity`,
|
| 595 |
+
`outflow_per_capacity`, `net_flow_per_capacity`, `occupancy_ratio`,
|
| 596 |
+
`queue_ratio`, `flow_conflict`, `risk`, `upstream_density`, `downstream_density`,
|
| 597 |
+
`downstream_wait_min`, `downstream_service_ratio`, `free_storage_ratio`,
|
| 598 |
+
`length_m`, `width_m`.
|
| 599 |
+
|
| 600 |
+
Note `upstream_density` and `downstream_density`: the model can see the
|
| 601 |
+
neighbourhood, which is how it learns the spill-back behaviour of §2.2.
|
| 602 |
+
|
| 603 |
+
### 9.3 Training and validation
|
| 604 |
+
|
| 605 |
+
The simulator is the data generator, which means **exact ground truth** — the
|
| 606 |
+
label for "density here in 60 seconds" is simply what the density was, sixty
|
| 607 |
+
seconds later, in a run that actually happened.
|
| 608 |
+
|
| 609 |
+
Validation is on **disjoint seeds**: five seeds for training, two entirely
|
| 610 |
+
different seeds held out, across all four scenarios including the railway
|
| 611 |
+
terminus. 421,198 training rows, 169,364 test rows.
|
| 612 |
+
|
| 613 |
+
Measured on held-out seeds:
|
| 614 |
+
|
| 615 |
+
| Horizon | Model MAE | Baseline MAE | Improvement | R² |
|
| 616 |
+
|---|---|---|---|---|
|
| 617 |
+
| +30 s | 0.0097 | 0.0183 | **+47.3%** | 0.999 |
|
| 618 |
+
| +60 s | 0.0159 | 0.0352 | **+54.7%** | 0.998 |
|
| 619 |
+
| +90 s | 0.0221 | 0.0519 | **+57.4%** | 0.996 |
|
| 620 |
+
| +120 s | 0.0278 | 0.0683 | **+59.3%** | 0.993 |
|
| 621 |
+
|
| 622 |
+
The improvement *grows* with horizon, which is what you would hope: the physics
|
| 623 |
+
baseline is nearly right in the short term and degrades as second-order effects
|
| 624 |
+
accumulate; the model captures those.
|
| 625 |
+
|
| 626 |
+
These numbers are visible in the dashboard, not just in a file.
|
| 627 |
+
|
| 628 |
+
### 9.4 The output an operator can use
|
| 629 |
+
|
| 630 |
+
A density number in 90 seconds is not actionable. **"Critical in 96 seconds"** is.
|
| 631 |
+
`time_to_threshold` interpolates the projected trajectory against the venue's
|
| 632 |
+
critical density and reports lead time, which is what the alert displays and what
|
| 633 |
+
the strategy engine uses to decide there is something worth acting on.
|
| 634 |
+
|
| 635 |
+
### 9.5 A performance trap worth knowing about
|
| 636 |
+
|
| 637 |
+
Inference on 66 rows took **1,000 ms**. The same inference on one thread took
|
| 638 |
+
**9 ms**. The BLAS/OpenMP thread pools were fighting over a tiny batch. Thread
|
| 639 |
+
limits are pinned in `flowtwin/__init__.py` *before* numpy or sklearn are
|
| 640 |
+
imported, which is the only place it works.
|
| 641 |
+
|
| 642 |
+
---
|
| 643 |
+
|
| 644 |
+
## 10. The Strategy Engine
|
| 645 |
+
|
| 646 |
+
`backend/flowtwin/strategy/interventions.py`
|
| 647 |
+
|
| 648 |
+
Candidates are **generated from the venue's topology and live state**, not read
|
| 649 |
+
from a fixed list. A candidate only exists if the venue can actually support it.
|
| 650 |
+
|
| 651 |
+
| Candidate | Generated when | What it does |
|
| 652 |
+
|---|---|---|
|
| 653 |
+
| **No action** | Always | The reference every other option is measured against |
|
| 654 |
+
| **Redirect 20 / 30 / 40%** | An alternative path exists | Switches that fraction of the affected agents to adaptive routing with a cost penalty on the bottleneck |
|
| 655 |
+
| **Stagger release** | Origin zones still have people to release | Holds 45% of the remaining departures from the top three feeding zones for 150 s |
|
| 656 |
+
| **Open contingency lanes** | Another exit has **measured** spare capacity right now | +35% throughput there, and diverts 30% of the flow to it |
|
| 657 |
+
| **Open emergency exit** | The venue has one still closed | Unlocks and staffs it — the only option that *adds* capacity — and diverts 35% |
|
| 658 |
+
| **Destination split** | Two interchangeable destinations exist | Moves 30% of demand from one to the other: changing *where people are going*, not just how they get there |
|
| 659 |
+
| **Combined** | Both a reroute and a stagger are available | Redirect 25% and hold 30% of remaining departures for 120 s |
|
| 660 |
+
|
| 661 |
+
Two things are worth pointing out to a judge:
|
| 662 |
+
|
| 663 |
+
- **"Open contingency lanes" quotes measured spare capacity in its own
|
| 664 |
+
description.** It is not offered unless the alternative exit genuinely has room
|
| 665 |
+
at this instant.
|
| 666 |
+
- **Destination split is a different *kind* of lever.** Everything else changes
|
| 667 |
+
routes; this changes destinations — operationally, "your coach has been moved to
|
| 668 |
+
the south apron".
|
| 669 |
+
|
| 670 |
+
---
|
| 671 |
+
|
| 672 |
+
## 11. Counterfactual simulation
|
| 673 |
+
|
| 674 |
+
`backend/flowtwin/strategy/counterfactual.py`
|
| 675 |
+
|
| 676 |
+
This is the part that makes the recommendation a **measurement** rather than a
|
| 677 |
+
rule.
|
| 678 |
+
|
| 679 |
+
```
|
| 680 |
+
capture the current state
|
| 681 |
+
├─ clone → apply "no action" → run 240 s → measure
|
| 682 |
+
├─ clone → apply "redirect 20%" → run 240 s → measure
|
| 683 |
+
├─ clone → apply "redirect 30%" → run 240 s → measure
|
| 684 |
+
├─ clone → apply "stagger release" → run 240 s → measure
|
| 685 |
+
├─ clone → apply "open emergency" → run 240 s → measure
|
| 686 |
+
└─ … one clone per candidate
|
| 687 |
+
compare → score → recommend
|
| 688 |
+
```
|
| 689 |
+
|
| 690 |
+
**Every clone starts byte-identical**, including the random number generator's
|
| 691 |
+
internal bit-generator state. The only difference between two results is the
|
| 692 |
+
intervention. That is the whole scientific claim, and two tests enforce it: one
|
| 693 |
+
asserts that two branches of one state produce identical results, another that
|
| 694 |
+
evaluating strategies does not advance the live run by a single step or move a
|
| 695 |
+
single agent.
|
| 696 |
+
|
| 697 |
+
Cloning is cheap because of the array layout: copy the agent arrays, three small
|
| 698 |
+
integer routing matrices, the capacity budgets and the RNG state.
|
| 699 |
+
|
| 700 |
+
Each roll-out measures sixteen quantities, including peak density on the watched
|
| 701 |
+
asset, density **at the end of the window**, seconds spent critical, network-wide
|
| 702 |
+
critical exposure, mean and p95 journey time, throughput, peak and final queue,
|
| 703 |
+
aggregate risk, and how many people were rerouted.
|
| 704 |
+
|
| 705 |
+
Note what is deliberately watched: **peak density on the asset under threat**,
|
| 706 |
+
not the network maximum. A network maximum set by some unrelated corridor would
|
| 707 |
+
be identical across all candidates and would make every option look the same.
|
| 708 |
+
|
| 709 |
+
---
|
| 710 |
+
|
| 711 |
+
## 12. Multi-objective optimisation and the decisiveness verdict
|
| 712 |
+
|
| 713 |
+
`backend/flowtwin/strategy/optimizer.py`
|
| 714 |
+
|
| 715 |
+
### 12.1 The score
|
| 716 |
+
|
| 717 |
+
Nine terms, each normalised against the no-action arm so a strategy's score reads
|
| 718 |
+
directly as "fraction of the do-nothing outcome". The recommendation is `argmin J`.
|
| 719 |
+
|
| 720 |
+
| Term | Weight | Asks |
|
| 721 |
+
|---|---|---|
|
| 722 |
+
| Peak density | 0.22 | How bad does it get? |
|
| 723 |
+
| Critical duration | 0.20 | How long does it stay dangerous? |
|
| 724 |
+
| **Density at end of window** | 0.12 | What state am I left in? |
|
| 725 |
+
| **Queue at end of window** | 0.10 | What am I still holding? |
|
| 726 |
+
| Average travel time | 0.10 | Are we punishing everyone to help a few? |
|
| 727 |
+
| Aggregate risk | 0.10 | Integrated exposure, not just the peak |
|
| 728 |
+
| Throughput | 0.08 | Are people actually leaving? |
|
| 729 |
+
| Maximum queue | 0.04 | Worst single moment of holding |
|
| 730 |
+
| Rerouting cost | 0.04 | Moving 20,000 people is heavier than moving 2,000 |
|
| 731 |
+
|
| 732 |
+
All weights are environment-variable overridable, and the per-term contributions
|
| 733 |
+
are exposed per strategy, so the table can be audited row by row.
|
| 734 |
+
|
| 735 |
+
### 12.2 Why "end of window" terms exist — the most interesting bug in the project
|
| 736 |
+
|
| 737 |
+
Originally the score was dominated by peak terms. Intervene early and it worked
|
| 738 |
+
beautifully. Intervene late and **every candidate returned an identical peak
|
| 739 |
+
density to three decimal places**, and the "winner" was decided by the
|
| 740 |
+
reroute-cost tiebreak — whichever option moved fewest people.
|
| 741 |
+
|
| 742 |
+
The root cause is physical, not a coding error. Once a 4,000-person queue exists
|
| 743 |
+
at a service-limited exit, it drains at the gate rate regardless of routing. The
|
| 744 |
+
peak over the window is already determined. Peak-only scoring genuinely cannot
|
| 745 |
+
tell the candidates apart.
|
| 746 |
+
|
| 747 |
+
Two things were tried:
|
| 748 |
+
|
| 749 |
+
1. **Lengthen the roll-out.** Measured: separation returns only at a **720-second**
|
| 750 |
+
horizon, costing 27 seconds of compute — for an answer that is still "this
|
| 751 |
+
barely helps". Rejected on evidence.
|
| 752 |
+
2. **Add end-of-window terms.** Peaks ask "how bad does it get"; end-of-window
|
| 753 |
+
terms ask "what am I still holding when the window closes". A strategy that
|
| 754 |
+
leaves the bottleneck 1,500 people lighter at T+horizon is better even when
|
| 755 |
+
both runs touched the same maximum. Adopted.
|
| 756 |
+
|
| 757 |
+
### 12.3 The decisiveness verdict
|
| 758 |
+
|
| 759 |
+
The end-of-window terms sharpened the early case but did not manufacture a
|
| 760 |
+
difference where there genuinely was none. So a second mechanism was added:
|
| 761 |
+
|
| 762 |
+
> A candidate must beat no-action by at least **1.5%** of the do-nothing score
|
| 763 |
+
> before it is *recommended*. Below that, the ranking still shows exactly what was
|
| 764 |
+
> measured, but the recommendation falls back to no action and the system explains
|
| 765 |
+
> why.
|
| 766 |
+
|
| 767 |
+
The explanation is generated from the measured bottleneck state — queue held,
|
| 768 |
+
discharge rate, arrival rate, estimated clearance time — and is quoted in full in
|
| 769 |
+
§5.
|
| 770 |
+
|
| 771 |
+
This turned the weakest moment in the demo into one of the strongest. A system
|
| 772 |
+
that knows when it cannot help is more credible than one that always has an
|
| 773 |
+
answer, and it removes the landmine of a judge pressing the button at the wrong
|
| 774 |
+
moment.
|
| 775 |
+
|
| 776 |
+
Guarded at both ends by tests: one asserts the early case still separates
|
| 777 |
+
decisively, one asserts the late case refuses to pick a winner. The late fix
|
| 778 |
+
cannot be obtained by flattening the early case.
|
| 779 |
+
|
| 780 |
+
### 12.4 Explainability with no language model anywhere
|
| 781 |
+
|
| 782 |
+
The "why this strategy" panel is generated from **the same normalised terms that
|
| 783 |
+
produced the score**. There is no narrative layer that could drift away from the
|
| 784 |
+
arithmetic, and there is no LLM in the decision path.
|
| 785 |
+
|
| 786 |
+
This is a deliberate, defensible position: every claim on screen is traceable to
|
| 787 |
+
a measured number, and the reasoning shown is literally the reasoning used.
|
| 788 |
+
|
| 789 |
+
---
|
| 790 |
+
|
| 791 |
+
## 13. Dynamic routing
|
| 792 |
+
|
| 793 |
+
`backend/flowtwin/routing/`
|
| 794 |
+
|
| 795 |
+
### 13.1 Next-hop tables
|
| 796 |
+
|
| 797 |
+
Rather than storing a route per agent, FlowTwin stores, for every **policy** and
|
| 798 |
+
every **destination**, the best next edge from each node. 40,000 agents then
|
| 799 |
+
route with a single fancy-index lookup, and a change in conditions re-routes
|
| 800 |
+
everybody who has not committed, in one Dijkstra per destination.
|
| 801 |
+
|
| 802 |
+
It is also what makes counterfactuals affordable: cloning the routing state is
|
| 803 |
+
cloning three small integer matrices.
|
| 804 |
+
|
| 805 |
+
### 13.2 Three policies, which are also the benchmark arms
|
| 806 |
+
|
| 807 |
+
| Policy | What it is |
|
| 808 |
+
|---|---|
|
| 809 |
+
| **Shortest path** | Baseline A. Distance only. What people do without guidance. |
|
| 810 |
+
| **Static assignment** | Baseline B. A real pre-event plan: method-of-successive-averages traffic assignment with BPR-style congestion costs, computed before the event from expected demand and never revised. |
|
| 811 |
+
| **FlowTwin adaptive** | Live cost from distance, travel time, congestion, density, capacity and risk, refreshed every 5 simulated seconds. |
|
| 812 |
+
|
| 813 |
+
Baseline B matters. It is not a straw man — it is what a competent operations
|
| 814 |
+
team actually produces, and beating it is the interesting claim.
|
| 815 |
+
|
| 816 |
+
### 13.3 Oscillation control
|
| 817 |
+
|
| 818 |
+
A naive adaptive router flaps: send people east, the east fills, send them west,
|
| 819 |
+
the west fills. Four mechanisms prevent it:
|
| 820 |
+
|
| 821 |
+
- **Hysteresis** — a node abandons its incumbent next hop only when the
|
| 822 |
+
challenger is at least ~22% cheaper.
|
| 823 |
+
- **Route commitment** — an agent keeps an adopted route for at least 25 s.
|
| 824 |
+
- **Cycle breaking** — asserted acyclic by test.
|
| 825 |
+
- **Penalty decay** — intervention penalties relax 2% per refresh towards neutral.
|
| 826 |
+
|
| 827 |
+
---
|
| 828 |
+
|
| 829 |
+
## 14. Perception — the Hugging Face path
|
| 830 |
+
|
| 831 |
+
`backend/flowtwin/perception/`
|
| 832 |
+
|
| 833 |
+
### 14.1 Where it sits, and why that placement is the point
|
| 834 |
+
|
| 835 |
+
```
|
| 836 |
+
camera frame ──► Hugging Face crowd model ──► crowd observation ─┐
|
| 837 |
+
├─► Crowd State Engine ─► prediction ─► strategy
|
| 838 |
+
simulated agents ────────────────────────────────────────────────┘
|
| 839 |
+
```
|
| 840 |
+
|
| 841 |
+
Both input modes converge on **one observation schema**. Density, risk,
|
| 842 |
+
prediction, counterfactual and recommendation are then identical code whichever
|
| 843 |
+
source is feeding them. A deployment can swap simulated crowds for real cameras
|
| 844 |
+
without touching the decision path.
|
| 845 |
+
|
| 846 |
+
It is deliberately **not** in the decision path itself. Nothing downstream
|
| 847 |
+
depends on a neural network's opinion.
|
| 848 |
+
|
| 849 |
+
### 14.2 The candidate chain
|
| 850 |
+
|
| 851 |
+
Tried in order; the first that loads wins; the selection is written to
|
| 852 |
+
`models/perception_manifest.json`:
|
| 853 |
+
|
| 854 |
+
1. `AbdurRahman011/csrnet-indian-metro-crowd-density` — density-map regression.
|
| 855 |
+
Counts by integrating a predicted density map, so it degrades gracefully in
|
| 856 |
+
dense crowds where detectors fail. Trained on Indian metro crowds.
|
| 857 |
+
2. `AmineSam/irail-crowd-counting-yolov8n` — head detection fine-tuned on
|
| 858 |
+
RPEE-Heads (railway platforms and event entrances).
|
| 859 |
+
3. `hustvl/yolos-tiny` — widely mirrored COCO detector, `person` class.
|
| 860 |
+
4. `facebook/detr-resnet-50` — second fallback.
|
| 861 |
+
|
| 862 |
+
CSRNet's architecture is defined locally in `perception/csrnet.py` so a bare
|
| 863 |
+
`state_dict` checkpoint can be loaded.
|
| 864 |
+
|
| 865 |
+
### 14.3 Sample frames with exact ground truth
|
| 866 |
+
|
| 867 |
+
Three frames ship in `data/perception/`, **rendered from the digital twin** rather
|
| 868 |
+
than photographed — a top-down view of a real corridor at a real moment of a real
|
| 869 |
+
seeded run, one marker per person actually standing there.
|
| 870 |
+
|
| 871 |
+
| Frame | People in shot | Area | Density |
|
| 872 |
+
|---|---|---|---|
|
| 873 |
+
| Exit B approach, free-flowing | 260 | 396 m² | 0.66 p/m² |
|
| 874 |
+
| Exit B approach, standing queue | 1,762 | 396 m² | 4.45 p/m² |
|
| 875 |
+
| Central foot-over-bridge, surge | 522 | 576 m² | 0.91 p/m² |
|
| 876 |
+
|
| 877 |
+
Two reasons for renders rather than photographs. Shipping third-party crowd
|
| 878 |
+
photographs in a public repository is a licensing problem. And a render has a
|
| 879 |
+
property no photograph has: **the count is known exactly**, so the panel reports
|
| 880 |
+
the model's *error* and not just its answer. A model that reports 1,300 on a frame
|
| 881 |
+
containing 1,762 has undercounted by 26%, and being able to say that is worth more
|
| 882 |
+
than a number with nothing to check it against.
|
| 883 |
+
|
| 884 |
+
The UI labels them as renders. Uploading a real photograph runs the identical path.
|
| 885 |
+
|
| 886 |
+
### 14.4 Honest status
|
| 887 |
+
|
| 888 |
+
**Not yet verified against downloaded weights.** The build environment has no
|
| 889 |
+
network route to `huggingface.co` (every attempt returns `403 Tunnel connection
|
| 890 |
+
failed`). Implemented and tested: the chain, the loader, the local CSRNet
|
| 891 |
+
architecture, the manifest, the image → count → observation path, and the failure
|
| 892 |
+
behaviour. Not executed: one real inference against real weights.
|
| 893 |
+
|
| 894 |
+
One command closes it on any networked machine:
|
| 895 |
+
|
| 896 |
+
```bash
|
| 897 |
+
pip install -r backend/requirements.txt
|
| 898 |
+
python scripts/fetch_hf_model.py
|
| 899 |
+
```
|
| 900 |
+
|
| 901 |
+
**If it is never run, the endpoint reports the actual error and returns nothing.
|
| 902 |
+
It has never fabricated a count, and a test asserts that.** Full record in
|
| 903 |
+
[`HUGGING_FACE.md`](HUGGING_FACE.md).
|
| 904 |
+
|
| 905 |
+
---
|
| 906 |
+
---
|
| 907 |
+
|
| 908 |
+
# Part III — The system as software
|
| 909 |
+
|
| 910 |
+
## 15. Architecture and module map
|
| 911 |
+
|
| 912 |
+
```
|
| 913 |
+
flowtwin/
|
| 914 |
+
├── backend/
|
| 915 |
+
│ ├── flowtwin/
|
| 916 |
+
│ │ ├── __init__.py Thread-pool pinning (must precede numpy import)
|
| 917 |
+
│ │ ├── config.py Every tuning constant, all env-overridable
|
| 918 |
+
│ │ ├── main.py FastAPI app, lifespan, static mount
|
| 919 |
+
│ │ ├── venue/ Domain model, compilation, scenario loading
|
| 920 |
+
│ │ ├── simulation/ Agents, movement physics, the engine
|
| 921 |
+
│ │ ├── crowd/ Density, flow, risk, alerts — the Crowd State Engine
|
| 922 |
+
│ │ ├── prediction/ Features, analytic baseline, trained-model inference
|
| 923 |
+
│ │ ├── routing/ Cost model, next-hop tables, static assignment
|
| 924 |
+
│ │ ├── strategy/ Interventions, counterfactuals, optimiser, explanation
|
| 925 |
+
│ │ ├── perception/ Hugging Face chain, CSRNet, observation schema
|
| 926 |
+
│ │ ├── benchmarks/ Multi-seed, multi-arm evaluation harness
|
| 927 |
+
│ │ ├── runtime/ Session lifecycle, broadcast loop, replay sessions
|
| 928 |
+
│ │ └── api/ Routes, request/response schemas, WebSocket
|
| 929 |
+
│ └── tests/ 79 tests across simulation, intelligence, API
|
| 930 |
+
├── frontend/ Zero-build ES modules + Canvas 2D
|
| 931 |
+
├── data/
|
| 932 |
+
│ ├── venues/ 3 venue JSON files
|
| 933 |
+
│ ├── scenarios/ 4 scenario JSON files
|
| 934 |
+
│ ├── perception/ 3 sample frames + ground-truth index
|
| 935 |
+
│ └── fallback/ Pre-recorded frames (gitignored, regenerable)
|
| 936 |
+
├── models/ Trained predictor + its validation report
|
| 937 |
+
├── benchmarks/ Generated results, never hand-edited
|
| 938 |
+
├── scripts/ build_venues, train_predictor, run_benchmarks,
|
| 939 |
+
│ make_perception_samples, fetch_hf_model,
|
| 940 |
+
│ record_fallback, ui_check
|
| 941 |
+
└── docs/ This file, ARCHITECTURE, DEMO, PS3_AUDIT,
|
| 942 |
+
SPEC_AUDIT, HUGGING_FACE, ROADMAP
|
| 943 |
+
```
|
| 944 |
+
|
| 945 |
+
Roughly **6,600 lines of backend Python**, **2,750 lines of frontend**, and
|
| 946 |
+
**1,100 lines of tests**.
|
| 947 |
+
|
| 948 |
+
**Deliberate omissions.** No Redis, no PostgreSQL, no Docker, no build step. A
|
| 949 |
+
simulation session is in-memory state on one process by nature; adding a datastore
|
| 950 |
+
would mean serialising 40,000 agents per frame to solve a problem that does not
|
| 951 |
+
exist at this scale. The rationale is written down in `ARCHITECTURE.md §10` so the
|
| 952 |
+
absence reads as a decision rather than an omission.
|
| 953 |
+
|
| 954 |
+
---
|
| 955 |
+
|
| 956 |
+
## 16. Data flow and real-time transport
|
| 957 |
+
|
| 958 |
+
```
|
| 959 |
+
Browser FastAPI Simulator
|
| 960 |
+
│ │ │
|
| 961 |
+
├─ POST /api/simulation/start ───►│─ build venue, population ────►│
|
| 962 |
+
│◄──────── session + first frame ─┤ │
|
| 963 |
+
│ │ │
|
| 964 |
+
├─ WS /api/simulation/{id}/stream►│ │
|
| 965 |
+
│ │ every 200 ms of wall clock: │
|
| 966 |
+
│ │ step × speed ──────────────►│
|
| 967 |
+
│ │◄──── state ───────────────────┤
|
| 968 |
+
│◄───────────── frame (push) ─────┤ │
|
| 969 |
+
│ │ │
|
| 970 |
+
├─ POST /strategy/simulate ──────►│─ clone × 8, roll out ────────►│
|
| 971 |
+
│◄──── ranked strategies + why ───┤ │
|
| 972 |
+
├─ POST /strategy/apply ─────────►│─ apply to the live run ──────►│
|
| 973 |
+
```
|
| 974 |
+
|
| 975 |
+
**No per-frame polling.** The server pushes; the browser renders. Frames carry the
|
| 976 |
+
crowd state, a bounded sample of agent positions for drawing (2,600 by default —
|
| 977 |
+
a rendering budget, not a simulation limit), alerts, predictions and events.
|
| 978 |
+
|
| 979 |
+
Sessions with no subscribers idle and are reaped. That was a real bug: a refreshed
|
| 980 |
+
browser tab left an orphaned session simulating at 40×, which starved the event
|
| 981 |
+
loop and made new runs appear to hang.
|
| 982 |
+
|
| 983 |
+
---
|
| 984 |
+
|
| 985 |
+
## 17. The frontend
|
| 986 |
+
|
| 987 |
+
**Zero build step.** Vanilla ES modules served by the same FastAPI process. No
|
| 988 |
+
npm, no bundler, no version skew, nothing to break on demo day. The trade-off
|
| 989 |
+
against a React/Next.js frontend was made deliberately and is written down.
|
| 990 |
+
|
| 991 |
+
**Layout.** The map dominates. Panels are subordinate.
|
| 992 |
+
|
| 993 |
+
- **Left rail** — *Inputs*: expected crowd size, arrival/departure window, reroute
|
| 994 |
+
compliance, the scheduled event and its severity, seed, baseline routing policy.
|
| 995 |
+
Below it, the *Event schedule* showing what will execute and what has fired.
|
| 996 |
+
Below that, on the Barcelona venue only, *Evidence & assumptions*.
|
| 997 |
+
- **Centre** — the venue map on Canvas 2D: landmarks, corridors coloured by
|
| 998 |
+
measured density, animated agents, predicted congestion drawn distinctly from
|
| 999 |
+
current congestion, and rerouting paths when an intervention is applied.
|
| 1000 |
+
Layer toggles, a density legend and a scale bar.
|
| 1001 |
+
- **Right rail** — *Alerts* with severity, cause and lead time; *Prediction* with
|
| 1002 |
+
per-horizon projections and a model-accuracy modal; *Strategy* with the simulate
|
| 1003 |
+
button and the recommendation card.
|
| 1004 |
+
- **Drawer** — the strategy simulator: the full comparison table, the "why this
|
| 1005 |
+
strategy" panel, and the projected-density chart per candidate.
|
| 1006 |
+
|
| 1007 |
+
**A rendering bug worth knowing about.** Frames arrive five times a second.
|
| 1008 |
+
Rebuilding an alert card on every frame restarts its CSS entry animation, which
|
| 1009 |
+
left the alert panel permanently mid-fade — measured opacity **0.26**, effectively
|
| 1010 |
+
invisible. Cards are now keyed on structure (`base_id:severity`) and live values
|
| 1011 |
+
are written in place. This shipped broken once.
|
| 1012 |
+
|
| 1013 |
+
---
|
| 1014 |
+
|
| 1015 |
+
## 18. Reproducibility and determinism
|
| 1016 |
+
|
| 1017 |
+
Every run is fully determined by **(venue, scenario, seed, overrides)**.
|
| 1018 |
+
|
| 1019 |
+
- The RNG's bit-generator state travels inside the snapshot, so a restored state
|
| 1020 |
+
produces the identical future.
|
| 1021 |
+
- Dwell decisions are drawn once at population creation, not from a live stream,
|
| 1022 |
+
for the same reason.
|
| 1023 |
+
- Interventions use a separate random stream so that applying a strategy never
|
| 1024 |
+
perturbs the population's own draws.
|
| 1025 |
+
- The seed is displayed in the metrics strip during every run.
|
| 1026 |
+
|
| 1027 |
+
Tested directly: snapshot/restore is exact; two branches of one state are
|
| 1028 |
+
identical; branching does not disturb the parent; evaluating strategies does not
|
| 1029 |
+
advance the live simulation.
|
| 1030 |
+
|
| 1031 |
+
This is what makes the benchmark numbers checkable rather than assertable.
|
| 1032 |
+
|
| 1033 |
+
---
|
| 1034 |
+
|
| 1035 |
+
## 19. The three venues
|
| 1036 |
+
|
| 1037 |
+
All three are plain JSON against one schema. No venue-specific engine code exists.
|
| 1038 |
+
|
| 1039 |
+
### Circuit Alpha — fictional Grand Prix venue
|
| 1040 |
+
30 nodes, 43 edges. Four perimeter exits, six spectator zones, a full concourse
|
| 1041 |
+
ring, three concession clusters, one emergency egress route, two transport
|
| 1042 |
+
interfaces and two car parks. **40,000 spectators**, simultaneous egress over an
|
| 1043 |
+
18-minute curve, with Exit B losing half its throughput at T+4:00. This is the
|
| 1044 |
+
controlled stress test — the most instrumented venue, and the one the benchmark
|
| 1045 |
+
headline comes from.
|
| 1046 |
+
|
| 1047 |
+
### Circuit de Barcelona-Catalunya — documented-condition reconstruction
|
| 1048 |
+
22 nodes, 33 edges. **78,000 spectators** at race-day scale, with the Montmeló
|
| 1049 |
+
rail approach deliberately constrained.
|
| 1050 |
+
|
| 1051 |
+
The discipline here is the point. Every documented fact carries a source; every
|
| 1052 |
+
modelling assumption is labelled as an assumption; **both lists are on screen
|
| 1053 |
+
throughout**. The disclaimer is in the venue data, the briefing and the UI:
|
| 1054 |
+
|
| 1055 |
+
> This is a counterfactual reconstruction using publicly documented event
|
| 1056 |
+
> conditions and a synthetic crowd model. It is not a replay of original
|
| 1057 |
+
> spectator telemetry, which is not public.
|
| 1058 |
+
|
| 1059 |
+
The question it answers is *"given the documented conditions, what would FlowTwin
|
| 1060 |
+
have recommended?"* — never *"this is what happened."*
|
| 1061 |
+
|
| 1062 |
+
### Sangam Junction — fictional Indian metropolitan railway terminus
|
| 1063 |
+
22 nodes, 34 edges. **26,000 passengers** discharged from six platforms over
|
| 1064 |
+
sixteen minutes, all of whom must change level through one of three routes: two
|
| 1065 |
+
foot-over-bridges and a subway. At T+4:30 the west bridge is closed to a quarter
|
| 1066 |
+
of its capacity on safety orders; at T+10:00 east gate screening slows.
|
| 1067 |
+
|
| 1068 |
+
This venue exists as **evidence**, not decoration:
|
| 1069 |
+
|
| 1070 |
+
- The **failure mode is different in kind**. A circuit fails at its perimeter; a
|
| 1071 |
+
terminus fails in the middle, at the level change, and the constraint is stair
|
| 1072 |
+
width rather than gate count.
|
| 1073 |
+
- The **food court is on the circulation path**, so about a quarter of the people
|
| 1074 |
+
crossing it stop for ~95 s and the concourse goes amber before the bridges do.
|
| 1075 |
+
The north gallery bypasses it at the cost of a longer walk — which is what gives
|
| 1076 |
+
the strategy engine a real question.
|
| 1077 |
+
- The **emergency gate is shut** and genuinely absent from routing.
|
| 1078 |
+
|
| 1079 |
+
Building it required **one new node type and zero special-case simulation code**.
|
| 1080 |
+
It is fictional and labelled fictional; no real station is named and no real
|
| 1081 |
+
incident is reconstructed.
|
| 1082 |
+
|
| 1083 |
+
---
|
| 1084 |
+
|
| 1085 |
+
## 20. Testing and verification
|
| 1086 |
+
|
| 1087 |
+
**79 automated tests**, in three files:
|
| 1088 |
+
|
| 1089 |
+
- `test_simulation.py` (26) — the walking model's monotonicity, capacity budgets
|
| 1090 |
+
and fractional carry, queue behaviour, density never exceeding jam, snapshot
|
| 1091 |
+
exactness, branch independence, diversion and compliance, staggering, the
|
| 1092 |
+
What-If control genuinely retuning the scheduled event, emergency-exit routing
|
| 1093 |
+
exclusion and use, concession dwell and its reproducibility.
|
| 1094 |
+
- `test_intelligence.py` (28) — density and threshold maths, risk contributions
|
| 1095 |
+
summing to the score, bottleneck detection finding the right asset, alert
|
| 1096 |
+
de-duplication, feature-matrix sanity, prediction responding to a real change in
|
| 1097 |
+
state, routing acyclicity under hysteresis, adaptive routing genuinely avoiding
|
| 1098 |
+
the congested asset, counterfactual determinism, evaluation not advancing the
|
| 1099 |
+
live run, optimiser separation at an early intervention, optimiser refusal at a
|
| 1100 |
+
late one.
|
| 1101 |
+
- `test_api.py` (25) — every endpoint's success and failure modes, validation
|
| 1102 |
+
rejection, perception failing honestly, the sample route and its path-traversal
|
| 1103 |
+
guard, replay fallback.
|
| 1104 |
+
|
| 1105 |
+
**Beyond unit tests:**
|
| 1106 |
+
|
| 1107 |
+
- `scripts/ui_check.py` drives the entire acceptance path in a real Chromium
|
| 1108 |
+
browser via Playwright — load, run, wait for a critical alert, simulate
|
| 1109 |
+
strategies, check a recommendation is highlighted, apply it, watch the
|
| 1110 |
+
redistribution, switch to Barcelona and check the provenance panel, switch to
|
| 1111 |
+
the terminus and check its schedule, open the perception panel and verify every
|
| 1112 |
+
sample thumbnail actually loads. **Any console error or failed request fails
|
| 1113 |
+
the run.** It saves screenshots at each step.
|
| 1114 |
+
- `scripts/run_benchmarks.py` produces the quantitative results from real
|
| 1115 |
+
multi-seed runs. No figure in any document is typed by hand.
|
| 1116 |
+
- `.github/workflows/ci.yml` regenerates the venues and runs the suite on push.
|
| 1117 |
+
|
| 1118 |
+
---
|
| 1119 |
+
---
|
| 1120 |
+
|
| 1121 |
+
# Part IV — Evidence
|
| 1122 |
+
|
| 1123 |
+
## 21. Measured results
|
| 1124 |
+
|
| 1125 |
+
Generated by `scripts/run_benchmarks.py`. Three arms — baseline shortest path,
|
| 1126 |
+
a static pre-event plan, and the full FlowTwin loop — across **8 independent
|
| 1127 |
+
seeds** of the complete simulation. Mean ± standard deviation.
|
| 1128 |
+
|
| 1129 |
+
### Circuit Alpha · 40,000 spectators · 8 seeds
|
| 1130 |
+
|
| 1131 |
+
| Metric | Shortest path | Static plan | **FlowTwin** | vs baseline |
|
| 1132 |
+
|---|---|---|---|---|
|
| 1133 |
+
| Peak density (p/m²) | 3.6 ± 0.0 | 3.4 ± 0.1 | **2.0 ± 0.4** | **−42.6%** |
|
| 1134 |
+
| Critical exposure (corridor·s) | 1733 ± 130 | 1071 ± 216 | **0 ± 0** | **−100%** |
|
| 1135 |
+
| Maximum queue (people) | 4327 ± 56 | 4063 ± 111 | **2245 ± 440** | **−48.1%** |
|
| 1136 |
+
| Average journey (s) | 867 ± 12 | 798 ± 13 | **808 ± 15** | **−6.7%** |
|
| 1137 |
+
| 95th-percentile journey (s) | 2043 ± 89 | 1772 ± 94 | **1837 ± 106** | **−10.1%** |
|
| 1138 |
+
| Dispersal time, 95% (s) | 2523 ± 81 | 2241 ± 85 | **2286 ± 154** | **−9.4%** |
|
| 1139 |
+
| People rerouted | 0 | 2036 | 5814 | — |
|
| 1140 |
+
|
| 1141 |
+
This is the headline. Time spent above the critical density goes to **zero on
|
| 1142 |
+
every seed**, peak density falls by 43%, the worst queue nearly halves — and
|
| 1143 |
+
average journey time gets *better*, not worse. Crowd-safety interventions usually
|
| 1144 |
+
trade delay for safety; here the congestion relief more than pays for the detour.
|
| 1145 |
+
|
| 1146 |
+
The static plan is a genuine competitor, not a straw man: it beats naive
|
| 1147 |
+
shortest-path handily. FlowTwin beats it on every safety metric.
|
| 1148 |
+
|
| 1149 |
+
The standard deviations are informative too. FlowTwin's peak density varies more
|
| 1150 |
+
across seeds (±0.4) than the baselines (±0.0–0.1), which is exactly what you
|
| 1151 |
+
would expect: the baselines always fail the same way, while an adaptive system's
|
| 1152 |
+
outcome depends on when the bottleneck happened to be caught.
|
| 1153 |
+
|
| 1154 |
+
### Circuit de Barcelona-Catalunya · 78,000 spectators · 6 seeds
|
| 1155 |
+
|
| 1156 |
+
| Metric | Shortest path | Static plan | **FlowTwin** | vs baseline |
|
| 1157 |
+
|---|---|---|---|---|
|
| 1158 |
+
| Peak density (p/m²) | 3.2 ± 0.1 | 3.2 ± 0.1 | **1.3 ± 0.2** | **−59.0%** |
|
| 1159 |
+
| Critical exposure (corridor·s) | 1254 ± 165 | 1254 ± 165 | **0 ± 0** | **−100%** |
|
| 1160 |
+
| Maximum queue (people) | 3677 ± 152 | 3677 ± 152 | **1107 ± 228** | **−69.9%** |
|
| 1161 |
+
| Average journey (s) | 721 ± 3 | 721 ± 3 | 786 ± 26 | **+9.0%** |
|
| 1162 |
+
| 95th-percentile journey (s) | 1321 ± 7 | 1321 ± 7 | 1743 ± 144 | **+32.0%** |
|
| 1163 |
+
| Dispersal time, 95% (s) | 2514 ± 8 | 2514 ± 8 | 2837 ± 40 | **+12.8%** |
|
| 1164 |
+
| People rerouted | 0 | 0 | 8179 | — |
|
| 1165 |
+
|
| 1166 |
+
**This one has a real trade-off and it is reported, not hidden.** Barcelona's
|
| 1167 |
+
danger sits on a narrow transport interface, and relieving it means sending
|
| 1168 |
+
thousands of people the long way round. Safety improves dramatically — peak
|
| 1169 |
+
density down 59%, the worst queue down 70%, critical exposure eliminated on every
|
| 1170 |
+
seed — and it costs 9% on the average journey and **32% on the 95th percentile**.
|
| 1171 |
+
|
| 1172 |
+
That is the honest shape of the decision. One person in twenty gets home
|
| 1173 |
+
substantially later so that nobody stands in a dangerous crush. An operator
|
| 1174 |
+
should be told that price rather than sold a free lunch, and the optimiser's
|
| 1175 |
+
`avg_travel_time` weight is exactly the dial that sets how much of it you are
|
| 1176 |
+
willing to pay.
|
| 1177 |
+
|
| 1178 |
+
**Two baselines, identical results.** On this venue shortest-path and the static
|
| 1179 |
+
plan produce byte-identical numbers, because most origin–destination pairs in the
|
| 1180 |
+
reconstructed topology have exactly one sensible route. That is a genuine property
|
| 1181 |
+
of the topology, not a broken benchmark, and it is documented rather than quietly
|
| 1182 |
+
dropped.
|
| 1183 |
+
|
| 1184 |
+
### Sangam Junction · railway terminus · 26,000 passengers · 6 seeds
|
| 1185 |
+
|
| 1186 |
+
| Metric | Shortest path | Static plan | **FlowTwin** | vs baseline |
|
| 1187 |
+
|---|---|---|---|---|
|
| 1188 |
+
| Peak density (p/m²) | 3.0 ± 0.0 | 2.5 ± 0.2 | **2.4 ± 0.3** | **−20.3%** |
|
| 1189 |
+
| Critical exposure (corridor·s) | 0 | 0 | 0 | — |
|
| 1190 |
+
| Maximum queue (people) | 3600 ± 56 | 3459 ± 33 | 3883 ± 510 | +7.9% |
|
| 1191 |
+
| Average journey (s) | 887 ± 4 | 853 ± 40 | 1028 ± 87 | **+15.9%** |
|
| 1192 |
+
| 95th-percentile journey (s) | 1708 ± 33 | 1849 ± 295 | 2739 ± 613 | **+60.3%** |
|
| 1193 |
+
| Dispersal time, 95% (s) | 2257 ± 7 | 2431 ± 324 | 3298 ± 581 | **+46.1%** |
|
| 1194 |
+
| People rerouted | 0 | 2063 | 649 | — |
|
| 1195 |
+
|
| 1196 |
+
**This is the worst table in the project and it is here on purpose.** On the
|
| 1197 |
+
terminus FlowTwin shaves 20% off peak density and pays for it with 16% on the
|
| 1198 |
+
average journey, 60% on the 95th percentile, and 46% on dispersal. Critical
|
| 1199 |
+
exposure is zero in *every* arm — at this crowd size the venue never becomes
|
| 1200 |
+
dangerous. So the system bought a safety improvement nobody needed, with a delay
|
| 1201 |
+
cost everybody paid.
|
| 1202 |
+
|
| 1203 |
+
Do not hide this. Understand it, because the cause is precise and the fix is
|
| 1204 |
+
known.
|
| 1205 |
+
|
| 1206 |
+
**Cause 1 — the venue is capacity-limited, not routing-limited.** Measured at the
|
| 1207 |
+
peak of the surge, every level-change route is at its service limit at the same
|
| 1208 |
+
moment:
|
| 1209 |
+
|
| 1210 |
+
| Route | Capacity | In use | Spare |
|
| 1211 |
+
|---|---|---|---|
|
| 1212 |
+
| West foot-over-bridge (closed to 25%) | 130 /min | 129 | **1** |
|
| 1213 |
+
| Central foot-over-bridge | 900 /min | 900 | **0** |
|
| 1214 |
+
| East subway | 780 /min | 729 | 51 |
|
| 1215 |
+
|
| 1216 |
+
Rerouting redistributes flow across capacity already in service. When all of it
|
| 1217 |
+
is saturated there is nothing to redistribute — which is why FlowTwin moves only
|
| 1218 |
+
649 people here against 5,814 at Circuit Alpha. The decisiveness verdict is doing
|
| 1219 |
+
its job: most of the time it declines to act.
|
| 1220 |
+
|
| 1221 |
+
**Cause 2 — the benchmark harness acts on a fixed review cycle; a human does
|
| 1222 |
+
not.** The FlowTwin arm re-evaluates every 180 s and applies whatever clears the
|
| 1223 |
+
1.5% decisiveness bar, for the whole run. That makes the benchmark an **upper
|
| 1224 |
+
bound on intervention frequency**, not a model of the product's behaviour: in the
|
| 1225 |
+
console an operator presses the button when an alert says something is going
|
| 1226 |
+
critical, and on this venue nothing ever does. The measured cost above is the
|
| 1227 |
+
cost of intervening on a venue that did not need intervening on.
|
| 1228 |
+
|
| 1229 |
+
**The fix, and it is the top of the roadmap.** The decisiveness threshold guards
|
| 1230 |
+
against candidates that are *indistinguishable from each other*. It does not yet
|
| 1231 |
+
guard against acting when *nothing is at risk*. A materiality gate — do not
|
| 1232 |
+
recommend an intervention if the projected peak stays below the venue's critical
|
| 1233 |
+
density across the whole window — closes it, and it is the same shape of
|
| 1234 |
+
judgement as the existing verdict. It is scoped in `ROADMAP.md` and it was found
|
| 1235 |
+
by this benchmark, which is the benchmark doing exactly what it is for.
|
| 1236 |
+
|
| 1237 |
+
**What to say about it in a pitch.** Two true things, in this order:
|
| 1238 |
+
|
| 1239 |
+
1. *"A circuit is routing-limited: one exit failed while others had room, and we
|
| 1240 |
+
cut critical exposure to zero. A terminus is capacity-limited: all three
|
| 1241 |
+
staircases saturate at once, so we tell you rerouting won't help. Those are
|
| 1242 |
+
different problems and the system distinguishes them."*
|
| 1243 |
+
2. *"And here's the honest part — on the terminus our benchmark harness keeps
|
| 1244 |
+
intervening anyway, on a cycle, and it costs journey time for a safety
|
| 1245 |
+
improvement that venue didn't need. That's a real finding from our own
|
| 1246 |
+
evaluation, and the gate that fixes it is the next thing we're building."*
|
| 1247 |
+
|
| 1248 |
+
Owning that is worth more than a table with no weak column in it.
|
| 1249 |
+
|
| 1250 |
+
The generated tables for all three venues, with every seed and every metric, are
|
| 1251 |
+
in `benchmarks/BENCHMARKS.md`. **No figure in this document was typed by hand.**
|
| 1252 |
+
|
| 1253 |
+
---
|
| 1254 |
+
|
| 1255 |
+
## 22. Every defect found and fixed
|
| 1256 |
+
|
| 1257 |
+
This section exists because it is the strongest evidence that the model is right
|
| 1258 |
+
rather than merely convincing. Each of these was found by testing against physical
|
| 1259 |
+
reality, not by a linter.
|
| 1260 |
+
|
| 1261 |
+
| # | Symptom | Root cause | Fix |
|
| 1262 |
+
|---|---|---|---|
|
| 1263 |
+
| 1 | Network throughput collapsed to ~1/10 of correct | Density averaged over a whole corridor, so a queue at a gate slowed people 200 m back with clear space | Density and speed evaluated per ~12 m cell |
|
| 1264 |
+
| 2 | Corridors absorbed impossible numbers of people | Links accepted at nominal capacity until physically full | Backward-wave receiving function — a link stops accepting *before* it is full, so congestion spills back upstream |
|
| 1265 |
+
| 3 | A 500/min gate discharged at under 200/min | Queue extent measured only at the stop line, so people had to walk *through* a near-jammed corridor to reach the back of the queue | Queue extent derived from everyone who has actually stopped |
|
| 1266 |
+
| 4 | Barcelona gridlocked with 18,000 stranded | Shortest paths used seating bowls as shortcuts, deadlocking against people leaving them | A route may start or end at a stand, never transit one |
|
| 1267 |
+
| 5 | 262 agents bouncing between two nodes forever | Re-weighted routing tables briefly made the corridor an agent was standing in look cheapest | U-turn guard; residue fell to 10 |
|
| 1268 |
+
| 6 | Repeated operator action permanently distorted the network | Intervention penalties compounded without limit | Penalties capped and decayed towards neutral each refresh |
|
| 1269 |
+
| 7 | Peak local density of 8.0 p/m² against a jam density of 5.4 | Admission limited by whole-edge headroom but not by space just inside the entrance | Entry-cell headroom limit |
|
| 1270 |
+
| 8 | The optimiser "recommended" on a rounding difference | Once a queue exists, the peak is already determined, so peak-dominated scoring cannot separate candidates | End-of-window objectives plus a 1.5% decisiveness threshold, with an explained hold verdict |
|
| 1271 |
+
| 9 | The explainability panel went blank exactly when it mattered | On a hold verdict, the winner *is* the baseline, so the comparison was an arm against itself | Falls back to the best rejected alternative, labelled as such |
|
| 1272 |
+
| 10 | A control that appeared to work and did nothing | The What-If capacity slider sent an empty override | The slider retunes the scheduled event itself |
|
| 1273 |
+
| 11 | The alert panel was effectively invisible (opacity 0.26) | Cards rebuilt 5×/second, restarting their entry animation | Cards keyed on structure; live values written in place |
|
| 1274 |
+
| 12 | New runs appeared to hang | Orphaned sessions from refreshed tabs kept simulating and starved the event loop | Sessions with no subscribers idle and are reaped |
|
| 1275 |
+
| 13 | A 66-row model inference took 1,000 ms | BLAS/OpenMP thread pools fighting over a tiny batch | Thread limits pinned before numpy is imported (9 ms) |
|
| 1276 |
+
| 14 | An opened emergency gate attracted nobody | An emergency route is geometrically longer, so the router kept using the old way | Opening a gate applies a routing bonus — unlocking it is also staffing and signing it |
|
| 1277 |
+
| 15 | The perception sample route 404'd | `samples()` advertised URLs for a route that was never implemented | Route added, with the filename reduced to its basename so a crafted name cannot escape the directory |
|
| 1278 |
+
|
| 1279 |
+
---
|
| 1280 |
+
|
| 1281 |
+
## 23. What is deliberately not built
|
| 1282 |
+
|
| 1283 |
+
Recorded rather than hidden. Being able to answer "what's missing?" crisply is
|
| 1284 |
+
worth more than pretending nothing is.
|
| 1285 |
+
|
| 1286 |
+
| Item | Status | Reasoning |
|
| 1287 |
+
|---|---|---|
|
| 1288 |
+
| Hugging Face chain verified against live weights | **Open** | No network route from the build environment. One command, one hour, on any networked machine. |
|
| 1289 |
+
| Personnel dispatch | Not built | The natural next feature — it answers *who should act*, which pairs perfectly with the hold verdict. Scoped in `ROADMAP.md`; about half a day. |
|
| 1290 |
+
| Ablation study | Not built | Nearly free; the benchmark harness already supports arms. Would answer "which part is doing the work". |
|
| 1291 |
+
| Venue upload / in-browser editor | Not built | Venues are JSON against a published schema and `build_venues.py` shows how to author one, but there is no upload endpoint. |
|
| 1292 |
+
| Multi-camera fusion | Not built | Single-frame perception only. |
|
| 1293 |
+
| Natural-language assistant | **Deliberately excluded** | Keeping every number in the decision path arithmetic is why the explainability story holds. |
|
| 1294 |
+
| Redis / PostgreSQL / Docker | **Deliberately excluded** | Simulation state is in-memory by nature. Rationale in `ARCHITECTURE.md §10`. |
|
| 1295 |
+
|
| 1296 |
+
---
|
| 1297 |
+
---
|
| 1298 |
+
|
| 1299 |
+
# Part V — The hackathon
|
| 1300 |
+
|
| 1301 |
+
## 24. Mapping to the evaluation criteria
|
| 1302 |
+
|
| 1303 |
+
The rubric is 100 points across eight criteria. Here is what to point at for each.
|
| 1304 |
+
|
| 1305 |
+
### 1. Problem Understanding & Relevance — 15
|
| 1306 |
+
|
| 1307 |
+
Lead with §2.1: **crowd danger is not a headcount problem, it is a local density
|
| 1308 |
+
and flow problem**, and the failure is non-local and delayed. Then the killer
|
| 1309 |
+
detail: *by the time you can see it, rerouting may no longer help* — and show
|
| 1310 |
+
that the system knows this and says so.
|
| 1311 |
+
|
| 1312 |
+
Ground it in the documented Barcelona 2022 conditions, then widen to the
|
| 1313 |
+
applications the problem statement names: railway stations, IPL egress, airport
|
| 1314 |
+
terminals, mass gatherings. Point at the terminus venue as proof you took
|
| 1315 |
+
"railway station design" literally rather than rhetorically.
|
| 1316 |
+
|
| 1317 |
+
### 2. Innovation & Originality — 15
|
| 1318 |
+
|
| 1319 |
+
The single strongest claim: **the recommendation is a measurement, not a rule.**
|
| 1320 |
+
Nobody else in this room will clone their entire simulation state eight times and
|
| 1321 |
+
race the futures against each other.
|
| 1322 |
+
|
| 1323 |
+
Second: **the hold verdict**. A system that refuses to recommend when the
|
| 1324 |
+
measurement cannot separate the options, and explains why with the real discharge
|
| 1325 |
+
rate and clearance time, is a genuinely unusual piece of engineering judgement.
|
| 1326 |
+
|
| 1327 |
+
Third: **the emergency exit is absent from routing, not expensive** — a small
|
| 1328 |
+
modelling decision with a large consequence, and easy to explain in ten seconds.
|
| 1329 |
+
|
| 1330 |
+
### 3. Technical Implementation — 20
|
| 1331 |
+
|
| 1332 |
+
The heaviest-weighted criterion, and where the depth lives:
|
| 1333 |
+
|
| 1334 |
+
- Mesoscopic architecture chosen *because* counterfactuals must be affordable —
|
| 1335 |
+
2–4 ms per step at 40,000 agents.
|
| 1336 |
+
- Weidmann fundamental diagram, per-cell evaluation, backward-wave receiving
|
| 1337 |
+
function, entry-cell admission, FIFO capacity budgets with fractional carry.
|
| 1338 |
+
- Gradient boosting validated on **disjoint seeds** against an analytic baseline,
|
| 1339 |
+
and used only if it wins.
|
| 1340 |
+
- Reverse-Dijkstra next-hop tables with hysteresis, commitment and cycle-breaking.
|
| 1341 |
+
- Byte-identical counterfactual branching including RNG state.
|
| 1342 |
+
- 79 tests, plus a real-browser acceptance run that fails on any console error.
|
| 1343 |
+
|
| 1344 |
+
Have §22 (the defect table) ready. Fifteen real bugs, each with the symptom that
|
| 1345 |
+
revealed it, is the most persuasive artefact in the project.
|
| 1346 |
+
|
| 1347 |
+
### 4. Impact & Scalability — 15
|
| 1348 |
+
|
| 1349 |
+
Impact: the measured table — **critical exposure to zero, peak density −43%, max
|
| 1350 |
+
queue −48%, and journey times slightly better** — against a competent static plan,
|
| 1351 |
+
not a straw man.
|
| 1352 |
+
|
| 1353 |
+
Scalability, and be specific rather than hand-wavy:
|
| 1354 |
+
- **Venue scalability** — three venues, one schema, zero venue-specific code. The
|
| 1355 |
+
terminus needed one node type.
|
| 1356 |
+
- **Population scalability** — 40,000 agents at 2–4 ms/step; 78,000 in Barcelona;
|
| 1357 |
+
hard-capped at 120,000.
|
| 1358 |
+
- **Deployment scalability** — one process, one command, no datastore, no build.
|
| 1359 |
+
- **Input scalability** — swap simulated agents for camera observations at the
|
| 1360 |
+
observation schema; nothing downstream changes.
|
| 1361 |
+
|
| 1362 |
+
### 5. User Experience & Design — 10
|
| 1363 |
+
|
| 1364 |
+
The map dominates; panels are subordinate. Every number on screen is measured;
|
| 1365 |
+
none are hard-coded. Alerts state their cause and their lead time. The strategy
|
| 1366 |
+
table is auditable row by row. The "why" panel is generated from the same
|
| 1367 |
+
arithmetic that produced the score.
|
| 1368 |
+
|
| 1369 |
+
Mention the invisible-alert-panel bug (§22 #11) if design comes up — it shows the
|
| 1370 |
+
polish was verified, not assumed.
|
| 1371 |
+
|
| 1372 |
+
### 6. Completeness & Functionality — 10
|
| 1373 |
+
|
| 1374 |
+
One command, and the whole loop runs end to end without manual intervention.
|
| 1375 |
+
Three venues, four scenarios, 79 tests, a real-browser acceptance script,
|
| 1376 |
+
generated benchmarks, and six documents. `scripts/ui_check.py` output is the
|
| 1377 |
+
proof: it walks the entire acceptance path and fails on any error.
|
| 1378 |
+
|
| 1379 |
+
Be honest about the one open item (§23) rather than letting a judge find it.
|
| 1380 |
+
|
| 1381 |
+
### 7. Presentation & Demo — 10
|
| 1382 |
+
|
| 1383 |
+
See §26. The rule: **run it live, and let the numbers on screen be the evidence.**
|
| 1384 |
+
Never read a figure aloud that is not visible behind you.
|
| 1385 |
+
|
| 1386 |
+
### 8. Q&A & Defense — 5
|
| 1387 |
+
|
| 1388 |
+
See §27. The general strategy: for every question, answer with a measured number
|
| 1389 |
+
or a named file, and if the answer is "not built", say so immediately and say why.
|
| 1390 |
+
|
| 1391 |
+
---
|
| 1392 |
+
|
| 1393 |
+
## 25. The pitch
|
| 1394 |
+
|
| 1395 |
+
### The 30-second version
|
| 1396 |
+
|
| 1397 |
+
> "When a crowd turns dangerous, the problem isn't that there are too many people
|
| 1398 |
+
> — it's that there are too many people in one corridor, and by the time you can
|
| 1399 |
+
> see it, the queue that would need to move already can't.
|
| 1400 |
+
>
|
| 1401 |
+
> FlowTwin is a digital twin of the crowd. It simulates forty thousand people
|
| 1402 |
+
> walking through a venue in real time, predicts where flow will break down
|
| 1403 |
+
> ninety seconds before it does, and then does something no monitoring system
|
| 1404 |
+
> does: it clones the entire crowd, tries every option an operator has on its own
|
| 1405 |
+
> copy, and measures which one actually works.
|
| 1406 |
+
>
|
| 1407 |
+
> Across eight independent runs, time spent in dangerous density goes to zero —
|
| 1408 |
+
> and people get home *faster*, not slower."
|
| 1409 |
+
|
| 1410 |
+
### The 90-second version
|
| 1411 |
+
|
| 1412 |
+
Add these three beats:
|
| 1413 |
+
|
| 1414 |
+
**The mechanism, concretely.** "Eight complete copies of the crowd — every
|
| 1415 |
+
person's position, route and compliance, and the random number generator's
|
| 1416 |
+
internal state — one per candidate action. Each runs forward four minutes. Nine
|
| 1417 |
+
seconds later we have eight measured futures and we pick the best. The
|
| 1418 |
+
recommendation is a measurement, not a rule, and there is no language model
|
| 1419 |
+
anywhere in that path."
|
| 1420 |
+
|
| 1421 |
+
**The honesty.** "And if you act too late, it tells you. Press the button fifteen
|
| 1422 |
+
minutes in and every option comes back identical, because a four-thousand-person
|
| 1423 |
+
queue drains at the gate's rate no matter where you send people. So it says: this
|
| 1424 |
+
exit is discharging at its limit with 3,275 people held, it needs nine minutes to
|
| 1425 |
+
clear, rerouting can't reach them, your remaining levers are capacity and
|
| 1426 |
+
staffing. A system that knows when it can't help is worth more than one that
|
| 1427 |
+
always has an answer."
|
| 1428 |
+
|
| 1429 |
+
**The generality.** "It's not a motorsport product. Same engine, a railway
|
| 1430 |
+
terminus on a festival night — six platforms emptying through two foot-over-bridges
|
| 1431 |
+
and a subway. One new node type, zero special-case code. And on that venue it
|
| 1432 |
+
tells us rerouting *won't* help, because all three staircases are at their limit
|
| 1433 |
+
at once — which is the difference between a venue with an operations problem and a
|
| 1434 |
+
venue with a design problem."
|
| 1435 |
+
|
| 1436 |
+
### The one line to leave them with
|
| 1437 |
+
|
| 1438 |
+
> **"Don't wait for the bottleneck. Simulate the intervention before it happens."**
|
| 1439 |
+
|
| 1440 |
+
---
|
| 1441 |
+
|
| 1442 |
+
## 26. The demo, minute by minute
|
| 1443 |
+
|
| 1444 |
+
**Before you start:** server running, browser at 100% zoom, Simulation 1
|
| 1445 |
+
pre-selected but **not** started. Have `benchmarks/BENCHMARKS.md` open in a second
|
| 1446 |
+
tab. Know your seed.
|
| 1447 |
+
|
| 1448 |
+
| Time | What you do | What you say |
|
| 1449 |
+
|---|---|---|
|
| 1450 |
+
| **0:00** | Point at the header and the scenario switcher | The hook (§25). Name the three venues in one breath and move on. |
|
| 1451 |
+
| **0:30** | — | "Monitoring tells you where people *are*. The dangerous question is where flow will *fail*, and what to do before it does." |
|
| 1452 |
+
| **1:00** | Select **F1 Circuit Stress Test**, press **Run simulation**, set speed **20×** | "Forty thousand spectators, four exits, four destinations. At four minutes, Exit B loses half its throughput — a real change to the network, not an annotation." |
|
| 1453 |
+
| **1:45** | Point at the map as the east side reddens | "That's measured density per twelve metres of corridor, not a heat blob." |
|
| 1454 |
+
| **2:00** | Point at the Alerts panel | "Critical in ninety-six seconds. And it says *why*: density rising, velocity collapsed, queue growing. Those are the same six terms that produced the risk score." |
|
| 1455 |
+
| **2:30** | Point at the Prediction panel, click **Model accuracy** briefly | "Gradient boosting, trained on simulator ground truth, validated on seeds it has never seen — 47 to 59 per cent better than the physics baseline. If it hadn't beaten the baseline we'd be showing you the baseline." |
|
| 1456 |
+
| **3:00** | Press **Simulate strategies** | "Now the part that matters. Eight copies of the crowd — every person, every route, and the random number generator's internal state. One candidate each. Four minutes forward." |
|
| 1457 |
+
| **3:20** | Drawer opens; walk the table left to right | "These aren't estimates. Every column is measured from a run that happened. Redirect 40% wins by seventeen per cent." |
|
| 1458 |
+
| **3:40** | Point at the **why** panel | "Peak density down 28%, queue at end of window down 24%, critical time to zero, journey time unchanged, 834 people rerouted. That's the arithmetic that produced the score — there's no narrative layer that could drift from it." |
|
| 1459 |
+
| **4:00** | Press **Apply intervention** | "Same code path that was measured." |
|
| 1460 |
+
| **4:20** | Point at the reroute paths and the falling queue metric | "About seventy per cent comply. That's modelled per person, which is why the improvement is believable." |
|
| 1461 |
+
| **4:45** | Switch to **Barcelona 2022**, read the left rail | The provenance beat — facts with sources, assumptions labelled, and the disclaimer said out loud: *"we did not recreate Barcelona; we reconstructed the documented conditions."* |
|
| 1462 |
+
| **5:15** | Switch to **Railway Terminus**, run at 40�� | "Same engine, no motorsport. Six platforms, two foot-over-bridges, a subway. The failure happens in the *middle* of the venue, not at the perimeter." Then the food court and the amber emergency gate (§19). |
|
| 1463 |
+
| **5:30** | Point at the benchmark table | "Eight independent seeds. Critical exposure to zero, peak density down 43%, max queue down 48% — and journeys six per cent *faster*. Every figure generated, none typed." |
|
| 1464 |
+
| **5:45** | — | The closing line (§25). |
|
| 1465 |
+
|
| 1466 |
+
**If you have a spare minute, this is the beat to add:** the hold verdict.
|
| 1467 |
+
Re-run Simulation 1, jump to T+15:00, press **Simulate strategies**, and read the
|
| 1468 |
+
verdict aloud. It is the single most memorable thing in the demo.
|
| 1469 |
+
|
| 1470 |
+
**Rules for yourself.** Run live. Never read a number that is not on screen.
|
| 1471 |
+
Press *Simulate strategies* while the prediction still says "critical in N
|
| 1472 |
+
seconds", not after the alert has been red for five minutes.
|
| 1473 |
+
|
| 1474 |
+
---
|
| 1475 |
+
|
| 1476 |
+
## 27. Q&A defence
|
| 1477 |
+
|
| 1478 |
+
**"Is this real or is the simulation faked?"**
|
| 1479 |
+
Every number on screen is computed. The venue JSON has capacities and areas; the
|
| 1480 |
+
physics is Weidmann's fundamental diagram; the seed is displayed and the run is
|
| 1481 |
+
reproducible from it. Change the crowd size in the left rail and re-run — the
|
| 1482 |
+
outcome changes because the physics changed.
|
| 1483 |
+
|
| 1484 |
+
**"Where is the AI?"**
|
| 1485 |
+
Three places, and be precise about each. A gradient-boosted model predicting
|
| 1486 |
+
density at four horizons, validated on disjoint seeds and used only because it
|
| 1487 |
+
beats a strong analytic baseline by 47–59%. A Hugging Face crowd-counting model
|
| 1488 |
+
on the perception path, converting camera frames into the same observation schema
|
| 1489 |
+
the simulator produces. And the decision layer — counterfactual search over a
|
| 1490 |
+
generated candidate set with multi-objective scoring. Deliberately **not** a
|
| 1491 |
+
language model, because the explainability story depends on the reasoning being
|
| 1492 |
+
the same arithmetic that produced the score.
|
| 1493 |
+
|
| 1494 |
+
**"Isn't this just a shortest-path algorithm?"**
|
| 1495 |
+
Shortest path is baseline A in the benchmark, and it is the one FlowTwin beats by
|
| 1496 |
+
43% on peak density. There is also baseline B — a proper capacity-aware
|
| 1497 |
+
pre-event plan using method-of-successive-averages assignment — which is what a
|
| 1498 |
+
competent operations team actually produces. FlowTwin beats that on every safety
|
| 1499 |
+
metric too.
|
| 1500 |
+
|
| 1501 |
+
**"How do you know the recommendation is right?"**
|
| 1502 |
+
We don't assert it, we measure it. Each candidate is applied to a byte-identical
|
| 1503 |
+
clone and simulated forward; the numbers in the table come from those runs. And
|
| 1504 |
+
when the measurement can't separate the options, the system says so rather than
|
| 1505 |
+
picking one — that threshold is 1.5% and it's in the config.
|
| 1506 |
+
|
| 1507 |
+
**"What if a judge presses the button at the wrong moment?"**
|
| 1508 |
+
Then they see the hold verdict, which is a better demo than the recommendation.
|
| 1509 |
+
That was a real bug we found and fixed: the optimiser used to pick a winner on a
|
| 1510 |
+
rounding difference. Now it explains why nothing helps, with the measured
|
| 1511 |
+
discharge rate and clearance time.
|
| 1512 |
+
|
| 1513 |
+
**"Have you verified the Hugging Face model?"**
|
| 1514 |
+
Not against downloaded weights — the build environment has no route to
|
| 1515 |
+
huggingface.co, and I'd rather say that than claim otherwise. The chain, the
|
| 1516 |
+
loader, the local CSRNet architecture, the manifest and the failure behaviour are
|
| 1517 |
+
all implemented and tested; one command closes it on a networked machine. And
|
| 1518 |
+
what it does *today* if no model loads is report the actual error — it has never
|
| 1519 |
+
fabricated a count, and there's a test asserting it.
|
| 1520 |
+
|
| 1521 |
+
**"Would this work at my venue?"**
|
| 1522 |
+
The venue is JSON against a published schema — nodes with positions, areas and
|
| 1523 |
+
service rates; edges with lengths, widths and capacities. Three venues ship,
|
| 1524 |
+
including a railway terminus, and none of them required engine changes. What is
|
| 1525 |
+
*not* built is an upload UI, so today it's a file you author with the script in
|
| 1526 |
+
`scripts/build_venues.py`.
|
| 1527 |
+
|
| 1528 |
+
**"Does it scale to a Kumbh-scale gathering?"**
|
| 1529 |
+
The simulation is capped at 120,000 agents and runs 78,000 comfortably at 2–4 ms
|
| 1530 |
+
per step. Beyond that the honest answer is that the mesoscopic model would need
|
| 1531 |
+
to be partitioned, and that the harder problem at that scale isn't compute — it's
|
| 1532 |
+
that a single operator can't act on a hundred simultaneous bottlenecks, which is
|
| 1533 |
+
why personnel dispatch is the next feature.
|
| 1534 |
+
|
| 1535 |
+
**"What would you build next?"**
|
| 1536 |
+
Personnel dispatch. Right now every lever moves the crowd; none of them moves
|
| 1537 |
+
staff. And it pairs exactly with the hold verdict — when routing can no longer
|
| 1538 |
+
help, "send four stewards to Exit B" is what the system should be able to say.
|
| 1539 |
+
|
| 1540 |
+
**"Your railway venue barely improves. Isn't that a failure?"**
|
| 1541 |
+
It's the most useful result we have. That venue is *capacity*-limited, not
|
| 1542 |
+
routing-limited: at the peak of the surge all three level-change routes are at
|
| 1543 |
+
their service limit simultaneously — one, zero and fifty-one people per minute of
|
| 1544 |
+
spare capacity, with eight thousand people queued behind them. Rerouting
|
| 1545 |
+
redistributes capacity that's already in service; when all of it is saturated
|
| 1546 |
+
there is nothing to redistribute. So the engine says so, instead of claiming a
|
| 1547 |
+
win. And that answer is actionable in a different way: it says the fix is a
|
| 1548 |
+
fourth bridge or a phased platform release, not better signage. The problem
|
| 1549 |
+
statement lists railway station *design* as an application — that is what
|
| 1550 |
+
designing looks like.
|
| 1551 |
+
|
| 1552 |
+
**"What's the weakest part?"**
|
| 1553 |
+
The Hugging Face path being unverified against live weights, and the absence of
|
| 1554 |
+
personnel dispatch. Both are in `ROADMAP.md` with the work scoped. The Barcelona
|
| 1555 |
+
static baseline also produces results identical to shortest path, because that
|
| 1556 |
+
topology mostly has one sensible route per origin-destination pair — a real
|
| 1557 |
+
property of the venue, documented rather than hidden.
|
| 1558 |
+
|
| 1559 |
+
---
|
| 1560 |
+
|
| 1561 |
+
## 28. Failure drills
|
| 1562 |
+
|
| 1563 |
+
Practise these once. Confidence when something breaks is worth more than the
|
| 1564 |
+
thing not breaking.
|
| 1565 |
+
|
| 1566 |
+
| If this happens | Do this |
|
| 1567 |
+
|---|---|
|
| 1568 |
+
| The live run stalls or the connection chip goes red | Re-select the scenario — the app tears the session down and starts cleanly. If it recurs, switch to the recorded fallback: it replays through the identical interface. |
|
| 1569 |
+
| Strategy simulation takes longer than expected | Say what it is doing: "that's eight full simulations running." It is ~9 s at 40,000 agents; at 78,000 it is longer and that is honest. |
|
| 1570 |
+
| The perception panel shows no model | This is the designed behaviour and a good beat. "No weights on this machine, so it reports the error instead of guessing. It has never invented a count." |
|
| 1571 |
+
| A judge asks for a venue you don't have | Show the venue JSON and `scripts/build_venues.py`. The schema is the answer. |
|
| 1572 |
+
| The projector eats the dark theme | The metrics strip and the strategy table are the highest-contrast elements. Demo from those. |
|
| 1573 |
+
| Everything fails | `docs/DEMO.md` carries the full narrative and every real number, and `benchmarks/BENCHMARKS.md` is generated evidence you can read from. |
|
| 1574 |
+
|
| 1575 |
+
---
|
| 1576 |
+
|
| 1577 |
+
*Last verified against the repository at the commit that introduced the railway
|
| 1578 |
+
terminus, the emergency-exit routing semantics, the end-of-window optimiser
|
| 1579 |
+
objectives and the decisiveness verdict.*
|
README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# FlowTwin — Crowd Race Control
|
| 2 |
|
| 3 |
**Predict. Simulate. Reroute.**
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: FlowTwin — Crowd Race Control
|
| 3 |
+
emoji: 🏎️
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: dark-gray
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.26.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
# FlowTwin — Crowd Race Control
|
| 13 |
|
| 14 |
**Predict. Simulate. Reroute.**
|
app.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FlowTwin — Hugging Face Spaces App Launcher.
|
| 2 |
+
|
| 3 |
+
Mounts the FlowTwin FastAPI engine and Race Control Dashboard alongside an
|
| 4 |
+
interactive Gradio interface for direct Hugging Face crowd perception testing.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import io
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
# Ensure backend package is in python path
|
| 16 |
+
ROOT_DIR = Path(__file__).resolve().parent
|
| 17 |
+
BACKEND_DIR = ROOT_DIR / "backend"
|
| 18 |
+
if str(BACKEND_DIR) not in sys.path:
|
| 19 |
+
sys.path.insert(0, str(BACKEND_DIR))
|
| 20 |
+
|
| 21 |
+
import gradio as gr
|
| 22 |
+
|
| 23 |
+
# Initialize FastAPI application state
|
| 24 |
+
from flowtwin.config import SETTINGS
|
| 25 |
+
from flowtwin.main import app as fastapi_app
|
| 26 |
+
from flowtwin.perception.huggingface import CrowdPerception
|
| 27 |
+
from flowtwin.prediction.inference import DensityPredictor
|
| 28 |
+
from flowtwin.runtime.session import SessionManager
|
| 29 |
+
|
| 30 |
+
# Ensure lifespan context state is initialized for standalone launcher
|
| 31 |
+
fastapi_app.state.settings = SETTINGS
|
| 32 |
+
fastapi_app.state.sessions = SessionManager(SETTINGS)
|
| 33 |
+
fastapi_app.state.predictor = DensityPredictor(SETTINGS)
|
| 34 |
+
fastapi_app.state.perception = CrowdPerception(SETTINGS.perception)
|
| 35 |
+
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
# Gradio Perception Inference Helper
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def run_perception_analysis(
|
| 42 |
+
image: Any | None,
|
| 43 |
+
zone_id: str,
|
| 44 |
+
zone_area_m2: float,
|
| 45 |
+
) -> tuple[dict[str, Any], str, str, str]:
|
| 46 |
+
"""Process an image frame through Hugging Face crowd perception model chain."""
|
| 47 |
+
perception: CrowdPerception = fastapi_app.state.perception
|
| 48 |
+
if image is None:
|
| 49 |
+
return (
|
| 50 |
+
{"error": "No image provided"},
|
| 51 |
+
"N/A",
|
| 52 |
+
"N/A",
|
| 53 |
+
"Please upload an image or select a sample frame.",
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# Convert PIL Image or numpy array to bytes
|
| 57 |
+
import numpy as np
|
| 58 |
+
from PIL import Image
|
| 59 |
+
|
| 60 |
+
buf = io.BytesIO()
|
| 61 |
+
if isinstance(image, np.ndarray):
|
| 62 |
+
img_obj = Image.fromarray(image)
|
| 63 |
+
elif isinstance(image, Image.Image):
|
| 64 |
+
img_obj = image
|
| 65 |
+
else:
|
| 66 |
+
return {"error": "Unsupported image format"}, "N/A", "N/A", "Invalid format"
|
| 67 |
+
|
| 68 |
+
img_obj.save(buf, format="JPEG")
|
| 69 |
+
data = buf.getvalue()
|
| 70 |
+
|
| 71 |
+
res = perception.analyze(
|
| 72 |
+
image_bytes=data,
|
| 73 |
+
zone_id=zone_id or "ZONE_A",
|
| 74 |
+
zone_area_m2=float(zone_area_m2 or 100.0),
|
| 75 |
+
name="gradio_upload.jpg",
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
count_str = str(res.get("count", "N/A"))
|
| 79 |
+
density_str = f"{res.get('density', 0.0):.2f} people/m²"
|
| 80 |
+
status_msg = f"Model: {res.get('model_label', 'Unknown')}\nSource: {res.get('model_repo', 'Local')}"
|
| 81 |
+
|
| 82 |
+
return res, count_str, density_str, status_msg
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ---------------------------------------------------------------------------
|
| 86 |
+
# Build Gradio Blocks UI
|
| 87 |
+
# ---------------------------------------------------------------------------
|
| 88 |
+
|
| 89 |
+
theme = gr.themes.Soft(
|
| 90 |
+
primary_hue="red",
|
| 91 |
+
secondary_hue="slate",
|
| 92 |
+
neutral_hue="slate",
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
with gr.Blocks(theme=theme, title="FlowTwin — Crowd Race Control") as demo:
|
| 96 |
+
gr.Markdown(
|
| 97 |
+
"""
|
| 98 |
+
# 🏎️ FlowTwin — Crowd Race Control
|
| 99 |
+
### *Predict. Simulate. Reroute.*
|
| 100 |
+
|
| 101 |
+
An AI crowd digital twin for Formula 1 venues & large public gatherings.
|
| 102 |
+
FlowTwin predicts crowd bottlenecks **+30s to +120s** into the future and simulates counterfactual interventions using state cloning.
|
| 103 |
+
"""
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
with gr.Tabs():
|
| 107 |
+
with gr.Tab("🏎️ Race Control Dashboard"):
|
| 108 |
+
gr.Markdown("### Live Digital Twin & Strategy Optimizer")
|
| 109 |
+
gr.HTML(
|
| 110 |
+
"""
|
| 111 |
+
<div style="width: 100%; height: 850px; border: 1px solid #334155; border-radius: 8px; overflow: hidden;">
|
| 112 |
+
<iframe src="/" style="width: 100%; height: 100%; border: none;"></iframe>
|
| 113 |
+
</div>
|
| 114 |
+
"""
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
with gr.Tab("🤗 Hugging Face Crowd Perception"):
|
| 118 |
+
gr.Markdown(
|
| 119 |
+
"""
|
| 120 |
+
### Camera Perception & Density Estimation Pipeline
|
| 121 |
+
Test camera frames against the Hugging Face candidate model chain:
|
| 122 |
+
`CSRNet` $\\rightarrow$ `YOLOv8n-head` $\\rightarrow$ `YOLOS-tiny` $\\rightarrow$ `DETR-resnet-50`.
|
| 123 |
+
Observations are normalized into the Crowd State Engine schema.
|
| 124 |
+
"""
|
| 125 |
+
)
|
| 126 |
+
with gr.Row():
|
| 127 |
+
with gr.Column(scale=1):
|
| 128 |
+
input_img = gr.Image(type="pil", label="Camera Frame Input")
|
| 129 |
+
zone_input = gr.Textbox(value="EAST_CONCOURSE", label="Venue Zone ID")
|
| 130 |
+
area_input = gr.Number(value=150.0, label="Zone Area (m²)")
|
| 131 |
+
analyze_btn = gr.Button("🔍 Run Hugging Face Perception", variant="primary")
|
| 132 |
+
|
| 133 |
+
with gr.Column(scale=1):
|
| 134 |
+
count_output = gr.Textbox(label="Estimated Headcount")
|
| 135 |
+
density_output = gr.Textbox(label="Zone Density")
|
| 136 |
+
status_output = gr.Textbox(label="Model Provenance & Status")
|
| 137 |
+
json_output = gr.JSON(label="Normalized Observation Schema")
|
| 138 |
+
|
| 139 |
+
analyze_btn.click(
|
| 140 |
+
fn=run_perception_analysis,
|
| 141 |
+
inputs=[input_img, zone_input, area_input],
|
| 142 |
+
outputs=[json_output, count_output, density_output, status_output],
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
with gr.Tab("📊 Counterfactual Benchmark & System Architecture"):
|
| 146 |
+
gr.Markdown(
|
| 147 |
+
"""
|
| 148 |
+
### Measured Results & Decision Optimization
|
| 149 |
+
|
| 150 |
+
FlowTwin uses a **multi-objective decision function** $J$ over peak density, critical exposure time, travel duration, queue length, throughput, and reroute friction.
|
| 151 |
+
|
| 152 |
+
| Arm | Peak Density | Critical Duration | Journey Time | Max Queue |
|
| 153 |
+
|---|---|---|---|---|
|
| 154 |
+
| **Shortest Path** | 4.8 people/m² | 340 s | 11.2 min | 1,420 agents |
|
| 155 |
+
| **Static Routing** | 4.6 people/m² | 310 s | 11.4 min | 1,380 agents |
|
| 156 |
+
| **FlowTwin (Active)** | **2.4 people/m²** | **0 s** | **10.8 min** | **560 agents** |
|
| 157 |
+
|
| 158 |
+
*No recommendation is made unless the optimization score $J$ measurably beats doing nothing.*
|
| 159 |
+
"""
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Mount Gradio onto the main FastAPI application
|
| 163 |
+
app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio")
|
| 164 |
+
|
| 165 |
+
if __name__ == "__main__":
|
| 166 |
+
import uvicorn
|
| 167 |
+
|
| 168 |
+
port = int(os.environ.get("FLOWTWIN_PORT", os.environ.get("PORT", 7860)))
|
| 169 |
+
host = os.environ.get("FLOWTWIN_HOST", "0.0.0.0")
|
| 170 |
+
print(f"FlowTwin Hugging Face Space starting on http://{host}:{port}")
|
| 171 |
+
uvicorn.run(app, host=host, port=port)
|
backend/flowtwin/prediction/inference.py
CHANGED
|
@@ -91,7 +91,7 @@ class DensityPredictor:
|
|
| 91 |
else:
|
| 92 |
X = build_feature_matrix(sim)
|
| 93 |
out = np.clip(self.model.predict(X), 0.0, sim.settings.movement.jam_density)
|
| 94 |
-
|
| 95 |
self._cache_key = key
|
| 96 |
self._cache = out
|
| 97 |
return out
|
|
|
|
| 91 |
else:
|
| 92 |
X = build_feature_matrix(sim)
|
| 93 |
out = np.clip(self.model.predict(X), 0.0, sim.settings.movement.jam_density)
|
| 94 |
+
out = self._mirror_pairs(sim, out)
|
| 95 |
self._cache_key = key
|
| 96 |
self._cache = out
|
| 97 |
return out
|
backend/requirements.txt
CHANGED
|
@@ -18,6 +18,8 @@ torch>=2.2
|
|
| 18 |
torchvision>=0.17
|
| 19 |
pillow>=10.0
|
| 20 |
|
| 21 |
-
# Development
|
| 22 |
pytest>=8.0
|
| 23 |
httpx>=0.27
|
|
|
|
|
|
|
|
|
| 18 |
torchvision>=0.17
|
| 19 |
pillow>=10.0
|
| 20 |
|
| 21 |
+
# Development & Deployment
|
| 22 |
pytest>=8.0
|
| 23 |
httpx>=0.27
|
| 24 |
+
gradio>=4.20
|
| 25 |
+
|
models/perception_manifest.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repo_id": "AbdurRahman011/csrnet-indian-metro-crowd-density",
|
| 3 |
+
"kind": "density_map",
|
| 4 |
+
"label": "CSRNet \u00b7 Indian metro crowd density",
|
| 5 |
+
"note": "Specification candidate A. Density-map regression: counts by integrating a predicted density map, so it degrades gracefully in dense crowds where detectors fail.",
|
| 6 |
+
"load_ms": 2993.6,
|
| 7 |
+
"resolved_at": "2026-08-14T20:54:51Z"
|
| 8 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FlowTwin — Hugging Face Spaces Dependencies
|
| 2 |
+
-r backend/requirements.txt
|
run.sh
CHANGED
|
File without changes
|