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/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
+ )