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/models.py ADDED
@@ -0,0 +1,296 @@
1
+ """
2
+ models.py
3
+ ---------
4
+ Vehicle model and default vehicle parameters.
5
+ Implements IDM & MOBIL logic.
6
+ """
7
+
8
+ import random
9
+ import math
10
+ from .utils import dist, define_exit_point
11
+
12
+ # Default vehicle parameters
13
+ DEFAULT_VEHICLE_TYPES = {
14
+ 'car': {
15
+ 'desired_speed': 20,
16
+ 'max_acceleration': 2.5,
17
+ 'comfortable_deceleration': 2.5,
18
+ 'minimum_gap': 2.5,
19
+ 'lane_change_aggressiveness': 0.7,
20
+ 'length': 4.5
21
+ },
22
+ 'truck': {
23
+ 'desired_speed': 15,
24
+ 'max_acceleration': 1.5,
25
+ 'comfortable_deceleration': 1.5,
26
+ 'minimum_gap': 3.5,
27
+ 'lane_change_aggressiveness': 0.5,
28
+ 'length': 10.0
29
+ },
30
+ 'bus': {
31
+ 'desired_speed': 15,
32
+ 'max_acceleration': 1.5,
33
+ 'comfortable_deceleration': 1.5,
34
+ 'minimum_gap': 4.0,
35
+ 'lane_change_aggressiveness': 0.5,
36
+ 'length': 12.0
37
+ },
38
+ 'scooter': {
39
+ 'desired_speed': 18,
40
+ 'max_acceleration': 3.0,
41
+ 'comfortable_deceleration': 3.0,
42
+ 'minimum_gap': 1.2,
43
+ 'lane_change_aggressiveness': 0.8,
44
+ 'length': 2.0
45
+ },
46
+ 'motorcycle': {
47
+ 'desired_speed': 22,
48
+ 'max_acceleration': 3.5,
49
+ 'comfortable_deceleration': 3.5,
50
+ 'minimum_gap': 1.0,
51
+ 'lane_change_aggressiveness': 0.9,
52
+ 'length': 2.2
53
+ }
54
+ }
55
+
56
+ # Colors for different vehicle types (for rendering usage)
57
+ VEHICLE_COLORS = {
58
+ 'car': (200, 200, 0),
59
+ 'truck': (180, 100, 50),
60
+ 'bus': (120, 40, 150),
61
+ 'scooter': (40, 220, 220),
62
+ 'motorcycle': (255, 100, 100),
63
+ }
64
+
65
+ class Vehicle:
66
+ """
67
+ Represents a single vehicle, using IDM for longitudinal and MOBIL for lane changing.
68
+ Also includes India Mode logic for side-by-side queue placement (if enabled).
69
+ """
70
+ def __init__(self, arrival_time, direction, vehicle_type=None,
71
+ simulate_full_route=True,
72
+ SCREEN_WIDTH=1000, SCREEN_HEIGHT=800):
73
+ self.arrival_time = arrival_time
74
+ self.direction = direction # 'N', 'S', 'E', 'W'
75
+
76
+ # Random turn choice
77
+ self.turn_direction = random.choices(
78
+ ['left', 'straight', 'right'],
79
+ weights=[0.34, 0.33, 0.33]
80
+ )[0]
81
+
82
+ # Vehicle type logic
83
+ if vehicle_type is None:
84
+ vehicle_type = random.choice(list(DEFAULT_VEHICLE_TYPES.keys()))
85
+ self.vehicle_type = vehicle_type
86
+ params = DEFAULT_VEHICLE_TYPES[self.vehicle_type]
87
+
88
+ # Basic physical parameters
89
+ self.length = params['length']
90
+ self.lane_change_aggressiveness = params['lane_change_aggressiveness']
91
+
92
+ self.state = 'queueing' # 'queueing', 'crossing', 'finished'
93
+ self.start_cross_time = None
94
+ self.finish_time = None
95
+
96
+ # Position & movement
97
+ self.x = 0.0
98
+ self.y = 0.0
99
+ self.current_speed = 0.0
100
+ self.distance_covered = 0.0
101
+
102
+ # Route details
103
+ self.start_position = None
104
+ self.center_position = None
105
+ self.exit_position = None
106
+ self.route_distance = 0.0
107
+ self.simulate_full_route = simulate_full_route
108
+ self.SCREEN_WIDTH = SCREEN_WIDTH
109
+ self.SCREEN_HEIGHT = SCREEN_HEIGHT
110
+
111
+ # Reaction time
112
+ self.reaction_time = random.uniform(0.8, 1.5)
113
+
114
+ # IDM parameters
115
+ self.idm_a0 = 2.0
116
+ self.idm_b = 2.5
117
+ self.idm_v0 = params['desired_speed']
118
+ self.idm_T = 1.5
119
+ self.idm_s0 = 2.0
120
+ self.idm_delta = 4.0
121
+
122
+ # Politeness factor for MOBIL
123
+ self.politeness = 0.2
124
+ self.delta_a_threshold = 0.2
125
+
126
+ # Additional indexing for queue positioning
127
+ self.lane_index = None
128
+ self.row_index = None
129
+ self.col_index = None
130
+
131
+ # Assign a driver profile to add variety
132
+ self.assign_driver_profile()
133
+
134
+ @property
135
+ def wait_time(self):
136
+ if self.start_cross_time is None:
137
+ return None
138
+ return self.start_cross_time - self.arrival_time
139
+
140
+ def assign_driver_profile(self):
141
+ """
142
+ Randomly assigns an 'aggressive', 'normal', or 'cautious' profile
143
+ to create variability in driving style.
144
+ """
145
+ profile_type = random.choices(
146
+ ["aggressive", "normal", "cautious"],
147
+ weights=[0.3, 0.5, 0.2]
148
+ )[0]
149
+
150
+ if profile_type == "aggressive":
151
+ self.idm_a0 = random.uniform(2.5, 3.0)
152
+ self.idm_b = 2.0
153
+ self.idm_T = 1.0
154
+ self.idm_v0 *= 1.3 # bump up desired speed ~30% for effect
155
+ self.politeness = 0.1
156
+ self.reaction_time = random.uniform(0.6, 1.0)
157
+ elif profile_type == "cautious":
158
+ self.idm_a0 = random.uniform(1.0, 1.5)
159
+ self.idm_b = 2.5
160
+ self.idm_T = 1.8
161
+ self.idm_v0 *= 0.9 # reduce desired speed
162
+ self.politeness = 0.3
163
+ self.reaction_time = random.uniform(1.5, 2.0)
164
+ else:
165
+ # 'normal' -> keep defaults
166
+ pass
167
+
168
+ def init_route(self, cx, cy):
169
+ """
170
+ Called once vehicle starts crossing. We define start/center/exit, compute route distance.
171
+ """
172
+ self.start_position = (self.x, self.y)
173
+ self.center_position = (cx, cy)
174
+ if self.simulate_full_route:
175
+ # Real exit point
176
+ self.exit_position = define_exit_point(
177
+ cx, cy, self.direction, self.turn_direction,
178
+ self.SCREEN_WIDTH, self.SCREEN_HEIGHT
179
+ )
180
+ d1 = dist(self.start_position, self.center_position)
181
+ d2 = dist(self.center_position, self.exit_position)
182
+ self.route_distance = d1 + d2
183
+ else:
184
+ # Vanish at center
185
+ self.exit_position = self.center_position
186
+ self.route_distance = dist(self.start_position, self.center_position)
187
+
188
+ self.distance_covered = 0.0
189
+
190
+ def compute_idm_acceleration(self, lead_vehicle):
191
+ """
192
+ Calculate acceleration using IDM, given the lead vehicle in the same lane (if any).
193
+ """
194
+ if lead_vehicle is None:
195
+ s = 1e9
196
+ delta_v = 0.0
197
+ else:
198
+ # gap = distance between centers - half lengths
199
+ s = dist((self.x, self.y), (lead_vehicle.x, lead_vehicle.y)) \
200
+ - 0.5*self.length - 0.5*lead_vehicle.length
201
+ delta_v = self.current_speed - lead_vehicle.current_speed
202
+ if s < 0.1:
203
+ s = 0.1
204
+
205
+ s_star = self.idm_s0 + max(
206
+ 0, self.current_speed * self.idm_T +
207
+ (self.current_speed*delta_v)/(2*math.sqrt(self.idm_a0*self.idm_b))
208
+ )
209
+
210
+ alpha_free = 1 - pow((self.current_speed / self.idm_v0), self.idm_delta)
211
+ alpha_int = - pow((s_star / s), 2)
212
+ a = self.idm_a0 * (alpha_free + alpha_int)
213
+ return a
214
+
215
+ def check_mobil_lane_change(self, sim, current_lane, target_lane,
216
+ lead_current, lead_target,
217
+ rear_current, rear_target):
218
+ """
219
+ Decide if lane change is beneficial by MOBIL:
220
+ (a_new_self - a_old_self) + p * (a_new_rear - a_old_rear) > delta_a_threshold
221
+ """
222
+ a_old_self = self.compute_idm_acceleration(lead_current)
223
+ a_new_self = self.compute_idm_acceleration(lead_target)
224
+
225
+ a_old_rear, a_new_rear = 0.0, 0.0
226
+ if rear_current:
227
+ lead_for_rear_current = sim.get_lead_vehicle_in_lane(current_lane, rear_current)
228
+ a_old_rear = rear_current.compute_idm_acceleration(lead_for_rear_current)
229
+ if rear_target:
230
+ lead_for_rear_target = sim.get_lead_vehicle_in_lane(target_lane, rear_target)
231
+ a_new_rear = rear_target.compute_idm_acceleration(lead_for_rear_target)
232
+
233
+ lhs = (a_new_self - a_old_self) + self.politeness * (a_new_rear - a_old_rear)
234
+ if lhs > self.delta_a_threshold:
235
+ # check safety
236
+ if self.is_safe_to_change_lane(lead_target, rear_target):
237
+ return True
238
+ return False
239
+
240
+ def is_safe_to_change_lane(self, lead_vehicle, rear_vehicle):
241
+ """
242
+ Very simple check: ensure gap front and gap rear are > 3.0m (adjust as desired).
243
+ """
244
+ safe_gap = 3.0
245
+ if lead_vehicle:
246
+ gap_front = dist((self.x, self.y), (lead_vehicle.x, lead_vehicle.y)) \
247
+ - 0.5*self.length - 0.5*lead_vehicle.length
248
+ if gap_front < safe_gap:
249
+ return False
250
+ if rear_vehicle:
251
+ gap_rear = dist((self.x, self.y), (rear_vehicle.x, rear_vehicle.y)) \
252
+ - 0.5*self.length - 0.5*rear_vehicle.length
253
+ if gap_rear < safe_gap:
254
+ return False
255
+ return True
256
+
257
+ def update_position(self, dt=1.0, lead_vehicle=None):
258
+ """
259
+ Update speed via IDM acceleration and move the vehicle along its route.
260
+ Returns True if vehicle has completed its route.
261
+ """
262
+ a = self.compute_idm_acceleration(lead_vehicle)
263
+ self.current_speed += a * dt
264
+ if self.current_speed < 0.0:
265
+ self.current_speed = 0.0
266
+
267
+ dist_step = self.current_speed * dt
268
+ self.distance_covered += dist_step
269
+
270
+ if self.route_distance < 0.1:
271
+ return True # edge case if start == center
272
+
273
+ frac = self.distance_covered / self.route_distance
274
+ if frac >= 1.0:
275
+ frac = 1.0
276
+
277
+ d1 = dist(self.start_position, self.center_position)
278
+ route_total = self.route_distance
279
+ if frac < d1 / route_total:
280
+ # in the segment start->center
281
+ sub_frac = frac / (d1 / route_total)
282
+ sx, sy = self.start_position
283
+ cx, cy = self.center_position
284
+ self.x = sx + (cx - sx)*sub_frac
285
+ self.y = sy + (cy - sy)*sub_frac
286
+ else:
287
+ # in the center->exit segment
288
+ ratio_remaining = frac - (d1/route_total)
289
+ segment_len = 1.0 - (d1/route_total)
290
+ sub_frac = ratio_remaining / segment_len if segment_len > 1e-6 else 1.0
291
+ cx, cy = self.center_position
292
+ ex, ey = self.exit_position
293
+ self.x = cx + (ex - cx)*sub_frac
294
+ self.y = cy + (ey - cy)*sub_frac
295
+
296
+ return (frac >= 1.0)
traffisim/rendering.py ADDED
@@ -0,0 +1,225 @@
1
+ """
2
+ rendering.py
3
+ ------------
4
+ Optional Pygame renderer for TraffiSim.
5
+ """
6
+
7
+ import pygame
8
+ from .models import VEHICLE_COLORS
9
+ from .intersection import SCREEN_WIDTH, SCREEN_HEIGHT, LANE_WIDTH, ROAD_WIDTH, ROAD_MARGIN
10
+ from .intersection import QUEUE_OFFSET, CAR_SPACING
11
+
12
+ class TrafficRenderer:
13
+ """
14
+ Class responsible for rendering the traffic simulation using Pygame.
15
+ """
16
+ COLOR_BG = (30, 30, 30)
17
+ COLOR_ROAD = (70, 70, 70)
18
+ COLOR_TEXT = (255, 255, 255)
19
+ LINE_COLOR = (140, 140, 140)
20
+
21
+ TRAFFIC_LIGHT_COLORS = {
22
+ "red": (255, 40, 40),
23
+ "yellow": (255, 255, 0),
24
+ "green": ( 40, 255, 40),
25
+ }
26
+
27
+ def __init__(self, sim):
28
+ """
29
+ Initialize the TrafficRenderer with a simulation instance.
30
+
31
+ Args:
32
+ sim (IntersectionSim): The simulation instance to render.
33
+ """
34
+ self.sim = sim
35
+ pygame.init()
36
+ pygame.display.set_caption(f"TraffiSim (type={sim.junction_type}, MLights={sim.multiple_lights})")
37
+ self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
38
+ self.clock = pygame.time.Clock()
39
+ self.font = pygame.font.SysFont(None, 22)
40
+
41
+ self.cx = SCREEN_WIDTH // 2
42
+ self.cy = SCREEN_HEIGHT // 2
43
+
44
+ def render(self):
45
+ """
46
+ Render the current state of the simulation.
47
+ """
48
+ self.screen.fill(self.COLOR_BG)
49
+ self.draw_roads()
50
+ self.draw_lane_barriers()
51
+ self.draw_traffic_lights()
52
+ self.draw_vehicles()
53
+ self.draw_ui_panel()
54
+ pygame.display.flip()
55
+
56
+ def draw_roads(self):
57
+ """
58
+ Draw the intersection roads as rectangles.
59
+ """
60
+ if self.sim.junction_type == '4way':
61
+ rect_h = pygame.Rect(0, self.cy - ROAD_WIDTH//2, SCREEN_WIDTH, ROAD_WIDTH)
62
+ pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_h)
63
+ rect_v = pygame.Rect(self.cx - ROAD_WIDTH//2, 0, ROAD_WIDTH, SCREEN_HEIGHT)
64
+ pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_v)
65
+ else:
66
+ rect_h = pygame.Rect(0, self.cy - ROAD_WIDTH//2, SCREEN_WIDTH, ROAD_WIDTH)
67
+ pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_h)
68
+ rect_v = pygame.Rect(self.cx - ROAD_WIDTH//2, 0, ROAD_WIDTH, self.cy + ROAD_WIDTH//2)
69
+ pygame.draw.rect(self.screen, self.COLOR_ROAD, rect_v)
70
+
71
+ def draw_lane_barriers(self):
72
+ """
73
+ Draw dashed lane lines.
74
+ """
75
+ dash_len = 10
76
+ gap_len = 6
77
+
78
+ top_road = self.cy - (ROAD_WIDTH//2)
79
+ bottom_road = self.cy + (ROAD_WIDTH//2)
80
+ left_road = self.cx - (ROAD_WIDTH//2)
81
+ right_road = self.cx + (ROAD_WIDTH//2)
82
+
83
+ lane_count_est = int(ROAD_WIDTH / LANE_WIDTH)
84
+ for i in range(1, lane_count_est):
85
+ x_line = left_road + i*LANE_WIDTH
86
+ y = 0
87
+ while y < SCREEN_HEIGHT:
88
+ pygame.draw.line(self.screen, self.LINE_COLOR,
89
+ (x_line, y), (x_line, min(y+dash_len, SCREEN_HEIGHT)), 2)
90
+ y += dash_len + gap_len
91
+
92
+ lane_count_est_v = int(ROAD_WIDTH / LANE_WIDTH)
93
+ for i in range(1, lane_count_est_v):
94
+ y_line = top_road + i*LANE_WIDTH
95
+ x = 0
96
+ while x < SCREEN_WIDTH:
97
+ pygame.draw.line(self.screen, self.LINE_COLOR,
98
+ (x, y_line), (min(x+dash_len, SCREEN_WIDTH), y_line), 2)
99
+ x += dash_len + gap_len
100
+
101
+ def draw_traffic_lights(self):
102
+ """
103
+ Draw traffic lights at the intersection.
104
+ """
105
+ offsets = {
106
+ 'N': (0, -60),
107
+ 'S': (0, 60),
108
+ 'E': (60, 0),
109
+ 'W': (-60, 0)
110
+ }
111
+ r = 9
112
+ for d in self.sim.directions:
113
+ if self.sim.adaptive_signals:
114
+ if self.sim.current_green == d and self.sim.signal_state == "green":
115
+ st = "green"
116
+ elif self.sim.current_green == d and self.sim.signal_state == "yellow":
117
+ st = "yellow"
118
+ else:
119
+ st = "red"
120
+ else:
121
+ st = self.sim.get_signal_state(d, self.sim.sim_time)
122
+
123
+ color = self.TRAFFIC_LIGHT_COLORS[st]
124
+ ox, oy = offsets.get(d, (0,0))
125
+ cx = self.cx + ox
126
+ cy = self.cy + oy
127
+
128
+ pygame.draw.circle(self.screen, (0,0,0), (cx, cy), r+2)
129
+ pygame.draw.circle(self.screen, color, (cx, cy), r)
130
+
131
+ def draw_vehicles(self):
132
+ """
133
+ Draw all vehicles in the simulation.
134
+ """
135
+ for d in self.sim.directions:
136
+ if self.sim.multiple_lanes:
137
+ for lane in self.sim.queues[d]:
138
+ for v in lane:
139
+ self.draw_single_vehicle(v)
140
+ else:
141
+ for v in self.sim.queues[d]:
142
+ self.draw_single_vehicle(v)
143
+ for v in self.sim.crossing_vehicles:
144
+ self.draw_single_vehicle(v)
145
+
146
+ def draw_single_vehicle(self, v):
147
+ """
148
+ Draw a single vehicle.
149
+
150
+ Args:
151
+ v (Vehicle): The vehicle to draw.
152
+ """
153
+ color = VEHICLE_COLORS.get(v.vehicle_type, (200,200,200))
154
+ shadow_color = (color[0]//4, color[1]//4, color[2]//4)
155
+
156
+ if v.vehicle_type in ['car', 'truck', 'bus']:
157
+ self.draw_rect_vehicle(v, color, shadow_color)
158
+ else:
159
+ self.draw_circle_vehicle(v, color, shadow_color)
160
+
161
+ def draw_rect_vehicle(self, v, color, shadow):
162
+ """
163
+ Draw a rectangular vehicle (car, truck, bus).
164
+
165
+ Args:
166
+ v (Vehicle): The vehicle to draw.
167
+ color (tuple): The color of the vehicle.
168
+ shadow (tuple): The shadow color of the vehicle.
169
+ """
170
+ if v.vehicle_type == 'car':
171
+ w, h = 12, 20
172
+ elif v.vehicle_type == 'truck':
173
+ w, h = 14, 35
174
+ else:
175
+ w, h = 14, 40 # bus
176
+
177
+ rect = pygame.Rect(0, 0, w, h)
178
+ rect.center = (int(v.x), int(v.y))
179
+ shadow_rect = rect.copy()
180
+ shadow_rect.x += 2
181
+ shadow_rect.y += 2
182
+
183
+ pygame.draw.rect(self.screen, shadow, shadow_rect, border_radius=3)
184
+ pygame.draw.rect(self.screen, color, rect, border_radius=3)
185
+
186
+ def draw_circle_vehicle(self, v, color, shadow):
187
+ """
188
+ Draw a circular vehicle (scooter, motorcycle).
189
+
190
+ Args:
191
+ v (Vehicle): The vehicle to draw.
192
+ color (tuple): The color of the vehicle.
193
+ shadow (tuple): The shadow color of the vehicle.
194
+ """
195
+ r = 5 if v.vehicle_type=='scooter' else 6
196
+ center = (int(v.x), int(v.y))
197
+ shadow_c = (center[0]+2, center[1]+2)
198
+
199
+ pygame.draw.circle(self.screen, shadow, shadow_c, r)
200
+ pygame.draw.circle(self.screen, color, center, r)
201
+
202
+ def draw_ui_panel(self):
203
+ """
204
+ Draw the UI panel with simulation information.
205
+ """
206
+ if self.sim.adaptive_signals:
207
+ sig_text = f"Signal: {self.sim.signal_state.upper()} for {self.sim.current_green} (timer: {self.sim.state_timer})"
208
+ else:
209
+ sig_text = ""
210
+ lines = [
211
+ f"Time= {self.sim.sim_time}/{self.sim.total_time}",
212
+ f"Speed= {self.sim.simulation_speed} steps/sec",
213
+ f"JunctionType= {self.sim.junction_type}, MultiLights= {self.sim.multiple_lights}",
214
+ f"MultiLanes= {self.sim.multiple_lanes}, LaneCount= {self.sim.lane_count}",
215
+ f"IndiaMode= {self.sim.india_mode}",
216
+ f"SimulateFullRoute= {self.sim.simulate_full_route}",
217
+ sig_text,
218
+ "Close window or press Ctrl+C to quit."
219
+ ]
220
+ x = 10
221
+ y = 10
222
+ for txt in lines:
223
+ surf = self.font.render(txt, True, (255,255,255))
224
+ self.screen.blit(surf, (x, y))
225
+ y += 20
traffisim/run.py ADDED
@@ -0,0 +1,173 @@
1
+ """
2
+ run.py
3
+ ------
4
+ Functions to run multiple simulations, collect data, and optionally
5
+ provide a CLI entry point.
6
+ """
7
+
8
+ import os
9
+ import csv
10
+
11
+ from .intersection import IntersectionSim, ARRIVAL_RATES
12
+ from .models import DEFAULT_VEHICLE_TYPES
13
+ from .rendering import TrafficRenderer
14
+
15
+ def run_multiple_simulations(
16
+ N_runs=1,
17
+ csv_filename="simulation_results.csv",
18
+ junction_type="4way",
19
+ multiple_lights=False,
20
+ total_time=300,
21
+ simulation_speed=30,
22
+ save_to_files=True,
23
+ output_folder="simulation_outputs",
24
+ multiple_lanes=False,
25
+ lane_count=2,
26
+ yellow_duration=5,
27
+ all_red_duration=2,
28
+ vehicle_distribution=None,
29
+ india_mode=False,
30
+ show_visuals=True,
31
+ simulate_full_route=True,
32
+ adaptive_signals=True
33
+ ):
34
+ directions = ['N','E','S','W'] if junction_type=='4way' else ['N','E','W']
35
+
36
+ summary_fieldnames = [
37
+ "SimulationRun",
38
+ "JunctionType",
39
+ "MultipleLights",
40
+ "MultipleLanes",
41
+ "LaneCount",
42
+ "TotalVehiclesProcessed",
43
+ "OverallAvgWait",
44
+ "SimulateFullRoute",
45
+ ]
46
+ for d in directions:
47
+ summary_fieldnames.append(f"RedEmpty{d}")
48
+ for vt in DEFAULT_VEHICLE_TYPES.keys():
49
+ summary_fieldnames.append(f"Count{vt.capitalize()}")
50
+
51
+ if save_to_files:
52
+ os.makedirs(output_folder, exist_ok=True)
53
+ summary_csv_path = os.path.join(output_folder, csv_filename) if save_to_files else None
54
+ file_exists = (save_to_files and os.path.isfile(summary_csv_path))
55
+
56
+ all_results = []
57
+ for run_idx in range(1, N_runs+1):
58
+ print(f"\n=== Starting Simulation Run {run_idx}/{N_runs} ===\n")
59
+ renderer_class = TrafficRenderer if show_visuals else None
60
+
61
+ sim = IntersectionSim(
62
+ junction_type=junction_type,
63
+ multiple_lights=multiple_lights,
64
+ total_time=total_time,
65
+ simulation_speed=simulation_speed,
66
+ multiple_lanes=multiple_lanes,
67
+ lane_count=lane_count,
68
+ yellow_duration=yellow_duration,
69
+ all_red_duration=all_red_duration,
70
+ vehicle_distribution=vehicle_distribution,
71
+ india_mode=india_mode,
72
+ show_visuals=show_visuals,
73
+ renderer_class=renderer_class,
74
+ simulate_full_route=simulate_full_route,
75
+ adaptive_signals=adaptive_signals
76
+ )
77
+ sim.run()
78
+ sim.print_statistics()
79
+
80
+ if save_to_files:
81
+ run_csv = os.path.join(output_folder, f"run_{run_idx}.csv")
82
+ if sim.per_timestep_data:
83
+ fields = list(sim.per_timestep_data[0].keys())
84
+ else:
85
+ fields = ["TimeStep"]
86
+
87
+ with open(run_csv, 'w', newline='') as f:
88
+ writer = csv.DictWriter(f, fieldnames=fields)
89
+ writer.writeheader()
90
+ for row in sim.per_timestep_data:
91
+ writer.writerow(row)
92
+
93
+ summary_row = sim.get_results_dict()
94
+ summary_row["SimulationRun"] = run_idx
95
+ with open(summary_csv_path, 'a', newline='') as f:
96
+ writer = csv.DictWriter(f, fieldnames=summary_fieldnames)
97
+ if (not file_exists) and run_idx == 1:
98
+ writer.writeheader()
99
+ writer.writerow(summary_row)
100
+ file_exists = True
101
+
102
+ all_results.append(sim.get_results_dict())
103
+
104
+ if save_to_files and len(all_results) > 0:
105
+ avg_row = compute_average_row(all_results, directions)
106
+ avg_row["SimulationRun"] = "Average"
107
+ with open(summary_csv_path, 'a', newline='') as f:
108
+ writer = csv.DictWriter(f, fieldnames=summary_fieldnames)
109
+ writer.writerow(avg_row)
110
+ print("Average row appended to summary CSV.")
111
+
112
+ def compute_average_row(all_results, directions):
113
+ n = len(all_results)
114
+ if n == 0:
115
+ return {}
116
+ sum_tvp = 0
117
+ sum_wait = 0
118
+ sum_red = {d: 0 for d in directions}
119
+ sum_counts = {vt: 0 for vt in DEFAULT_VEHICLE_TYPES.keys()}
120
+
121
+ # We'll copy some fields from the last result (assuming they are all same config).
122
+ jt = all_results[-1]["JunctionType"]
123
+ ml = all_results[-1]["MultipleLights"]
124
+ mls = all_results[-1]["MultipleLanes"]
125
+ lc = all_results[-1]["LaneCount"]
126
+ sfr = all_results[-1]["SimulateFullRoute"]
127
+
128
+ for r in all_results:
129
+ sum_tvp += r["TotalVehiclesProcessed"]
130
+ sum_wait += r["OverallAvgWait"]
131
+ for d in directions:
132
+ sum_red[d] += r[f"RedEmpty{d}"]
133
+ for vt in DEFAULT_VEHICLE_TYPES.keys():
134
+ key = "Count" + vt.capitalize()
135
+ sum_counts[vt] += r[key]
136
+
137
+ avg_row = {
138
+ "JunctionType": jt,
139
+ "MultipleLights": ml,
140
+ "MultipleLanes": mls,
141
+ "LaneCount": lc,
142
+ "SimulateFullRoute": sfr,
143
+ "TotalVehiclesProcessed": sum_tvp / n,
144
+ "OverallAvgWait": sum_wait / n
145
+ }
146
+ for d in directions:
147
+ avg_row[f"RedEmpty{d}"] = sum_red[d] / n
148
+ for vt in DEFAULT_VEHICLE_TYPES.keys():
149
+ key = "Count" + vt.capitalize()
150
+ avg_row[key] = sum_counts[vt] / n
151
+ return avg_row
152
+
153
+ def main_cli():
154
+ """
155
+ Very minimal CLI for demonstration.
156
+ You can expand with argparse to parse arguments from the command line.
157
+ """
158
+ print("Running a default simulation from CLI for demonstration...")
159
+ run_multiple_simulations(
160
+ N_runs=1,
161
+ csv_filename="cli_results.csv",
162
+ junction_type="4way",
163
+ multiple_lights=False,
164
+ total_time=300,
165
+ simulation_speed=10,
166
+ save_to_files=False,
167
+ multiple_lanes=True,
168
+ lane_count=3,
169
+ india_mode=False,
170
+ show_visuals=False,
171
+ simulate_full_route=True,
172
+ adaptive_signals=True
173
+ )