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
@@ -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
|