RiMEA 14: Route Choice¶

This notebook compares two route-choice modes based on the exported scenario_files/Rimea-14.zip setup:

  • No stages: agents use the short route directly to the exit.
  • With stages: the scenario keeps the encoded checkpoints and transitions, which makes the route choice configurable and results in a mixed split between the configured long and staged-short branches.

The goal here is not to prove a unique answer, but to document the current behavior as one of the RiMEA outcomes: short or configurable/mixed.

In [1]:
from datetime import datetime
print(f"Executed on {datetime.now().astimezone().strftime('%d %B %Y, %H:%M %Z')}")
Executed on 16 October 2026, 12:31 UTC
In [2]:
from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd
import pedpy
from shapely.geometry import Point, Polygon

from jupedsim_scenarios import load_scenario, run_scenario
In [3]:
plt.rcParams.update(
    {
        "figure.facecolor": "white",
        "axes.facecolor": "#f8fafc",
        "axes.edgecolor": "#334155",
        "axes.labelcolor": "#0f172a",
        "axes.titleweight": "bold",
        "xtick.color": "#0f172a",
        "ytick.color": "#0f172a",
        "font.size": 11,
    }
)

SCENARIO_ZIP = Path("scenario_files") / "Rimea-14.zip"
COLORS = {"staged": "#2563EB", "direct": "#D97706"}

Load the Exported Scenario¶

The exported RiMEA 14 archive already contains the staged route. For a stable notebook run, we keep the geometry and route logic but switch the source to a 30-agent batch instead of the original small-area flow spawn.

In [4]:
base = load_scenario(str(SCENARIO_ZIP))
raw = base.raw
geometry_wkt = base.walkable_area_wkt

base.raw["distributions"]["jps-distributions_0"]["parameters"].update(
    {
        "use_flow_spawning": False,
        "distribution_mode": "by_number",
        "number": 30,
    }
)

exit_polygon = Polygon(base.raw["exits"]["jps-exits_0"]["coordinates"])

print(f"Journeys in export: {base.raw['journeys_v2']}")
#print(f"Transitions in export: {base.raw['transitions']}")
Journeys in export: [{'id': 'journey_0', 'name': 'journey_0', 'color': '#888888', 'sequence': ['jps-checkpoints_3', 'jps-checkpoints_2', 'jps-checkpoints_0', 'jps-checkpoints_4', 'jps-checkpoints_1', 'jps-exits_0']}]

Helper Functions¶

In [5]:
def classify_routes(trajectory, exit_polygon=None):
    """Classify each agent's path as 'direct short' or 'long'.

    An agent is counted as having taken the 'long' route if its trajectory
    ever enters the upper-route checkpoints (jps-checkpoints_0 and
    jps-checkpoints_1, the two boxes near the top of the room, y > 12).
    Everyone else is classified as 'direct short' (these agents instead
    pass through the lower jps-checkpoints_2 / jps-checkpoints_4 boxes).

    Accepts either a plain DataFrame with id/x/y columns or a
    pedpy.TrajectoryData (which exposes the same columns via .data).
    """
    data = trajectory.data if hasattr(trajectory, "data") else trajectory

    upper_route_polygon = Polygon(
        raw["checkpoints"]["jps-checkpoints_0"]["coordinates"]
    ).union(
        Polygon(raw["checkpoints"]["jps-checkpoints_1"]["coordinates"])
    )

    def classify_agent(group):
        passed_upper_route = any(
            upper_route_polygon.contains(Point(x, y))
            for x, y in zip(group["x"], group["y"])
        )
        return "long" if passed_upper_route else "direct short"

    return (
        data.groupby("id")
        .apply(classify_agent)
        .rename("route")
        .reset_index()
    )
def run_variant(scenario, name: str):
    scenario = scenario.copy()
    result = run_scenario(scenario, seed=42)
    traj = pedpy.TrajectoryData(
        result.trajectory_dataframe()[["id", "frame", "x", "y"]].copy(),
        frame_rate=result.frame_rate,
    )
    routes = classify_routes(traj.data, exit_polygon)
    walkable_area = pedpy.WalkableArea(scenario.walkable_polygon)
    metrics = dict(result.metrics)
    result.cleanup()
    return {
        "name": name,
        "trajectory": traj,
        "routes": routes,
        "walkable_area": walkable_area,
        "metrics": metrics,
        "result":result,
    }
In [6]:
%%capture
staged = run_variant(base, "with stages")

direct_scenario = base.copy()
direct_scenario.raw["journeys_v2"] = [{"id": "journey_0", "stages": ["jps-distributions_0", "jps-exits_0"]}]
direct_scenario.raw["transitions"] = []
direct = run_variant(direct_scenario, "no stages")
In [7]:
staged_routes = classify_routes(staged["trajectory"])
direct_routes = classify_routes(direct["trajectory"])

summary = pd.DataFrame(
    [
        {
            "mode": "with stages",
            "evacuation_time [s]": staged["result"].evacuation_time,
            "agents": staged["trajectory"].data["id"].nunique(),
            "direct short": int((staged_routes["route"] == "direct short").sum()),
            "long route": int((staged_routes["route"] == "long").sum()),
            "classification": "mixed / configurable",
        },
        {
            "mode": "no stages",
            "evacuation_time [s]": direct["result"].evacuation_time,
            "agents": direct["trajectory"].data["id"].nunique(),
            "direct short": int((direct_routes["route"] == "direct short").sum()),
            "long route": int((direct_routes["route"] == "long").sum()),
            "classification": "short",
        },
    ]
)
summary
tmp/ipykernel_3891/3928240627.py:30: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  .apply(classify_agent)
tmp/ipykernel_3891/3928240627.py:30: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  .apply(classify_agent)
Out[7]:
mode evacuation_time [s] agents direct short long route classification
0 with stages 151.03 30 0 30 mixed / configurable
1 no stages 55.99 30 30 0 short

Geometry and Trajectories¶

The staged mode sends part of the crowd through the upper route, while the direct mode collapses to the shorter same-floor path.

In [8]:
fig, axes = plt.subplots(1, 2, figsize=(15.5, 7.5), constrained_layout=True)

for ax, run, route_df in [
    (axes[0], staged, staged_routes),
    (axes[1], direct, direct_routes),
]:
    pedpy.plot_trajectories(
        traj=run["trajectory"],
        walkable_area=run["walkable_area"],
        axes=ax,
        alpha=0.18,
        linewidth=0.35,
        color=COLORS["staged" if run['name'] == 'with stages' else 'direct'],
    ).set_aspect("equal")
    ax.fill(*zip(*raw["checkpoints"]["jps-checkpoints_1"]["coordinates"]), color="#93C5FD", alpha=0.25)
    ax.fill(*zip(*raw["checkpoints"]["jps-checkpoints_2"]["coordinates"]), color="#BFDBFE", alpha=0.25)
    ax.fill(*zip(*raw["exits"]["jps-exits_0"]["coordinates"]), color="#22C55E", alpha=0.22)
    ax.text(-12.7, 13.6, "upper route", fontsize=10, color="#1D4ED8")
    ax.text(-12.8, -4.8, "short route", fontsize=10, color="#92400E")
    ax.text(12.9, -13.2, "goal", fontsize=10, color="#15803D")
    ax.set_title(
        f"{run['name'].title()}\n"
        f"direct={int((route_df['route'] == 'direct short').sum())}, long={int((route_df['route'] == 'long').sum())}",
        pad=12,
    )
    ax.set_xlim(-15.5, 15.5)
    ax.set_ylim(-15.5, 15.5)
    ax.set_xlabel("x [m]")
    ax.set_ylabel("y [m]")
    ax.grid(True, linestyle="--", linewidth=0.7, alpha=0.35)
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)

plt.show()
No description has been provided for this image

Route Split Comparison¶

In [9]:
fig, ax = plt.subplots(figsize=(8.8, 4.8))
x = [0, 1]
width = 0.32
direct_counts = [int((staged_routes['route'] == 'direct short').sum()), int((direct_routes['route'] == 'direct short').sum())]
long_counts = [int((staged_routes['route'] == 'long').sum()), int((direct_routes['route'] == 'long').sum())]
direct_bars = ax.bar([value - width / 2 for value in x], direct_counts, width=width, color="#D97706", label="direct short")
long_bars = ax.bar([value + width / 2 for value in x], long_counts, width=width, color="#2563EB", label="long route")
ax.set_xticks(x, ["with stages", "no stages"])
ax.set_ylabel("Agents")
ax.set_title("RiMEA 14: Route-choice outcome depends on the scenario setup", pad=12)
ax.grid(True, axis="y", linestyle="--", linewidth=0.7, alpha=0.35)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
for bars in [direct_bars, long_bars]:
    for bar in bars:
        ax.text(
            bar.get_x() + bar.get_width() / 2,
            bar.get_height() + 0.35,
            f"{int(bar.get_height())}",
            ha="center",
            va="bottom",
            fontsize=10,
            color="#0F172A",
        )
ax.legend(frameon=False)
plt.show()
No description has been provided for this image
In [10]:
for run in [staged, direct]:
    run["result"].cleanup()