TraffiSim 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- TraffiSim-0.1.0.dist-info/LICENSE +185 -0
- TraffiSim-0.1.0.dist-info/METADATA +21 -0
- TraffiSim-0.1.0.dist-info/RECORD +13 -0
- TraffiSim-0.1.0.dist-info/WHEEL +5 -0
- TraffiSim-0.1.0.dist-info/entry_points.txt +2 -0
- TraffiSim-0.1.0.dist-info/top_level.txt +1 -0
- traffisim/__init__.py +0 -0
- traffisim/analyzer.py +355 -0
- traffisim/intersection.py +652 -0
- traffisim/models.py +296 -0
- traffisim/rendering.py +225 -0
- traffisim/run.py +173 -0
- traffisim/utils.py +50 -0
traffisim/models.py
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
"""
|
2
|
+
models.py
|
3
|
+
---------
|
4
|
+
Vehicle model and default vehicle parameters.
|
5
|
+
Implements IDM & MOBIL logic.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import random
|
9
|
+
import math
|
10
|
+
from .utils import dist, define_exit_point
|
11
|
+
|
12
|
+
# Default vehicle parameters
|
13
|
+
DEFAULT_VEHICLE_TYPES = {
|
14
|
+
'car': {
|
15
|
+
'desired_speed': 20,
|
16
|
+
'max_acceleration': 2.5,
|
17
|
+
'comfortable_deceleration': 2.5,
|
18
|
+
'minimum_gap': 2.5,
|
19
|
+
'lane_change_aggressiveness': 0.7,
|
20
|
+
'length': 4.5
|
21
|
+
},
|
22
|
+
'truck': {
|
23
|
+
'desired_speed': 15,
|
24
|
+
'max_acceleration': 1.5,
|
25
|
+
'comfortable_deceleration': 1.5,
|
26
|
+
'minimum_gap': 3.5,
|
27
|
+
'lane_change_aggressiveness': 0.5,
|
28
|
+
'length': 10.0
|
29
|
+
},
|
30
|
+
'bus': {
|
31
|
+
'desired_speed': 15,
|
32
|
+
'max_acceleration': 1.5,
|
33
|
+
'comfortable_deceleration': 1.5,
|
34
|
+
'minimum_gap': 4.0,
|
35
|
+
'lane_change_aggressiveness': 0.5,
|
36
|
+
'length': 12.0
|
37
|
+
},
|
38
|
+
'scooter': {
|
39
|
+
'desired_speed': 18,
|
40
|
+
'max_acceleration': 3.0,
|
41
|
+
'comfortable_deceleration': 3.0,
|
42
|
+
'minimum_gap': 1.2,
|
43
|
+
'lane_change_aggressiveness': 0.8,
|
44
|
+
'length': 2.0
|
45
|
+
},
|
46
|
+
'motorcycle': {
|
47
|
+
'desired_speed': 22,
|
48
|
+
'max_acceleration': 3.5,
|
49
|
+
'comfortable_deceleration': 3.5,
|
50
|
+
'minimum_gap': 1.0,
|
51
|
+
'lane_change_aggressiveness': 0.9,
|
52
|
+
'length': 2.2
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
# Colors for different vehicle types (for rendering usage)
|
57
|
+
VEHICLE_COLORS = {
|
58
|
+
'car': (200, 200, 0),
|
59
|
+
'truck': (180, 100, 50),
|
60
|
+
'bus': (120, 40, 150),
|
61
|
+
'scooter': (40, 220, 220),
|
62
|
+
'motorcycle': (255, 100, 100),
|
63
|
+
}
|
64
|
+
|
65
|
+
class Vehicle:
|
66
|
+
"""
|
67
|
+
Represents a single vehicle, using IDM for longitudinal and MOBIL for lane changing.
|
68
|
+
Also includes India Mode logic for side-by-side queue placement (if enabled).
|
69
|
+
"""
|
70
|
+
def __init__(self, arrival_time, direction, vehicle_type=None,
|
71
|
+
simulate_full_route=True,
|
72
|
+
SCREEN_WIDTH=1000, SCREEN_HEIGHT=800):
|
73
|
+
self.arrival_time = arrival_time
|
74
|
+
self.direction = direction # 'N', 'S', 'E', 'W'
|
75
|
+
|
76
|
+
# Random turn choice
|
77
|
+
self.turn_direction = random.choices(
|
78
|
+
['left', 'straight', 'right'],
|
79
|
+
weights=[0.34, 0.33, 0.33]
|
80
|
+
)[0]
|
81
|
+
|
82
|
+
# Vehicle type logic
|
83
|
+
if vehicle_type is None:
|
84
|
+
vehicle_type = random.choice(list(DEFAULT_VEHICLE_TYPES.keys()))
|
85
|
+
self.vehicle_type = vehicle_type
|
86
|
+
params = DEFAULT_VEHICLE_TYPES[self.vehicle_type]
|
87
|
+
|
88
|
+
# Basic physical parameters
|
89
|
+
self.length = params['length']
|
90
|
+
self.lane_change_aggressiveness = params['lane_change_aggressiveness']
|
91
|
+
|
92
|
+
self.state = 'queueing' # 'queueing', 'crossing', 'finished'
|
93
|
+
self.start_cross_time = None
|
94
|
+
self.finish_time = None
|
95
|
+
|
96
|
+
# Position & movement
|
97
|
+
self.x = 0.0
|
98
|
+
self.y = 0.0
|
99
|
+
self.current_speed = 0.0
|
100
|
+
self.distance_covered = 0.0
|
101
|
+
|
102
|
+
# Route details
|
103
|
+
self.start_position = None
|
104
|
+
self.center_position = None
|
105
|
+
self.exit_position = None
|
106
|
+
self.route_distance = 0.0
|
107
|
+
self.simulate_full_route = simulate_full_route
|
108
|
+
self.SCREEN_WIDTH = SCREEN_WIDTH
|
109
|
+
self.SCREEN_HEIGHT = SCREEN_HEIGHT
|
110
|
+
|
111
|
+
# Reaction time
|
112
|
+
self.reaction_time = random.uniform(0.8, 1.5)
|
113
|
+
|
114
|
+
# IDM parameters
|
115
|
+
self.idm_a0 = 2.0
|
116
|
+
self.idm_b = 2.5
|
117
|
+
self.idm_v0 = params['desired_speed']
|
118
|
+
self.idm_T = 1.5
|
119
|
+
self.idm_s0 = 2.0
|
120
|
+
self.idm_delta = 4.0
|
121
|
+
|
122
|
+
# Politeness factor for MOBIL
|
123
|
+
self.politeness = 0.2
|
124
|
+
self.delta_a_threshold = 0.2
|
125
|
+
|
126
|
+
# Additional indexing for queue positioning
|
127
|
+
self.lane_index = None
|
128
|
+
self.row_index = None
|
129
|
+
self.col_index = None
|
130
|
+
|
131
|
+
# Assign a driver profile to add variety
|
132
|
+
self.assign_driver_profile()
|
133
|
+
|
134
|
+
@property
|
135
|
+
def wait_time(self):
|
136
|
+
if self.start_cross_time is None:
|
137
|
+
return None
|
138
|
+
return self.start_cross_time - self.arrival_time
|
139
|
+
|
140
|
+
def assign_driver_profile(self):
|
141
|
+
"""
|
142
|
+
Randomly assigns an 'aggressive', 'normal', or 'cautious' profile
|
143
|
+
to create variability in driving style.
|
144
|
+
"""
|
145
|
+
profile_type = random.choices(
|
146
|
+
["aggressive", "normal", "cautious"],
|
147
|
+
weights=[0.3, 0.5, 0.2]
|
148
|
+
)[0]
|
149
|
+
|
150
|
+
if profile_type == "aggressive":
|
151
|
+
self.idm_a0 = random.uniform(2.5, 3.0)
|
152
|
+
self.idm_b = 2.0
|
153
|
+
self.idm_T = 1.0
|
154
|
+
self.idm_v0 *= 1.3 # bump up desired speed ~30% for effect
|
155
|
+
self.politeness = 0.1
|
156
|
+
self.reaction_time = random.uniform(0.6, 1.0)
|
157
|
+
elif profile_type == "cautious":
|
158
|
+
self.idm_a0 = random.uniform(1.0, 1.5)
|
159
|
+
self.idm_b = 2.5
|
160
|
+
self.idm_T = 1.8
|
161
|
+
self.idm_v0 *= 0.9 # reduce desired speed
|
162
|
+
self.politeness = 0.3
|
163
|
+
self.reaction_time = random.uniform(1.5, 2.0)
|
164
|
+
else:
|
165
|
+
# 'normal' -> keep defaults
|
166
|
+
pass
|
167
|
+
|
168
|
+
def init_route(self, cx, cy):
|
169
|
+
"""
|
170
|
+
Called once vehicle starts crossing. We define start/center/exit, compute route distance.
|
171
|
+
"""
|
172
|
+
self.start_position = (self.x, self.y)
|
173
|
+
self.center_position = (cx, cy)
|
174
|
+
if self.simulate_full_route:
|
175
|
+
# Real exit point
|
176
|
+
self.exit_position = define_exit_point(
|
177
|
+
cx, cy, self.direction, self.turn_direction,
|
178
|
+
self.SCREEN_WIDTH, self.SCREEN_HEIGHT
|
179
|
+
)
|
180
|
+
d1 = dist(self.start_position, self.center_position)
|
181
|
+
d2 = dist(self.center_position, self.exit_position)
|
182
|
+
self.route_distance = d1 + d2
|
183
|
+
else:
|
184
|
+
# Vanish at center
|
185
|
+
self.exit_position = self.center_position
|
186
|
+
self.route_distance = dist(self.start_position, self.center_position)
|
187
|
+
|
188
|
+
self.distance_covered = 0.0
|
189
|
+
|
190
|
+
def compute_idm_acceleration(self, lead_vehicle):
|
191
|
+
"""
|
192
|
+
Calculate acceleration using IDM, given the lead vehicle in the same lane (if any).
|
193
|
+
"""
|
194
|
+
if lead_vehicle is None:
|
195
|
+
s = 1e9
|
196
|
+
delta_v = 0.0
|
197
|
+
else:
|
198
|
+
# gap = distance between centers - half lengths
|
199
|
+
s = dist((self.x, self.y), (lead_vehicle.x, lead_vehicle.y)) \
|
200
|
+
- 0.5*self.length - 0.5*lead_vehicle.length
|
201
|
+
delta_v = self.current_speed - lead_vehicle.current_speed
|
202
|
+
if s < 0.1:
|
203
|
+
s = 0.1
|
204
|
+
|
205
|
+
s_star = self.idm_s0 + max(
|
206
|
+
0, self.current_speed * self.idm_T +
|
207
|
+
(self.current_speed*delta_v)/(2*math.sqrt(self.idm_a0*self.idm_b))
|
208
|
+
)
|
209
|
+
|
210
|
+
alpha_free = 1 - pow((self.current_speed / self.idm_v0), self.idm_delta)
|
211
|
+
alpha_int = - pow((s_star / s), 2)
|
212
|
+
a = self.idm_a0 * (alpha_free + alpha_int)
|
213
|
+
return a
|
214
|
+
|
215
|
+
def check_mobil_lane_change(self, sim, current_lane, target_lane,
|
216
|
+
lead_current, lead_target,
|
217
|
+
rear_current, rear_target):
|
218
|
+
"""
|
219
|
+
Decide if lane change is beneficial by MOBIL:
|
220
|
+
(a_new_self - a_old_self) + p * (a_new_rear - a_old_rear) > delta_a_threshold
|
221
|
+
"""
|
222
|
+
a_old_self = self.compute_idm_acceleration(lead_current)
|
223
|
+
a_new_self = self.compute_idm_acceleration(lead_target)
|
224
|
+
|
225
|
+
a_old_rear, a_new_rear = 0.0, 0.0
|
226
|
+
if rear_current:
|
227
|
+
lead_for_rear_current = sim.get_lead_vehicle_in_lane(current_lane, rear_current)
|
228
|
+
a_old_rear = rear_current.compute_idm_acceleration(lead_for_rear_current)
|
229
|
+
if rear_target:
|
230
|
+
lead_for_rear_target = sim.get_lead_vehicle_in_lane(target_lane, rear_target)
|
231
|
+
a_new_rear = rear_target.compute_idm_acceleration(lead_for_rear_target)
|
232
|
+
|
233
|
+
lhs = (a_new_self - a_old_self) + self.politeness * (a_new_rear - a_old_rear)
|
234
|
+
if lhs > self.delta_a_threshold:
|
235
|
+
# check safety
|
236
|
+
if self.is_safe_to_change_lane(lead_target, rear_target):
|
237
|
+
return True
|
238
|
+
return False
|
239
|
+
|
240
|
+
def is_safe_to_change_lane(self, lead_vehicle, rear_vehicle):
|
241
|
+
"""
|
242
|
+
Very simple check: ensure gap front and gap rear are > 3.0m (adjust as desired).
|
243
|
+
"""
|
244
|
+
safe_gap = 3.0
|
245
|
+
if lead_vehicle:
|
246
|
+
gap_front = dist((self.x, self.y), (lead_vehicle.x, lead_vehicle.y)) \
|
247
|
+
- 0.5*self.length - 0.5*lead_vehicle.length
|
248
|
+
if gap_front < safe_gap:
|
249
|
+
return False
|
250
|
+
if rear_vehicle:
|
251
|
+
gap_rear = dist((self.x, self.y), (rear_vehicle.x, rear_vehicle.y)) \
|
252
|
+
- 0.5*self.length - 0.5*rear_vehicle.length
|
253
|
+
if gap_rear < safe_gap:
|
254
|
+
return False
|
255
|
+
return True
|
256
|
+
|
257
|
+
def update_position(self, dt=1.0, lead_vehicle=None):
|
258
|
+
"""
|
259
|
+
Update speed via IDM acceleration and move the vehicle along its route.
|
260
|
+
Returns True if vehicle has completed its route.
|
261
|
+
"""
|
262
|
+
a = self.compute_idm_acceleration(lead_vehicle)
|
263
|
+
self.current_speed += a * dt
|
264
|
+
if self.current_speed < 0.0:
|
265
|
+
self.current_speed = 0.0
|
266
|
+
|
267
|
+
dist_step = self.current_speed * dt
|
268
|
+
self.distance_covered += dist_step
|
269
|
+
|
270
|
+
if self.route_distance < 0.1:
|
271
|
+
return True # edge case if start == center
|
272
|
+
|
273
|
+
frac = self.distance_covered / self.route_distance
|
274
|
+
if frac >= 1.0:
|
275
|
+
frac = 1.0
|
276
|
+
|
277
|
+
d1 = dist(self.start_position, self.center_position)
|
278
|
+
route_total = self.route_distance
|
279
|
+
if frac < d1 / route_total:
|
280
|
+
# in the segment start->center
|
281
|
+
sub_frac = frac / (d1 / route_total)
|
282
|
+
sx, sy = self.start_position
|
283
|
+
cx, cy = self.center_position
|
284
|
+
self.x = sx + (cx - sx)*sub_frac
|
285
|
+
self.y = sy + (cy - sy)*sub_frac
|
286
|
+
else:
|
287
|
+
# in the center->exit segment
|
288
|
+
ratio_remaining = frac - (d1/route_total)
|
289
|
+
segment_len = 1.0 - (d1/route_total)
|
290
|
+
sub_frac = ratio_remaining / segment_len if segment_len > 1e-6 else 1.0
|
291
|
+
cx, cy = self.center_position
|
292
|
+
ex, ey = self.exit_position
|
293
|
+
self.x = cx + (ex - cx)*sub_frac
|
294
|
+
self.y = cy + (ey - cy)*sub_frac
|
295
|
+
|
296
|
+
return (frac >= 1.0)
|
traffisim/rendering.py
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
"""
|
2
|
+
rendering.py
|
3
|
+
------------
|
4
|
+
Optional Pygame renderer for TraffiSim.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import pygame
|
8
|
+
from .models import VEHICLE_COLORS
|
9
|
+
from .intersection import SCREEN_WIDTH, SCREEN_HEIGHT, LANE_WIDTH, ROAD_WIDTH, ROAD_MARGIN
|
10
|
+
from .intersection import QUEUE_OFFSET, CAR_SPACING
|
11
|
+
|
12
|
+
class TrafficRenderer:
|
13
|
+
"""
|
14
|
+
Class responsible for rendering the traffic simulation using Pygame.
|
15
|
+
"""
|
16
|
+
COLOR_BG = (30, 30, 30)
|
17
|
+
COLOR_ROAD = (70, 70, 70)
|
18
|
+
COLOR_TEXT = (255, 255, 255)
|
19
|
+
LINE_COLOR = (140, 140, 140)
|
20
|
+
|
21
|
+
TRAFFIC_LIGHT_COLORS = {
|
22
|
+
"red": (255, 40, 40),
|
23
|
+
"yellow": (255, 255, 0),
|
24
|
+
"green": ( 40, 255, 40),
|
25
|
+
}
|
26
|
+
|
27
|
+
def __init__(self, sim):
|
28
|
+
"""
|
29
|
+
Initialize the TrafficRenderer with a simulation instance.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
sim (IntersectionSim): The simulation instance to render.
|
33
|
+
"""
|
34
|
+
self.sim = sim
|
35
|
+
pygame.init()
|
36
|
+
pygame.display.set_caption(f"TraffiSim (type={sim.junction_type}, MLights={sim.multiple_lights})")
|
37
|
+
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
38
|
+
self.clock = pygame.time.Clock()
|
39
|
+
self.font = pygame.font.SysFont(None, 22)
|
40
|
+
|
41
|
+
self.cx = SCREEN_WIDTH // 2
|
42
|
+
self.cy = SCREEN_HEIGHT // 2
|
43
|
+
|
44
|
+
def render(self):
|
45
|
+
"""
|
46
|
+
Render the current state of the simulation.
|
47
|
+
"""
|
48
|
+
self.screen.fill(self.COLOR_BG)
|
49
|
+
self.draw_roads()
|
50
|
+
self.draw_lane_barriers()
|
51
|
+
self.draw_traffic_lights()
|
52
|
+
self.draw_vehicles()
|
53
|
+
self.draw_ui_panel()
|
54
|
+
pygame.display.flip()
|
55
|
+
|
56
|
+
def draw_roads(self):
|
57
|
+
"""
|
58
|
+
Draw the intersection roads as rectangles.
|
59
|
+
"""
|
60
|
+
if self.sim.junction_type == '4way':
|
61
|
+
rect_h = pygame.Rect(0, self.cy - ROAD_WIDTH//2, SCREEN_WIDTH, ROAD_WIDTH)
|
62
|
+
pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_h)
|
63
|
+
rect_v = pygame.Rect(self.cx - ROAD_WIDTH//2, 0, ROAD_WIDTH, SCREEN_HEIGHT)
|
64
|
+
pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_v)
|
65
|
+
else:
|
66
|
+
rect_h = pygame.Rect(0, self.cy - ROAD_WIDTH//2, SCREEN_WIDTH, ROAD_WIDTH)
|
67
|
+
pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_h)
|
68
|
+
rect_v = pygame.Rect(self.cx - ROAD_WIDTH//2, 0, ROAD_WIDTH, self.cy + ROAD_WIDTH//2)
|
69
|
+
pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_v)
|
70
|
+
|
71
|
+
def draw_lane_barriers(self):
|
72
|
+
"""
|
73
|
+
Draw dashed lane lines.
|
74
|
+
"""
|
75
|
+
dash_len = 10
|
76
|
+
gap_len = 6
|
77
|
+
|
78
|
+
top_road = self.cy - (ROAD_WIDTH//2)
|
79
|
+
bottom_road = self.cy + (ROAD_WIDTH//2)
|
80
|
+
left_road = self.cx - (ROAD_WIDTH//2)
|
81
|
+
right_road = self.cx + (ROAD_WIDTH//2)
|
82
|
+
|
83
|
+
lane_count_est = int(ROAD_WIDTH / LANE_WIDTH)
|
84
|
+
for i in range(1, lane_count_est):
|
85
|
+
x_line = left_road + i*LANE_WIDTH
|
86
|
+
y = 0
|
87
|
+
while y < SCREEN_HEIGHT:
|
88
|
+
pygame.draw.line(self.screen, self.LINE_COLOR,
|
89
|
+
(x_line, y), (x_line, min(y+dash_len, SCREEN_HEIGHT)), 2)
|
90
|
+
y += dash_len + gap_len
|
91
|
+
|
92
|
+
lane_count_est_v = int(ROAD_WIDTH / LANE_WIDTH)
|
93
|
+
for i in range(1, lane_count_est_v):
|
94
|
+
y_line = top_road + i*LANE_WIDTH
|
95
|
+
x = 0
|
96
|
+
while x < SCREEN_WIDTH:
|
97
|
+
pygame.draw.line(self.screen, self.LINE_COLOR,
|
98
|
+
(x, y_line), (min(x+dash_len, SCREEN_WIDTH), y_line), 2)
|
99
|
+
x += dash_len + gap_len
|
100
|
+
|
101
|
+
def draw_traffic_lights(self):
|
102
|
+
"""
|
103
|
+
Draw traffic lights at the intersection.
|
104
|
+
"""
|
105
|
+
offsets = {
|
106
|
+
'N': (0, -60),
|
107
|
+
'S': (0, 60),
|
108
|
+
'E': (60, 0),
|
109
|
+
'W': (-60, 0)
|
110
|
+
}
|
111
|
+
r = 9
|
112
|
+
for d in self.sim.directions:
|
113
|
+
if self.sim.adaptive_signals:
|
114
|
+
if self.sim.current_green == d and self.sim.signal_state == "green":
|
115
|
+
st = "green"
|
116
|
+
elif self.sim.current_green == d and self.sim.signal_state == "yellow":
|
117
|
+
st = "yellow"
|
118
|
+
else:
|
119
|
+
st = "red"
|
120
|
+
else:
|
121
|
+
st = self.sim.get_signal_state(d, self.sim.sim_time)
|
122
|
+
|
123
|
+
color = self.TRAFFIC_LIGHT_COLORS[st]
|
124
|
+
ox, oy = offsets.get(d, (0,0))
|
125
|
+
cx = self.cx + ox
|
126
|
+
cy = self.cy + oy
|
127
|
+
|
128
|
+
pygame.draw.circle(self.screen, (0,0,0), (cx, cy), r+2)
|
129
|
+
pygame.draw.circle(self.screen, color, (cx, cy), r)
|
130
|
+
|
131
|
+
def draw_vehicles(self):
|
132
|
+
"""
|
133
|
+
Draw all vehicles in the simulation.
|
134
|
+
"""
|
135
|
+
for d in self.sim.directions:
|
136
|
+
if self.sim.multiple_lanes:
|
137
|
+
for lane in self.sim.queues[d]:
|
138
|
+
for v in lane:
|
139
|
+
self.draw_single_vehicle(v)
|
140
|
+
else:
|
141
|
+
for v in self.sim.queues[d]:
|
142
|
+
self.draw_single_vehicle(v)
|
143
|
+
for v in self.sim.crossing_vehicles:
|
144
|
+
self.draw_single_vehicle(v)
|
145
|
+
|
146
|
+
def draw_single_vehicle(self, v):
|
147
|
+
"""
|
148
|
+
Draw a single vehicle.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
v (Vehicle): The vehicle to draw.
|
152
|
+
"""
|
153
|
+
color = VEHICLE_COLORS.get(v.vehicle_type, (200,200,200))
|
154
|
+
shadow_color = (color[0]//4, color[1]//4, color[2]//4)
|
155
|
+
|
156
|
+
if v.vehicle_type in ['car', 'truck', 'bus']:
|
157
|
+
self.draw_rect_vehicle(v, color, shadow_color)
|
158
|
+
else:
|
159
|
+
self.draw_circle_vehicle(v, color, shadow_color)
|
160
|
+
|
161
|
+
def draw_rect_vehicle(self, v, color, shadow):
|
162
|
+
"""
|
163
|
+
Draw a rectangular vehicle (car, truck, bus).
|
164
|
+
|
165
|
+
Args:
|
166
|
+
v (Vehicle): The vehicle to draw.
|
167
|
+
color (tuple): The color of the vehicle.
|
168
|
+
shadow (tuple): The shadow color of the vehicle.
|
169
|
+
"""
|
170
|
+
if v.vehicle_type == 'car':
|
171
|
+
w, h = 12, 20
|
172
|
+
elif v.vehicle_type == 'truck':
|
173
|
+
w, h = 14, 35
|
174
|
+
else:
|
175
|
+
w, h = 14, 40 # bus
|
176
|
+
|
177
|
+
rect = pygame.Rect(0, 0, w, h)
|
178
|
+
rect.center = (int(v.x), int(v.y))
|
179
|
+
shadow_rect = rect.copy()
|
180
|
+
shadow_rect.x += 2
|
181
|
+
shadow_rect.y += 2
|
182
|
+
|
183
|
+
pygame.draw.rect(self.screen, shadow, shadow_rect, border_radius=3)
|
184
|
+
pygame.draw.rect(self.screen, color, rect, border_radius=3)
|
185
|
+
|
186
|
+
def draw_circle_vehicle(self, v, color, shadow):
|
187
|
+
"""
|
188
|
+
Draw a circular vehicle (scooter, motorcycle).
|
189
|
+
|
190
|
+
Args:
|
191
|
+
v (Vehicle): The vehicle to draw.
|
192
|
+
color (tuple): The color of the vehicle.
|
193
|
+
shadow (tuple): The shadow color of the vehicle.
|
194
|
+
"""
|
195
|
+
r = 5 if v.vehicle_type=='scooter' else 6
|
196
|
+
center = (int(v.x), int(v.y))
|
197
|
+
shadow_c = (center[0]+2, center[1]+2)
|
198
|
+
|
199
|
+
pygame.draw.circle(self.screen, shadow, shadow_c, r)
|
200
|
+
pygame.draw.circle(self.screen, color, center, r)
|
201
|
+
|
202
|
+
def draw_ui_panel(self):
|
203
|
+
"""
|
204
|
+
Draw the UI panel with simulation information.
|
205
|
+
"""
|
206
|
+
if self.sim.adaptive_signals:
|
207
|
+
sig_text = f"Signal: {self.sim.signal_state.upper()} for {self.sim.current_green} (timer: {self.sim.state_timer})"
|
208
|
+
else:
|
209
|
+
sig_text = ""
|
210
|
+
lines = [
|
211
|
+
f"Time= {self.sim.sim_time}/{self.sim.total_time}",
|
212
|
+
f"Speed= {self.sim.simulation_speed} steps/sec",
|
213
|
+
f"JunctionType= {self.sim.junction_type}, MultiLights= {self.sim.multiple_lights}",
|
214
|
+
f"MultiLanes= {self.sim.multiple_lanes}, LaneCount= {self.sim.lane_count}",
|
215
|
+
f"IndiaMode= {self.sim.india_mode}",
|
216
|
+
f"SimulateFullRoute= {self.sim.simulate_full_route}",
|
217
|
+
sig_text,
|
218
|
+
"Close window or press Ctrl+C to quit."
|
219
|
+
]
|
220
|
+
x = 10
|
221
|
+
y = 10
|
222
|
+
for txt in lines:
|
223
|
+
surf = self.font.render(txt, True, (255,255,255))
|
224
|
+
self.screen.blit(surf, (x, y))
|
225
|
+
y += 20
|
traffisim/run.py
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
"""
|
2
|
+
run.py
|
3
|
+
------
|
4
|
+
Functions to run multiple simulations, collect data, and optionally
|
5
|
+
provide a CLI entry point.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import csv
|
10
|
+
|
11
|
+
from .intersection import IntersectionSim, ARRIVAL_RATES
|
12
|
+
from .models import DEFAULT_VEHICLE_TYPES
|
13
|
+
from .rendering import TrafficRenderer
|
14
|
+
|
15
|
+
def run_multiple_simulations(
|
16
|
+
N_runs=1,
|
17
|
+
csv_filename="simulation_results.csv",
|
18
|
+
junction_type="4way",
|
19
|
+
multiple_lights=False,
|
20
|
+
total_time=300,
|
21
|
+
simulation_speed=30,
|
22
|
+
save_to_files=True,
|
23
|
+
output_folder="simulation_outputs",
|
24
|
+
multiple_lanes=False,
|
25
|
+
lane_count=2,
|
26
|
+
yellow_duration=5,
|
27
|
+
all_red_duration=2,
|
28
|
+
vehicle_distribution=None,
|
29
|
+
india_mode=False,
|
30
|
+
show_visuals=True,
|
31
|
+
simulate_full_route=True,
|
32
|
+
adaptive_signals=True
|
33
|
+
):
|
34
|
+
directions = ['N','E','S','W'] if junction_type=='4way' else ['N','E','W']
|
35
|
+
|
36
|
+
summary_fieldnames = [
|
37
|
+
"SimulationRun",
|
38
|
+
"JunctionType",
|
39
|
+
"MultipleLights",
|
40
|
+
"MultipleLanes",
|
41
|
+
"LaneCount",
|
42
|
+
"TotalVehiclesProcessed",
|
43
|
+
"OverallAvgWait",
|
44
|
+
"SimulateFullRoute",
|
45
|
+
]
|
46
|
+
for d in directions:
|
47
|
+
summary_fieldnames.append(f"RedEmpty{d}")
|
48
|
+
for vt in DEFAULT_VEHICLE_TYPES.keys():
|
49
|
+
summary_fieldnames.append(f"Count{vt.capitalize()}")
|
50
|
+
|
51
|
+
if save_to_files:
|
52
|
+
os.makedirs(output_folder, exist_ok=True)
|
53
|
+
summary_csv_path = os.path.join(output_folder, csv_filename) if save_to_files else None
|
54
|
+
file_exists = (save_to_files and os.path.isfile(summary_csv_path))
|
55
|
+
|
56
|
+
all_results = []
|
57
|
+
for run_idx in range(1, N_runs+1):
|
58
|
+
print(f"\n=== Starting Simulation Run {run_idx}/{N_runs} ===\n")
|
59
|
+
renderer_class = TrafficRenderer if show_visuals else None
|
60
|
+
|
61
|
+
sim = IntersectionSim(
|
62
|
+
junction_type=junction_type,
|
63
|
+
multiple_lights=multiple_lights,
|
64
|
+
total_time=total_time,
|
65
|
+
simulation_speed=simulation_speed,
|
66
|
+
multiple_lanes=multiple_lanes,
|
67
|
+
lane_count=lane_count,
|
68
|
+
yellow_duration=yellow_duration,
|
69
|
+
all_red_duration=all_red_duration,
|
70
|
+
vehicle_distribution=vehicle_distribution,
|
71
|
+
india_mode=india_mode,
|
72
|
+
show_visuals=show_visuals,
|
73
|
+
renderer_class=renderer_class,
|
74
|
+
simulate_full_route=simulate_full_route,
|
75
|
+
adaptive_signals=adaptive_signals
|
76
|
+
)
|
77
|
+
sim.run()
|
78
|
+
sim.print_statistics()
|
79
|
+
|
80
|
+
if save_to_files:
|
81
|
+
run_csv = os.path.join(output_folder, f"run_{run_idx}.csv")
|
82
|
+
if sim.per_timestep_data:
|
83
|
+
fields = list(sim.per_timestep_data[0].keys())
|
84
|
+
else:
|
85
|
+
fields = ["TimeStep"]
|
86
|
+
|
87
|
+
with open(run_csv, 'w', newline='') as f:
|
88
|
+
writer = csv.DictWriter(f, fieldnames=fields)
|
89
|
+
writer.writeheader()
|
90
|
+
for row in sim.per_timestep_data:
|
91
|
+
writer.writerow(row)
|
92
|
+
|
93
|
+
summary_row = sim.get_results_dict()
|
94
|
+
summary_row["SimulationRun"] = run_idx
|
95
|
+
with open(summary_csv_path, 'a', newline='') as f:
|
96
|
+
writer = csv.DictWriter(f, fieldnames=summary_fieldnames)
|
97
|
+
if (not file_exists) and run_idx == 1:
|
98
|
+
writer.writeheader()
|
99
|
+
writer.writerow(summary_row)
|
100
|
+
file_exists = True
|
101
|
+
|
102
|
+
all_results.append(sim.get_results_dict())
|
103
|
+
|
104
|
+
if save_to_files and len(all_results) > 0:
|
105
|
+
avg_row = compute_average_row(all_results, directions)
|
106
|
+
avg_row["SimulationRun"] = "Average"
|
107
|
+
with open(summary_csv_path, 'a', newline='') as f:
|
108
|
+
writer = csv.DictWriter(f, fieldnames=summary_fieldnames)
|
109
|
+
writer.writerow(avg_row)
|
110
|
+
print("Average row appended to summary CSV.")
|
111
|
+
|
112
|
+
def compute_average_row(all_results, directions):
|
113
|
+
n = len(all_results)
|
114
|
+
if n == 0:
|
115
|
+
return {}
|
116
|
+
sum_tvp = 0
|
117
|
+
sum_wait = 0
|
118
|
+
sum_red = {d: 0 for d in directions}
|
119
|
+
sum_counts = {vt: 0 for vt in DEFAULT_VEHICLE_TYPES.keys()}
|
120
|
+
|
121
|
+
# We'll copy some fields from the last result (assuming they are all same config).
|
122
|
+
jt = all_results[-1]["JunctionType"]
|
123
|
+
ml = all_results[-1]["MultipleLights"]
|
124
|
+
mls = all_results[-1]["MultipleLanes"]
|
125
|
+
lc = all_results[-1]["LaneCount"]
|
126
|
+
sfr = all_results[-1]["SimulateFullRoute"]
|
127
|
+
|
128
|
+
for r in all_results:
|
129
|
+
sum_tvp += r["TotalVehiclesProcessed"]
|
130
|
+
sum_wait += r["OverallAvgWait"]
|
131
|
+
for d in directions:
|
132
|
+
sum_red[d] += r[f"RedEmpty{d}"]
|
133
|
+
for vt in DEFAULT_VEHICLE_TYPES.keys():
|
134
|
+
key = "Count" + vt.capitalize()
|
135
|
+
sum_counts[vt] += r[key]
|
136
|
+
|
137
|
+
avg_row = {
|
138
|
+
"JunctionType": jt,
|
139
|
+
"MultipleLights": ml,
|
140
|
+
"MultipleLanes": mls,
|
141
|
+
"LaneCount": lc,
|
142
|
+
"SimulateFullRoute": sfr,
|
143
|
+
"TotalVehiclesProcessed": sum_tvp / n,
|
144
|
+
"OverallAvgWait": sum_wait / n
|
145
|
+
}
|
146
|
+
for d in directions:
|
147
|
+
avg_row[f"RedEmpty{d}"] = sum_red[d] / n
|
148
|
+
for vt in DEFAULT_VEHICLE_TYPES.keys():
|
149
|
+
key = "Count" + vt.capitalize()
|
150
|
+
avg_row[key] = sum_counts[vt] / n
|
151
|
+
return avg_row
|
152
|
+
|
153
|
+
def main_cli():
|
154
|
+
"""
|
155
|
+
Very minimal CLI for demonstration.
|
156
|
+
You can expand with argparse to parse arguments from the command line.
|
157
|
+
"""
|
158
|
+
print("Running a default simulation from CLI for demonstration...")
|
159
|
+
run_multiple_simulations(
|
160
|
+
N_runs=1,
|
161
|
+
csv_filename="cli_results.csv",
|
162
|
+
junction_type="4way",
|
163
|
+
multiple_lights=False,
|
164
|
+
total_time=300,
|
165
|
+
simulation_speed=10,
|
166
|
+
save_to_files=False,
|
167
|
+
multiple_lanes=True,
|
168
|
+
lane_count=3,
|
169
|
+
india_mode=False,
|
170
|
+
show_visuals=False,
|
171
|
+
simulate_full_route=True,
|
172
|
+
adaptive_signals=True
|
173
|
+
)
|