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)

Since the entrance gates are very close to each other it can happen that the agents can get in each other’s way and might be pushed into another gate that is actually not a part of their journey. In this example, the agents are able to solve their conflicts at the beginne. Please note that the position and radius of the distribution waypoint, the speed and starting positions of the agents have a considerable influence on the initial filling of the gates. Agents could be stuck in a queue they didn’t want to go to. To implement the distributing process in a more orderly manner, pre-filtering could be implemented using an additional queue instead of the waypoint for distributing.

Different Routing Strategy#

In the scenario above it is assumed that the people are evenly distributed among the gates. Therefore, the entrances are evenly occupied. In reality, this is not always the case and often the entrance with the shortest route is chosen. For this reason, we look at another scenario with a different distribution strategy. We can reuse the general settings from above but will change the journey.

trajectory_file_uneven = "queues_waiting_uneven.sqlite"  # output file
simulation_uneven = jps.Simulation(
    model=jps.CollisionFreeSpeedModel(),
    geometry=geo,
    trajectory_writer=jps.SqliteTrajectoryWriter(
        output_file=pathlib.Path(trajectory_file_uneven)
    ),
)

waypoint_for_distributing = simulation_uneven.add_waypoint_stage(
    waypoint_coords, waypoint_dist
)
exit = simulation_uneven.add_exit_stage(exit_area.exterior.coords[:-1])

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

journey_uneven = jps.JourneyDescription(
    [waypoint_for_distributing, exit] + waypoints_gates
)

Now we are using the round robin approach to distribute the agents on the gates. We define that two people are walking to gate 1 and 2, and the following ones move to gate 3 and 4 (one person each). This means that twice as many agents use gate 1 and 2.

journey_uneven.set_transition_for_stage(
    waypoint_for_distributing,
    jps.Transition.create_round_robin_transition(
        [
            (waypoints_gates[0], 2),
            (waypoints_gates[1], 2),
            (waypoints_gates[2], 1),
            (waypoints_gates[3], 1),
        ]
    ),
)

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

journey_id = simulation_uneven.add_journey(journey_uneven)

As a last step we add the agents to the simulation, start the loop and configure the ticket control as in the other scenario. The results show that the entrance on the right side is used more frequently.

Hide code cell source
for pos, v0 in zip(agent_start_positions, v_distribution):
    simulation_uneven.add_agent(
        jps.CollisionFreeSpeedModelAgentParameters(
            journey_id=journey_id,
            stage_id=waypoint_for_distributing,
            position=pos,
            v0=v0,
        )
    )

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_uneven.agent_count() > 0
    and simulation_uneven.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_uneven.iteration_count()
        elif (
            queue_started[i]
            and (simulation_uneven.iteration_count() - gate_offsets[i]) % 1000
            == 0
        ):
            queue_gates[i].pop(1)

    simulation_uneven.iterate()
from jupedsim.internal.notebook_utils import animate, read_sqlite_file

trajectory_data_uneven, walkable_area = read_sqlite_file(trajectory_file_uneven)
animate(trajectory_data_uneven, walkable_area, every_nth_frame=5)

Download#

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