This commit is contained in:
Varyngoth
2026-01-28 13:31:49 -04:00
commit 676659e5b9
239 changed files with 7509 additions and 0 deletions

132
_source_/agent_data.py Normal file
View File

@@ -0,0 +1,132 @@
grade_data = {
"Kindergarden":{
#"Door":0,
"Pop Current":34,
"Pop Mean":31.43,
"Pop Std Dev":5.65,
"Speed Mean":1.21,
"Speed Std Dev":0.24,
"Radius":0.407,
#"Spawn Time":None
},
"Grade 1":{
#"Door":0,
"Pop Current":26,
"Pop Mean":32.57,
"Pop Std Dev":6.27,
"Speed Mean":1.35,
"Speed Std Dev":0.26,
"Radius":0.407,
#"Spawn Time":None
},
"Grade 2":{
#"Door":0,
"Pop Current":42,
"Pop Mean":34.43,
"Pop Std Dev":6.80,
"Speed Mean":1.42,
"Speed Std Dev":0.28,
"Radius":0.407,
#"Spawn Time":None
},
"Grade 3":{
#"Door":0,
"Pop Current":39,
"Pop Mean":35.43,
"Pop Std Dev":5.19,
"Speed Mean":1.48,
"Speed Std Dev":0.23,
"Radius":0.407,
#"Spawn Time":None
},
"Grade 4":{
#"Door":1,
"Pop Current":30,
"Pop Mean":34.86,
"Pop Std Dev":6.77,
"Speed Mean":1.58,
"Speed Std Dev":0.26,
"Radius":0.417,
#"Spawn Time":None
},
"Grade 5":{
#"Door":1,
"Pop Current":43,
"Pop Mean":36.71,
"Pop Std Dev":7.09,
"Speed Mean":1.59,
"Speed Std Dev":0.24,
"Radius":0.434,
#"Spawn Time":None
},
"Grade 6":{
#"Door":1,
"Pop Current":29,
"Pop Mean":37.71,
"Pop Std Dev":6.99,
"Speed Mean":1.65,
"Speed Std Dev":0.24,
"Radius":0.454,
#"Spawn Time":None
},
"Grade 7":{
#"Door":2,
"Pop Current":45,
"Pop Mean":40.43,
"Pop Std Dev":6.02,
"Speed Mean":1.61,
"Speed Std Dev":0.25,
"Radius":0.471,
#"Spawn Time":None
},
"Grade 8":{
#"Door":2,
"Pop Current":36,
"Pop Mean":40.43,
"Pop Std Dev":5.50,
"Speed Mean":1.66,
"Speed Std Dev":0.24,
"Radius":0.488,
#"Spawn Time":None
},
"Grade 9":{
#"Door":2,
"Pop Current":44,
"Pop Mean":44.14,
"Pop Std Dev":4.85,
"Speed Mean":1.60,
"Speed Std Dev":0.24,
"Radius":0.500,
#"Spawn Time":None
},
"Grade 10":{
#"Door":2,
"Pop Current":36,
"Pop Mean":46.29,
"Pop Std Dev":6.29,
"Speed Mean":1.57,
"Speed Std Dev":0.23,
"Radius":0.507,
#"Spawn Time":None
},
"Grade 11":{
#"Door":2,
"Pop Current":54,
"Pop Mean":48.29,
"Pop Std Dev":3.30,
"Speed Mean":1.51,
"Speed Std Dev":0.22,
"Radius":0.515,
#"Spawn Time":None
},
"Grade 12":{
#"Door":2,
"Pop Current":46,
"Pop Mean":43.71,
"Pop Std Dev":6.02,
"Speed Mean":1.54,
"Speed Std Dev":0.23,
"Radius":0.520,
#"Spawn Time":None
}}

35
_source_/agent_setup.py Normal file
View File

@@ -0,0 +1,35 @@
from agent_data import grade_data
from dataclasses import dataclass
import numpy as np
from typing import List,Tuple,Dict
@dataclass
class AgentSetup:
id:int
grade:str
speed:float
radius:float
def AgentConfig()->Tuple[List[AgentSetup],Dict[str,int]]:
agent_id = 1
pop_count = {}
all_agents = []
for key in grade_data.keys():
grade = grade_data[key]
#pop_count.append(grade["Pop Current"])
pop_count[key] = grade["Pop Current"]
for _ in range(grade["Pop Current"]):
rng = np.random.default_rng()
agent = AgentSetup(
id=agent_id,
grade=key,
speed=max(0.1,(rng.normal(
grade["Speed Mean"],
grade["Speed Std Dev"]
))),
radius=grade["Radius"]
)
all_agents.append(agent)
agent_id +=1
return all_agents,pop_count

17
_source_/config.py Normal file
View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
E.Drake - ENGN-2220
Thu Jan 22 23:48:50 2026
"""
from pathlib import Path
ROOT = Path(__file__).parent.parent
SOURCE_DIR = ROOT/"_source_"
ARCHIVE_DIR = ROOT/"archive"
PATH_DIR = ROOT/"path"
AGENTS_DIR = ROOT/SOURCE_DIR/"agents"
#GEO_DIR = ROOT/SOURCE_DIR/"sim_geometry"
#TEST_DIR = ROOT/SOURCE_DIR/"test"

153
_source_/crosswalk_setup.py Normal file
View File

@@ -0,0 +1,153 @@
from dataclasses import dataclass,field
from enum import Enum
from typing import List,Dict,Tuple,Set
from shapely import Polygon,Point
from geometry_setup import GeometrySetup
import shapely
import numpy as np
class CrosswalkStatus(Enum):
INACTIVE = "inactive"
ACTIVE = "active"
COOLDOWN = "cooldown"
@dataclass(frozen=True)
class CrosswalkConfig:
trigger_dist:float
activation_time:float
cooldown_time:float
min_agents_activation:int
activation_delay:float
crosswalk_area:Polygon
trigger_area:Polygon
@dataclass
class CrosswalkState:
status:CrosswalkStatus
is_active:bool
time_active:float
time_remaining:float
agents_in_trigger:int
agents_waiting:int
activation_count:int
class CrosswalkController:
def __init__(self,config:CrosswalkConfig,current_setup:bool):
self.config = config
self.crosswalk_area = config.crosswalk_area
self.trigger_area = config.trigger_area
self.status = CrosswalkStatus.INACTIVE
self.state_start_time = 0.0
self.current_time = 0.0
self.activation_count = 0
self.agents_in_trigger: Set[int] = set()
self.agents_waiting: Set[int] = set()
self.total_active_time = 0.0
self.agents_served = 0
def update(
self,
agent_pos:Dict[int,Tuple[float,float]],
time_step:float,
current_time:float=None
)->CrosswalkState:
if current_time is not None:
self.current_time = current_time
else:
self.current_time +=time_step
self.update_agents_in_trigger(agent_pos)
self.update_state(time_step)
self.update_statistics(time_step)
return self.get_state()
def update_agents_in_trigger(
self,
agent_pos:Dict[int,Tuple[float,float]]
)->None:
self.agents_in_trigger.clear()
if not agent_pos:
return
agent_ids = list(agent_pos.keys())
positions = np.array(list(agent_pos.values()),dtype=np.float32)
in_trigger = self.points_in_area(positions,self.trigger_area)
for i, agent_id in enumerate(agent_ids):
if in_trigger[i]:
self.agents_in_trigger.add(agent_id)
@staticmethod
def points_in_area(points:np.ndarray,polygon:Polygon)->np.ndarray:
if len(points) ==0:
return np.array([],dtype=bool)
buffered_area = polygon.buffer(1e-9)
inside = np.zeros(len(points),dtype=bool)
for i,(x,y) in enumerate(points):
point = Point(x,y)
inside[i] = buffered_area.contains(point)
return inside
def update_state(self,time_step:float)->None:
elapsed = self.current_time - self.state_start_time
if self.status == CrosswalkStatus.ACTIVE:
if elapsed >= self.config.activation_time:
self.deactivate()
elif self.status == CrosswalkStatus.COOLDOWN:
if elapsed >= self.config.cooldown_time:
self.status = CrosswalkStatus.INACTIVE
self.state_start_time = self.current_time
elif self.status ==CrosswalkStatus.INACTIVE:
if (len(self.agents_in_trigger)>=self.config.min_agents_activation and \
elapsed >= self.config.activation_delay):
self.activate()
def activate(self)->None:
self.status = CrosswalkStatus.ACTIVE
self.state_start_time = self.current_time
self.activation_count +=1
self.agents_served += len(self.agents_waiting)
self.agents_waiting.clear()
def deactivate(self)->None:
self.status = CrosswalkStatus.COOLDOWN
self.state_start_time = self.current_time
def update_statistics(self,time_step:float)->None:
if self.status == CrosswalkStatus.ACTIVE:
self.total_active_time += time_step
def get_state(self)->CrosswalkState:
elapsed = self.current_time-self.state_start_time
if self.status == CrosswalkStatus.ACTIVE:
time_remaining = max(0.0,self.config.activation_time-elapsed)
elif self.status == CrosswalkStatus.COOLDOWN:
time_remaining = max(0.0,self.config.cooldown_time-elapsed)
else:
time_remaining = 0.0
return CrosswalkState(
status=self.status,
is_active=(self.status == CrosswalkStatus.ACTIVE),
time_active=elapsed if self.status == CrosswalkStatus.ACTIVE else 0.0,
time_remaining=time_remaining,
agents_in_trigger=len(self.agents_in_trigger),
agents_waiting=len(self.agents_waiting),
activation_count = self.activation_count
)
def can_agent_cross(self,agent_id:int)->bool:
if self.status == CrosswalkStatus.ACTIVE:
return True
else:
if agent_id in self.agents_in_trigger:
self.agents_waiting.add(agent_id)
return False
@property
def is_active(self)->bool:
return self.status == CrosswalkStatus.ACTIVE
def get_statistics(self)->Dict[str,float]:
return {
"total_activations":self.activation_count,
"total_active_time":self.total_active_time,
"agents_served":self.agents_served,
"current_agents_in_trigger":len(self.agents_in_trigger),
"current_agents_waiting":len(self.agents_waiting)
}

279
_source_/geometry_setup.py Normal file
View File

@@ -0,0 +1,279 @@
import sys
import matplotlib
matplotlib.use('QtAgg')
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from shapely import Polygon
from shapely.plotting import plot_polygon
from typing import Tuple
class GeometrySetup:
def __init__(self,current_setup:bool):
self.current_setup = current_setup
self.spawn_area = self.spawn()
self.queue_area = self.queue()
self.grass_area = self.grass()
self.crosswalk_area = self.crosswalk()
self.entry_area = self.entry_polygon()
self.exit_area = self.exit_polygon()
def spawn(self)->Polygon:
spawn_area = Polygon([
(25.0,16.823),
(25.0,4.569),
(26.163,4.569),
(28.214,5.07),
(28.214,17.761),
(25.787,17.761),
])
return spawn_area
def queue(self)->Polygon:
if self.current_setup is True:
queue_area = Polygon([
(18.767,9.395),
(16.924,9.395),
(15.214,9.395),
(15.214,0),
(16.924,0),
(16.924,4.569),
(25.0,4.569),
(25.0,16.823)
])
else:
queue_area = Polygon([
(19.531,10.306),
(15.214,10.306),
(13.88,10.306),
(12.98,9.896),
(12.98,0),
(15.214,0),
(16.924,0),
(16.924,4.569),
(25.0,4.569),
(25.0,16.823)
])
return queue_area
def grass(self)->Polygon:
if self.current_setup is True:
grass_area = Polygon([
(4,0),
(0,0),
(0,17.761),
(4,17.761)
])
else:
grass_area = Polygon([
(6.23,0),
(0,0),
(0,17.761),
(4,17.761),
(4,10.306),
(5.33,10.306),
(6.23,9.896)
])
return grass_area
def crosswalk(self)->Polygon:
if self.current_setup is True:
crosswalk_area = Polygon([
(4,6.068),
(4,7.896),
(15.214,7.896),
(15.214,6.068)
])
else:
crosswalk_area = Polygon([
(6.23,4.982),
(6.23,8.982),
(12.98,8.982),
(12.98,4.982)
])
return crosswalk_area
def entry_polygon(self)->Polygon:
if self.current_setup is True:
entry_area = Polygon([ # x: 2.9m, y: 3.428m
(15.314,5.268), # dx: 0.1m, dy: 0.8m
(15.314,8.696),
(18.214,8.696),
(18.214,5.268)
])
else:
entry_area = Polygon([ # x: 2.9m, y: 5.6m
(15.98,9.782),
(15.98,4.182),
(13.08,4.182),
(13.08,9.782)
])
return entry_area
def exit_polygon(self)->Polygon:
if self.current_setup is True:
exit_area = Polygon([ # x: 2.9m, y: 3.428m
(1,5.268),
(1,8.696),
(3.9,8.696),
(3.9,5.268)
])
else:
exit_area = Polygon([ # x: 2.9m, y: 5.6m
(3.23,4.182),
(3.23,9.782),
(6.13,9.782),
(6.13,4.182)
])
return exit_area
def plot_all(self)->Tuple[Figure,Axes]:
fig,ax = plt.subplots(figsize=(12,8))
plot_polygon(
self.spawn_area,
color="lightblue",
add_points=False,
ax=ax,
alpha=0.4
)
plot_polygon(
self.queue_area,
color="blue",
add_points=False,
ax=ax,
alpha=0.4
)
plot_polygon(
self.grass_area,
color="blue",
add_points=False,
ax=ax,
alpha=0.4
)
plot_polygon(
self.crosswalk_area,
color="red",
add_points=False,
ax=ax,
alpha=0.4
)
plot_polygon(
self.entry_area,
color="green",
add_points=False,
ax=ax,
alpha=0.4
)
plot_polygon(
self.exit_area,
color="green",
add_points=False,
ax=ax,
alpha=0.4
)
plt.plot(
*self.spawn_area.exterior.xy,
color='black',
alpha=0.7,
linewidth=1
)
plt.plot(
*self.queue_area.exterior.xy,
color='black',
alpha=0.7,
linewidth=1
)
plt.plot(
*self.grass_area.exterior.xy,
color='black',
alpha=0.7,
linewidth=1
)
plt.plot(
*self.crosswalk_area.exterior.xy,
color='black',
alpha=0.7,
linewidth=1
)
plt.plot(
*self.entry_area.exterior.xy,
color='black',
alpha=0.7,
linewidth=1
)
plt.plot(
*self.exit_area.exterior.xy,
color='black',
alpha=0.7,
linewidth=1
)
ax.text(
self.spawn_area.centroid.x,
self.spawn_area.centroid.y,
'Spawn\nArea',
ha='center',
va='center',
fontweight='bold'
)
ax.text(
self.queue_area.centroid.x,
self.queue_area.centroid.y,
'Queue',
ha='center',
va='center',
fontweight='bold'
)
ax.text(
self.crosswalk_area.centroid.x,
self.crosswalk_area.centroid.y,
'Crosswalk',
ha='center',
va='center',
fontweight='bold',
)
ax.text(
self.entry_area.centroid.x,
self.entry_area.centroid.y,
'Crosswalk\nEntry',
ha='center',
va='center',
fontsize=9,
fontweight='bold',
#color="white"
)
ax.text(
self.exit_area.centroid.x,
self.exit_area.centroid.y,
'Crosswalk\nExit',
ha='center',
va='center',
fontsize=9,
fontweight='bold',
#color="white"
)
ax.text(
(self.grass_area.centroid.x-0.9 if not\
self.current_setup else self.grass_area.centroid.x),
self.grass_area.centroid.y+2,
'Grass',
ha='center',
va='center',
fontweight='bold'
)
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_title(
f'{"Current" if self.current_setup else "Smart"} Crosswalk Design',
fontweight='bold',
fontsize=15
)
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
return fig,ax
if __name__ == "__main__":
from PyQt6 import QtWidgets
app = QtWidgets.QApplication(sys.argv)
fig,ax = GeometrySetup(True).plot_all()
plt.show(block=False)
sys.exit(app.exec())

View File

@@ -0,0 +1,9 @@
matplotlib
PyQt5
numpy
scipy
shapely
pedpy
jupedsim
pandas
plotly

253
_source_/simulation.py Normal file
View File

@@ -0,0 +1,253 @@
from typing import Tuple,List,Dict
import sys
from datetime import datetime as dt
import matplotlib
matplotlib.use('QtAgg')
import matplotlib.pyplot as plt
from shapely import Polygon,Point,GeometryCollection,get_coordinates
from shapely.plotting import plot_polygon
import jupedsim as jps
import numpy as np
import pathlib
from geometry_setup import GeometrySetup
from spawning import SpawnManager
from crosswalk_setup import CrosswalkStatus,CrosswalkController,CrosswalkState,CrosswalkConfig
from agent_setup import AgentConfig
from dataclasses import dataclass
@dataclass
class SimulationSetup:
simulation_time:float=200.0
time_step:float=0.01
max_iterations:int=50000
trajectory_file:str="crosswalk_sim.sqlite"
current_setup:bool=True
trigger_dist:float=1.0
activation_time:float=30.0
cooldown_time:float=10.0
min_agents_activation:int=1
activation_delay:float=2.0
class CrosswalkSimulation:
def __init__(self,config:SimulationSetup):
self.config = config
self.RED = "\033[1;31m"
self.GREEN = "\033[1;32m"
self.YELLOW = "\033[1;33m"
self.BLUE = "\033[1;34m"
self.PURPLE = "\033[1;35m"
self.CYAN = "\033[1;36m"
self.RESET = "\033[0m"
print(f"{self.YELLOW}\nInitializing Geometry...")
self.geo = GeometrySetup(self.config.current_setup)
self.spawn_area = self.geo.spawn()
self.queue_area = self.geo.queue()
self.grass_area = self.geo.grass()
self.crosswalk_area = self.geo.crosswalk()
self.entry_area = self.geo.entry_polygon()
self.exit_area = self.geo.exit_polygon()
self.walkable_area = GeometryCollection([
self.spawn_area,
self.queue_area,
self.grass_area,
self.crosswalk_area,
])
print(f"{self.GREEN}Geometry Configuration Complete!")
print(f"{self.YELLOW}\nInitializing Agent Setup...")
self.all_agents, pop_count = AgentConfig()
self.total_agents = len(self.all_agents)
self.req_spacing = (max([agent.radius for agent in self.all_agents])*2.2)
print(f"{self.GREEN}Agent Setup Complete!")
print(f"{self.RESET}Generated {self.total_agents} Agents")
print(f"Population Breakdown:\n"+"".join(f"\t{k}: {v}\n" for k,v in pop_count.items()))
print(f"{self.YELLOW}Initializing Spawn Manager...")
self.spawn_manager = SpawnManager(self.spawn_area,min_spacing=self.req_spacing)
self.all_coords = self.spawn_manager.generate_coords()
print(f"{self.GREEN}Spawn Manager Setup Complete!")
print(f"{self.RESET}Generated {len(self.all_coords)} Spawn Coordinates")
print(f"{self.YELLOW}\nInitializing Crosswalk Controller...")
self.crosswalk_controller = CrosswalkController(
CrosswalkConfig(
trigger_dist=self.config.trigger_dist,
activation_time=self.config.activation_time,
cooldown_time=self.config.cooldown_time,
min_agents_activation=self.config.min_agents_activation,
activation_delay=self.config.activation_delay,
crosswalk_area=self.crosswalk_area,
trigger_area=self.entry_area
),
self.config.current_setup
)
print(f"{self.GREEN}Crosswalk Controller Setup Complete!")
print(f"{self.YELLOW}\nInitializing JuPedSim Model...")
self.trajectory_file = self.config.trajectory_file
self.simulation = jps.Simulation(
model=jps.CollisionFreeSpeedModel(),
geometry=self.walkable_area,
trajectory_writer=jps.SqliteTrajectoryWriter(
output_file=pathlib.Path(self.trajectory_file)
),
dt=self.config.time_step
)
self.journey_setup()
self.jps_agent_ids:Dict[int,int] = {}
self.iteration_count = 0
self.current_time = 0.0
print(f"{self.GREEN}JuPedSim Model Initialized{self.RESET}")
print(f"\tTime Step: {self.config.time_step}s")
print(f"\tMax Iterations: {self.config.max_iterations}")
print(f"\tCrosswalk Activation Length: {self.config.activation_time}s")
print(f"\tCrosswalk Cooldown Length: {self.config.cooldown_time}s")
def journey_setup(self):
self.queue_waiting_coords = self.waiting_coords(
self.queue_area,
self.spawn_manager.min_spacing
)
self.queue_stage_id = self.simulation.add_waiting_set_stage(
self.queue_waiting_coords
)
self.queue_stage = self.simulation.get_stage(self.queue_stage_id)
self.queue_stage.state = jps.WaitingSetState.ACTIVE
self.crosswalk_entry_id = self.simulation.add_waypoint_stage((
self.entry_area.centroid.x,
self.entry_area.centroid.y),
self.spawn_manager.min_spacing
)
self.crosswalk_exit_id = self.simulation.add_waypoint_stage((
self.exit_area.centroid.x,
self.exit_area.centroid.y),
self.spawn_manager.min_spacing
)
self.exit_stage_id = self.simulation.add_waypoint_stage((
self.grass_area.centroid.x,
self.grass_area.centroid.y),
self.spawn_manager.min_spacing
)
self.journey = jps.JourneyDescription([
self.queue_stage_id,
self.crosswalk_entry_id,
self.crosswalk_exit_id,
self.exit_stage_id
])
self.journey.set_transition_for_stage(
self.queue_stage_id,
jps.Transition.create_fixed_transition(
self.crosswalk_entry_id
))
self.journey.set_transition_for_stage(
self.crosswalk_entry_id,
jps.Transition.create_fixed_transition(
self.crosswalk_exit_id
))
self.journey.set_transition_for_stage(
self.crosswalk_exit_id,
jps.Transition.create_fixed_transition(
self.exit_stage_id
))
self.journey_id = self.simulation.add_journey(
self.journey)
def waiting_coords(self,area:Polygon,spacing:float)->List[Tuple[float,float]]:
min_x,min_y,max_x,max_y = area.bounds
points = []
x = (min_x+spacing)/2
while x<max_x:
y = (min_y+spacing)/2
while y < max_y:
if area.contains(Point(x, y)):
points.append((x,y))
y += spacing
x += spacing
return points if points else [(min_x+0.5, min_y+0.5)]
def run(self):
print(f"{self.CYAN}\nStarting Simulation")
print(f"Target Agents: {len(self.all_agents)}\n{self.RESET}")
while self.iteration_count < self.config.max_iterations:
if self.spawn_manager.check_spawn_complete(self.all_agents):
new_agent = self.spawn_manager.spawn_agent(self.all_agents)
if new_agent:
jps_agent_params = jps.CollisionFreeSpeedModelAgentParameters(
journey_id=self.journey_id,
stage_id=self.queue_stage_id,
position=self.spawn_manager.get_agent_pos(new_agent.id),
desired_speed=new_agent.speed,
radius=new_agent.radius
)
jps_agent_id = self.simulation.add_agent(jps_agent_params)
self.jps_agent_ids[new_agent.id] = jps_agent_id
#if self.iteration_count % 100 ==0:
# print(f"{self.YELLOW}[Iteration: {self.iteration_count:05d}]{self.RESET} Total Agents Spawned {len(self.jps_agent_ids)}")
print(f"{self.YELLOW}[Iteration: {self.iteration_count:05d}]{self.RESET} Total Agents Spawned {len(self.jps_agent_ids)}")
current_agent_pos = {}
for spawn_id,jps_id in self.jps_agent_ids.items():
try:
agent = self.simulation.agent(jps_id)
current_agent_pos[spawn_id] = (agent.position[0],agent.position[1])
except RuntimeError:
continue
crosswalk_state = self.crosswalk_controller.update(
current_agent_pos,
self.config.time_step,
self.current_time
)
if crosswalk_state.is_active:
if self.queue_stage.state != jps.WaitingSetState.INACTIVE:
self.queue_stage.state = jps.WaitingSetState.INACTIVE
print(f"{self.YELLOW}[Iteration: {self.iteration_count:05d}]{self.RESET} Crosswalk ACTIVATED")
else:
if self.queue_stage.state != jps.WaitingSetState.ACTIVE:
self.queue_stage.state = jps.WaitingSetState.ACTIVE
self.simulation.iterate()
self.current_time += self.config.time_step
self.iteration_count +=1
agents_to_remove = []
for spawn_id, jps_id in list(self.jps_agent_ids.items()):
try:
_ = self.simulation.agent(jps_id)
except RuntimeError:
agents_to_remove.append(spawn_id)
for spawn_id in agents_to_remove:
self.spawn_manager.unfill_coords(spawn_id)
del self.jps_agent_ids[spawn_id]
print(f"{self.YELLOW}[Iteration: {self.iteration_count:05d}]{self.RESET} JOURNEY COMPLETE: Agent {spawn_id}")
if (len(self.jps_agent_ids) ==0 and not self.spawn_manager.check_spawn_complete(self.all_agents)):
print(f"{self.GREEN}\nSimulation Completed at Iteration: {self.iteration_count}")
print(f"Total Simulation Time: {self.current_time:.2f} s")
break
print("\n")
print(f"{self.GREEN}=== SIMULATION COMPLETE ===")
stats = self.crosswalk_controller.get_statistics()
print(f"{self.YELLOW}Crosswalk Activations: {self.RESET}{stats['total_activations']}")
print(f"{self.YELLOW}Agents Served: {self.RESET}{stats['agents_served']}")
print(f"{self.PURPLE}Trajectory File:\n{self.trajectory_file}{self.RESET}\n")
return
def main():
config = SimulationSetup(
simulation_time=200.0,
time_step=0.01,
max_iterations=50000,
trajectory_file=f"_SRS_sim_{dt.now().strftime("%H%M%S")}_.sqlite",
current_setup=True,
trigger_dist=1.0,
activation_time=30.0,
cooldown_time=10.0,
min_agents_activation=1,
activation_delay=2.0,
)
sim = CrosswalkSimulation(config)
sim.run()
if __name__ == "__main__":
main()

102
_source_/spawning.py Normal file
View File

@@ -0,0 +1,102 @@
from shapely.geometry import Polygon,Point
from typing import Tuple, List, Dict, Set
from scipy.spatial import cKDTree
import sys
import numpy as np
from agent_setup import AgentSetup
class SpawnManager:
def __init__(self,spawn_area:Polygon,min_spacing:float=0.55):
"""
self.spawn_area: geometry where agents may spawn
self.min_spacing: minimum spacing for spawn_points
self.spawn_coords: all spawn points available
self.filled_coords: spawn points currently filled by agents
self.spawned_agents: all agents already spawned in sim
self.agent_pos: connects agent_id to spawn_index
self.rng: random number generator object
"""
self.spawn_area = spawn_area
self.min_spacing = min_spacing
self.spawn_coords: np.ndarray = np.array([])
self.filled_coords: np.ndarray = np.array([])
self.spawned_agents:Set[int] = set()
self.agent_pos:Dict[int,int] = {}
self.rng = np.random.default_rng()
def generate_coords(self,max_samples:int=1000)->None:
buffered_spawn = self.spawn_area.buffer(-self.min_spacing)
min_x,min_y,max_x,max_y = buffered_spawn.bounds
points = []
while len(points) ==0:
x = self.rng.uniform(min_x,max_x)
y = self.rng.uniform(min_y,max_y)
if buffered_spawn.contains(Point(x,y)):
points.append([x,y])
for _ in range(max_samples):
idx = self.rng.integers(0,len(points))
base = points[idx]
for _ in range(25):
angle = self.rng.uniform(0,2*np.pi)
radius = self.rng.uniform(self.min_spacing,2*self.min_spacing)
x = base[0]+radius*np.cos(angle)
y = base[1]+radius*np.sin(angle)
if not buffered_spawn.contains(Point(x,y)):
continue
if len(points)>0:
tree = cKDTree(points)
distance,_ = tree.query([[x,y]],k=1)
if distance[0]<self.min_spacing:
continue
points.append([x,y])
break
self.spawn_coords = np.array(points)
self.filled_coords = np.zeros(len(points),dtype=bool)
return self.spawn_coords
def get_coords(self)->Tuple[float,float]|None:
free_idx = np.where(~self.filled_coords)[0]
if len(free_idx) == 0:
return None
idx = self.rng.choice(free_idx)
return tuple(self.spawn_coords[idx])
def spawn_agent(self,all_agents:List[AgentSetup])->AgentSetup|None:
if len(self.spawned_agents) >= len(all_agents):
return None
spawn_point = self.get_coords()
if not spawn_point:
return None
free_agents = [agent for agent in all_agents \
if agent.id not in self.spawned_agents
]
if not free_agents:
return None
agent = self.rng.choice(free_agents)
self.spawned_agents.add(agent.id)
distances = np.linalg.norm(self.spawn_coords-spawn_point,axis=1)
spawn_idx = np.argmin(distances)
self.filled_coords[spawn_idx] = True
self.agent_pos[agent.id] = spawn_idx
return agent
def unfill_coords(self,agent_id:int)->None:
if agent_id in self.agent_pos:
spawn_idx = self.agent_pos[agent_id]
self.filled_coords[spawn_idx] = False
del self.agent_pos[agent_id]
self.spawned_agents.discard(agent_id)
def get_agent_pos(self,agent_id:int)->Tuple[float,float]:
if agent_id in self.agent_pos:
spawn_idx = self.agent_pos[agent_id]
return tuple(self.spawn_coords[spawn_idx])
return (0,0)
def check_spawn_complete(self,all_agents:List[AgentSetup])->bool:
return (len(self.spawned_agents)<len(all_agents) and\
np.sum(~self.filled_coords) > 0)

47
_source_/visualizer.py Normal file
View File

@@ -0,0 +1,47 @@
import sys
import matplotlib
matplotlib.use('QtAgg')
from PyQt5 import QtWidgets
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
from jupedsim.internal.notebook_utils import read_sqlite_file
def visualize(trajectory_file):
traj_data,walkable_area = read_sqlite_file(trajectory_file)
df = traj_data.data
fig,ax = plt.subplots(figsize=(12,8))
if hasattr(walkable_area,"polygon"):
x,y = walkable_area.polygon.exterior.xy
ax.plot(x,y,'k-',alpha=0.3)
scatter = ax.scatter([],[],s=20,c="blue",alpha=0.7)
frame_text = ax.text(0.02,0.98,'',transform=ax.transAxes)
frames=sorted(df["frame"].unique())
frames=frames[::5]
def update(frame):
frame_data = df[df["frame"]==frame]
if len(frame_data)>0:
positions=np.column_stack(
[frame_data["x"].values,frame_data["y"].values])
scatter.set_offsets(positions)
frame_text.set_text(f"Frame: {frame}, Agents: {len(frame_data)}")
return scatter, frame_text
anim = FuncAnimation(fig,update,frames=frames,blit=False,interval=1)
app = QtWidgets.QApplication(sys.argv)
plt.show(block=False)
sys.exit(app.exec())
if __name__ == "__main__":
file_1 = "SRS_evac_2025-11-23_23-03-09.sqlite"
file_2 = "SRS_evac_2025-11-24_01-03-06.sqlite"
file_3 = "SRS_evac_2025-11-30_19-43-15.sqlite"
file_4 = "crosswalk_sim.sqlite"
file_5 = "_SRS_sim_232009_.sqlite"
file_6 = "_SRS_sim_231809_.sqlite"
file_7 = "_SRS_sim_231233_.sqlite"
file_8 = "_SRS_sim_010542_.sqlite"
file_9 = "_SRS_sim_013834_.sqlite"
visualize(file_3)