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