I'm creating a simulation using JuPedSim to see how pedestrians interact with a crosswalk system. The crosswalk system will have a user-defined "active" interval, and will be activated by agents being within a predefined distance. All agents will spawn in a specified spawn area, and are attempting to get from one side of the road to the other. They may only use the crosswalk to cross, and can only cross when the crosswalk is "active". Below are provided 4 separate files to create the JuPedSim simulation model: ============================ (1.1) ============================ =>geometry_setup.py(file): class GeometrySetup(current_setup:bool) spawn:defines the Polygon for the spawn_area queue:defines the Polygon for the queue_area trigger:defines the Polygon for the trigger_area crosswalk:defines the Polygon for the crosswalk_area grass:defines the Polygon for the grass_area ============================ (1.2) ============================ =>spawning.py(file): class SpawnManager(spawn_area:Polygon,min_spacing:float): __init__: spawn_area: area for valid spawns min_spacing: minimum spacing between spawn points spawn_coords: all coordinates for spawning agents filled_coords: spawn points currently occupied by agents spawned_agents: set of agent ids already spawned agent_pos: dictionary which connects agent ids to a spawn point generate_coords: uses Poisson disk method with CDTree from scipy.spatial for all spawn points get_coords: provides a tuple[float,float] or None based on if there is room to spawn agents spawn_agent: provides AgentSetup object or None depending on if there is room to spawn and there are agents left to spawn unfill_coords: returns None, used to indicate a spawn point has been made available by an agent moving get_agent_pos; returns (0,0) or the spawn coordinates of agents if they are in agent_pos check_spawn_complete: checks whether the number of agents spawned is the same as all_agents list, and if all spawn points are empty. ============================ (1.3) ============================ =>agents(folder): |>agent_data.py(file): | dictionary with group keys and values |>agent_setup.py(file): defines the AgentSetup class and computes list of all_agents: """ @dataclass class AgentSetup: id:int grade:str speed:float radius:float """ ============================ (1.4) ============================ =>crosswalk_setup.py(file): class CrosswalkStatus(Enum): INACTIVE ACTIVE 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: __init__(self,config:CrosswalkConfig,current_setup:bool): update: calls the following methods, then returns get_state() update_agents_in_trigger update_state update_statistics update_agents_in_trigger: updates what agents are in the trigger_area for the crosswalk @staticmethod points_in_area:finding what points lie in a given area update_state:updates crosswalk state to be COOLDOWN,ACTIVE or INACTIVE based on specified parameters activate:changes the system to the ACTIVE state deactivate:changes the system to the COOLDOWN state update_statistics:updates info on crosswalks if status is ACTIVE get_state: provides the state based on the current time in relation to the activation time, then updates the system can_agent_cross: checks whether ACTIVE or INACTIVE to either let agent pass or add agent to waiting group @property is_active: provides a boolean response whether the system is ACTIVE or not get_statistics:provides stats from the system at the current time ============ here is the code: ============================ (2.1) (geometry_setup.py) ============================ import sys import matplotlib matplotlib.use('QtAgg') import matplotlib.pyplot as plt from shapely import Polygon from shapely.plotting import plot_polygon class GeometrySetup: def __init__(self,current_setup:bool): self.current_setup = current_setup self.spawn() self.queue() self.trigger() self.crosswalk() self.grass() 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), ]) plot_polygon(spawn_area,color="purple",add_points=False) return spawn_area def queue(self)->Polygon: if self.current_setup is True: queue_area = Polygon([ (18.767,9.395), (16.924,9.395), (16.924,4.569), (25.0,4.569), (25.0,16.823) ]) else: queue_area = Polygon([ (19.531,10.306), (15.214,10.306), (15.214,0), (16.924,0), (16.924,4.569), (25.0,4.569), (25.0,16.823) ]) plot_polygon(queue_area,color="blue",add_points=False) return queue_area def trigger(self)->Polygon: if self.current_setup is True: trigger_area = Polygon([ (15.214,0), (15.214,9.395), (16.924,9.395), (16.924,0) ]) else: trigger_area = Polygon([ (15.214,10.306), (15.214,0), (12.98,0), (12.98,9.896), (13.88,10.306) ]) plot_polygon(trigger_area,color="green",add_points=False) return trigger_area def crosswalk(self)->Polygon: if self.current_setup is True: crosswalk_area = Polygon([ (4,6.068), (4,7.896), (4,7.896), (15.214,7.896), (15.214,6.068), (4,6.068) ]) else: crosswalk_area = Polygon([ (6.23,4.982), (6.23,8.982), (12.98,8.982), (12.98,4.982) ]) plot_polygon(crosswalk_area,color="red",add_points=False) return crosswalk_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) ]) plot_polygon(grass_area,color="blue",add_points=False) return grass_area if __name__ == "__main__": from PyQt6 import QtWidgets app = QtWidgets.QApplication(sys.argv) geo_map = GeometrySetup(False) plt.show(block=False) sys.exit(app.exec()) ============================ (2.2) (spawning.py) ============================ from shapely.geometry import Polygon,Point from typing import Tuple, List, Dict, Set from scipy.spatial import cKDTree import sys import math import numpy as np import config sys.path.insert(0,str(config.AGENTS_DIR)) from agents.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 = set() self.agent_pos:dict={} self.rng = np.random.default_rng() def generate_coords(self,max_samples:int=1000)->None: min_x,min_y,max_x,max_y = self.spawn_area.bounds points = [] while len(points) ==0: x = self.rng.uniform(min_x,max_x) y = self.rng.uniform(min_y,max_y) if self.spawn_area.contains(Point(x,y)): points.append([x,y]) for _ in range(max_samples): idx = self.rng.integers(0,len(points),endpoint=True) 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 self.spawn_area.contains(Point(x,y)): continue if len(points)>0: tree = cKDTree(points) distance,_ = tree.query([[x,y]],k=1) if distance[0]Tuple[float,float]|None: ''' points = [ pt for pt in self.generate_coords if pt \ not in self.filled_coords.values() ] return self.rng.choice(points) if points else 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) 0) ============================ (2.3) (agent_setup.py) ============================ from agent_data import grade_data from dataclasses import dataclass import numpy as np from typing import List @dataclass class AgentSetup: id:int grade:str speed:float radius:float def AgentConfig()->List[List[AgentSetup],List[int]]: agent_id = 1 pop_list = [] all_agents = [] for idx,key in enumerate(grade_data.keys()): grade = grade_data[key] pop_list.append(grade["Pop Current"]) for i in range(pop_list[idx]): rng = np.random.default_rng() agent = AgentSetup( id=agent_id, grade=key, speed=rng.normal( grade["Speed Mean"], grade["Speed Std Dev"] ), radius=grade["Radius"] ) all_agents.append(agent) agent_id +=1 return all_agents, pop_list ============================ (2.4) (crosswalk_setup.py) ============================ 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) x,y = points[:,0],points[:,1] vertices = shapely.get_coordinates(polygon) n = len(vertices) inside = np.zeros(len(points),dtype=bool) j = n-1 for i in range(n): xi, yi = vertices[i] xj, yj = vertices[j] mask = ((yi>y) != (yj>y)) & (x<(xj-xi)*(y-yi)/(yj-yi)+xi) inside ^= mask j = i 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_duration-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) } ========================= The last part which needs to be done is using the JuPedSim module to create a full simulation using all these pieces;