Waiting in Queues#

This notebook can be directly downloaded here to run it locally.

In the following, we’ll simulate scenarios where agents are waiting in queues as an example for implementing crowd management measures. Since waiting behaviour is not a process that can be modelled by the operational model itself we explicitely need to define (and trigger) the waiting behaviour of the agents. In this example, we’ll simulate a scenario where people arrive to a concert and can approach the entrance by four line-up gates. At the gates a ticket control is performed that lasts 10 seconds for each person.

Let’s import the required package:

import pathlib

import jupedsim as jps
import pedpy
from matplotlib.patches import Circle
from numpy.random import normal  # normal distribution of free movement speed
from shapely import Polygon, from_wkt, intersection

Definition of the Geometry#

The geometry is given in wkt format and can be easily converted:

geo_wkt = "GEOMETRYCOLLECTION (POLYGON ((33.07 62.14, 32.43 60.21, 32.28 58.14, 32.28 56.07, 32.28 53.99, 32.28 51.92, 32.28 49.84, 32.28 47.77, 32.28 44.83, 31.02 44.83, 26.13 44.83, 25.28 44.83, 25.58 49.98, 25.7 52.07, 25.77 54.17, 25.85 56.27, 26.04 58.14, 25.87 60.26, 24.27 61.67, 22.17 61.96, 21.89 62.17, 22.16 67.48, 22.09 68.2, 23.57 68.2, 24.21 68.57, 24.21 71.37, 21.12 71.37, 21.12 76.37, 24.11 76.37, 26.11 76.37, 31.12 76.37, 31.12 71.37, 27.4 71.37, 27.4 68.55, 27.99 68.2, 34.29 68.2, 34.83 68.56, 35.02 65.35, 35.2 62.14, 33.07 62.14), (29.15 57.02, 30.15 58.79, 28.15 58.79, 29.15 57.02), (24.95 68.67, 25.05 68.67, 25.05 71.31, 24.95 71.31, 24.95 68.67), (25.75 68.67, 25.85 68.67, 25.85 71.31, 25.75 71.31, 25.75 68.67), (26.55 68.67, 26.65 68.67, 26.65 71.31, 26.55 71.31, 26.55 68.67)))"
geo = from_wkt(geo_wkt)
walkable_area = pedpy.WalkableArea(geo.geoms[0])
pedpy.plot_walkable_area(walkable_area=walkable_area).set_aspect("equal")
../_images/a5e780504f309a7b27e7928bf99715d3609c7cb32416410018ea70636b99465b.png

The geometry consists of four entrance gates at the top and an obstacle in the middle. The people should arrive at the bottom. The way to the entrance gates is enclosed by barriers which results in the shown geometry.

Definition of Starting Positions and Exit#

Let’s calculate 20 positions in an area at the bottom of the geometry. The exit area is placed at the very top.

num_agents = 20
spawning_polygon = Polygon([(25, 45), (35, 45), (35, 54), (25, 54)])
spawning_area = intersection(spawning_polygon, geo)

agent_start_positions = jps.distribute_by_number(
    polygon=spawning_area,
    number_of_agents=num_agents,
    distance_to_agents=0.4,
    distance_to_polygon=0.2,
    seed=123,
)
exit_area = Polygon([(22, 76), (30, 76), (30, 74), (22, 74)])

Let’s have a look at the setup:

Hide code cell source
def plot_simulation_configuration(
    walkable_area, spawning_area, starting_positions, exit_area
):
    axes = pedpy.plot_walkable_area(walkable_area=walkable_area)
    axes.fill(*spawning_area.exterior.xy, color="lightgrey")
    axes.fill(*exit_area.exterior.xy, color="indianred")
    axes.scatter(*zip(*starting_positions), s=1)
    axes.set_xlabel("x/m")
    axes.set_ylabel("y/m")
    axes.set_aspect("equal")

    return axes
plot_simulation_configuration(
    walkable_area, spawning_area, agent_start_positions, exit_area
)
<Axes: xlabel='x/m', ylabel='y/m'>
../_images/33ba9d37d7f7eff4edf6e3669aa774c20785ed92c14d9251af046ea861067ec9.png

Setting up the Simulation#

Let’s setup a simulation object using the collision-free speed model:

trajectory_file = "queues_waiting.sqlite"  # output file
simulation = jps.Simulation(
    model=jps.CollisionFreeSpeedModel(),
    geometry=geo,
    trajectory_writer=jps.SqliteTrajectoryWriter(
        output_file=pathlib.Path(trajectory_file)
    ),
)

Configure the Queues#

JuPedSim is providing the concept of queues which can be defined as a stage on the agents’ journeys. To let the agents wait at the gates before they walk to the exit, we need to create a queue for each gate by defining several ordered waiting positions.

We define five waiting positions for each gate - three positions in the gate and two infront:

waiting_positions_gate1 = [
    (27.1, 71),
    (27.1, 70),
    (27.1, 69),
    (27.1, 67),
    (27.1, 66),
]
waiting_positions_gate2 = [
    (26.2, 71),
    (26.2, 70),
    (26.2, 69),
    (26.2, 67),
    (26.2, 66),
]
waiting_positions_gate3 = [
    (25.35, 71),
    (25.35, 70),
    (25.35, 69),
    (25.35, 67),
    (25.35, 66),
]

waiting_positions_gate4 = [
    (24.5, 71),
    (24.5, 70),
    (24.5, 69),
    (24.5, 67),
    (24.5, 66),
]


waiting_positions_gates = [
    waiting_positions_gate1,
    waiting_positions_gate2,
    waiting_positions_gate3,
    waiting_positions_gate4,
]

Now we create the queues based on these points and add them to the simulation. The handle for the queues is needed at a later point in the simulation loop to control the waiting.

waypoints_gates = [
    simulation.add_queue_stage(i) for i in waiting_positions_gates
]
queue_gates = [simulation.get_stage(i) for i in waypoints_gates]

Configure the Journeys#

We want to spread the agents evenly on the queues based on their current load. To do so we define an additional waypoint which implements distributing the agents to the desired (least targeted) entrance gate. In this way, all agents share the same journey but may chose different gates. We place the waypoint for distributing on the left above the obstacle.

waypoint_coords = (27.2, 59)
waypoint_dist = 0.75

waypoint_for_distributing = simulation.add_waypoint_stage(
    waypoint_coords, waypoint_dist
)

exit = simulation.add_exit_stage(exit_area.exterior.coords[:-1])
journey = jps.JourneyDescription(
    waypoints_gates + [exit, waypoint_for_distributing]
)

Now we need to set the transitions on the journey. For the transitions between the waypoint and the gates we choose the least targeted approach.

journey.set_transition_for_stage(
    waypoint_for_distributing,
    jps.Transition.create_least_targeted_transition(waypoints_gates),
)

for wp in waypoints_gates:
    journey.set_transition_for_stage(
        wp, jps.Transition.create_fixed_transition(exit)
    )

journey_id = simulation.add_journey(journey)

Let’s have a look at our setup:

def plot_journey_details(
    walkable_area,
    source_area,
    agent_start_positions,
    exit_area,
    waiting_positions_queues,
    waypoints,
    dists,
):
    axes = plot_simulation_configuration(
        walkable_area, source_area, agent_start_positions, exit_area
    )
    for queue_positions in waiting_positions_queues:
        axes.scatter(*zip(*queue_positions), s=1)

    for coords, dist in zip(waypoints, dists):
        circle = Circle(coords, dist, color="lightsteelblue")
        axes.add_patch(circle)
        axes.scatter(coords[0], coords[1], marker="x", color="black")
plot_journey_details(
    walkable_area,
    spawning_area,
    agent_start_positions,
    exit_area,
    waiting_positions_gates,
    [waypoint_coords],
    [waypoint_dist],
)
../_images/73299edeee968035bfd1250cda9897c54653b166e9d52e51559b9cdb0ccfa228.png

The plot shows the several waiting positions per gate and the waypoint for distributing. The agents start from the grey area at the bottom and walk to the center of the waypoint. When they reached the blue area they will decide for the least targeted gate and line up in the subsequent queue.

The last two waiting positions are defined further away from the gates. As agents move to the first available waiting positions in the queue, the congestion (unordered waiting behaviour) forms at the last waiting point in the queue when the gates are full.

Running the Simulation#

As a last step we set the missing agent parameters and add the agents to the simulation:

v_distribution = normal(1.34, 0.05, num_agents)

for pos, v0 in zip(agent_start_positions, v_distribution):
    simulation.add_agent(
        jps.CollisionFreeSpeedModelAgentParameters(
            journey_id=journey_id,
            stage_id=waypoint_for_distributing,
            position=pos,
            v0=v0,
        )
    )

Now we run the simulation and control the queues. Once an agent has entered one of the four queues, the indivdual waiting time of 10 seconds starts. After the waiting time the agent on the first waiting position in the queue is realeased and the others move up.

number_of_gates = 4
queue_started = [False for i in range(number_of_gates)]
gate_offsets = [0 for i in range(number_of_gates)]

while (
    simulation.agent_count() > 0 and simulation.iteration_count() < 5 * 60 * 100
):
    for i in range(number_of_gates):
        if queue_gates[i].count_enqueued() == 0:
            queue_started[i] = False
        elif not queue_started[i] and queue_gates[i].count_enqueued() > 0:
            queue_started[i] = True
            gate_offsets[i] = simulation.iteration_count()
        elif (
            queue_started[i]
            and (simulation.iteration_count() - gate_offsets[i]) % 1000 == 0
        ):
            queue_gates[i].pop(1)

    simulation.iterate()

Visualization of the Results#

from jupedsim.internal.notebook_utils import animate, read_sqlite_file

trajectory_data, walkable_area = read_sqlite_file(trajectory_file)
animate(trajectory_data, walkable_area, every_nth_frame=5)