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
@@ -0,0 +1,652 @@
|
|
1
|
+
"""
|
2
|
+
intersection.py
|
3
|
+
---------------
|
4
|
+
Core simulation logic in the IntersectionSim class.
|
5
|
+
Manages queueing, arrivals, signals, vehicle movements, and statistics.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import random
|
9
|
+
import math
|
10
|
+
import csv
|
11
|
+
import os
|
12
|
+
import pygame
|
13
|
+
|
14
|
+
from .models import Vehicle, DEFAULT_VEHICLE_TYPES, VEHICLE_COLORS
|
15
|
+
from .utils import dist
|
16
|
+
|
17
|
+
LEFT_TURN_OPEN_PROB = 0.5
|
18
|
+
|
19
|
+
# Default arrival rates (vehicles/sec)
|
20
|
+
ARRIVAL_RATES = {
|
21
|
+
'N': 0.35,
|
22
|
+
'E': 0.25,
|
23
|
+
'S': 0.25,
|
24
|
+
'W': 0.20
|
25
|
+
}
|
26
|
+
|
27
|
+
# Some default constants for screen size, lane widths, etc.
|
28
|
+
SCREEN_WIDTH = 1000
|
29
|
+
SCREEN_HEIGHT = 800
|
30
|
+
LANE_WIDTH = 30
|
31
|
+
ROAD_MARGIN = 10
|
32
|
+
ROAD_WIDTH = LANE_WIDTH * 4 + ROAD_MARGIN * 2
|
33
|
+
QUEUE_OFFSET = 200
|
34
|
+
CAR_SPACING = 25
|
35
|
+
|
36
|
+
class IntersectionSim:
|
37
|
+
"""
|
38
|
+
Manages the overall intersection simulation.
|
39
|
+
Coordinates vehicle arrivals, traffic signals (adaptive or fixed),
|
40
|
+
queues, and data recording.
|
41
|
+
"""
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
junction_type="4way",
|
45
|
+
multiple_lights=False,
|
46
|
+
total_time=300,
|
47
|
+
sim_steps_per_sec=10,
|
48
|
+
arrival_rates=None,
|
49
|
+
print_interval=60,
|
50
|
+
min_queue_empty=0,
|
51
|
+
simulation_speed=30,
|
52
|
+
multiple_lanes=False,
|
53
|
+
lane_count=2,
|
54
|
+
yellow_duration=5,
|
55
|
+
all_red_duration=2,
|
56
|
+
vehicle_distribution=None,
|
57
|
+
india_mode=False,
|
58
|
+
show_visuals=True,
|
59
|
+
renderer_class=None,
|
60
|
+
simulate_full_route=True,
|
61
|
+
adaptive_signals=True
|
62
|
+
):
|
63
|
+
if arrival_rates is None:
|
64
|
+
arrival_rates = ARRIVAL_RATES
|
65
|
+
|
66
|
+
self.junction_type = junction_type
|
67
|
+
self.multiple_lights = multiple_lights
|
68
|
+
self.total_time = total_time
|
69
|
+
self.sim_steps_per_sec = sim_steps_per_sec
|
70
|
+
self.print_interval = print_interval
|
71
|
+
self.min_queue_empty = min_queue_empty
|
72
|
+
self.simulation_speed = simulation_speed
|
73
|
+
self.multiple_lanes = multiple_lanes
|
74
|
+
self.lane_count = lane_count
|
75
|
+
self.yellow_duration = yellow_duration
|
76
|
+
self.all_red_duration = all_red_duration
|
77
|
+
self.india_mode = india_mode
|
78
|
+
self.show_visuals = show_visuals
|
79
|
+
self.simulate_full_route = simulate_full_route
|
80
|
+
self.adaptive_signals = adaptive_signals
|
81
|
+
|
82
|
+
self.SCREEN_WIDTH = SCREEN_WIDTH
|
83
|
+
self.SCREEN_HEIGHT = SCREEN_HEIGHT
|
84
|
+
|
85
|
+
# Vehicle distribution
|
86
|
+
if vehicle_distribution is None:
|
87
|
+
self.vehicle_distribution = {
|
88
|
+
'car': 0.5,
|
89
|
+
'scooter': 0.3,
|
90
|
+
'motorcycle': 0.1,
|
91
|
+
'truck': 0.05,
|
92
|
+
'bus': 0.05
|
93
|
+
}
|
94
|
+
else:
|
95
|
+
self.vehicle_distribution = vehicle_distribution
|
96
|
+
|
97
|
+
self.arrival_rates = arrival_rates
|
98
|
+
self.sim_time = 0
|
99
|
+
self.running = True
|
100
|
+
|
101
|
+
# Directions for 3way vs 4way
|
102
|
+
if junction_type == "3way":
|
103
|
+
self.directions = ['N','E','W']
|
104
|
+
else:
|
105
|
+
self.directions = ['N','E','S','W']
|
106
|
+
|
107
|
+
# Build queues
|
108
|
+
if self.multiple_lanes:
|
109
|
+
self.queues = {d: [[] for _ in range(self.lane_count)] for d in self.directions}
|
110
|
+
else:
|
111
|
+
self.queues = {d: [] for d in self.directions}
|
112
|
+
|
113
|
+
self.crossing_vehicles = []
|
114
|
+
self.processed_vehicles = []
|
115
|
+
self.vehicle_type_counts = {vt: 0 for vt in DEFAULT_VEHICLE_TYPES.keys()}
|
116
|
+
self.red_no_car_time = {d: 0 for d in self.directions}
|
117
|
+
self.arrivals_count = {d: 0 for d in self.directions}
|
118
|
+
|
119
|
+
# Time-step data recording
|
120
|
+
self.per_timestep_data = []
|
121
|
+
|
122
|
+
# If not adaptive, define a fixed cycle
|
123
|
+
if not self.adaptive_signals:
|
124
|
+
self.phases = []
|
125
|
+
self.cycle_length = 0
|
126
|
+
self.define_signal_phases()
|
127
|
+
else:
|
128
|
+
# Adaptive signals
|
129
|
+
self.signal_state = "green" # "green","yellow","all_red"
|
130
|
+
self.current_green = None
|
131
|
+
self.state_timer = 0
|
132
|
+
self.min_green_time = 10
|
133
|
+
self.max_green_time = 30
|
134
|
+
|
135
|
+
# Create renderer if desired
|
136
|
+
self.renderer = None
|
137
|
+
if self.show_visuals and renderer_class:
|
138
|
+
self.renderer = renderer_class(self)
|
139
|
+
|
140
|
+
def define_signal_phases(self):
|
141
|
+
"""
|
142
|
+
Define fixed signal phases for the junction.
|
143
|
+
Only used if adaptive_signals=False.
|
144
|
+
"""
|
145
|
+
if self.junction_type == "4way":
|
146
|
+
if not self.multiple_lights:
|
147
|
+
base_green = 30
|
148
|
+
self.phases = [
|
149
|
+
{'green': ['N'], 'green_duration': base_green,
|
150
|
+
'yellow_duration': self.yellow_duration,
|
151
|
+
'all_red_duration': self.all_red_duration},
|
152
|
+
{'green': ['E'], 'green_duration': base_green,
|
153
|
+
'yellow_duration': self.yellow_duration,
|
154
|
+
'all_red_duration': self.all_red_duration},
|
155
|
+
{'green': ['S'], 'green_duration': base_green,
|
156
|
+
'yellow_duration': self.yellow_duration,
|
157
|
+
'all_red_duration': self.all_red_duration},
|
158
|
+
{'green': ['W'], 'green_duration': base_green,
|
159
|
+
'yellow_duration': self.yellow_duration,
|
160
|
+
'all_red_duration': self.all_red_duration}
|
161
|
+
]
|
162
|
+
else:
|
163
|
+
base_green = 30
|
164
|
+
self.phases = [
|
165
|
+
{'green': ['N','S'], 'green_duration': base_green,
|
166
|
+
'yellow_duration': self.yellow_duration,
|
167
|
+
'all_red_duration': self.all_red_duration},
|
168
|
+
{'green': ['E','W'], 'green_duration': base_green,
|
169
|
+
'yellow_duration': self.yellow_duration,
|
170
|
+
'all_red_duration': self.all_red_duration}
|
171
|
+
]
|
172
|
+
else:
|
173
|
+
base_green = 30
|
174
|
+
self.phases = [
|
175
|
+
{'green': ['N'], 'green_duration': base_green,
|
176
|
+
'yellow_duration': self.yellow_duration,
|
177
|
+
'all_red_duration': self.all_red_duration},
|
178
|
+
{'green': ['E'], 'green_duration': base_green,
|
179
|
+
'yellow_duration': self.yellow_duration,
|
180
|
+
'all_red_duration': self.all_red_duration},
|
181
|
+
{'green': ['W'], 'green_duration': base_green,
|
182
|
+
'yellow_duration': self.yellow_duration,
|
183
|
+
'all_red_duration': self.all_red_duration}
|
184
|
+
]
|
185
|
+
self.cycle_length = sum(ph['green_duration'] + ph['yellow_duration'] + ph['all_red_duration']
|
186
|
+
for ph in self.phases)
|
187
|
+
|
188
|
+
def update_adaptive_signals(self):
|
189
|
+
"""
|
190
|
+
Adaptive signal logic. Chooses green direction based on queue lengths.
|
191
|
+
"""
|
192
|
+
if self.current_green is None:
|
193
|
+
self.choose_new_green()
|
194
|
+
self.signal_state = "green"
|
195
|
+
return
|
196
|
+
|
197
|
+
self.state_timer -= 1
|
198
|
+
if self.state_timer <= 0:
|
199
|
+
if self.signal_state == "green":
|
200
|
+
self.signal_state = "yellow"
|
201
|
+
self.state_timer = self.yellow_duration
|
202
|
+
elif self.signal_state == "yellow":
|
203
|
+
self.signal_state = "all_red"
|
204
|
+
self.state_timer = self.all_red_duration
|
205
|
+
elif self.signal_state == "all_red":
|
206
|
+
self.choose_new_green()
|
207
|
+
self.signal_state = "green"
|
208
|
+
|
209
|
+
def choose_new_green(self):
|
210
|
+
"""
|
211
|
+
Pick the next green direction based on the largest queue
|
212
|
+
(simple adaptive logic).
|
213
|
+
"""
|
214
|
+
max_queue = -1
|
215
|
+
candidate = None
|
216
|
+
for d in self.directions:
|
217
|
+
if self.multiple_lanes:
|
218
|
+
q_len = sum(len(lane) for lane in self.queues[d])
|
219
|
+
else:
|
220
|
+
q_len = len(self.queues[d])
|
221
|
+
if q_len > max_queue:
|
222
|
+
max_queue = q_len
|
223
|
+
candidate = d
|
224
|
+
if candidate is None or max_queue == 0:
|
225
|
+
candidate = self.directions[0] # fallback
|
226
|
+
green_time = self.min_green_time
|
227
|
+
else:
|
228
|
+
k = 2 # factor
|
229
|
+
green_time = min(self.max_green_time, self.min_green_time + k * max_queue)
|
230
|
+
self.current_green = candidate
|
231
|
+
self.state_timer = green_time
|
232
|
+
|
233
|
+
def get_signal_state(self, direction, t):
|
234
|
+
"""
|
235
|
+
Return 'green','yellow','red' for a direction at time t in the cycle.
|
236
|
+
Only relevant if adaptive_signals=False.
|
237
|
+
"""
|
238
|
+
cycle_pos = t % self.cycle_length
|
239
|
+
accum = 0
|
240
|
+
for ph in self.phases:
|
241
|
+
g = ph['green_duration']
|
242
|
+
y = ph['yellow_duration']
|
243
|
+
r = ph['all_red_duration']
|
244
|
+
phase_len = g + y + r
|
245
|
+
if cycle_pos < accum + phase_len:
|
246
|
+
pos_in_ph = cycle_pos - accum
|
247
|
+
if direction in ph['green']:
|
248
|
+
if pos_in_ph < g:
|
249
|
+
return "green"
|
250
|
+
elif pos_in_ph < g + y:
|
251
|
+
return "yellow"
|
252
|
+
else:
|
253
|
+
return "red"
|
254
|
+
else:
|
255
|
+
return "red"
|
256
|
+
accum += phase_len
|
257
|
+
return "red"
|
258
|
+
|
259
|
+
def get_green_directions(self, t):
|
260
|
+
"""
|
261
|
+
Return a list of directions that are green at time t
|
262
|
+
if using fixed signals.
|
263
|
+
"""
|
264
|
+
cycle_pos = t % self.cycle_length
|
265
|
+
accum = 0
|
266
|
+
for ph in self.phases:
|
267
|
+
phase_len = ph['green_duration'] + ph['yellow_duration'] + ph['all_red_duration']
|
268
|
+
if cycle_pos < accum + phase_len:
|
269
|
+
pos_in_ph = cycle_pos - accum
|
270
|
+
if pos_in_ph < ph['green_duration']:
|
271
|
+
return ph['green']
|
272
|
+
else:
|
273
|
+
return []
|
274
|
+
accum += phase_len
|
275
|
+
return []
|
276
|
+
|
277
|
+
def run(self):
|
278
|
+
"""
|
279
|
+
Main simulation loop.
|
280
|
+
"""
|
281
|
+
while self.running:
|
282
|
+
if self.renderer is not None:
|
283
|
+
for event in pygame.event.get():
|
284
|
+
if event.type == pygame.QUIT:
|
285
|
+
self.running = False
|
286
|
+
|
287
|
+
if self.sim_time < self.total_time:
|
288
|
+
self.sim_update()
|
289
|
+
else:
|
290
|
+
self.running = False
|
291
|
+
|
292
|
+
if self.renderer:
|
293
|
+
self.renderer.render()
|
294
|
+
if self.simulation_speed > 0:
|
295
|
+
self.renderer.clock.tick(self.simulation_speed)
|
296
|
+
|
297
|
+
if self.renderer:
|
298
|
+
pygame.quit()
|
299
|
+
|
300
|
+
def sim_update(self):
|
301
|
+
"""
|
302
|
+
One step update: arrivals, signal updates, queue movement, crossing, stats.
|
303
|
+
"""
|
304
|
+
for d in self.directions:
|
305
|
+
self.arrivals_count[d] = 0
|
306
|
+
|
307
|
+
self.generate_arrivals(self.sim_time)
|
308
|
+
|
309
|
+
if self.adaptive_signals:
|
310
|
+
self.update_adaptive_signals()
|
311
|
+
if self.signal_state == "green":
|
312
|
+
green_dirs = [self.current_green]
|
313
|
+
else:
|
314
|
+
green_dirs = []
|
315
|
+
else:
|
316
|
+
green_dirs = self.get_green_directions(self.sim_time)
|
317
|
+
|
318
|
+
for d in green_dirs:
|
319
|
+
self.start_crossing_one_vehicle(d, self.sim_time)
|
320
|
+
|
321
|
+
self.update_crossing_vehicles()
|
322
|
+
self.track_empty_red_time(green_dirs)
|
323
|
+
|
324
|
+
if self.sim_time % self.print_interval == 0:
|
325
|
+
self.print_state(self.sim_time, green_dirs)
|
326
|
+
|
327
|
+
self.record_timestep_data()
|
328
|
+
self.sim_time += 1
|
329
|
+
|
330
|
+
def generate_arrivals(self, t):
|
331
|
+
"""
|
332
|
+
Generate arrivals according to Poisson process for each direction.
|
333
|
+
"""
|
334
|
+
for d in self.directions:
|
335
|
+
rate = self.arrival_rates.get(d, 0)
|
336
|
+
arrivals = self.poisson_random(rate)
|
337
|
+
self.arrivals_count[d] += arrivals
|
338
|
+
for _ in range(arrivals):
|
339
|
+
if any(v.direction == d and v.state == 'crossing' for v in self.crossing_vehicles):
|
340
|
+
continue
|
341
|
+
vt = random.choices(
|
342
|
+
population=list(self.vehicle_distribution.keys()),
|
343
|
+
weights=list(self.vehicle_distribution.values())
|
344
|
+
)[0]
|
345
|
+
v = Vehicle(t, d, vt,
|
346
|
+
simulate_full_route=self.simulate_full_route,
|
347
|
+
SCREEN_WIDTH=self.SCREEN_WIDTH,
|
348
|
+
SCREEN_HEIGHT=self.SCREEN_HEIGHT)
|
349
|
+
self.vehicle_type_counts[vt] += 1
|
350
|
+
self.place_in_queue(v, d)
|
351
|
+
|
352
|
+
def poisson_random(self, rate):
|
353
|
+
"""
|
354
|
+
Basic Poisson random for arrivals: expected value = rate
|
355
|
+
(assuming 1-second intervals).
|
356
|
+
"""
|
357
|
+
L = math.exp(-rate)
|
358
|
+
p = 1.0
|
359
|
+
k = 0
|
360
|
+
while p > L:
|
361
|
+
p *= random.random()
|
362
|
+
k += 1
|
363
|
+
return k - 1
|
364
|
+
|
365
|
+
def place_in_queue(self, v, direction):
|
366
|
+
"""
|
367
|
+
Places a vehicle into the shortest lane (if multiple lanes).
|
368
|
+
"""
|
369
|
+
if self.multiple_lanes:
|
370
|
+
L = self.queues[direction]
|
371
|
+
lane_index = min(range(self.lane_count), key=lambda i: len(L[i]))
|
372
|
+
L[lane_index].append(v)
|
373
|
+
v.lane_index = lane_index
|
374
|
+
else:
|
375
|
+
self.queues[direction].append(v)
|
376
|
+
|
377
|
+
def start_crossing_one_vehicle(self, direction, t):
|
378
|
+
"""
|
379
|
+
Remove exactly one vehicle (front) from the queue if reaction time has passed,
|
380
|
+
and start crossing.
|
381
|
+
"""
|
382
|
+
if self.multiple_lanes:
|
383
|
+
lanes = self.queues[direction]
|
384
|
+
for lane in lanes:
|
385
|
+
if lane:
|
386
|
+
front = lane[0]
|
387
|
+
if t - front.arrival_time >= front.reaction_time:
|
388
|
+
if front.turn_direction == 'left':
|
389
|
+
if random.random() >= (LEFT_TURN_OPEN_PROB * front.lane_change_aggressiveness):
|
390
|
+
continue
|
391
|
+
else:
|
392
|
+
front = lane.pop(0)
|
393
|
+
else:
|
394
|
+
front = lane.pop(0)
|
395
|
+
front.state = 'crossing'
|
396
|
+
front.start_cross_time = t
|
397
|
+
front.init_route(self.SCREEN_WIDTH//2, self.SCREEN_HEIGHT//2)
|
398
|
+
self.crossing_vehicles.append(front)
|
399
|
+
break
|
400
|
+
else:
|
401
|
+
Q = self.queues[direction]
|
402
|
+
if Q:
|
403
|
+
front = Q[0]
|
404
|
+
if t - front.arrival_time >= front.reaction_time:
|
405
|
+
if front.turn_direction == 'left':
|
406
|
+
if random.random() >= LEFT_TURN_OPEN_PROB:
|
407
|
+
return
|
408
|
+
else:
|
409
|
+
front = Q.pop(0)
|
410
|
+
else:
|
411
|
+
front = Q.pop(0)
|
412
|
+
front.state = 'crossing'
|
413
|
+
front.start_cross_time = t
|
414
|
+
front.init_route(self.SCREEN_WIDTH//2, self.SCREEN_HEIGHT//2)
|
415
|
+
self.crossing_vehicles.append(front)
|
416
|
+
|
417
|
+
def update_crossing_vehicles(self):
|
418
|
+
"""
|
419
|
+
Update crossing vehicles (IDM, positions). Remove when done.
|
420
|
+
"""
|
421
|
+
done_list = []
|
422
|
+
for v in self.crossing_vehicles:
|
423
|
+
is_done = v.update_position(dt=1.0, lead_vehicle=None)
|
424
|
+
if is_done:
|
425
|
+
done_list.append(v)
|
426
|
+
|
427
|
+
for v in done_list:
|
428
|
+
self.crossing_vehicles.remove(v)
|
429
|
+
v.state = 'finished'
|
430
|
+
self.processed_vehicles.append(v)
|
431
|
+
|
432
|
+
# Reposition queues for visualization
|
433
|
+
for d in self.directions:
|
434
|
+
self.reposition_queue(d)
|
435
|
+
|
436
|
+
def reposition_queue(self, direction):
|
437
|
+
"""
|
438
|
+
Repositions queued vehicles visually in a line or multi-lane arrangement.
|
439
|
+
India mode allows side-by-side for 2-wheelers or smaller vehicles.
|
440
|
+
"""
|
441
|
+
lane_gap = 40
|
442
|
+
base_x, base_y = self.SCREEN_WIDTH//2, self.SCREEN_HEIGHT//2
|
443
|
+
|
444
|
+
if self.multiple_lanes:
|
445
|
+
for lane_idx, lane in enumerate(self.queues[direction]):
|
446
|
+
if self.india_mode:
|
447
|
+
rows = []
|
448
|
+
for veh in lane:
|
449
|
+
if rows:
|
450
|
+
last_row = rows[-1]
|
451
|
+
# Some simplistic logic for side-by-side
|
452
|
+
if (len(last_row) < 3
|
453
|
+
and all(x.vehicle_type in ['scooter','motorcycle'] for x in last_row)
|
454
|
+
and random.random() < 0.3):
|
455
|
+
last_row.append(veh)
|
456
|
+
veh.row_index = len(rows)-1
|
457
|
+
veh.col_index = len(last_row)-1
|
458
|
+
elif (len(last_row) < 2
|
459
|
+
and all(x.vehicle_type in ['scooter','motorcycle','car'] for x in last_row)
|
460
|
+
and random.random() < 0.5):
|
461
|
+
last_row.append(veh)
|
462
|
+
veh.row_index = len(rows)-1
|
463
|
+
veh.col_index = len(last_row)-1
|
464
|
+
else:
|
465
|
+
rows.append([veh])
|
466
|
+
veh.row_index = len(rows)-1
|
467
|
+
veh.col_index = 0
|
468
|
+
else:
|
469
|
+
rows.append([veh])
|
470
|
+
veh.row_index = 0
|
471
|
+
veh.col_index = 0
|
472
|
+
|
473
|
+
for row_idx, row in enumerate(rows):
|
474
|
+
offset_lane = (lane_idx - (self.lane_count - 1) / 2.0)*lane_gap
|
475
|
+
for col_idx, veh in enumerate(row):
|
476
|
+
extra_x, extra_y = 0, 0
|
477
|
+
if len(row) == 2:
|
478
|
+
extra_x = -5 if col_idx == 0 else 5
|
479
|
+
elif len(row) == 3:
|
480
|
+
extra_x = -10 + (10*col_idx)
|
481
|
+
if direction == 'N':
|
482
|
+
veh.x = base_x + offset_lane + extra_x
|
483
|
+
veh.y = base_y - QUEUE_OFFSET - row_idx*(CAR_SPACING+5)
|
484
|
+
elif direction == 'S':
|
485
|
+
veh.x = base_x + offset_lane + extra_x
|
486
|
+
veh.y = base_y + QUEUE_OFFSET + row_idx*(CAR_SPACING+5)
|
487
|
+
elif direction == 'E':
|
488
|
+
veh.x = base_x + QUEUE_OFFSET + row_idx*(CAR_SPACING+5)
|
489
|
+
veh.y = base_y + offset_lane + extra_x
|
490
|
+
else: # 'W'
|
491
|
+
veh.x = base_x - QUEUE_OFFSET - row_idx*(CAR_SPACING+5)
|
492
|
+
veh.y = base_y + offset_lane + extra_x
|
493
|
+
else:
|
494
|
+
for i, veh in enumerate(lane):
|
495
|
+
offset_lane = (lane_idx - (self.lane_count-1)/2.0)*lane_gap
|
496
|
+
if direction == 'N':
|
497
|
+
veh.x = base_x + offset_lane
|
498
|
+
veh.y = base_y - QUEUE_OFFSET - i*CAR_SPACING
|
499
|
+
elif direction == 'S':
|
500
|
+
veh.x = base_x + offset_lane
|
501
|
+
veh.y = base_y + QUEUE_OFFSET + i*CAR_SPACING
|
502
|
+
elif direction == 'E':
|
503
|
+
veh.x = base_x + QUEUE_OFFSET + i*CAR_SPACING
|
504
|
+
veh.y = base_y + offset_lane
|
505
|
+
else:
|
506
|
+
veh.x = base_x - QUEUE_OFFSET - i*CAR_SPACING
|
507
|
+
veh.y = base_y + offset_lane
|
508
|
+
else:
|
509
|
+
lane = self.queues[direction]
|
510
|
+
if self.india_mode:
|
511
|
+
rows = []
|
512
|
+
for veh in lane:
|
513
|
+
if rows:
|
514
|
+
last_row = rows[-1]
|
515
|
+
if (len(last_row) < 3
|
516
|
+
and all(x.vehicle_type in ['scooter','motorcycle','car'] for x in last_row)
|
517
|
+
and random.random() < 0.5):
|
518
|
+
last_row.append(veh)
|
519
|
+
veh.row_index = len(rows)-1
|
520
|
+
veh.col_index = len(last_row)-1
|
521
|
+
else:
|
522
|
+
rows.append([veh])
|
523
|
+
veh.row_index = len(rows)-1
|
524
|
+
veh.col_index = 0
|
525
|
+
else:
|
526
|
+
rows.append([veh])
|
527
|
+
veh.row_index = len(rows)-1
|
528
|
+
veh.col_index = 0
|
529
|
+
for row_idx, row in enumerate(rows):
|
530
|
+
for col_idx, veh in enumerate(row):
|
531
|
+
extra_x = 0
|
532
|
+
if len(row) == 2:
|
533
|
+
extra_x = -5 if col_idx == 0 else 5
|
534
|
+
elif len(row) == 3:
|
535
|
+
extra_x = -10 + (10*col_idx)
|
536
|
+
if direction == 'N':
|
537
|
+
veh.x = base_x + extra_x
|
538
|
+
veh.y = base_y - QUEUE_OFFSET - row_idx*(CAR_SPACING+5)
|
539
|
+
elif direction == 'S':
|
540
|
+
veh.x = base_x + extra_x
|
541
|
+
veh.y = base_y + QUEUE_OFFSET + row_idx*(CAR_SPACING+5)
|
542
|
+
elif direction == 'E':
|
543
|
+
veh.x = base_x + QUEUE_OFFSET + row_idx*(CAR_SPACING+5)
|
544
|
+
veh.y = base_y + extra_x
|
545
|
+
else:
|
546
|
+
veh.x = base_x - QUEUE_OFFSET - row_idx*(CAR_SPACING+5)
|
547
|
+
veh.y = base_y + extra_x
|
548
|
+
else:
|
549
|
+
for i, veh in enumerate(lane):
|
550
|
+
if direction == 'N':
|
551
|
+
veh.x = base_x
|
552
|
+
veh.y = base_y - QUEUE_OFFSET - i*CAR_SPACING
|
553
|
+
elif direction == 'S':
|
554
|
+
veh.x = base_x
|
555
|
+
veh.y = base_y + QUEUE_OFFSET + i*CAR_SPACING
|
556
|
+
elif direction == 'E':
|
557
|
+
veh.x = base_x + QUEUE_OFFSET + i*CAR_SPACING
|
558
|
+
veh.y = base_y
|
559
|
+
else:
|
560
|
+
veh.x = base_x - QUEUE_OFFSET - i*CAR_SPACING
|
561
|
+
veh.y = base_y
|
562
|
+
|
563
|
+
def track_empty_red_time(self, green_dirs):
|
564
|
+
"""
|
565
|
+
Increment counters for time steps when a direction is red AND has no cars in queue.
|
566
|
+
"""
|
567
|
+
for d in self.directions:
|
568
|
+
if d not in green_dirs:
|
569
|
+
if self.multiple_lanes:
|
570
|
+
size = sum(len(x) for x in self.queues[d])
|
571
|
+
else:
|
572
|
+
size = len(self.queues[d])
|
573
|
+
if size <= self.min_queue_empty:
|
574
|
+
self.red_no_car_time[d] += 1
|
575
|
+
|
576
|
+
def print_state(self, t, green_dirs):
|
577
|
+
if self.adaptive_signals:
|
578
|
+
sig_info = f"(Signal: {self.signal_state.upper()} for {self.current_green}, timer={self.state_timer})"
|
579
|
+
else:
|
580
|
+
sig_info = ""
|
581
|
+
print(f"Time={t}, green={green_dirs} {sig_info}")
|
582
|
+
for d in self.directions:
|
583
|
+
if self.multiple_lanes:
|
584
|
+
qsize = sum(len(x) for x in self.queues[d])
|
585
|
+
else:
|
586
|
+
qsize = len(self.queues[d])
|
587
|
+
aw = self.average_wait_time_for_direction(d)
|
588
|
+
print(f" {d} queue={qsize}, avg_wait={aw:.2f}")
|
589
|
+
|
590
|
+
def record_timestep_data(self):
|
591
|
+
row = {"TimeStep": self.sim_time}
|
592
|
+
for d in self.directions:
|
593
|
+
if self.multiple_lanes:
|
594
|
+
row[f"Queue{d}"] = sum(len(L) for L in self.queues[d])
|
595
|
+
else:
|
596
|
+
row[f"Queue{d}"] = len(self.queues[d])
|
597
|
+
row[f"Arrivals{d}"] = self.arrivals_count[d]
|
598
|
+
row[f"AvgWait{d}"] = self.average_wait_time_for_direction(d)
|
599
|
+
row["CrossingCount"] = len(self.crossing_vehicles)
|
600
|
+
row["ProcessedCount"] = len(self.processed_vehicles)
|
601
|
+
row["OverallAvgWait"] = self.overall_average_wait_time()
|
602
|
+
self.per_timestep_data.append(row)
|
603
|
+
|
604
|
+
def average_wait_time_for_direction(self, d):
|
605
|
+
done = [v for v in self.processed_vehicles if v.direction == d and v.wait_time is not None]
|
606
|
+
if not done:
|
607
|
+
return 0.0
|
608
|
+
return sum(v.wait_time for v in done) / len(done)
|
609
|
+
|
610
|
+
def overall_average_wait_time(self):
|
611
|
+
done = [v for v in self.processed_vehicles if v.wait_time is not None]
|
612
|
+
if not done:
|
613
|
+
return 0.0
|
614
|
+
return sum(v.wait_time for v in done) / len(done)
|
615
|
+
|
616
|
+
def print_statistics(self):
|
617
|
+
total_processed = len(self.processed_vehicles)
|
618
|
+
avg_wait = self.overall_average_wait_time()
|
619
|
+
print("\n=== Simulation Stats ===")
|
620
|
+
print(f"JunctionType: {self.junction_type}")
|
621
|
+
print(f"MultipleLights: {self.multiple_lights}")
|
622
|
+
print(f"MultipleLanes: {self.multiple_lanes} (LaneCount={self.lane_count})")
|
623
|
+
print(f"SimulateFullRoute: {self.simulate_full_route}")
|
624
|
+
print(f"AdaptiveSignals: {self.adaptive_signals}")
|
625
|
+
print(f"TotalVehiclesProcessed: {total_processed}")
|
626
|
+
print(f"OverallAvgWait: {avg_wait:.2f}")
|
627
|
+
print("\nVehicle Type Counts:")
|
628
|
+
for vt, cnt in self.vehicle_type_counts.items():
|
629
|
+
print(f" {vt}: {cnt}")
|
630
|
+
print("\nRed-empty stats:")
|
631
|
+
for d in self.directions:
|
632
|
+
print(f" {d}: {self.red_no_car_time[d]}")
|
633
|
+
print("========================")
|
634
|
+
|
635
|
+
def get_results_dict(self):
|
636
|
+
total_processed = len(self.processed_vehicles)
|
637
|
+
avg_wait = self.overall_average_wait_time()
|
638
|
+
result = {
|
639
|
+
"JunctionType": self.junction_type,
|
640
|
+
"MultipleLights": self.multiple_lights,
|
641
|
+
"MultipleLanes": self.multiple_lanes,
|
642
|
+
"LaneCount": self.lane_count,
|
643
|
+
"TotalVehiclesProcessed": total_processed,
|
644
|
+
"OverallAvgWait": avg_wait,
|
645
|
+
"SimulateFullRoute": self.simulate_full_route
|
646
|
+
}
|
647
|
+
for d in self.directions:
|
648
|
+
result[f"RedEmpty{d}"] = self.red_no_car_time[d]
|
649
|
+
for vt in DEFAULT_VEHICLE_TYPES.keys():
|
650
|
+
key = "Count" + vt.capitalize()
|
651
|
+
result[key] = self.vehicle_type_counts[vt]
|
652
|
+
return result
|