TraffiSim 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
)
|