558 lines
19 KiB
Plaintext
558 lines
19 KiB
Plaintext
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]<self.min_spacing:
|
|
continue
|
|
points.append([x,y])
|
|
break
|
|
self.spawn_coords = np.array(points)
|
|
self.filled_coords = np.zeros(len(points),dtype=bool)
|
|
|
|
|
|
def get_coords(self)->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)<len(all_agents) and\
|
|
np.sum(~self.filled_coords) > 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;
|
|
|