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()
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()
In [10]:
for run in [staged, direct]:
run["result"].cleanup()