ykirpichev commited on
Commit
15c7b0c
·
verified ·
1 Parent(s): d888e52

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +204 -0
  2. Dockerfile +81 -0
  3. README.md +250 -5
  4. __init__.py +16 -0
  5. client.py +84 -0
  6. index.html +613 -0
  7. make_video.py +91 -0
  8. make_video_dijkstra.py +84 -0
  9. models.py +73 -0
  10. openenv.yaml +7 -0
  11. openenv_road_traffic_simulator_env.egg-info/PKG-INFO +14 -0
  12. openenv_road_traffic_simulator_env.egg-info/SOURCES.txt +21 -0
  13. openenv_road_traffic_simulator_env.egg-info/dependency_links.txt +1 -0
  14. openenv_road_traffic_simulator_env.egg-info/entry_points.txt +2 -0
  15. openenv_road_traffic_simulator_env.egg-info/requires.txt +10 -0
  16. openenv_road_traffic_simulator_env.egg-info/top_level.txt +1 -0
  17. pyproject.toml +43 -0
  18. server/__init__.py +11 -0
  19. server/app.py +81 -0
  20. server/requirements.txt +6 -0
  21. server/road_traffic_simulator_env_environment.py +729 -0
  22. test_dijkstra_policy.py +133 -0
  23. test_generate_images.py +63 -0
  24. test_images/img_000_ep1_step0.png +3 -0
  25. test_images/img_001_ep1_step1.png +3 -0
  26. test_images/img_002_ep1_step2.png +3 -0
  27. test_images/img_003_ep1_step3.png +3 -0
  28. test_images/img_004_ep1_step4.png +3 -0
  29. test_images/img_005_ep1_step5.png +3 -0
  30. test_images/img_006_ep1_step6.png +3 -0
  31. test_images/img_007_ep1_step7.png +3 -0
  32. test_images/img_008_ep1_step8.png +3 -0
  33. test_images/img_009_ep1_step9.png +3 -0
  34. test_images/img_010_ep1_step10.png +3 -0
  35. test_images/img_011_ep1_step11.png +3 -0
  36. test_images/img_012_ep1_step12.png +3 -0
  37. test_images/img_013_ep1_step13.png +3 -0
  38. test_images/img_014_ep1_step14.png +3 -0
  39. test_images/img_015_ep1_step15.png +3 -0
  40. test_images/img_016_ep1_step16.png +3 -0
  41. test_images/img_017_ep1_step17.png +3 -0
  42. test_images/img_018_ep1_step18.png +3 -0
  43. test_images/img_019_ep1_step19.png +3 -0
  44. test_images/img_020_ep1_step20.png +3 -0
  45. test_images/img_021_ep1_step21.png +3 -0
  46. test_images/img_022_ep1_step22.png +3 -0
  47. test_images/img_023_ep1_step23.png +3 -0
  48. test_images/img_024_ep1_step24.png +3 -0
  49. test_images/img_025_ep1_step25.png +3 -0
  50. test_images/img_026_ep1_step26.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,207 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ test_images/img_000_ep1_step0.png filter=lfs diff=lfs merge=lfs -text
37
+ test_images/img_001_ep1_step1.png filter=lfs diff=lfs merge=lfs -text
38
+ test_images/img_002_ep1_step2.png filter=lfs diff=lfs merge=lfs -text
39
+ test_images/img_003_ep1_step3.png filter=lfs diff=lfs merge=lfs -text
40
+ test_images/img_004_ep1_step4.png filter=lfs diff=lfs merge=lfs -text
41
+ test_images/img_005_ep1_step5.png filter=lfs diff=lfs merge=lfs -text
42
+ test_images/img_006_ep1_step6.png filter=lfs diff=lfs merge=lfs -text
43
+ test_images/img_007_ep1_step7.png filter=lfs diff=lfs merge=lfs -text
44
+ test_images/img_008_ep1_step8.png filter=lfs diff=lfs merge=lfs -text
45
+ test_images/img_009_ep1_step9.png filter=lfs diff=lfs merge=lfs -text
46
+ test_images/img_010_ep1_step10.png filter=lfs diff=lfs merge=lfs -text
47
+ test_images/img_011_ep1_step11.png filter=lfs diff=lfs merge=lfs -text
48
+ test_images/img_012_ep1_step12.png filter=lfs diff=lfs merge=lfs -text
49
+ test_images/img_013_ep1_step13.png filter=lfs diff=lfs merge=lfs -text
50
+ test_images/img_014_ep1_step14.png filter=lfs diff=lfs merge=lfs -text
51
+ test_images/img_015_ep1_step15.png filter=lfs diff=lfs merge=lfs -text
52
+ test_images/img_016_ep1_step16.png filter=lfs diff=lfs merge=lfs -text
53
+ test_images/img_017_ep1_step17.png filter=lfs diff=lfs merge=lfs -text
54
+ test_images/img_018_ep1_step18.png filter=lfs diff=lfs merge=lfs -text
55
+ test_images/img_019_ep1_step19.png filter=lfs diff=lfs merge=lfs -text
56
+ test_images/img_020_ep1_step20.png filter=lfs diff=lfs merge=lfs -text
57
+ test_images/img_021_ep1_step21.png filter=lfs diff=lfs merge=lfs -text
58
+ test_images/img_022_ep1_step22.png filter=lfs diff=lfs merge=lfs -text
59
+ test_images/img_023_ep1_step23.png filter=lfs diff=lfs merge=lfs -text
60
+ test_images/img_024_ep1_step24.png filter=lfs diff=lfs merge=lfs -text
61
+ test_images/img_025_ep1_step25.png filter=lfs diff=lfs merge=lfs -text
62
+ test_images/img_026_ep1_step26.png filter=lfs diff=lfs merge=lfs -text
63
+ test_images/img_027_ep1_step27.png filter=lfs diff=lfs merge=lfs -text
64
+ test_images/img_028_ep1_step28.png filter=lfs diff=lfs merge=lfs -text
65
+ test_images/img_029_ep1_step29.png filter=lfs diff=lfs merge=lfs -text
66
+ test_images/img_030_ep1_step30.png filter=lfs diff=lfs merge=lfs -text
67
+ test_images/img_031_ep1_step31.png filter=lfs diff=lfs merge=lfs -text
68
+ test_images/img_032_ep1_step32.png filter=lfs diff=lfs merge=lfs -text
69
+ test_images/img_033_ep1_step33.png filter=lfs diff=lfs merge=lfs -text
70
+ test_images/img_034_ep1_step34.png filter=lfs diff=lfs merge=lfs -text
71
+ test_images/img_035_ep1_step35.png filter=lfs diff=lfs merge=lfs -text
72
+ test_images/img_036_ep1_step36.png filter=lfs diff=lfs merge=lfs -text
73
+ test_images/img_037_ep1_step37.png filter=lfs diff=lfs merge=lfs -text
74
+ test_images/img_038_ep1_step38.png filter=lfs diff=lfs merge=lfs -text
75
+ test_images/img_039_ep1_step39.png filter=lfs diff=lfs merge=lfs -text
76
+ test_images/img_040_ep1_step40.png filter=lfs diff=lfs merge=lfs -text
77
+ test_images/img_041_ep1_step41.png filter=lfs diff=lfs merge=lfs -text
78
+ test_images/img_042_ep1_step42.png filter=lfs diff=lfs merge=lfs -text
79
+ test_images/img_043_ep1_step43.png filter=lfs diff=lfs merge=lfs -text
80
+ test_images/img_044_ep1_step44.png filter=lfs diff=lfs merge=lfs -text
81
+ test_images/img_045_ep1_step45.png filter=lfs diff=lfs merge=lfs -text
82
+ test_images/img_046_ep1_step46.png filter=lfs diff=lfs merge=lfs -text
83
+ test_images/img_047_ep1_step47.png filter=lfs diff=lfs merge=lfs -text
84
+ test_images/img_048_ep1_step48.png filter=lfs diff=lfs merge=lfs -text
85
+ test_images/img_049_ep1_step49.png filter=lfs diff=lfs merge=lfs -text
86
+ test_images/img_050_ep1_step50.png filter=lfs diff=lfs merge=lfs -text
87
+ test_images/img_051_ep1_step51.png filter=lfs diff=lfs merge=lfs -text
88
+ test_images/img_052_ep1_step52.png filter=lfs diff=lfs merge=lfs -text
89
+ test_images/img_053_ep1_step53.png filter=lfs diff=lfs merge=lfs -text
90
+ test_images/img_054_ep1_step54.png filter=lfs diff=lfs merge=lfs -text
91
+ test_images/img_055_ep1_step55.png filter=lfs diff=lfs merge=lfs -text
92
+ test_images/img_056_ep1_step56.png filter=lfs diff=lfs merge=lfs -text
93
+ test_images/img_057_ep1_step57.png filter=lfs diff=lfs merge=lfs -text
94
+ test_images/img_058_ep1_step58.png filter=lfs diff=lfs merge=lfs -text
95
+ test_images/img_059_ep1_step59.png filter=lfs diff=lfs merge=lfs -text
96
+ test_images/img_060_ep1_step60.png filter=lfs diff=lfs merge=lfs -text
97
+ test_images/img_061_ep1_step61.png filter=lfs diff=lfs merge=lfs -text
98
+ test_images/img_062_ep1_step62.png filter=lfs diff=lfs merge=lfs -text
99
+ test_images/img_063_ep1_step63.png filter=lfs diff=lfs merge=lfs -text
100
+ test_images/img_064_ep1_step64.png filter=lfs diff=lfs merge=lfs -text
101
+ test_images/img_065_ep1_step65.png filter=lfs diff=lfs merge=lfs -text
102
+ test_images/img_066_ep1_step66.png filter=lfs diff=lfs merge=lfs -text
103
+ test_images/img_067_ep1_step67.png filter=lfs diff=lfs merge=lfs -text
104
+ test_images/img_068_ep1_step68.png filter=lfs diff=lfs merge=lfs -text
105
+ test_images/img_069_ep1_step69.png filter=lfs diff=lfs merge=lfs -text
106
+ test_images/img_070_ep1_step70.png filter=lfs diff=lfs merge=lfs -text
107
+ test_images/img_071_ep2_step0.png filter=lfs diff=lfs merge=lfs -text
108
+ test_images/img_072_ep2_step1.png filter=lfs diff=lfs merge=lfs -text
109
+ test_images/img_073_ep2_step2.png filter=lfs diff=lfs merge=lfs -text
110
+ test_images/img_074_ep2_step3.png filter=lfs diff=lfs merge=lfs -text
111
+ test_images/img_075_ep2_step4.png filter=lfs diff=lfs merge=lfs -text
112
+ test_images/img_076_ep2_step5.png filter=lfs diff=lfs merge=lfs -text
113
+ test_images/img_077_ep2_step6.png filter=lfs diff=lfs merge=lfs -text
114
+ test_images/img_078_ep2_step7.png filter=lfs diff=lfs merge=lfs -text
115
+ test_images/img_079_ep2_step8.png filter=lfs diff=lfs merge=lfs -text
116
+ test_images/img_080_ep2_step9.png filter=lfs diff=lfs merge=lfs -text
117
+ test_images/img_081_ep2_step10.png filter=lfs diff=lfs merge=lfs -text
118
+ test_images/img_082_ep2_step11.png filter=lfs diff=lfs merge=lfs -text
119
+ test_images/img_083_ep2_step12.png filter=lfs diff=lfs merge=lfs -text
120
+ test_images/img_084_ep2_step13.png filter=lfs diff=lfs merge=lfs -text
121
+ test_images/img_085_ep2_step14.png filter=lfs diff=lfs merge=lfs -text
122
+ test_images/img_086_ep2_step15.png filter=lfs diff=lfs merge=lfs -text
123
+ test_images/img_087_ep2_step16.png filter=lfs diff=lfs merge=lfs -text
124
+ test_images/img_088_ep2_step17.png filter=lfs diff=lfs merge=lfs -text
125
+ test_images/img_089_ep2_step18.png filter=lfs diff=lfs merge=lfs -text
126
+ test_images/img_090_ep2_step19.png filter=lfs diff=lfs merge=lfs -text
127
+ test_images/img_091_ep2_step20.png filter=lfs diff=lfs merge=lfs -text
128
+ test_images/img_092_ep2_step21.png filter=lfs diff=lfs merge=lfs -text
129
+ test_images/img_093_ep2_step22.png filter=lfs diff=lfs merge=lfs -text
130
+ test_images/img_094_ep2_step23.png filter=lfs diff=lfs merge=lfs -text
131
+ test_images/img_095_ep2_step24.png filter=lfs diff=lfs merge=lfs -text
132
+ test_images/img_096_ep2_step25.png filter=lfs diff=lfs merge=lfs -text
133
+ test_images/img_097_ep2_step26.png filter=lfs diff=lfs merge=lfs -text
134
+ test_images/img_098_ep2_step27.png filter=lfs diff=lfs merge=lfs -text
135
+ test_images/img_099_ep2_step28.png filter=lfs diff=lfs merge=lfs -text
136
+ test_images/traffic_demo.gif filter=lfs diff=lfs merge=lfs -text
137
+ test_images/traffic_demo.mp4 filter=lfs diff=lfs merge=lfs -text
138
+ test_images_dijkstra/img_000_ep1_step0.png filter=lfs diff=lfs merge=lfs -text
139
+ test_images_dijkstra/img_001_ep1_step1.png filter=lfs diff=lfs merge=lfs -text
140
+ test_images_dijkstra/img_002_ep1_step2.png filter=lfs diff=lfs merge=lfs -text
141
+ test_images_dijkstra/img_003_ep1_step3.png filter=lfs diff=lfs merge=lfs -text
142
+ test_images_dijkstra/img_004_ep1_step4.png filter=lfs diff=lfs merge=lfs -text
143
+ test_images_dijkstra/img_005_ep1_step5.png filter=lfs diff=lfs merge=lfs -text
144
+ test_images_dijkstra/img_006_ep1_step6.png filter=lfs diff=lfs merge=lfs -text
145
+ test_images_dijkstra/img_007_ep1_step7.png filter=lfs diff=lfs merge=lfs -text
146
+ test_images_dijkstra/img_008_ep1_step8.png filter=lfs diff=lfs merge=lfs -text
147
+ test_images_dijkstra/img_009_ep1_step9.png filter=lfs diff=lfs merge=lfs -text
148
+ test_images_dijkstra/img_010_ep1_step10.png filter=lfs diff=lfs merge=lfs -text
149
+ test_images_dijkstra/img_011_ep1_step11.png filter=lfs diff=lfs merge=lfs -text
150
+ test_images_dijkstra/img_012_ep1_step12.png filter=lfs diff=lfs merge=lfs -text
151
+ test_images_dijkstra/img_013_ep1_step13.png filter=lfs diff=lfs merge=lfs -text
152
+ test_images_dijkstra/img_014_ep1_step14.png filter=lfs diff=lfs merge=lfs -text
153
+ test_images_dijkstra/img_015_ep1_step15.png filter=lfs diff=lfs merge=lfs -text
154
+ test_images_dijkstra/img_016_ep1_step16.png filter=lfs diff=lfs merge=lfs -text
155
+ test_images_dijkstra/img_017_ep1_step17.png filter=lfs diff=lfs merge=lfs -text
156
+ test_images_dijkstra/img_018_ep1_step18.png filter=lfs diff=lfs merge=lfs -text
157
+ test_images_dijkstra/img_019_ep1_step19.png filter=lfs diff=lfs merge=lfs -text
158
+ test_images_dijkstra/img_020_ep1_step20.png filter=lfs diff=lfs merge=lfs -text
159
+ test_images_dijkstra/img_021_ep1_step21.png filter=lfs diff=lfs merge=lfs -text
160
+ test_images_dijkstra/img_022_ep1_step22.png filter=lfs diff=lfs merge=lfs -text
161
+ test_images_dijkstra/img_023_ep1_step23.png filter=lfs diff=lfs merge=lfs -text
162
+ test_images_dijkstra/img_024_ep1_step24.png filter=lfs diff=lfs merge=lfs -text
163
+ test_images_dijkstra/img_025_ep1_step25.png filter=lfs diff=lfs merge=lfs -text
164
+ test_images_dijkstra/img_026_ep1_step26.png filter=lfs diff=lfs merge=lfs -text
165
+ test_images_dijkstra/img_027_ep1_step27.png filter=lfs diff=lfs merge=lfs -text
166
+ test_images_dijkstra/img_028_ep1_step28.png filter=lfs diff=lfs merge=lfs -text
167
+ test_images_dijkstra/img_029_ep1_step29.png filter=lfs diff=lfs merge=lfs -text
168
+ test_images_dijkstra/img_030_ep1_step30.png filter=lfs diff=lfs merge=lfs -text
169
+ test_images_dijkstra/img_031_ep1_step31.png filter=lfs diff=lfs merge=lfs -text
170
+ test_images_dijkstra/img_032_ep1_step32.png filter=lfs diff=lfs merge=lfs -text
171
+ test_images_dijkstra/img_033_ep1_step33.png filter=lfs diff=lfs merge=lfs -text
172
+ test_images_dijkstra/img_034_ep1_step34.png filter=lfs diff=lfs merge=lfs -text
173
+ test_images_dijkstra/img_035_ep1_step35.png filter=lfs diff=lfs merge=lfs -text
174
+ test_images_dijkstra/img_036_ep1_step36.png filter=lfs diff=lfs merge=lfs -text
175
+ test_images_dijkstra/img_037_ep1_step37.png filter=lfs diff=lfs merge=lfs -text
176
+ test_images_dijkstra/img_038_ep1_step38.png filter=lfs diff=lfs merge=lfs -text
177
+ test_images_dijkstra/img_039_ep1_step39.png filter=lfs diff=lfs merge=lfs -text
178
+ test_images_dijkstra/img_040_ep1_step40.png filter=lfs diff=lfs merge=lfs -text
179
+ test_images_dijkstra/img_041_ep1_step41.png filter=lfs diff=lfs merge=lfs -text
180
+ test_images_dijkstra/img_042_ep1_step42.png filter=lfs diff=lfs merge=lfs -text
181
+ test_images_dijkstra/img_043_ep1_step43.png filter=lfs diff=lfs merge=lfs -text
182
+ test_images_dijkstra/img_044_ep1_step44.png filter=lfs diff=lfs merge=lfs -text
183
+ test_images_dijkstra/img_045_ep1_step45.png filter=lfs diff=lfs merge=lfs -text
184
+ test_images_dijkstra/img_046_ep1_step46.png filter=lfs diff=lfs merge=lfs -text
185
+ test_images_dijkstra/img_047_ep1_step47.png filter=lfs diff=lfs merge=lfs -text
186
+ test_images_dijkstra/img_048_ep1_step48.png filter=lfs diff=lfs merge=lfs -text
187
+ test_images_dijkstra/img_049_ep1_step49.png filter=lfs diff=lfs merge=lfs -text
188
+ test_images_dijkstra/img_050_ep1_step50.png filter=lfs diff=lfs merge=lfs -text
189
+ test_images_dijkstra/img_051_ep1_step51.png filter=lfs diff=lfs merge=lfs -text
190
+ test_images_dijkstra/img_052_ep1_step52.png filter=lfs diff=lfs merge=lfs -text
191
+ test_images_dijkstra/img_053_ep1_step53.png filter=lfs diff=lfs merge=lfs -text
192
+ test_images_dijkstra/img_054_ep1_step54.png filter=lfs diff=lfs merge=lfs -text
193
+ test_images_dijkstra/img_055_ep1_step55.png filter=lfs diff=lfs merge=lfs -text
194
+ test_images_dijkstra/img_056_ep1_step56.png filter=lfs diff=lfs merge=lfs -text
195
+ test_images_dijkstra/img_057_ep1_step57.png filter=lfs diff=lfs merge=lfs -text
196
+ test_images_dijkstra/img_058_ep1_step58.png filter=lfs diff=lfs merge=lfs -text
197
+ test_images_dijkstra/img_059_ep1_step59.png filter=lfs diff=lfs merge=lfs -text
198
+ test_images_dijkstra/img_060_ep1_step60.png filter=lfs diff=lfs merge=lfs -text
199
+ test_images_dijkstra/img_061_ep1_step61.png filter=lfs diff=lfs merge=lfs -text
200
+ test_images_dijkstra/img_062_ep1_step62.png filter=lfs diff=lfs merge=lfs -text
201
+ test_images_dijkstra/img_063_ep1_step63.png filter=lfs diff=lfs merge=lfs -text
202
+ test_images_dijkstra/img_064_ep1_step64.png filter=lfs diff=lfs merge=lfs -text
203
+ test_images_dijkstra/img_065_ep1_step65.png filter=lfs diff=lfs merge=lfs -text
204
+ test_images_dijkstra/img_066_ep1_step66.png filter=lfs diff=lfs merge=lfs -text
205
+ test_images_dijkstra/img_067_ep1_step67.png filter=lfs diff=lfs merge=lfs -text
206
+ test_images_dijkstra/img_068_ep1_step68.png filter=lfs diff=lfs merge=lfs -text
207
+ test_images_dijkstra/img_069_ep1_step69.png filter=lfs diff=lfs merge=lfs -text
208
+ test_images_dijkstra/img_070_ep1_step70.png filter=lfs diff=lfs merge=lfs -text
209
+ test_images_dijkstra/img_071_ep1_step71.png filter=lfs diff=lfs merge=lfs -text
210
+ test_images_dijkstra/img_072_ep1_step72.png filter=lfs diff=lfs merge=lfs -text
211
+ test_images_dijkstra/img_073_ep1_step73.png filter=lfs diff=lfs merge=lfs -text
212
+ test_images_dijkstra/img_074_ep1_step74.png filter=lfs diff=lfs merge=lfs -text
213
+ test_images_dijkstra/img_075_ep1_step75.png filter=lfs diff=lfs merge=lfs -text
214
+ test_images_dijkstra/img_076_ep1_step76.png filter=lfs diff=lfs merge=lfs -text
215
+ test_images_dijkstra/img_077_ep1_step77.png filter=lfs diff=lfs merge=lfs -text
216
+ test_images_dijkstra/img_078_ep1_step78.png filter=lfs diff=lfs merge=lfs -text
217
+ test_images_dijkstra/img_079_ep1_step79.png filter=lfs diff=lfs merge=lfs -text
218
+ test_images_dijkstra/img_080_ep1_step80.png filter=lfs diff=lfs merge=lfs -text
219
+ test_images_dijkstra/img_081_ep1_step81.png filter=lfs diff=lfs merge=lfs -text
220
+ test_images_dijkstra/img_082_ep1_step82.png filter=lfs diff=lfs merge=lfs -text
221
+ test_images_dijkstra/img_083_ep1_step83.png filter=lfs diff=lfs merge=lfs -text
222
+ test_images_dijkstra/img_084_ep1_step84.png filter=lfs diff=lfs merge=lfs -text
223
+ test_images_dijkstra/img_085_ep1_step85.png filter=lfs diff=lfs merge=lfs -text
224
+ test_images_dijkstra/img_086_ep1_step86.png filter=lfs diff=lfs merge=lfs -text
225
+ test_images_dijkstra/img_087_ep1_step87.png filter=lfs diff=lfs merge=lfs -text
226
+ test_images_dijkstra/img_088_ep1_step88.png filter=lfs diff=lfs merge=lfs -text
227
+ test_images_dijkstra/img_089_ep1_step89.png filter=lfs diff=lfs merge=lfs -text
228
+ test_images_dijkstra/img_090_ep1_step90.png filter=lfs diff=lfs merge=lfs -text
229
+ test_images_dijkstra/img_091_ep2_step0.png filter=lfs diff=lfs merge=lfs -text
230
+ test_images_dijkstra/img_092_ep2_step1.png filter=lfs diff=lfs merge=lfs -text
231
+ test_images_dijkstra/img_093_ep2_step2.png filter=lfs diff=lfs merge=lfs -text
232
+ test_images_dijkstra/img_094_ep2_step3.png filter=lfs diff=lfs merge=lfs -text
233
+ test_images_dijkstra/img_095_ep2_step4.png filter=lfs diff=lfs merge=lfs -text
234
+ test_images_dijkstra/img_096_ep2_step5.png filter=lfs diff=lfs merge=lfs -text
235
+ test_images_dijkstra/img_097_ep2_step6.png filter=lfs diff=lfs merge=lfs -text
236
+ test_images_dijkstra/img_098_ep2_step7.png filter=lfs diff=lfs merge=lfs -text
237
+ test_images_dijkstra/img_099_ep2_step8.png filter=lfs diff=lfs merge=lfs -text
238
+ test_images_dijkstra/traffic_demo_dijkstra.gif filter=lfs diff=lfs merge=lfs -text
239
+ test_images_dijkstra/traffic_demo_dijkstra.mp4 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local OpenEnv sources)
10
+ # - Standalone environments (with openenv from PyPI/Git)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=road_traffic_simulator_env
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv is already vendored in the build context
31
+ # For standalone builds, openenv will be installed via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f http://localhost:8000/health || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ ENV ENABLE_WEB_INTERFACE=true
81
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
README.md CHANGED
@@ -1,10 +1,255 @@
1
  ---
2
- title: Road Traffic Simulator Env
3
- emoji: 📊
4
- colorFrom: indigo
5
- colorTo: red
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Road Traffic Simulator Env Environment Server
3
+ emoji: 🖲️
4
+ colorFrom: purple
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Road Traffic Simulator Env Environment
15
+
16
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
+
18
+ ## Quick Start
19
+
20
+ The simplest way to use the Road Traffic Simulator Env environment is through the `RoadTrafficSimulatorEnv` class:
21
+
22
+ ```python
23
+ from road_traffic_simulator_env import RoadTrafficSimulatorAction, RoadTrafficSimulatorEnv
24
+
25
+ try:
26
+ # Create environment from Docker image
27
+ road_traffic_simulator_envenv = RoadTrafficSimulatorEnv.from_docker_image("road_traffic_simulator_env-env:latest")
28
+
29
+ # Reset
30
+ result = road_traffic_simulator_envenv.reset()
31
+ print(f"Reset: {result.observation.echoed_message}")
32
+
33
+ # Send multiple messages
34
+ messages = ["Hello, World!", "Testing echo", "Final message"]
35
+
36
+ for msg in messages:
37
+ result = road_traffic_simulator_envenv.step(RoadTrafficSimulatorAction(message=msg))
38
+ print(f"Sent: '{msg}'")
39
+ print(f" → Echoed: '{result.observation.echoed_message}'")
40
+ print(f" → Length: {result.observation.message_length}")
41
+ print(f" → Reward: {result.reward}")
42
+
43
+ finally:
44
+ # Always clean up
45
+ road_traffic_simulator_envenv.close()
46
+ ```
47
+
48
+ That's it! The `RoadTrafficSimulatorEnv.from_docker_image()` method handles:
49
+ - Starting the Docker container
50
+ - Waiting for the server to be ready
51
+ - Connecting to the environment
52
+ - Container cleanup when you call `close()`
53
+
54
+ ## Building the Docker Image
55
+
56
+ Before using the environment, you need to build the Docker image:
57
+
58
+ ```bash
59
+ # From project root
60
+ docker build -t road_traffic_simulator_env-env:latest -f server/Dockerfile .
61
+ ```
62
+
63
+ ## Deploying to Hugging Face Spaces
64
+
65
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
66
+
67
+ ```bash
68
+ # From the environment directory (where openenv.yaml is located)
69
+ openenv push
70
+
71
+ # Or specify options
72
+ openenv push --namespace my-org --private
73
+ ```
74
+
75
+ The `openenv push` command will:
76
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
77
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
78
+ 3. Upload to Hugging Face (ensuring you're logged in)
79
+
80
+ ### Prerequisites
81
+
82
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
83
+
84
+ ### Options
85
+
86
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
87
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
88
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
89
+ - `--private`: Deploy the space as private (default: public)
90
+
91
+ ### Examples
92
+
93
+ ```bash
94
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
95
+ openenv push
96
+
97
+ # Push to a specific repository
98
+ openenv push --repo-id my-org/my-env
99
+
100
+ # Push with a custom base image
101
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
102
+
103
+ # Push as a private space
104
+ openenv push --private
105
+
106
+ # Combine options
107
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
108
+ ```
109
+
110
+ After deployment, your space will be available at:
111
+ `https://huggingface.co/spaces/<repo-id>`
112
+
113
+ The deployed space includes:
114
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
115
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
116
+ - **Health Check** at `/health` - Container health monitoring
117
+ - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
118
+
119
+ ## Environment Details
120
+
121
+ ### Action
122
+ **RoadTrafficSimulatorAction**: Contains a single field
123
+ - `message` (str) - The message to echo back
124
+
125
+ ### Observation
126
+ **RoadTrafficSimulatorObservation**: Contains the echo response and metadata
127
+ - `echoed_message` (str) - The message echoed back
128
+ - `message_length` (int) - Length of the message
129
+ - `reward` (float) - Reward based on message length (length × 0.1)
130
+ - `done` (bool) - Always False for echo environment
131
+ - `metadata` (dict) - Additional info like step count
132
+
133
+ ### Reward
134
+ The reward is calculated as: `message_length × 0.1`
135
+ - "Hi" → reward: 0.2
136
+ - "Hello, World!" → reward: 1.3
137
+ - Empty message → reward: 0.0
138
+
139
+ ## Advanced Usage
140
+
141
+ ### Connecting to an Existing Server
142
+
143
+ If you already have a Road Traffic Simulator Env environment server running, you can connect directly:
144
+
145
+ ```python
146
+ from road_traffic_simulator_env import RoadTrafficSimulatorEnv
147
+
148
+ # Connect to existing server
149
+ road_traffic_simulator_envenv = RoadTrafficSimulatorEnv(base_url="<ENV_HTTP_URL_HERE>")
150
+
151
+ # Use as normal
152
+ result = road_traffic_simulator_envenv.reset()
153
+ result = road_traffic_simulator_envenv.step(RoadTrafficSimulatorAction(message="Hello!"))
154
+ ```
155
+
156
+ Note: When connecting to an existing server, `road_traffic_simulator_envenv.close()` will NOT stop the server.
157
+
158
+ ### Using the Context Manager
159
+
160
+ The client supports context manager usage for automatic connection management:
161
+
162
+ ```python
163
+ from road_traffic_simulator_env import RoadTrafficSimulatorAction, RoadTrafficSimulatorEnv
164
+
165
+ # Connect with context manager (auto-connects and closes)
166
+ with RoadTrafficSimulatorEnv(base_url="http://localhost:8000") as env:
167
+ result = env.reset()
168
+ print(f"Reset: {result.observation.echoed_message}")
169
+ # Multiple steps with low latency
170
+ for msg in ["Hello", "World", "!"]:
171
+ result = env.step(RoadTrafficSimulatorAction(message=msg))
172
+ print(f"Echoed: {result.observation.echoed_message}")
173
+ ```
174
+
175
+ The client uses WebSocket connections for:
176
+ - **Lower latency**: No HTTP connection overhead per request
177
+ - **Persistent session**: Server maintains your environment state
178
+ - **Efficient for episodes**: Better for many sequential steps
179
+
180
+ ### Concurrent WebSocket Sessions
181
+
182
+ The server supports multiple concurrent WebSocket connections. To enable this,
183
+ modify `server/app.py` to use factory mode:
184
+
185
+ ```python
186
+ # In server/app.py - use factory mode for concurrent sessions
187
+ app = create_app(
188
+ RoadTrafficSimulatorEnvironment, # Pass class, not instance
189
+ RoadTrafficSimulatorAction,
190
+ RoadTrafficSimulatorObservation,
191
+ max_concurrent_envs=4, # Allow 4 concurrent sessions
192
+ )
193
+ ```
194
+
195
+ Then multiple clients can connect simultaneously:
196
+
197
+ ```python
198
+ from road_traffic_simulator_env import RoadTrafficSimulatorAction, RoadTrafficSimulatorEnv
199
+ from concurrent.futures import ThreadPoolExecutor
200
+
201
+ def run_episode(client_id: int):
202
+ with RoadTrafficSimulatorEnv(base_url="http://localhost:8000") as env:
203
+ result = env.reset()
204
+ for i in range(10):
205
+ result = env.step(RoadTrafficSimulatorAction(message=f"Client {client_id}, step {i}"))
206
+ return client_id, result.observation.message_length
207
+
208
+ # Run 4 episodes concurrently
209
+ with ThreadPoolExecutor(max_workers=4) as executor:
210
+ results = list(executor.map(run_episode, range(4)))
211
+ ```
212
+
213
+ ## Development & Testing
214
+
215
+ ### Direct Environment Testing
216
+
217
+ Test the environment logic directly without starting the HTTP server:
218
+
219
+ ```bash
220
+ # From the server directory
221
+ python3 server/road_traffic_simulator_env_environment.py
222
+ ```
223
+
224
+ This verifies that:
225
+ - Environment resets correctly
226
+ - Step executes actions properly
227
+ - State tracking works
228
+ - Rewards are calculated correctly
229
+
230
+ ### Running Locally
231
+
232
+ Run the server locally for development:
233
+
234
+ ```bash
235
+ uvicorn server.app:app --reload
236
+ ```
237
+
238
+ ## Project Structure
239
+
240
+ ```
241
+ road_traffic_simulator_env/
242
+ ├── .dockerignore # Docker build exclusions
243
+ ├── __init__.py # Module exports
244
+ ├── README.md # This file
245
+ ├── openenv.yaml # OpenEnv manifest
246
+ ├── pyproject.toml # Project metadata and dependencies
247
+ ├── uv.lock # Locked dependencies (generated)
248
+ ├── client.py # RoadTrafficSimulatorEnv client
249
+ ├── models.py # Action and Observation models
250
+ └── server/
251
+ ├── __init__.py # Server module exports
252
+ ├── road_traffic_simulator_env_environment.py # Core environment logic
253
+ ├── app.py # FastAPI application (HTTP + WebSocket endpoints)
254
+ └── Dockerfile # Container image definition
255
+ ```
__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Road Traffic Simulator Env Environment."""
8
+
9
+ from .client import RoadTrafficSimulatorEnv
10
+ from .models import RoadTrafficSimulatorAction, RoadTrafficSimulatorObservation
11
+
12
+ __all__ = [
13
+ "RoadTrafficSimulatorAction",
14
+ "RoadTrafficSimulatorObservation",
15
+ "RoadTrafficSimulatorEnv",
16
+ ]
client.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Road Traffic Simulator Environment Client."""
8
+
9
+ from typing import Dict
10
+
11
+ from openenv.core.client_types import StepResult
12
+ from openenv.core.env_server.types import State
13
+ from openenv.core import EnvClient
14
+
15
+ from .models import RoadTrafficSimulatorAction, RoadTrafficSimulatorObservation
16
+
17
+
18
+ class RoadTrafficSimulatorEnv(
19
+ EnvClient[RoadTrafficSimulatorAction, RoadTrafficSimulatorObservation]
20
+ ):
21
+ """
22
+ Client for the Road Traffic Simulator Environment.
23
+
24
+ The agent controls a single car navigating from a random start to a random
25
+ end node in downtown San Francisco, surrounded by background traffic.
26
+
27
+ Action space
28
+ ------------
29
+ RoadTrafficSimulatorAction(next_edge_index=<int>)
30
+ Choose which outgoing road to take at the current intersection.
31
+ Valid range: [0, observation.available_actions - 1].
32
+
33
+ Observation
34
+ -----------
35
+ RoadTrafficSimulatorObservation
36
+ .lat / .lon – agent GPS position
37
+ .goal_lat / .goal_lon – destination GPS position
38
+ .distance_to_goal – straight-line metres to goal
39
+ .available_actions – number of choices at current node
40
+ .map_screenshot – base64 PNG (400×400) traffic heatmap
41
+
42
+ Example
43
+ -------
44
+ >>> with RoadTrafficSimulatorEnv(base_url="http://localhost:8000") as env:
45
+ ... result = env.reset()
46
+ ... obs = result.observation
47
+ ... print(f"Start: ({obs.lat:.4f}, {obs.lon:.4f})")
48
+ ... print(f"Goal: ({obs.goal_lat:.4f}, {obs.goal_lon:.4f})")
49
+ ... print(f"Dist: {obs.distance_to_goal:.0f} m")
50
+ ...
51
+ ... # Greedy: always pick edge 0
52
+ ... while not result.done:
53
+ ... result = env.step(RoadTrafficSimulatorAction(next_edge_index=0))
54
+ ... print(f"Done after {result.observation.metadata['step']} steps")
55
+ """
56
+
57
+ def _step_payload(self, action: RoadTrafficSimulatorAction) -> Dict:
58
+ return {"next_edge_index": action.next_edge_index}
59
+
60
+ def _parse_result(self, payload: Dict) -> StepResult[RoadTrafficSimulatorObservation]:
61
+ obs_data = payload.get("observation", {})
62
+ observation = RoadTrafficSimulatorObservation(
63
+ lat=obs_data.get("lat", 0.0),
64
+ lon=obs_data.get("lon", 0.0),
65
+ goal_lat=obs_data.get("goal_lat", 0.0),
66
+ goal_lon=obs_data.get("goal_lon", 0.0),
67
+ distance_to_goal=obs_data.get("distance_to_goal", 0.0),
68
+ available_actions=obs_data.get("available_actions", 1),
69
+ map_screenshot=obs_data.get("map_screenshot", ""),
70
+ done=payload.get("done", False),
71
+ reward=payload.get("reward"),
72
+ metadata=obs_data.get("metadata", {}),
73
+ )
74
+ return StepResult(
75
+ observation=observation,
76
+ reward=payload.get("reward"),
77
+ done=payload.get("done", False),
78
+ )
79
+
80
+ def _parse_state(self, payload: Dict) -> State:
81
+ return State(
82
+ episode_id=payload.get("episode_id"),
83
+ step_count=payload.get("step_count", 0),
84
+ )
index.html ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>San Francisco Traffic Simulator</title>
7
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
8
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
17
+ }
18
+ #map {
19
+ width: 100vw;
20
+ height: 100vh;
21
+ }
22
+ .controls {
23
+ position: absolute;
24
+ top: 10px;
25
+ right: 10px;
26
+ z-index: 1000;
27
+ background: white;
28
+ padding: 15px;
29
+ border-radius: 8px;
30
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
31
+ min-width: 220px;
32
+ }
33
+ .controls h3 {
34
+ margin-bottom: 10px;
35
+ color: #333;
36
+ }
37
+ .controls button {
38
+ padding: 8px 16px;
39
+ margin: 5px;
40
+ border: none;
41
+ border-radius: 4px;
42
+ cursor: pointer;
43
+ font-size: 14px;
44
+ }
45
+ .controls button.primary {
46
+ background: #007bff;
47
+ color: white;
48
+ }
49
+ .controls button.secondary {
50
+ background: #6c757d;
51
+ color: white;
52
+ }
53
+ .controls button:hover {
54
+ opacity: 0.9;
55
+ }
56
+ .controls button:disabled {
57
+ opacity: 0.5;
58
+ cursor: not-allowed;
59
+ }
60
+ .stats {
61
+ margin-top: 10px;
62
+ font-size: 13px;
63
+ color: #555;
64
+ }
65
+ .stats div {
66
+ margin: 3px 0;
67
+ }
68
+ .legend {
69
+ position: absolute;
70
+ bottom: 30px;
71
+ right: 10px;
72
+ z-index: 1000;
73
+ background: white;
74
+ padding: 10px;
75
+ border-radius: 8px;
76
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
77
+ }
78
+ .legend-item {
79
+ display: flex;
80
+ align-items: center;
81
+ margin: 5px 0;
82
+ font-size: 12px;
83
+ }
84
+ .legend-color {
85
+ width: 30px;
86
+ height: 10px;
87
+ margin-right: 8px;
88
+ border-radius: 2px;
89
+ }
90
+ .loading {
91
+ position: absolute;
92
+ top: 50%;
93
+ left: 50%;
94
+ transform: translate(-50%, -50%);
95
+ z-index: 2000;
96
+ background: rgba(255,255,255,0.95);
97
+ padding: 30px 50px;
98
+ border-radius: 10px;
99
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
100
+ text-align: center;
101
+ }
102
+ .loading h2 {
103
+ margin-bottom: 10px;
104
+ color: #333;
105
+ }
106
+ .loading p {
107
+ color: #666;
108
+ }
109
+ .spinner {
110
+ width: 40px;
111
+ height: 40px;
112
+ border: 4px solid #f3f3f3;
113
+ border-top: 4px solid #007bff;
114
+ border-radius: 50%;
115
+ animation: spin 1s linear infinite;
116
+ margin: 15px auto;
117
+ }
118
+ @keyframes spin {
119
+ 0% { transform: rotate(0deg); }
120
+ 100% { transform: rotate(360deg); }
121
+ }
122
+ </style>
123
+ </head>
124
+ <body>
125
+ <div id="map"></div>
126
+
127
+ <div class="loading" id="loading">
128
+ <h2>Loading Road Network</h2>
129
+ <div class="spinner"></div>
130
+ <p id="loadingStatus">Fetching OpenStreetMap data...</p>
131
+ </div>
132
+
133
+ <div class="controls">
134
+ <h3>Traffic Simulator</h3>
135
+ <div>
136
+ <button class="primary" id="startBtn" disabled>Start</button>
137
+ <button class="secondary" id="resetBtn" disabled>Reset</button>
138
+ </div>
139
+ <div class="stats">
140
+ <div>Agents: <span id="agentCount">10000</span></div>
141
+ <div>Road Segments: <span id="roadCount">-</span></div>
142
+ <div>Intersections: <span id="nodeCount">-</span></div>
143
+ <div>Simulation Speed: <span id="speed">1x</span></div>
144
+ <input type="range" id="speedSlider" min="1" max="10" value="1" style="width: 100%; margin-top: 5px;">
145
+ </div>
146
+ </div>
147
+
148
+ <div class="legend">
149
+ <strong>Traffic Density</strong>
150
+ <div class="legend-item"><div class="legend-color" style="background: #00ff00;"></div> Low</div>
151
+ <div class="legend-item"><div class="legend-color" style="background: #ffff00;"></div> Medium</div>
152
+ <div class="legend-item"><div class="legend-color" style="background: #ff8800;"></div> High</div>
153
+ <div class="legend-item"><div class="legend-color" style="background: #ff0000;"></div> Congested</div>
154
+ </div>
155
+
156
+ <script>
157
+ // Initialize map centered on downtown San Francisco
158
+ const SF_CENTER = [37.7849, -122.4094];
159
+ const map = L.map('map').setView(SF_CENTER, 16);
160
+
161
+ // Add OpenStreetMap tiles
162
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
163
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
164
+ }).addTo(map);
165
+
166
+ // Road network data structures
167
+ let nodes = {}; // nodeId -> {lat, lng, connections: [edgeIds]}
168
+ let edges = []; // {id, startNode, endNode, coords: [[lat,lng],...], length, capacity, wayId}
169
+ let edgeById = {}; // edgeId -> edge
170
+ let trafficCount = {}; // edgeId -> count
171
+ let roadPolylines = {}; // edgeId -> L.polyline
172
+
173
+ // Bounding box for downtown SF (small area for faster loading)
174
+ const BBOX = {
175
+ south: 37.7800,
176
+ west: -122.4180,
177
+ north: 37.7900,
178
+ east: -122.4020
179
+ };
180
+
181
+ // Fetch road network from Overpass API
182
+ async function fetchRoadNetwork() {
183
+ const loadingStatus = document.getElementById('loadingStatus');
184
+ loadingStatus.textContent = 'Querying OpenStreetMap Overpass API...';
185
+
186
+ // Overpass query for roads in the bounding box
187
+ const query = `
188
+ [out:json][timeout:30];
189
+ (
190
+ way["highway"~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified)$"]
191
+ (${BBOX.south},${BBOX.west},${BBOX.north},${BBOX.east});
192
+ );
193
+ out body;
194
+ >;
195
+ out skel qt;
196
+ `;
197
+
198
+ const url = 'https://overpass-api.de/api/interpreter';
199
+
200
+ try {
201
+ const response = await fetch(url, {
202
+ method: 'POST',
203
+ body: `data=${encodeURIComponent(query)}`,
204
+ headers: {
205
+ 'Content-Type': 'application/x-www-form-urlencoded'
206
+ }
207
+ });
208
+
209
+ if (!response.ok) {
210
+ throw new Error(`HTTP error! status: ${response.status}`);
211
+ }
212
+
213
+ const data = await response.json();
214
+ loadingStatus.textContent = 'Processing road network...';
215
+
216
+ return data;
217
+ } catch (error) {
218
+ console.error('Error fetching OSM data:', error);
219
+ loadingStatus.textContent = 'Error loading data. Retrying...';
220
+
221
+ // Retry once with different server
222
+ try {
223
+ const response = await fetch('https://overpass.kumi.systems/api/interpreter', {
224
+ method: 'POST',
225
+ body: `data=${encodeURIComponent(query)}`,
226
+ headers: {
227
+ 'Content-Type': 'application/x-www-form-urlencoded'
228
+ }
229
+ });
230
+ const data = await response.json();
231
+ return data;
232
+ } catch (e) {
233
+ throw e;
234
+ }
235
+ }
236
+ }
237
+
238
+ // Parse OSM data into graph structure
239
+ function parseOSMData(osmData) {
240
+ const nodeMap = {}; // OSM node id -> {lat, lng}
241
+ const ways = []; // Array of ways with their nodes
242
+
243
+ // First pass: collect all nodes
244
+ osmData.elements.forEach(el => {
245
+ if (el.type === 'node') {
246
+ nodeMap[el.id] = { lat: el.lat, lng: el.lon };
247
+ }
248
+ });
249
+
250
+ // Second pass: collect ways
251
+ osmData.elements.forEach(el => {
252
+ if (el.type === 'way' && el.nodes && el.nodes.length >= 2) {
253
+ const highway = el.tags?.highway;
254
+ ways.push({
255
+ id: el.id,
256
+ nodeIds: el.nodes,
257
+ highway: highway,
258
+ name: el.tags?.name || '',
259
+ oneway: el.tags?.oneway === 'yes',
260
+ lanes: parseInt(el.tags?.lanes) || 2
261
+ });
262
+ }
263
+ });
264
+
265
+ // Build graph: create edges for each way segment
266
+ let edgeId = 0;
267
+ const graphNodes = {}; // Will store unique intersection nodes
268
+
269
+ ways.forEach(way => {
270
+ for (let i = 0; i < way.nodeIds.length - 1; i++) {
271
+ const startNodeId = way.nodeIds[i];
272
+ const endNodeId = way.nodeIds[i + 1];
273
+
274
+ const startCoord = nodeMap[startNodeId];
275
+ const endCoord = nodeMap[endNodeId];
276
+
277
+ if (!startCoord || !endCoord) continue;
278
+
279
+ // Calculate edge length (in meters, approximate)
280
+ const length = haversineDistance(
281
+ startCoord.lat, startCoord.lng,
282
+ endCoord.lat, endCoord.lng
283
+ );
284
+
285
+ // Determine capacity based on road type
286
+ let capacity = 10;
287
+ switch (way.highway) {
288
+ case 'motorway': capacity = 50; break;
289
+ case 'trunk': capacity = 40; break;
290
+ case 'primary': capacity = 30; break;
291
+ case 'secondary': capacity = 25; break;
292
+ case 'tertiary': capacity = 20; break;
293
+ case 'residential': capacity = 15; break;
294
+ default: capacity = 10;
295
+ }
296
+ capacity *= (way.lanes / 2);
297
+
298
+ // Create edge
299
+ const edge = {
300
+ id: `e${edgeId++}`,
301
+ startNode: startNodeId,
302
+ endNode: endNodeId,
303
+ coords: [[startCoord.lat, startCoord.lng], [endCoord.lat, endCoord.lng]],
304
+ length: length,
305
+ capacity: Math.max(5, Math.round(capacity)),
306
+ wayId: way.id,
307
+ highway: way.highway,
308
+ oneway: way.oneway
309
+ };
310
+
311
+ edges.push(edge);
312
+ edgeById[edge.id] = edge;
313
+ trafficCount[edge.id] = 0;
314
+
315
+ // Register nodes
316
+ if (!graphNodes[startNodeId]) {
317
+ graphNodes[startNodeId] = {
318
+ id: startNodeId,
319
+ lat: startCoord.lat,
320
+ lng: startCoord.lng,
321
+ outEdges: [],
322
+ inEdges: []
323
+ };
324
+ }
325
+ if (!graphNodes[endNodeId]) {
326
+ graphNodes[endNodeId] = {
327
+ id: endNodeId,
328
+ lat: endCoord.lat,
329
+ lng: endCoord.lng,
330
+ outEdges: [],
331
+ inEdges: []
332
+ };
333
+ }
334
+
335
+ // Add edge connections
336
+ graphNodes[startNodeId].outEdges.push(edge.id);
337
+ graphNodes[endNodeId].inEdges.push(edge.id);
338
+
339
+ // If not one-way, add reverse connections
340
+ if (!way.oneway) {
341
+ graphNodes[endNodeId].outEdges.push(edge.id);
342
+ graphNodes[startNodeId].inEdges.push(edge.id);
343
+ }
344
+ }
345
+ });
346
+
347
+ nodes = graphNodes;
348
+
349
+ console.log(`Parsed ${Object.keys(nodes).length} nodes and ${edges.length} edges`);
350
+ }
351
+
352
+ // Haversine formula to calculate distance between two points
353
+ function haversineDistance(lat1, lon1, lat2, lon2) {
354
+ const R = 6371000; // Earth's radius in meters
355
+ const dLat = (lat2 - lat1) * Math.PI / 180;
356
+ const dLon = (lon2 - lon1) * Math.PI / 180;
357
+ const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
358
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
359
+ Math.sin(dLon/2) * Math.sin(dLon/2);
360
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
361
+ return R * c;
362
+ }
363
+
364
+ // Draw roads on map
365
+ function drawRoads() {
366
+ edges.forEach(edge => {
367
+ const polyline = L.polyline(edge.coords, {
368
+ color: '#00ff00',
369
+ weight: getEdgeWeight(edge),
370
+ opacity: 0.8
371
+ }).addTo(map);
372
+ roadPolylines[edge.id] = polyline;
373
+ });
374
+ }
375
+
376
+ // Get line weight based on road type
377
+ function getEdgeWeight(edge) {
378
+ switch (edge.highway) {
379
+ case 'motorway': return 8;
380
+ case 'trunk': return 7;
381
+ case 'primary': return 6;
382
+ case 'secondary': return 5;
383
+ case 'tertiary': return 4;
384
+ default: return 3;
385
+ }
386
+ }
387
+
388
+ // Agent class
389
+ class Agent {
390
+ constructor(id) {
391
+ this.id = id;
392
+ this.edge = edges[Math.floor(Math.random() * edges.length)];
393
+ this.progress = Math.random();
394
+ this.direction = Math.random() < 0.5 ? 1 : -1; // Direction on edge
395
+ if (this.edge.oneway) this.direction = 1;
396
+ this.baseSpeed = 0.01 + Math.random() * 0.02;
397
+ }
398
+
399
+ getPosition() {
400
+ const p = this.direction === 1 ? this.progress : (1 - this.progress);
401
+ const start = this.edge.coords[0];
402
+ const end = this.edge.coords[1];
403
+ return [
404
+ start[0] + (end[0] - start[0]) * p,
405
+ start[1] + (end[1] - start[1]) * p
406
+ ];
407
+ }
408
+
409
+ update(speedMultiplier) {
410
+ // Calculate speed based on traffic density
411
+ const density = trafficCount[this.edge.id] / this.edge.capacity;
412
+ const trafficFactor = Math.max(0.05, 1 - density * 0.9);
413
+
414
+ // Adjust speed based on edge length (longer edges = slower progress rate)
415
+ const lengthFactor = 50 / Math.max(20, this.edge.length);
416
+
417
+ this.progress += this.baseSpeed * speedMultiplier * trafficFactor * lengthFactor;
418
+
419
+ // Move to next edge when reaching end
420
+ if (this.progress >= 1) {
421
+ this.moveToNextEdge();
422
+ }
423
+ }
424
+
425
+ moveToNextEdge() {
426
+ this.progress = 0;
427
+
428
+ // Find current node (end of current edge based on direction)
429
+ const currentNodeId = this.direction === 1 ? this.edge.endNode : this.edge.startNode;
430
+ const node = nodes[currentNodeId];
431
+
432
+ if (!node) {
433
+ // Teleport to random edge
434
+ this.edge = edges[Math.floor(Math.random() * edges.length)];
435
+ this.direction = this.edge.oneway ? 1 : (Math.random() < 0.5 ? 1 : -1);
436
+ return;
437
+ }
438
+
439
+ // Get available outgoing edges (excluding current edge)
440
+ let availableEdges = node.outEdges
441
+ .map(eid => edgeById[eid])
442
+ .filter(e => e && e.id !== this.edge.id);
443
+
444
+ if (availableEdges.length === 0) {
445
+ // Dead end - reverse direction or teleport
446
+ if (!this.edge.oneway) {
447
+ this.direction *= -1;
448
+ this.progress = 0;
449
+ } else {
450
+ this.edge = edges[Math.floor(Math.random() * edges.length)];
451
+ this.direction = 1;
452
+ }
453
+ return;
454
+ }
455
+
456
+ // Pick random next edge
457
+ const nextEdge = availableEdges[Math.floor(Math.random() * availableEdges.length)];
458
+ this.edge = nextEdge;
459
+
460
+ // Determine direction based on which node we're at
461
+ if (nextEdge.startNode === currentNodeId) {
462
+ this.direction = 1;
463
+ } else if (nextEdge.endNode === currentNodeId) {
464
+ this.direction = nextEdge.oneway ? 1 : -1;
465
+ } else {
466
+ this.direction = 1;
467
+ }
468
+ }
469
+ }
470
+
471
+ // Simulation state
472
+ const NUM_AGENTS = 10000;
473
+ let agents = [];
474
+ let running = false;
475
+ let animationId = null;
476
+ let speedMultiplier = 1;
477
+ let agentMarkersLayer = L.layerGroup().addTo(map);
478
+
479
+ function initAgents() {
480
+ agentMarkersLayer.clearLayers();
481
+ agents = [];
482
+ for (let i = 0; i < NUM_AGENTS; i++) {
483
+ agents.push(new Agent(i));
484
+ }
485
+ }
486
+
487
+ function updateTrafficCounts() {
488
+ edges.forEach(e => trafficCount[e.id] = 0);
489
+ agents.forEach(agent => {
490
+ trafficCount[agent.edge.id]++;
491
+ });
492
+ }
493
+
494
+ function getTrafficColor(edgeId) {
495
+ const edge = edgeById[edgeId];
496
+ const density = trafficCount[edgeId] / edge.capacity;
497
+
498
+ if (density < 0.3) return '#00ff00';
499
+ if (density < 0.6) return '#ffff00';
500
+ if (density < 0.85) return '#ff8800';
501
+ return '#ff0000';
502
+ }
503
+
504
+ function updateRoadColors() {
505
+ edges.forEach(edge => {
506
+ if (roadPolylines[edge.id]) {
507
+ roadPolylines[edge.id].setStyle({ color: getTrafficColor(edge.id) });
508
+ }
509
+ });
510
+ }
511
+
512
+ function updateAgentMarkers() {
513
+ agentMarkersLayer.clearLayers();
514
+
515
+ // Show subset of agents for performance
516
+ const showEvery = Math.max(1, Math.floor(agents.length / 200));
517
+ agents.forEach((agent, i) => {
518
+ if (i % showEvery === 0) {
519
+ const pos = agent.getPosition();
520
+ L.circleMarker(pos, {
521
+ radius: 3,
522
+ fillColor: '#0066ff',
523
+ fillOpacity: 0.9,
524
+ stroke: false
525
+ }).addTo(agentMarkersLayer);
526
+ }
527
+ });
528
+ }
529
+
530
+ function simulate() {
531
+ if (!running) return;
532
+
533
+ agents.forEach(agent => agent.update(speedMultiplier));
534
+ updateTrafficCounts();
535
+ updateRoadColors();
536
+ updateAgentMarkers();
537
+
538
+ animationId = requestAnimationFrame(simulate);
539
+ }
540
+
541
+ // Controls
542
+ const startBtn = document.getElementById('startBtn');
543
+ const resetBtn = document.getElementById('resetBtn');
544
+ const speedSlider = document.getElementById('speedSlider');
545
+ const speedDisplay = document.getElementById('speed');
546
+
547
+ startBtn.addEventListener('click', () => {
548
+ if (running) {
549
+ running = false;
550
+ startBtn.textContent = 'Start';
551
+ if (animationId) cancelAnimationFrame(animationId);
552
+ } else {
553
+ running = true;
554
+ startBtn.textContent = 'Pause';
555
+ simulate();
556
+ }
557
+ });
558
+
559
+ resetBtn.addEventListener('click', () => {
560
+ running = false;
561
+ startBtn.textContent = 'Start';
562
+ if (animationId) cancelAnimationFrame(animationId);
563
+ initAgents();
564
+ updateTrafficCounts();
565
+ updateRoadColors();
566
+ updateAgentMarkers();
567
+ });
568
+
569
+ speedSlider.addEventListener('input', (e) => {
570
+ speedMultiplier = parseInt(e.target.value);
571
+ speedDisplay.textContent = speedMultiplier + 'x';
572
+ });
573
+
574
+ // Initialize application
575
+ async function init() {
576
+ try {
577
+ const osmData = await fetchRoadNetwork();
578
+
579
+ document.getElementById('loadingStatus').textContent = 'Building road graph...';
580
+ parseOSMData(osmData);
581
+
582
+ document.getElementById('loadingStatus').textContent = 'Drawing roads...';
583
+ drawRoads();
584
+
585
+ document.getElementById('loadingStatus').textContent = 'Initializing agents...';
586
+ initAgents();
587
+ updateTrafficCounts();
588
+ updateRoadColors();
589
+ updateAgentMarkers();
590
+
591
+ // Update UI
592
+ document.getElementById('roadCount').textContent = edges.length;
593
+ document.getElementById('nodeCount').textContent = Object.keys(nodes).length;
594
+
595
+ // Enable controls
596
+ startBtn.disabled = false;
597
+ resetBtn.disabled = false;
598
+
599
+ // Hide loading
600
+ document.getElementById('loading').style.display = 'none';
601
+
602
+ console.log('Traffic Simulator initialized with', NUM_AGENTS, 'agents on', edges.length, 'road segments');
603
+ } catch (error) {
604
+ console.error('Failed to initialize:', error);
605
+ document.getElementById('loadingStatus').textContent = 'Failed to load road data. Please refresh the page.';
606
+ }
607
+ }
608
+
609
+ // Start initialization
610
+ init();
611
+ </script>
612
+ </body>
613
+ </html>
make_video.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stitch test_images/*.png into test_images/traffic_demo.mp4 (and .gif fallback).
3
+ Uses imageio which is already available via matplotlib's dependency chain.
4
+ """
5
+
6
+ import glob
7
+ import os
8
+ import sys
9
+
10
+ # Ensure imageio[ffmpeg] is available; fall back to gif if not
11
+ try:
12
+ import imageio.v3 as iio
13
+ import imageio
14
+ HAVE_IMAGEIO = True
15
+ except ImportError:
16
+ HAVE_IMAGEIO = False
17
+
18
+ if not HAVE_IMAGEIO:
19
+ print("imageio not found – install with: pip install imageio[ffmpeg]")
20
+ sys.exit(1)
21
+
22
+ IMG_DIR = os.path.join(os.path.dirname(__file__), "test_images")
23
+ frames_paths = sorted(glob.glob(os.path.join(IMG_DIR, "img_*.png")))
24
+
25
+ if not frames_paths:
26
+ print("No images found in test_images/. Run test_generate_images.py first.")
27
+ sys.exit(1)
28
+
29
+ print(f"Found {len(frames_paths)} frames.")
30
+
31
+ # Load frames: ensure RGB and uniform size (tight layout may vary by 1-2 px)
32
+ from PIL import Image as _PILImage
33
+ import numpy as _np
34
+
35
+ raw = []
36
+ for p in frames_paths:
37
+ img = iio.imread(p)
38
+ if img.ndim == 3 and img.shape[2] == 4:
39
+ img = img[:, :, :3]
40
+ raw.append(img)
41
+
42
+ # Use the most common size as target
43
+ from collections import Counter as _Counter
44
+ size_counts = _Counter((f.shape[1], f.shape[0]) for f in raw)
45
+ target_w, target_h = size_counts.most_common(1)[0][0]
46
+ # Round to even dimensions (required by h264 / hevc chroma subsampling)
47
+ target_w += target_w % 2
48
+ target_h += target_h % 2
49
+ frames = []
50
+ for img in raw:
51
+ if img.shape[1] != target_w or img.shape[0] != target_h:
52
+ img = _np.array(_PILImage.fromarray(img).resize((target_w, target_h), _PILImage.LANCZOS))
53
+ frames.append(img)
54
+ print(f"Frames normalised to {target_w}×{target_h}")
55
+
56
+ # --- MP4 via imageio / ffmpeg plugin ---
57
+ mp4_path = os.path.join(IMG_DIR, "traffic_demo.mp4")
58
+ try:
59
+ fps = 10
60
+ for codec in ("libx264", "h264", "libx265", "mpeg4"):
61
+ try:
62
+ iio.imwrite(mp4_path, frames, fps=fps, codec=codec)
63
+ print(f" codec: {codec}")
64
+ break
65
+ except Exception as ce:
66
+ last_err = ce
67
+ else:
68
+ raise last_err
69
+ print(f"MP4 saved: {mp4_path} ({len(frames)} frames @ {fps} fps)")
70
+ except Exception as e:
71
+ print(f"MP4 failed ({e}), falling back to GIF…")
72
+ mp4_path = None
73
+
74
+ # --- GIF (full resolution, works without ffmpeg) ---
75
+ gif_path = os.path.join(IMG_DIR, "traffic_demo.gif")
76
+ try:
77
+ from PIL import Image
78
+
79
+ pil_frames = [Image.fromarray(f) for f in frames]
80
+ pil_frames[0].save(
81
+ gif_path,
82
+ save_all=True,
83
+ append_images=pil_frames[1:],
84
+ duration=100, # ms per frame (~10 fps)
85
+ loop=0,
86
+ optimize=False,
87
+ )
88
+ w, h = pil_frames[0].size
89
+ print(f"GIF saved: {gif_path} ({len(pil_frames)} frames @ ~10 fps, {w}×{h})")
90
+ except Exception as e:
91
+ print(f"GIF failed: {e}")
make_video_dijkstra.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stitch test_images_dijkstra/*.png into test_images_dijkstra/traffic_demo_dijkstra.mp4 (and .gif fallback).
3
+ """
4
+
5
+ import glob
6
+ import os
7
+ import sys
8
+
9
+ try:
10
+ import imageio.v3 as iio
11
+ import imageio
12
+ HAVE_IMAGEIO = True
13
+ except ImportError:
14
+ HAVE_IMAGEIO = False
15
+
16
+ if not HAVE_IMAGEIO:
17
+ print("imageio not found – install with: pip install imageio[ffmpeg]")
18
+ sys.exit(1)
19
+
20
+ IMG_DIR = os.path.join(os.path.dirname(__file__), "test_images_dijkstra")
21
+ frames_paths = sorted(glob.glob(os.path.join(IMG_DIR, "img_*.png")))
22
+
23
+ if not frames_paths:
24
+ print("No images found in test_images_dijkstra/. Run test_dijkstra_policy.py first.")
25
+ sys.exit(1)
26
+
27
+ print(f"Found {len(frames_paths)} frames.")
28
+
29
+ from PIL import Image as _PILImage
30
+ import numpy as _np
31
+ from collections import Counter as _Counter
32
+
33
+ raw = []
34
+ for p in frames_paths:
35
+ img = iio.imread(p)
36
+ if img.ndim == 3 and img.shape[2] == 4:
37
+ img = img[:, :, :3]
38
+ raw.append(img)
39
+
40
+ size_counts = _Counter((f.shape[1], f.shape[0]) for f in raw)
41
+ target_w, target_h = size_counts.most_common(1)[0][0]
42
+ target_w += target_w % 2
43
+ target_h += target_h % 2
44
+ frames = []
45
+ for img in raw:
46
+ if img.shape[1] != target_w or img.shape[0] != target_h:
47
+ img = _np.array(_PILImage.fromarray(img).resize((target_w, target_h), _PILImage.LANCZOS))
48
+ frames.append(img)
49
+ print(f"Frames normalised to {target_w}×{target_h}")
50
+
51
+ mp4_path = os.path.join(IMG_DIR, "traffic_demo_dijkstra.mp4")
52
+ try:
53
+ fps = 10
54
+ for codec in ("libx264", "h264", "libx265", "mpeg4"):
55
+ try:
56
+ iio.imwrite(mp4_path, frames, fps=fps, codec=codec)
57
+ print(f" codec: {codec}")
58
+ break
59
+ except Exception as ce:
60
+ last_err = ce
61
+ else:
62
+ raise last_err
63
+ print(f"MP4 saved: {mp4_path} ({len(frames)} frames @ {fps} fps)")
64
+ except Exception as e:
65
+ print(f"MP4 failed ({e}), falling back to GIF…")
66
+ mp4_path = None
67
+
68
+ gif_path = os.path.join(IMG_DIR, "traffic_demo_dijkstra.gif")
69
+ try:
70
+ from PIL import Image
71
+
72
+ pil_frames = [Image.fromarray(f) for f in frames]
73
+ pil_frames[0].save(
74
+ gif_path,
75
+ save_all=True,
76
+ append_images=pil_frames[1:],
77
+ duration=100,
78
+ loop=0,
79
+ optimize=False,
80
+ )
81
+ w, h = pil_frames[0].size
82
+ print(f"GIF saved: {gif_path} ({len(pil_frames)} frames @ ~10 fps, {w}×{h})")
83
+ except Exception as e:
84
+ print(f"GIF failed: {e}")
models.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Data models for the Road Traffic Simulator Environment.
9
+
10
+ The agent controls a single car navigating from a start to end point
11
+ in San Francisco, surrounded by background traffic.
12
+ """
13
+
14
+ from typing import Optional
15
+ from pydantic import Field
16
+
17
+ from openenv.core.env_server.types import Action, Observation
18
+
19
+
20
+ class RoadTrafficSimulatorAction(Action):
21
+ """
22
+ Action for the traffic navigation environment.
23
+
24
+ At each intersection, the agent chooses which outgoing road segment to take.
25
+ The index is into the list of available outgoing edges at the current node,
26
+ sorted by edge ID for determinism. Invalid indices are clipped to valid range.
27
+ """
28
+
29
+ next_edge_index: int = Field(
30
+ default=0,
31
+ description=(
32
+ "Index (0-based) of the outgoing edge to take at the current intersection. "
33
+ "Must be in [0, available_actions - 1]. Out-of-range values are clipped."
34
+ ),
35
+ )
36
+
37
+
38
+ class RoadTrafficSimulatorObservation(Observation):
39
+ """
40
+ Observation from the traffic navigation environment.
41
+
42
+ Includes the agent car's current GPS position, goal position,
43
+ distance to goal, number of route choices available, and a
44
+ bird's-eye-view screenshot of the map showing real-time traffic
45
+ density (green=free-flow → red=congested).
46
+ """
47
+
48
+ # Agent position
49
+ lat: float = Field(default=0.0, description="Current latitude of the agent car")
50
+ lon: float = Field(default=0.0, description="Current longitude of the agent car")
51
+
52
+ # Goal
53
+ goal_lat: float = Field(default=0.0, description="Destination latitude")
54
+ goal_lon: float = Field(default=0.0, description="Destination longitude")
55
+ distance_to_goal: float = Field(
56
+ default=0.0, description="Straight-line distance to goal in meters"
57
+ )
58
+
59
+ # Action space info
60
+ available_actions: int = Field(
61
+ default=1,
62
+ description="Number of valid outgoing edges from current node (action range: [0, available_actions-1])",
63
+ )
64
+
65
+ # Visual observation: 400x400 PNG, base64-encoded
66
+ map_screenshot: str = Field(
67
+ default="",
68
+ description=(
69
+ "Base64-encoded PNG (400×400 px) showing the road network coloured by "
70
+ "traffic density. Blue dot = agent, green star = start, red star = goal, "
71
+ "dashed cyan = planned shortest route."
72
+ ),
73
+ )
openenv.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: road_traffic_simulator_env
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+
openenv_road_traffic_simulator_env.egg-info/PKG-INFO ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: openenv-road_traffic_simulator_env
3
+ Version: 0.1.0
4
+ Summary: Road Traffic Simulator Env environment for OpenEnv
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: openenv-core[core]>=0.2.0
7
+ Requires-Dist: requests>=2.31.0
8
+ Requires-Dist: matplotlib>=3.8.0
9
+ Requires-Dist: numpy>=1.26.0
10
+ Requires-Dist: imageio[pyav]>=2.37.2
11
+ Requires-Dist: pillow>=12.1.1
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
14
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
openenv_road_traffic_simulator_env.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ __init__.py
3
+ client.py
4
+ make_video.py
5
+ models.py
6
+ pyproject.toml
7
+ test_generate_images.py
8
+ ./__init__.py
9
+ ./client.py
10
+ ./make_video.py
11
+ ./models.py
12
+ ./test_generate_images.py
13
+ openenv_road_traffic_simulator_env.egg-info/PKG-INFO
14
+ openenv_road_traffic_simulator_env.egg-info/SOURCES.txt
15
+ openenv_road_traffic_simulator_env.egg-info/dependency_links.txt
16
+ openenv_road_traffic_simulator_env.egg-info/entry_points.txt
17
+ openenv_road_traffic_simulator_env.egg-info/requires.txt
18
+ openenv_road_traffic_simulator_env.egg-info/top_level.txt
19
+ server/__init__.py
20
+ server/app.py
21
+ server/road_traffic_simulator_env_environment.py
openenv_road_traffic_simulator_env.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
openenv_road_traffic_simulator_env.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = road_traffic_simulator_env.server.app:main
openenv_road_traffic_simulator_env.egg-info/requires.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core[core]>=0.2.0
2
+ requests>=2.31.0
3
+ matplotlib>=3.8.0
4
+ numpy>=1.26.0
5
+ imageio[pyav]>=2.37.2
6
+ pillow>=12.1.1
7
+
8
+ [dev]
9
+ pytest>=8.0.0
10
+ pytest-cov>=4.0.0
openenv_road_traffic_simulator_env.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ road_traffic_simulator_env
pyproject.toml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-road_traffic_simulator_env"
13
+ version = "0.1.0"
14
+ description = "Road Traffic Simulator Env environment for OpenEnv"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv runtime (provides FastAPI server + HTTP client types)
18
+ # install from github
19
+ # "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
20
+ "openenv-core[core]>=0.2.0",
21
+ # Environment-specific dependencies
22
+ "requests>=2.31.0",
23
+ "matplotlib>=3.8.0",
24
+ "numpy>=1.26.0",
25
+ "imageio[pyav]>=2.37.2",
26
+ "pillow>=12.1.1",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=8.0.0",
32
+ "pytest-cov>=4.0.0",
33
+ ]
34
+
35
+ [project.scripts]
36
+ # Server entry point - enables running via: uv run --project . server
37
+ # or: python -m road_traffic_simulator_env.server.app
38
+ server = "road_traffic_simulator_env.server.app:main"
39
+
40
+ [tool.setuptools]
41
+ include-package-data = true
42
+ packages = ["road_traffic_simulator_env", "road_traffic_simulator_env.server"]
43
+ package-dir = { "road_traffic_simulator_env" = ".", "road_traffic_simulator_env.server" = "server" }
server/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Road Traffic Simulator Env environment server components."""
8
+
9
+ from .road_traffic_simulator_env_environment import RoadTrafficSimulatorEnvironment
10
+
11
+ __all__ = ["RoadTrafficSimulatorEnvironment"]
server/app.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ FastAPI application for the Road Traffic Simulator Env Environment.
9
+
10
+ This module creates an HTTP server that exposes the RoadTrafficSimulatorEnvironment
11
+ over HTTP and WebSocket endpoints, compatible with EnvClient.
12
+
13
+ Endpoints:
14
+ - POST /reset: Reset the environment
15
+ - POST /step: Execute an action
16
+ - GET /state: Get current environment state
17
+ - GET /schema: Get action/observation schemas
18
+ - WS /ws: WebSocket endpoint for persistent sessions
19
+
20
+ Usage:
21
+ # Development (with auto-reload):
22
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
+
24
+ # Production:
25
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
+
27
+ # Or run directly:
28
+ python -m server.app
29
+ """
30
+
31
+ try:
32
+ from openenv.core.env_server.http_server import create_app
33
+ except Exception as e: # pragma: no cover
34
+ raise ImportError(
35
+ "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
+ ) from e
37
+
38
+ # Import from local models.py (PYTHONPATH includes /app/env in Docker)
39
+ from models import RoadTrafficSimulatorAction, RoadTrafficSimulatorObservation
40
+ from .road_traffic_simulator_env_environment import RoadTrafficSimulatorEnvironment
41
+
42
+
43
+ # Create the app with web interface and README integration
44
+ app = create_app(
45
+ RoadTrafficSimulatorEnvironment,
46
+ RoadTrafficSimulatorAction,
47
+ RoadTrafficSimulatorObservation,
48
+ env_name="road_traffic_simulator_env",
49
+ max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
50
+ )
51
+
52
+
53
+ def main(host: str = "0.0.0.0", port: int = 8000):
54
+ """
55
+ Entry point for direct execution via uv run or python -m.
56
+
57
+ This function enables running the server without Docker:
58
+ uv run --project . server
59
+ uv run --project . server --port 8001
60
+ python -m road_traffic_simulator_env.server.app
61
+
62
+ Args:
63
+ host: Host address to bind to (default: "0.0.0.0")
64
+ port: Port number to listen on (default: 8000)
65
+
66
+ For production deployments, consider using uvicorn directly with
67
+ multiple workers:
68
+ uvicorn road_traffic_simulator_env.server.app:app --workers 4
69
+ """
70
+ import uvicorn
71
+
72
+ uvicorn.run(app, host=host, port=port)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ import argparse
77
+
78
+ parser = argparse.ArgumentParser()
79
+ parser.add_argument("--port", type=int, default=8000)
80
+ args = parser.parse_args()
81
+ main(port=args.port)
server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ openenv[core]>=0.2.0
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.24.0
4
+ requests>=2.31.0
5
+ matplotlib>=3.8.0
6
+ numpy>=1.26.0
server/road_traffic_simulator_env_environment.py ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Road Traffic Simulator – RL Environment.
9
+
10
+ The agent controls a single car navigating from a random start node to a
11
+ random end node in downtown San Francisco. 2 000 background vehicles create
12
+ realistic traffic that the agent must route around.
13
+
14
+ Observation
15
+ -----------
16
+ * lat / lon – current GPS position
17
+ * goal_lat/goal_lon – destination GPS position
18
+ * distance_to_goal – straight-line metres to goal
19
+ * available_actions – number of outgoing edges (action range)
20
+ * map_screenshot – 400×400 PNG base64, roads coloured by traffic density
21
+
22
+ Action
23
+ ------
24
+ * next_edge_index – which outgoing edge to follow at the current node
25
+
26
+ Reward
27
+ ------
28
+ -1.0 per step (time penalty)
29
+ -3.0 * edge_density (congestion penalty for traversed edge, density ∈ [0,1])
30
+ +progress_reward (metres gained toward goal × 0.05)
31
+ +500.0 on reaching goal
32
+ """
33
+
34
+ import base64
35
+ import heapq
36
+ import io
37
+ import math
38
+ import random
39
+ from collections import defaultdict
40
+ from uuid import uuid4
41
+ from typing import Dict, List, Optional, Tuple
42
+
43
+ import matplotlib
44
+ matplotlib.use("Agg")
45
+ import matplotlib.pyplot as plt
46
+ from matplotlib.collections import LineCollection
47
+
48
+ import requests
49
+
50
+ from openenv.core.env_server.interfaces import Environment
51
+ from openenv.core.env_server.types import State
52
+
53
+ from models import RoadTrafficSimulatorAction, RoadTrafficSimulatorObservation
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Constants
58
+ # ---------------------------------------------------------------------------
59
+
60
+ BBOX = dict(south=37.770, west=-122.430, north=37.800, east=-122.395)
61
+
62
+ NUM_BG_AGENTS = 5000
63
+ BG_SUBSTEPS_PER_RL_STEP = 60 # background traffic advances this many mini-steps per RL step
64
+ MAX_RL_STEPS = 300 # episode time limit
65
+ GOAL_RADIUS_M = 50.0 # metres – agent "arrives" when within this distance
66
+
67
+ # Road rendering: draw in ascending z-order so major roads appear on top
68
+ _HW_LAYERS = ["unclassified", "residential", "tertiary", "secondary", "primary", "trunk", "motorway"]
69
+ _HW_ROAD_LW = {"motorway": 4.0, "trunk": 3.4, "primary": 2.8, "secondary": 2.2,
70
+ "tertiary": 1.6, "residential": 1.1, "unclassified": 0.8}
71
+ _HW_OUTLINE_LW= {"motorway": 6.2, "trunk": 5.4, "primary": 4.6, "secondary": 3.6,
72
+ "tertiary": 2.7, "residential": 1.9, "unclassified": 1.4}
73
+
74
+ # Map tile settings
75
+ _TILE_ZOOM = 16
76
+ _TILE_URLS = [
77
+ "https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
78
+ "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
79
+ ]
80
+ _TILE_HEADERS = {"User-Agent": "SFTrafficSimEnv/1.0 (rl-research)"}
81
+
82
+ # Module-level caches
83
+ _ROAD_NETWORK: Optional[Tuple] = None
84
+ _BASEMAP: Optional[Tuple] = None # (np.ndarray, [left_lon, right_lon, bottom_lat, top_lat])
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Helpers
89
+ # ---------------------------------------------------------------------------
90
+
91
+ def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
92
+ """Return distance in metres between two GPS points."""
93
+ R = 6_371_000.0
94
+ dlat = math.radians(lat2 - lat1)
95
+ dlon = math.radians(lon2 - lon1)
96
+ a = math.sin(dlat / 2) ** 2 + (
97
+ math.cos(math.radians(lat1))
98
+ * math.cos(math.radians(lat2))
99
+ * math.sin(dlon / 2) ** 2
100
+ )
101
+ return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
102
+
103
+
104
+ def _traffic_color(density: float) -> tuple:
105
+ """Return RGBA tuple with alpha tied to congestion: free-flow nearly transparent."""
106
+ if density < 0.20:
107
+ return (0.00, 0.80, 0.27, 0.22) # green – barely visible, let map show
108
+ if density < 0.50:
109
+ return (1.00, 0.87, 0.00, 0.60) # yellow – semi-transparent
110
+ if density < 0.80:
111
+ return (1.00, 0.53, 0.00, 0.80) # orange
112
+ return (1.00, 0.13, 0.00, 0.95) # red jam – nearly opaque
113
+
114
+
115
+ def _capacity(highway: str, lanes: int) -> int:
116
+ base = {"motorway": 50, "trunk": 40, "primary": 30, "secondary": 25,
117
+ "tertiary": 20, "residential": 15}.get(highway, 10)
118
+ return max(5, round(base * lanes / 2))
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Map tile helpers (basemap cached at module level)
123
+ # ---------------------------------------------------------------------------
124
+
125
+ def _tile_coords(lat: float, lon: float, zoom: int) -> Tuple[int, int]:
126
+ n = 2 ** zoom
127
+ x = int((lon + 180.0) / 360.0 * n)
128
+ lat_r = math.radians(lat)
129
+ y = int((1.0 - math.asinh(math.tan(lat_r)) / math.pi) / 2.0 * n)
130
+ return x, y
131
+
132
+
133
+ def _tile_nw(tx: int, ty: int, zoom: int) -> Tuple[float, float]:
134
+ """Return (lat, lon) of the NW corner of tile (tx, ty)."""
135
+ n = 2 ** zoom
136
+ lon = tx / n * 360.0 - 180.0
137
+ lat = math.degrees(math.atan(math.sinh(math.pi * (1.0 - 2.0 * ty / n))))
138
+ return lat, lon
139
+
140
+
141
+ def _fetch_basemap() -> Tuple:
142
+ from PIL import Image as PILImage
143
+ import numpy as np
144
+
145
+ zoom = _TILE_ZOOM
146
+ tx_min, ty_min = _tile_coords(BBOX["north"], BBOX["west"], zoom)
147
+ tx_max, ty_max = _tile_coords(BBOX["south"], BBOX["east"], zoom)
148
+
149
+ cols = tx_max - tx_min + 1
150
+ rows = ty_max - ty_min + 1
151
+ canvas = PILImage.new("RGB", (cols * 256, rows * 256), (17, 17, 27))
152
+
153
+ for tx in range(tx_min, tx_max + 1):
154
+ for ty in range(ty_min, ty_max + 1):
155
+ for url_tmpl in _TILE_URLS:
156
+ url = url_tmpl.format(z=zoom, x=tx, y=ty)
157
+ try:
158
+ r = requests.get(url, headers=_TILE_HEADERS, timeout=15)
159
+ r.raise_for_status()
160
+ tile = PILImage.open(io.BytesIO(r.content)).convert("RGB")
161
+ canvas.paste(tile, ((tx - tx_min) * 256, (ty - ty_min) * 256))
162
+ break
163
+ except Exception:
164
+ continue
165
+
166
+ img_arr = np.array(canvas)
167
+ nw_lat, nw_lon = _tile_nw(tx_min, ty_min, zoom)
168
+ se_lat, se_lon = _tile_nw(tx_max + 1, ty_max + 1, zoom)
169
+ # extent for imshow: [left, right, bottom, top]
170
+ extent = [nw_lon, se_lon, se_lat, nw_lat]
171
+ return img_arr, extent
172
+
173
+
174
+ def _get_basemap() -> Tuple:
175
+ global _BASEMAP
176
+ if _BASEMAP is None:
177
+ _BASEMAP = _fetch_basemap()
178
+ return _BASEMAP
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # OSM fetch + parse (cached at module level so only fetched once)
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ def _fetch_road_network() -> Tuple[Dict, List[Dict], Dict]:
187
+ query = (
188
+ "[out:json][timeout:60];\n"
189
+ "(\n"
190
+ ' way["highway"~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified)$"]\n'
191
+ f" ({BBOX['south']},{BBOX['west']},{BBOX['north']},{BBOX['east']});\n"
192
+ ");\n"
193
+ "out body;\n"
194
+ ">;\n"
195
+ "out skel qt;\n"
196
+ )
197
+ servers = [
198
+ "https://overpass-api.de/api/interpreter",
199
+ "https://overpass.kumi.systems/api/interpreter",
200
+ ]
201
+ data = None
202
+ for url in servers:
203
+ try:
204
+ r = requests.post(
205
+ url,
206
+ data={"data": query},
207
+ timeout=90,
208
+ )
209
+ r.raise_for_status()
210
+ data = r.json()
211
+ break
212
+ except Exception:
213
+ continue
214
+ if data is None:
215
+ raise RuntimeError("Failed to fetch OSM data from all Overpass servers.")
216
+
217
+ return _parse_osm(data)
218
+
219
+
220
+ def _parse_osm(osm_data: Dict) -> Tuple[Dict, List[Dict], Dict]:
221
+ node_coords: Dict[int, Tuple[float, float]] = {}
222
+ for el in osm_data["elements"]:
223
+ if el["type"] == "node":
224
+ node_coords[el["id"]] = (el["lat"], el["lon"])
225
+
226
+ nodes: Dict = {}
227
+ edges: List[Dict] = []
228
+ edge_by_id: Dict[str, Dict] = {}
229
+ edge_id_counter = 0
230
+
231
+ for el in osm_data["elements"]:
232
+ if el["type"] != "way" or not el.get("nodes") or len(el["nodes"]) < 2:
233
+ continue
234
+ tags = el.get("tags", {})
235
+ highway = tags.get("highway", "unclassified")
236
+ oneway = tags.get("oneway") == "yes"
237
+ lanes = int(tags.get("lanes", 2))
238
+ cap = _capacity(highway, lanes)
239
+
240
+ for i in range(len(el["nodes"]) - 1):
241
+ s_id, e_id = el["nodes"][i], el["nodes"][i + 1]
242
+ if s_id not in node_coords or e_id not in node_coords:
243
+ continue
244
+ slat, slon = node_coords[s_id]
245
+ elat, elon = node_coords[e_id]
246
+ length = _haversine(slat, slon, elat, elon)
247
+
248
+ eid = f"e{edge_id_counter}"
249
+ edge_id_counter += 1
250
+ edge = {
251
+ "id": eid,
252
+ "start_node": s_id,
253
+ "end_node": e_id,
254
+ "coords": [(slat, slon), (elat, elon)],
255
+ "length": length,
256
+ "capacity": cap,
257
+ "highway": highway,
258
+ "oneway": oneway,
259
+ }
260
+ edges.append(edge)
261
+ edge_by_id[eid] = edge
262
+
263
+ for nid, lat, lon in [(s_id, slat, slon), (e_id, elat, elon)]:
264
+ if nid not in nodes:
265
+ nodes[nid] = {"id": nid, "lat": lat, "lon": lon,
266
+ "out_edges": [], "in_edges": []}
267
+
268
+ nodes[s_id]["out_edges"].append(eid)
269
+ nodes[e_id]["in_edges"].append(eid)
270
+ if not oneway:
271
+ nodes[e_id]["out_edges"].append(eid)
272
+ nodes[s_id]["in_edges"].append(eid)
273
+
274
+ return nodes, edges, edge_by_id
275
+
276
+
277
+ def _get_road_network() -> Tuple[Dict, List[Dict], Dict]:
278
+ global _ROAD_NETWORK
279
+ if _ROAD_NETWORK is None:
280
+ _ROAD_NETWORK = _fetch_road_network()
281
+ return _ROAD_NETWORK
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # Background agent
286
+ # ---------------------------------------------------------------------------
287
+
288
+ class _BgAgent:
289
+ __slots__ = ("edge", "progress", "direction", "speed")
290
+
291
+ def __init__(self, edges: List[Dict]):
292
+ self.edge = random.choice(edges)
293
+ self.progress: float = random.random()
294
+ self.direction: int = 1 if self.edge["oneway"] else random.choice([1, -1])
295
+ self.speed: float = 0.012 + random.random() * 0.018
296
+
297
+ def _current_node_id(self):
298
+ if self.direction == 1:
299
+ return self.edge["end_node"]
300
+ return self.edge["start_node"]
301
+
302
+ def step(
303
+ self,
304
+ traffic_count: Dict[str, int],
305
+ nodes: Dict,
306
+ edges: List[Dict],
307
+ edge_by_id: Dict[str, Dict],
308
+ ):
309
+ density = traffic_count.get(self.edge["id"], 0) / self.edge["capacity"]
310
+ # Near-standstill in heavy jams (density 1.0 → speed_factor 0.02)
311
+ speed_factor = max(0.02, 1.0 - density ** 0.7 * 0.98)
312
+ length_factor = 50.0 / max(20.0, self.edge["length"])
313
+ self.progress += self.speed * speed_factor * length_factor
314
+
315
+ if self.progress >= 1.0:
316
+ self.progress = 0.0
317
+ nid = self._current_node_id()
318
+ node = nodes.get(nid)
319
+ if not node:
320
+ self.edge = random.choice(edges)
321
+ self.direction = 1 if self.edge["oneway"] else random.choice([1, -1])
322
+ return
323
+
324
+ choices = [
325
+ e for eid in node["out_edges"]
326
+ if (e := edge_by_id.get(eid)) and e["id"] != self.edge["id"]
327
+ ]
328
+ if not choices:
329
+ if not self.edge["oneway"]:
330
+ self.direction *= -1
331
+ else:
332
+ self.edge = random.choice(edges)
333
+ self.direction = 1
334
+ return
335
+
336
+ next_edge = random.choice(choices)
337
+ self.edge = next_edge
338
+ if next_edge["start_node"] == nid:
339
+ self.direction = 1
340
+ elif not next_edge["oneway"]:
341
+ self.direction = -1
342
+ else:
343
+ self.direction = 1
344
+
345
+ def get_position(self) -> Tuple[float, float]:
346
+ p = self.progress if self.direction == 1 else 1.0 - self.progress
347
+ (slat, slon), (elat, elon) = self.edge["coords"]
348
+ return (slat + (elat - slat) * p, slon + (elon - slon) * p)
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Dijkstra shortest path
353
+ # ---------------------------------------------------------------------------
354
+
355
+ def _dijkstra(
356
+ nodes: Dict, edge_by_id: Dict[str, Dict], start_id: int, end_id: int
357
+ ) -> Tuple[List[str], float]:
358
+ dist: Dict[int, float] = {start_id: 0.0}
359
+ prev: Dict[int, Optional[Tuple[int, str]]] = {start_id: None}
360
+ pq = [(0.0, start_id)]
361
+
362
+ while pq:
363
+ d, u = heapq.heappop(pq)
364
+ if d > dist.get(u, math.inf):
365
+ continue
366
+ if u == end_id:
367
+ break
368
+ node = nodes.get(u)
369
+ if not node:
370
+ continue
371
+ for eid in node["out_edges"]:
372
+ edge = edge_by_id.get(eid)
373
+ if not edge:
374
+ continue
375
+ # Determine the neighbour node
376
+ if edge["start_node"] == u:
377
+ v = edge["end_node"]
378
+ elif not edge["oneway"]:
379
+ v = edge["start_node"]
380
+ else:
381
+ continue
382
+ nd = d + edge["length"]
383
+ if nd < dist.get(v, math.inf):
384
+ dist[v] = nd
385
+ prev[v] = (u, eid)
386
+ heapq.heappush(pq, (nd, v))
387
+
388
+ # Reconstruct edge path
389
+ path: List[str] = []
390
+ cur = end_id
391
+ while cur in prev and prev[cur] is not None:
392
+ parent, eid = prev[cur]
393
+ path.append(eid)
394
+ cur = parent
395
+ path.reverse()
396
+ return path, dist.get(end_id, math.inf)
397
+
398
+
399
+ # ---------------------------------------------------------------------------
400
+ # Outgoing edges from a node (respecting one-way)
401
+ # ---------------------------------------------------------------------------
402
+
403
+ def _outgoing_edges(
404
+ node_id: int,
405
+ nodes: Dict,
406
+ edge_by_id: Dict,
407
+ prev_node_id: Optional[int] = None,
408
+ ) -> List[Dict]:
409
+ """Return outgoing edges from node_id, excluding U-turns back to prev_node_id.
410
+
411
+ If excluding the U-turn would leave no options, all edges are returned
412
+ (so the agent can always move).
413
+ """
414
+ node = nodes.get(node_id)
415
+ if not node:
416
+ return []
417
+ result = []
418
+ for eid in node["out_edges"]:
419
+ e = edge_by_id.get(eid)
420
+ if not e:
421
+ continue
422
+ # For oneway edges, must originate at start_node
423
+ if e["oneway"] and e["start_node"] != node_id:
424
+ continue
425
+ result.append(e)
426
+ result = sorted(result, key=lambda e: e["id"]) # deterministic order
427
+
428
+ # Remove U-turn (edge that leads straight back to where we came from)
429
+ if prev_node_id is not None and len(result) > 1:
430
+ def _neighbour(e: Dict) -> int:
431
+ return e["end_node"] if e["start_node"] == node_id else e["start_node"]
432
+ filtered = [e for e in result if _neighbour(e) != prev_node_id]
433
+ if filtered:
434
+ return filtered
435
+
436
+ return result
437
+
438
+
439
+ # ---------------------------------------------------------------------------
440
+ # Environment
441
+ # ---------------------------------------------------------------------------
442
+
443
+ class RoadTrafficSimulatorEnvironment(Environment):
444
+ """
445
+ Single-car navigation environment in downtown San Francisco.
446
+
447
+ The agent selects which road segment to follow at each intersection.
448
+ Background traffic creates dynamic congestion that the agent should avoid.
449
+ """
450
+
451
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
452
+
453
+ def __init__(self):
454
+ self._state = State(episode_id=str(uuid4()), step_count=0)
455
+
456
+ self.nodes, self.edges, self.edge_by_id = _get_road_network()
457
+
458
+ # Nodes with ≥2 outgoing edges so the agent always has a real choice
459
+ self._viable_nodes = [
460
+ nid for nid, n in self.nodes.items()
461
+ if len(_outgoing_edges(nid, self.nodes, self.edge_by_id)) >= 2
462
+ ]
463
+
464
+ self.traffic_count: Dict[str, int] = {e["id"]: 0 for e in self.edges}
465
+ self.bg_agents: List[_BgAgent] = []
466
+
467
+ # Agent car state
468
+ self.agent_node_id: Optional[int] = None
469
+ self.prev_node_id: Optional[int] = None # for U-turn prevention
470
+ self.start_node_id: Optional[int] = None
471
+ self.end_node_id: Optional[int] = None
472
+ self.planned_route: List[str] = [] # edge ids of shortest path
473
+ self.prev_dist_to_goal: float = 0.0
474
+
475
+ # ------------------------------------------------------------------
476
+ # OpenEnv interface
477
+ # ------------------------------------------------------------------
478
+
479
+ def reset(self) -> RoadTrafficSimulatorObservation:
480
+ self._state = State(episode_id=str(uuid4()), step_count=0)
481
+
482
+ # Pick start and end nodes that are at least 400 m apart
483
+ for _ in range(200):
484
+ s = random.choice(self._viable_nodes)
485
+ e = random.choice(self._viable_nodes)
486
+ if s == e:
487
+ continue
488
+ sn, en = self.nodes[s], self.nodes[e]
489
+ if _haversine(sn["lat"], sn["lon"], en["lat"], en["lon"]) >= 400.0:
490
+ self.start_node_id = s
491
+ self.end_node_id = e
492
+ break
493
+ else:
494
+ self.start_node_id = self._viable_nodes[0]
495
+ self.end_node_id = self._viable_nodes[-1]
496
+
497
+ self.agent_node_id = self.start_node_id
498
+ self.prev_node_id = None
499
+
500
+ # Pre-compute shortest path
501
+ self.planned_route, _ = _dijkstra(
502
+ self.nodes, self.edge_by_id, self.start_node_id, self.end_node_id
503
+ )
504
+
505
+ # Initialise background traffic
506
+ self.bg_agents = [_BgAgent(self.edges) for _ in range(NUM_BG_AGENTS)]
507
+ self._update_traffic_counts()
508
+
509
+ sn = self.nodes[self.start_node_id]
510
+ en = self.nodes[self.end_node_id]
511
+ self.prev_dist_to_goal = _haversine(sn["lat"], sn["lon"], en["lat"], en["lon"])
512
+
513
+ return self._make_observation(reward=0.0, done=False)
514
+
515
+ def step(self, action: RoadTrafficSimulatorAction) -> RoadTrafficSimulatorObservation:
516
+ self._state.step_count += 1
517
+
518
+ # ---- Agent moves ----
519
+ out_edges = _outgoing_edges(
520
+ self.agent_node_id, self.nodes, self.edge_by_id, self.prev_node_id
521
+ )
522
+ if not out_edges:
523
+ # True dead end – episode over
524
+ return self._make_observation(reward=-50.0, done=True)
525
+
526
+ idx = max(0, min(action.next_edge_index, len(out_edges) - 1))
527
+ chosen_edge = out_edges[idx]
528
+
529
+ # Edge density at time of traversal
530
+ density = self.traffic_count.get(chosen_edge["id"], 0) / chosen_edge["capacity"]
531
+
532
+ # Move agent to next node
533
+ self.prev_node_id = self.agent_node_id
534
+ if chosen_edge["start_node"] == self.agent_node_id:
535
+ self.agent_node_id = chosen_edge["end_node"]
536
+ else:
537
+ self.agent_node_id = chosen_edge["start_node"]
538
+
539
+ # ---- Advance background traffic ----
540
+ for _ in range(BG_SUBSTEPS_PER_RL_STEP):
541
+ for ag in self.bg_agents:
542
+ ag.step(self.traffic_count, self.nodes, self.edges, self.edge_by_id)
543
+ self._update_traffic_counts()
544
+
545
+ # ---- Reward ----
546
+ an = self.nodes[self.agent_node_id]
547
+ en = self.nodes[self.end_node_id]
548
+ dist_to_goal = _haversine(an["lat"], an["lon"], en["lat"], en["lon"])
549
+
550
+ # Time cost scales with congestion: congested edge takes up to 5× longer
551
+ time_penalty = -(1.0 + density * 4.0)
552
+ traffic_penalty = 0.0 # absorbed into time_penalty
553
+ progress_reward = (self.prev_dist_to_goal - dist_to_goal) * 0.05
554
+ self.prev_dist_to_goal = dist_to_goal
555
+
556
+ reward = time_penalty + traffic_penalty + progress_reward
557
+
558
+ # ---- Termination ----
559
+ done = False
560
+ if dist_to_goal <= GOAL_RADIUS_M:
561
+ reward += 500.0
562
+ done = True
563
+ elif self._state.step_count >= MAX_RL_STEPS:
564
+ done = True
565
+
566
+ return self._make_observation(reward=reward, done=done)
567
+
568
+ @property
569
+ def state(self) -> State:
570
+ return self._state
571
+
572
+ # ------------------------------------------------------------------
573
+ # Internal helpers
574
+ # ------------------------------------------------------------------
575
+
576
+ def _update_traffic_counts(self):
577
+ for eid in self.traffic_count:
578
+ self.traffic_count[eid] = 0
579
+ for ag in self.bg_agents:
580
+ eid = ag.edge["id"]
581
+ self.traffic_count[eid] = self.traffic_count.get(eid, 0) + 1
582
+
583
+ def _make_observation(self, reward: float, done: bool) -> RoadTrafficSimulatorObservation:
584
+ an = self.nodes[self.agent_node_id]
585
+ en = self.nodes[self.end_node_id]
586
+ dist = _haversine(an["lat"], an["lon"], en["lat"], en["lon"])
587
+ out_edges = _outgoing_edges(
588
+ self.agent_node_id, self.nodes, self.edge_by_id, self.prev_node_id
589
+ )
590
+ screenshot = self._render()
591
+
592
+ return RoadTrafficSimulatorObservation(
593
+ lat=an["lat"],
594
+ lon=an["lon"],
595
+ goal_lat=en["lat"],
596
+ goal_lon=en["lon"],
597
+ distance_to_goal=dist,
598
+ available_actions=max(1, len(out_edges)),
599
+ map_screenshot=screenshot,
600
+ done=done,
601
+ reward=reward,
602
+ metadata={
603
+ "step": self._state.step_count,
604
+ "episode_id": self._state.episode_id,
605
+ "start_node": self.start_node_id,
606
+ "end_node": self.end_node_id,
607
+ },
608
+ )
609
+
610
+ def _render(self) -> str:
611
+ """Render 512×512 traffic map and return as base64 PNG string."""
612
+ BG = "#0d1117"
613
+ BORDER = "#111827"
614
+
615
+ fig, ax = plt.subplots(figsize=(5.12, 5.12), dpi=200)
616
+ fig.patch.set_facecolor(BG)
617
+ ax.set_facecolor(BG)
618
+ ax.set_xlim(BBOX["west"], BBOX["east"])
619
+ ax.set_ylim(BBOX["south"], BBOX["north"])
620
+ ax.set_aspect("auto")
621
+ ax.axis("off")
622
+
623
+ # --- Real SF map tiles as background ---
624
+ bm_img, bm_extent = _get_basemap()
625
+ ax.imshow(bm_img, extent=bm_extent, aspect="auto",
626
+ zorder=0, interpolation="bilinear", alpha=1.0)
627
+
628
+ # --- Group edges by highway type, compute traffic colour per segment ---
629
+ by_hw: Dict[str, Dict] = {hw: {"segs": [], "colors": []} for hw in _HW_LAYERS}
630
+ for edge in self.edges:
631
+ hw = edge["highway"] if edge["highway"] in _HW_LAYERS else "unclassified"
632
+ density = self.traffic_count.get(edge["id"], 0) / edge["capacity"]
633
+ (slat, slon), (elat, elon) = edge["coords"]
634
+ by_hw[hw]["segs"].append([(slon, slat), (elon, elat)])
635
+ by_hw[hw]["colors"].append(_traffic_color(density))
636
+
637
+ # Single-pass draw with RGBA colors (alpha encodes congestion level).
638
+ # No dark outline pass – we want the basemap to show through clearly.
639
+ for z, hw in enumerate(_HW_LAYERS):
640
+ data = by_hw[hw]
641
+ if not data["segs"]:
642
+ continue
643
+ rlw = _HW_ROAD_LW.get(hw, 0.8)
644
+ ax.add_collection(LineCollection(
645
+ data["segs"], colors=data["colors"], linewidths=rlw, zorder=z + 1))
646
+
647
+ # --- Planned route (bright cyan dashed, on top of roads) ---
648
+ if self.planned_route:
649
+ route_segs = []
650
+ for eid in self.planned_route:
651
+ e = self.edge_by_id.get(eid)
652
+ if e:
653
+ (slat, slon), (elat, elon) = e["coords"]
654
+ route_segs.append([(slon, slat), (elon, elat)])
655
+ if route_segs:
656
+ zr = len(_HW_LAYERS) + 2
657
+ ax.add_collection(LineCollection(
658
+ route_segs, colors="#00ccff", linewidths=2.5,
659
+ linestyles="dashed", alpha=0.95, zorder=zr))
660
+
661
+ zm = len(_HW_LAYERS) + 4 # marker z-base
662
+
663
+ # --- Start marker ---
664
+ sn = self.nodes[self.start_node_id]
665
+ ax.plot(sn["lon"], sn["lat"], marker="*", color="#00dd66",
666
+ markersize=16, zorder=zm, markeredgecolor="white", markeredgewidth=0.8)
667
+ ax.text(sn["lon"], sn["lat"] + 0.00035, "START", color="white",
668
+ fontsize=5.5, ha="center", va="bottom", zorder=zm + 1,
669
+ bbox=dict(boxstyle="round,pad=0.25", fc="#00884a", ec="none", alpha=0.85))
670
+
671
+ # --- Goal marker ---
672
+ en = self.nodes[self.end_node_id]
673
+ ax.plot(en["lon"], en["lat"], marker="*", color="#ff3311",
674
+ markersize=16, zorder=zm, markeredgecolor="white", markeredgewidth=0.8)
675
+ ax.text(en["lon"], en["lat"] + 0.00035, "GOAL", color="white",
676
+ fontsize=5.5, ha="center", va="bottom", zorder=zm + 1,
677
+ bbox=dict(boxstyle="round,pad=0.25", fc="#cc2200", ec="none", alpha=0.85))
678
+
679
+ # --- Agent car with direction arrow ---
680
+ an = self.nodes[self.agent_node_id]
681
+ ax.plot(an["lon"], an["lat"], marker="o", color="#3388ff",
682
+ markersize=9, zorder=zm + 2,
683
+ markeredgecolor="white", markeredgewidth=1.5)
684
+ if self.prev_node_id and self.prev_node_id in self.nodes:
685
+ pn = self.nodes[self.prev_node_id]
686
+ dx = an["lon"] - pn["lon"]
687
+ dy = an["lat"] - pn["lat"]
688
+ length = math.hypot(dx, dy)
689
+ if length > 1e-9:
690
+ # Arrow tail starts 60 % of the way back from the agent
691
+ tail_lon = an["lon"] - dx * 0.6
692
+ tail_lat = an["lat"] - dy * 0.6
693
+ ax.annotate(
694
+ "", xy=(an["lon"], an["lat"]),
695
+ xytext=(tail_lon, tail_lat),
696
+ arrowprops=dict(
697
+ arrowstyle="-|>", color="white", lw=1.2,
698
+ mutation_scale=12),
699
+ zorder=zm + 3)
700
+
701
+ # --- HUD: step count + distance ---
702
+ dist = _haversine(an["lat"], an["lon"], en["lat"], en["lon"])
703
+ hud_style = dict(boxstyle="round,pad=0.3", fc=BG, ec="#334455", alpha=0.88)
704
+ ax.text(0.01, 0.99, f"Step {self._state.step_count}",
705
+ transform=ax.transAxes, color="#aaaacc", fontsize=6.5,
706
+ va="top", ha="left", zorder=zm + 5, bbox=hud_style)
707
+ ax.text(0.99, 0.99, f"Dist {dist:.0f} m",
708
+ transform=ax.transAxes, color="#ffdd88", fontsize=6.5,
709
+ va="top", ha="right", zorder=zm + 5, bbox=hud_style)
710
+
711
+ # --- Legend ---
712
+ from matplotlib.patches import Patch
713
+ legend_els = [
714
+ Patch(fc="#00cc44", ec="none", label="Free flow"),
715
+ Patch(fc="#ffdd00", ec="none", label="Moderate"),
716
+ Patch(fc="#ff8800", ec="none", label="Heavy"),
717
+ Patch(fc="#ff2200", ec="none", label="Jam"),
718
+ ]
719
+ ax.legend(handles=legend_els, loc="lower left", fontsize=5.5,
720
+ facecolor="#131b26", edgecolor="#334455", labelcolor="#cccccc",
721
+ framealpha=0.88, ncol=2, handlelength=1.2,
722
+ borderpad=0.5, handletextpad=0.4, columnspacing=0.8)
723
+
724
+ buf = io.BytesIO()
725
+ plt.savefig(buf, format="png", dpi=200,
726
+ bbox_inches="tight", pad_inches=0.02, facecolor=BG)
727
+ plt.close(fig)
728
+ buf.seek(0)
729
+ return base64.b64encode(buf.read()).decode("utf-8")
test_dijkstra_policy.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test the Dijkstra (shortest-path) policy.
3
+
4
+ At each intersection the policy picks the outgoing edge that matches the
5
+ next edge in the pre-computed shortest route (env.planned_route).
6
+ If the agent drifts off-route (dead-end escape, etc.) it falls back to
7
+ the edge that gets geometrically closest to the goal.
8
+
9
+ Results are saved to test_images_dijkstra/.
10
+ """
11
+
12
+ import base64
13
+ import math
14
+ import os
15
+ import sys
16
+
17
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "server"))
18
+ sys.path.insert(0, os.path.dirname(__file__))
19
+
20
+ from server.road_traffic_simulator_env_environment import (
21
+ RoadTrafficSimulatorEnvironment,
22
+ _outgoing_edges,
23
+ _haversine,
24
+ )
25
+ from models import RoadTrafficSimulatorAction
26
+
27
+ OUT_DIR = os.path.join(os.path.dirname(__file__), "test_images_dijkstra")
28
+ os.makedirs(OUT_DIR, exist_ok=True)
29
+
30
+ TARGET = 100
31
+
32
+
33
+ def save_png(b64: str, path: str):
34
+ with open(path, "wb") as f:
35
+ f.write(base64.b64decode(b64))
36
+
37
+
38
+ class DijkstraPolicy:
39
+ """Follow env.planned_route; fall back to closest-to-goal edge if off-route."""
40
+
41
+ def __init__(self):
42
+ self._route_idx = 0
43
+
44
+ def reset(self):
45
+ self._route_idx = 0
46
+
47
+ def act(self, env: RoadTrafficSimulatorEnvironment) -> int:
48
+ out_edges = _outgoing_edges(
49
+ env.agent_node_id, env.nodes, env.edge_by_id, env.prev_node_id
50
+ )
51
+ if not out_edges:
52
+ return 0
53
+
54
+ # Try to find the next planned-route edge among the outgoing options
55
+ while self._route_idx < len(env.planned_route):
56
+ target_eid = env.planned_route[self._route_idx]
57
+ for i, e in enumerate(out_edges):
58
+ if e["id"] == target_eid:
59
+ self._route_idx += 1
60
+ return i
61
+ # Planned edge not reachable from here — advance route pointer and retry
62
+ self._route_idx += 1
63
+
64
+ # Off-route or past end: pick the edge whose far end is closest to goal
65
+ goal = env.nodes[env.end_node_id]
66
+ best_i, best_d = 0, math.inf
67
+ for i, e in enumerate(out_edges):
68
+ nid = e["end_node"] if e["start_node"] == env.agent_node_id else e["start_node"]
69
+ n = env.nodes.get(nid)
70
+ if n:
71
+ d = _haversine(n["lat"], n["lon"], goal["lat"], goal["lon"])
72
+ if d < best_d:
73
+ best_d, best_i = d, i
74
+ return best_i
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+
79
+ print("Initialising environment (OSM data cached from previous run if available)…")
80
+ env = RoadTrafficSimulatorEnvironment()
81
+ policy = DijkstraPolicy()
82
+
83
+ img_idx = 0
84
+ episode = 0
85
+ total_reward = 0.0
86
+ episodes_done = 0
87
+
88
+ while img_idx < TARGET:
89
+ episode += 1
90
+ obs = env.reset()
91
+ policy.reset()
92
+ ep_reward = 0.0
93
+
94
+ fname = os.path.join(OUT_DIR, f"img_{img_idx:03d}_ep{episode}_step0.png")
95
+ save_png(obs.map_screenshot, fname)
96
+ print(f"[{img_idx+1:3d}/{TARGET}] ep={episode} step=0 "
97
+ f"start=({obs.lat:.4f},{obs.lon:.4f}) "
98
+ f"goal=({obs.goal_lat:.4f},{obs.goal_lon:.4f}) "
99
+ f"dist={obs.distance_to_goal:.0f}m route_len={len(env.planned_route)} edges"
100
+ f" → {os.path.basename(fname)}")
101
+ img_idx += 1
102
+ if img_idx >= TARGET:
103
+ break
104
+
105
+ step = 0
106
+ done = False
107
+ while not done and img_idx < TARGET:
108
+ step += 1
109
+ action_idx = policy.act(env)
110
+ obs = env.step(RoadTrafficSimulatorAction(next_edge_index=action_idx))
111
+ done = obs.done
112
+ ep_reward += obs.reward
113
+
114
+ fname = os.path.join(OUT_DIR, f"img_{img_idx:03d}_ep{episode}_step{step}.png")
115
+ save_png(obs.map_screenshot, fname)
116
+ print(f"[{img_idx+1:3d}/{TARGET}] ep={episode} step={step} "
117
+ f"pos=({obs.lat:.4f},{obs.lon:.4f}) "
118
+ f"dist={obs.distance_to_goal:.0f}m "
119
+ f"reward={obs.reward:+.2f} "
120
+ f"done={done} → {os.path.basename(fname)}")
121
+ img_idx += 1
122
+
123
+ if done:
124
+ episodes_done += 1
125
+ total_reward += ep_reward
126
+ goal_reached = obs.distance_to_goal <= 50.0
127
+ print(f" ↳ Episode {episode} finished: steps={step} "
128
+ f"total_reward={ep_reward:.1f} "
129
+ f"goal={'REACHED ✓' if goal_reached else 'timeout/deadend'}")
130
+
131
+ print(f"\nDone. {img_idx} images saved to: {OUT_DIR}")
132
+ print(f"Episodes completed: {episodes_done} avg reward: "
133
+ f"{total_reward/max(1,episodes_done):.1f}")
test_generate_images.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generate 100 screenshots from the traffic environment and save to test_images/.
3
+ Runs multiple episodes; each reset picks a new random start/end.
4
+ """
5
+
6
+ import base64
7
+ import os
8
+ import sys
9
+
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "server"))
11
+ sys.path.insert(0, os.path.dirname(__file__))
12
+
13
+ from server.road_traffic_simulator_env_environment import RoadTrafficSimulatorEnvironment
14
+ from models import RoadTrafficSimulatorAction
15
+
16
+ OUT_DIR = os.path.join(os.path.dirname(__file__), "test_images")
17
+ os.makedirs(OUT_DIR, exist_ok=True)
18
+
19
+ TARGET = 100
20
+
21
+ def save_png(b64: str, path: str):
22
+ with open(path, "wb") as f:
23
+ f.write(base64.b64decode(b64))
24
+
25
+ print("Initialising environment (fetching OSM data – first time only, ~10-30 s)…")
26
+ env = RoadTrafficSimulatorEnvironment()
27
+
28
+ img_idx = 0
29
+ episode = 0
30
+
31
+ while img_idx < TARGET:
32
+ episode += 1
33
+ obs = env.reset()
34
+
35
+ fname = os.path.join(OUT_DIR, f"img_{img_idx:03d}_ep{episode}_step0.png")
36
+ save_png(obs.map_screenshot, fname)
37
+ print(f"[{img_idx+1:3d}/{TARGET}] ep={episode} step=0 "
38
+ f"start=({obs.lat:.4f},{obs.lon:.4f}) "
39
+ f"goal=({obs.goal_lat:.4f},{obs.goal_lon:.4f}) "
40
+ f"dist={obs.distance_to_goal:.0f}m → {os.path.basename(fname)}")
41
+ img_idx += 1
42
+ if img_idx >= TARGET:
43
+ break
44
+
45
+ step = 0
46
+ done = False
47
+ while not done and img_idx < TARGET:
48
+ step += 1
49
+ # Cycle through edge choices to get varied paths
50
+ action_idx = (step - 1) % max(1, obs.available_actions)
51
+ obs = env.step(RoadTrafficSimulatorAction(next_edge_index=action_idx))
52
+ done = obs.done
53
+
54
+ fname = os.path.join(OUT_DIR, f"img_{img_idx:03d}_ep{episode}_step{step}.png")
55
+ save_png(obs.map_screenshot, fname)
56
+ print(f"[{img_idx+1:3d}/{TARGET}] ep={episode} step={step} "
57
+ f"pos=({obs.lat:.4f},{obs.lon:.4f}) "
58
+ f"dist={obs.distance_to_goal:.0f}m "
59
+ f"reward={obs.reward:+.2f} "
60
+ f"done={done} → {os.path.basename(fname)}")
61
+ img_idx += 1
62
+
63
+ print(f"\nDone. {img_idx} images saved to: {OUT_DIR}")
test_images/img_000_ep1_step0.png ADDED

Git LFS Details

  • SHA256: 2d9f7864546fa0f08126b421d73a9ad2d18ee0d0c18ee8245ddcbcbc372c5c4f
  • Pointer size: 131 Bytes
  • Size of remote file: 830 kB
test_images/img_001_ep1_step1.png ADDED

Git LFS Details

  • SHA256: 80a915d0defdb0ba0d288a01d585875fcfe8e000d3cfe75597ac177a72046211
  • Pointer size: 131 Bytes
  • Size of remote file: 838 kB
test_images/img_002_ep1_step2.png ADDED

Git LFS Details

  • SHA256: 7f8516b673bddf9996ee9d7e6ac373eda65227d45418981fdfcd4f230c4acf57
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_003_ep1_step3.png ADDED

Git LFS Details

  • SHA256: 1ab5f4e5380175cccdedb4e03c203bf27c51c018a3d62e7a8670cf34fa4f4b06
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_004_ep1_step4.png ADDED

Git LFS Details

  • SHA256: 26816f5875e68d4baaa870c9e0082396ab12022b4669e509e7e83085505c7850
  • Pointer size: 131 Bytes
  • Size of remote file: 844 kB
test_images/img_005_ep1_step5.png ADDED

Git LFS Details

  • SHA256: 31b21f8ac8e3aff1a5038e497ab7ff8403fdde07fd5335025e127ecccefba316
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_006_ep1_step6.png ADDED

Git LFS Details

  • SHA256: 78f4272874e270dd7bb4192109cf8a100b54a24276050822f96212e114f3be03
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_007_ep1_step7.png ADDED

Git LFS Details

  • SHA256: 5689c9a974940034d67a41b84431a3fa2bdeda054e67c99a1b681c8adf50c182
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_008_ep1_step8.png ADDED

Git LFS Details

  • SHA256: 6fe0285f343b27e2cc788de67b652b02f10cf4c4b3ce20b99538d5128269f643
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_009_ep1_step9.png ADDED

Git LFS Details

  • SHA256: e6a46ce8dbc8f63b73fb2635264f37ac69dd31a7946a89eaa7990e52e4dc6883
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_010_ep1_step10.png ADDED

Git LFS Details

  • SHA256: 1e845e00907d22083ca8fb707264686ac0db119237b80863097f9160a05f63b7
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_011_ep1_step11.png ADDED

Git LFS Details

  • SHA256: 2b88491c6f31579558ea65a1ab54068ce4dbf6771db1097cb1a8b18cd28aab13
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_012_ep1_step12.png ADDED

Git LFS Details

  • SHA256: 126a38802af1d8fe8c53208013691e619760d8bb5bd8d6485dbbd48785bc7d0f
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_013_ep1_step13.png ADDED

Git LFS Details

  • SHA256: b59a6b2c7ad081fd1f5f0b6780ac0949509ee8a247c6c7cecc4711738fd7cdf6
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_014_ep1_step14.png ADDED

Git LFS Details

  • SHA256: 7fc6274e49c4f3c68be5933a19b156ec3069fdf278688bf1a03edf2012e5414c
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_015_ep1_step15.png ADDED

Git LFS Details

  • SHA256: d9acca290b91a24590263540c29692f15d2637a4062f67554f9a0e5904ec64e5
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_016_ep1_step16.png ADDED

Git LFS Details

  • SHA256: bfe885df0b03bae8d6661045b1bbd905eba72b6d3da38badcf5d992154ccd79f
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_017_ep1_step17.png ADDED

Git LFS Details

  • SHA256: d378c2eef8bd5817b06aa6a9fd60376c6c8a7b9f6fd2f1745abda67c0cd36cff
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_018_ep1_step18.png ADDED

Git LFS Details

  • SHA256: e78dc1febd00e3e6c657cd279da4d660d9895fe7b98d55f1b6781650a841e8d9
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_019_ep1_step19.png ADDED

Git LFS Details

  • SHA256: 339aeda4b8169b487bddb25de8d514c179fab37e15bb0dd6709e7e88670d7354
  • Pointer size: 131 Bytes
  • Size of remote file: 843 kB
test_images/img_020_ep1_step20.png ADDED

Git LFS Details

  • SHA256: 2d489b63cae4f290ac4b386741950052a7f3ecc88c5880acd0feca81db537ed1
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_021_ep1_step21.png ADDED

Git LFS Details

  • SHA256: a2a0f024b1a90ac9fe6b31c9a3bc8a9cc80f02573058e499342765fff6006f1f
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_022_ep1_step22.png ADDED

Git LFS Details

  • SHA256: 7c5273862ec7f2968bea02e08a891ee725f16e22cf75ebd195101c11dcb0e5f0
  • Pointer size: 131 Bytes
  • Size of remote file: 841 kB
test_images/img_023_ep1_step23.png ADDED

Git LFS Details

  • SHA256: 670b159f9e009a6678b22d1f59250bfcea538ad2f94a5c028b5389b87ab2ed89
  • Pointer size: 131 Bytes
  • Size of remote file: 840 kB
test_images/img_024_ep1_step24.png ADDED

Git LFS Details

  • SHA256: cbb2ae1a373f6477de989132f3bd81514950b8b6e51a3a8dd67db0a08541e9d8
  • Pointer size: 131 Bytes
  • Size of remote file: 841 kB
test_images/img_025_ep1_step25.png ADDED

Git LFS Details

  • SHA256: 54cd4683118a2f1ae5be703241031ad9a74f90a1be502a64018a08af7e071763
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
test_images/img_026_ep1_step26.png ADDED

Git LFS Details

  • SHA256: 6928ea2eb0ece25bc33d26f0132bddaf3bb06441ec2ad04a870d5131578e0fe0
  • Pointer size: 131 Bytes
  • Size of remote file: 841 kB