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