diff --git a/notebooks/demo-data/journey/shortest_path.gif b/notebooks/demo-data/journey/shortest_path.gif new file mode 100644 index 0000000000..ca3db7b96b Binary files /dev/null and b/notebooks/demo-data/journey/shortest_path.gif differ diff --git a/notebooks/journey.ipynb b/notebooks/journey.ipynb new file mode 100644 index 0000000000..fe0e384f80 --- /dev/null +++ b/notebooks/journey.ipynb @@ -0,0 +1,1173 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ae798e28-45c8-401a-891d-fdfa71c6516a", + "metadata": {}, + "source": [ + "# Journey demonstration\n", + "\n", + "With JuPedSim, directing agents towards exits and ensuring a smooth evacuation from the simulation area is straightforward and versatile. \n", + "There might be scenarios where it's vital to navigate agents along various paths, thus creating diverse evacuation situations. \n", + "Let's explore different routing strategies of agents using a simple geometric space - a corner.\n", + "\n", + "JuPedSim manages routing by geometrically triangulating the simulation area. Without user-defined routing strategies, agents, for example, in a corner simulation, naturally move towards the inner edge of the corner. Look at this visualization where the given direction of each agent is shown by a red line. You'll observe all red lines lead towards the exit along the inner edge of the corner. While it seems logical, this path isn’t always optimal and could result in a bottleneck, thereby slowing down the evacuation process.\n", + "\n", + "![](demo-data/journey/shortest_path.gif)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9dba16d9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "from shapely import Polygon\n", + "import pathlib\n", + "import pandas as pd\n", + "import numpy as np\n", + "import jupedsim as jps\n", + "import pedpy\n", + "from pedpy.column_identifier import ID_COL, FRAME_COL\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "439df27a", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"JuPedSim: {jps.__version__}\\nPedPy: {pedpy.__version__}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4c1eae67-0c1e-4f0b-b1f7-4e383e9092c4", + "metadata": {}, + "source": [ + "## Preparing the Simulation: Geometry and Routing Instructions\n", + "\n", + "Let's start by setting up a basic polygon. This will serve as our main simulation area where agents will be distributed. \n", + "Additionally, we'll mark an exit area using another polygon. When agents enter this exit area, they're deemed to have safely evacuated and will be removed from the ongoing simulation.\n", + "\n", + "Next, we'll introduce an initial target for the agents: a sizable circular area (known as a switch). After the simulation kickstarts, agents will first head towards this circle. Once they enter the circle, they'll be directed to one of three distinct waypoints, set diagonally along the curve of the corner.\n", + "\n", + "For the simulation's onset, all agents will be positioned inside a rectangular zone at the corner's base." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a45d0955-7092-4dda-bc44-707893e4449b", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "simulation_polygon = Polygon(\n", + " [(-7, 15), (-7, -7), (23, -7), (23, 0), (0, 0), (0, 15)]\n", + ")\n", + "exit_polygon = [(-6.8, 14.8), (-0.2, 14.8), (-0.2, 13.5), (-6.8, 13.5)]\n", + "switch_point = (7, -3.5)\n", + "waypoints = [\n", + " (-0.5, -0.5),\n", + " (-3, -2),\n", + " (-6, -4),\n", + "]\n", + "distance_to_waypoints = 3\n", + "distance_to_switch = 3\n", + "\n", + "distribution_polygon = Polygon(\n", + " [[22.8, -0.3], [10.8, -0.3], [10.8, -6.8], [22.8, -6.8]]\n", + ")\n", + "walkable_area = pedpy.WalkableArea(simulation_polygon)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b762d6d", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(nrows=1, ncols=1)\n", + "ax.set_aspect(\"equal\")\n", + "pedpy.plot_walkable_area(walkable_area=walkable_area, axes=ax)\n", + "\n", + "x, y = distribution_polygon.exterior.xy\n", + "plt.fill(x, y, alpha=0.1)\n", + "plt.plot(x, y, color=\"white\")\n", + "centroid = distribution_polygon.centroid\n", + "plt.text(\n", + " centroid.x, centroid.y, \"Start\", ha=\"center\", va=\"center\", fontsize=10\n", + ")\n", + "\n", + "x, y = Polygon(exit_polygon).exterior.xy\n", + "plt.fill(x, y, alpha=0.1)\n", + "plt.plot(x, y, color=\"white\")\n", + "centroid = Polygon(exit_polygon).centroid\n", + "plt.text(centroid.x, centroid.y, \"Exit\", ha=\"center\", va=\"center\", fontsize=10)\n", + "\n", + "ax.plot(switch_point[0], switch_point[1], \"bo\")\n", + "circle = Circle(\n", + " (switch_point[0], switch_point[1]),\n", + " distance_to_switch,\n", + " fc=\"blue\",\n", + " ec=\"blue\",\n", + " alpha=0.1,\n", + ")\n", + "ax.add_patch(circle)\n", + "ax.annotate(\n", + " f\"Switch\",\n", + " (switch_point[0], switch_point[1]),\n", + " textcoords=\"offset points\",\n", + " xytext=(-5, -15),\n", + " ha=\"center\",\n", + ")\n", + "for idx, waypoint in enumerate(waypoints):\n", + " ax.plot(waypoint[0], waypoint[1], \"ro\")\n", + " ax.annotate(\n", + " f\"WP {idx+1}\",\n", + " (waypoint[0], waypoint[1]),\n", + " textcoords=\"offset points\",\n", + " xytext=(10, -15),\n", + " ha=\"center\",\n", + " )\n", + " circle = Circle(\n", + " (waypoint[0], waypoint[1]),\n", + " distance_to_waypoints,\n", + " fc=\"red\",\n", + " ec=\"red\",\n", + " alpha=0.1,\n", + " )\n", + " ax.add_patch(circle)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36627194", + "metadata": {}, + "outputs": [], + "source": [ + "num_agents = 100\n", + "positions = jps.distribute_by_number(\n", + " polygon=distribution_polygon,\n", + " number_of_agents=num_agents,\n", + " distance_to_agents=0.4,\n", + " seed=12,\n", + " distance_to_polygon=0.2,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0efebebe", + "metadata": {}, + "source": [ + "## Exploring Transition Strategies\n", + "\n", + "All agents initially set their course towards the switch_point. After reaching it, they navigate towards intermediate goals (waypoints) before making their way to the final exit. The challenge lies in deciding which waypoint each agent should target next.\n", + "\n", + "Let's explore three unique methods to determine these transition strategies:\n", + "\n", + "1. **Direct Path Strategy**: Here, every agent simply aims for the first waypoint, mirroring a shortest path algorithm.\n", + "2. **Balanced Load Strategy**: Agents are directed towards the least occupied waypoint, ensuring a more balanced distribution.\n", + "3. **Round Robin Strategy**: Waypoints are sequentially assigned to agents, rotating through each in turn.\n", + "---------\n", + "\n", + "### Direct Path Strategy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04d54de9", + "metadata": {}, + "outputs": [], + "source": [ + "def shortest_path(\n", + " simulation: jps.Simulation, switch_id, waypoint_ids, exit_id\n", + "):\n", + " \"\"\"Build a journey with fixed transitions for a given simulation.\"\"\"\n", + "\n", + " journey = jps.JourneyDescription([switch_id, *waypoint_ids, exit_id])\n", + " # switch ---> 1st waypoint\n", + " journey.set_transition_for_stage(\n", + " switch_id, jps.Transition.create_fixed_transition(waypoint_ids[0])\n", + " )\n", + " # 1st waypoint ---> exit\n", + " journey.set_transition_for_stage(\n", + " waypoint_ids[0], jps.Transition.create_fixed_transition(exit_id)\n", + " )\n", + "\n", + " journey_id = simulation.add_journey(journey)\n", + " return journey_id" + ] + }, + { + "cell_type": "markdown", + "id": "37495c3e", + "metadata": {}, + "source": [ + "### Balanced Load Strategy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "609d2eaa", + "metadata": {}, + "outputs": [], + "source": [ + "def least_targeted(\n", + " simulation: jps.Simulation, switch_id, waypoint_ids, exit_id\n", + "):\n", + " \"\"\"Build a journey with least targeted transitions for a given simulation.\"\"\"\n", + "\n", + " journey = jps.JourneyDescription([switch_id, *waypoint_ids, exit_id])\n", + " # switch ---> least targeted waypoint\n", + " journey.set_transition_for_stage(\n", + " switch_id,\n", + " jps.Transition.create_least_targeted_transition(waypoint_ids),\n", + " )\n", + " # from all waypoints ---> exit\n", + " for waypoint_id in waypoint_ids:\n", + " journey.set_transition_for_stage(\n", + " waypoint_id, jps.Transition.create_fixed_transition(exit_id)\n", + " )\n", + "\n", + " journey_id = simulation.add_journey(journey)\n", + " return journey_id" + ] + }, + { + "cell_type": "markdown", + "id": "a90e0089", + "metadata": {}, + "source": [ + "### Round Robin Strategy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bad06382", + "metadata": {}, + "outputs": [], + "source": [ + "def round_robin(simulation: jps.Simulation, switch_id, waypoint_ids, exit_id):\n", + " \"\"\"Build a journey with least round-robin transitions for a given simulation.\"\"\"\n", + "\n", + " journey = jps.JourneyDescription([switch_id, *waypoint_ids, exit_id])\n", + " # switch ---> 1st waypoint with weight1\n", + " # switch ---> 2s waypoint with weight2\n", + " # switch ---> 3th waypoint with weight3\n", + " weight1, weight2, weight3 = 1, 1, 1\n", + " journey.set_transition_for_stage(\n", + " switch_id,\n", + " jps.Transition.create_round_robin_transition(\n", + " [\n", + " (waypoint_ids[0], weight1),\n", + " (waypoint_ids[1], weight2),\n", + " (waypoint_ids[2], weight3),\n", + " ]\n", + " ),\n", + " )\n", + " # from all waypoints ---> exit\n", + " for waypoint_id in waypoint_ids:\n", + " journey.set_transition_for_stage(\n", + " waypoint_id, jps.Transition.create_fixed_transition(exit_id)\n", + " )\n", + "\n", + " journey_id = simulation.add_journey(journey)\n", + " return journey_id" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4835947a", + "metadata": {}, + "outputs": [], + "source": [ + "scenarios = [\n", + " shortest_path,\n", + " least_targeted,\n", + " round_robin,\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "569d86fe", + "metadata": {}, + "source": [ + "## Executing the Simulation\n", + "\n", + "With all components in place, we're set to initiate the simulation.\n", + "For this demonstration, the trajectories will be recorded in an sqlite database.\n", + "\n", + "First we setup some agent parameters then run three simulation with the different strategies:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86ee1ae0", + "metadata": {}, + "outputs": [], + "source": [ + "def run_scenario_simulation(scenario, agent_parameters, positions, geometry):\n", + " \"\"\"Runs a simulation for a given scenario using the provided simulation object, agent parameters, and positions.\"\"\"\n", + " filename = f\"{scenario.__name__}.sqlite\"\n", + "\n", + " simulation = jps.Simulation(\n", + " dt=0.05,\n", + " model=jps.CollisionFreeSpeedModel(\n", + " strength_neighbor_repulsion=2.6,\n", + " range_neighbor_repulsion=0.1,\n", + " range_geometry_repulsion=0.05,\n", + " ),\n", + " geometry=geometry,\n", + " trajectory_writer=jps.SqliteTrajectoryWriter(\n", + " output_file=pathlib.Path(filename)\n", + " ),\n", + " )\n", + " exit_id = simulation.add_exit_stage(exit_polygon)\n", + " switch_id = simulation.add_waypoint_stage(switch_point, distance_to_switch)\n", + " waypoint_ids = [\n", + " simulation.add_waypoint_stage(waypoint, distance_to_waypoints)\n", + " for waypoint in waypoints\n", + " ]\n", + " agent_parameters.stage_id = switch_id\n", + " journey_id = scenario(simulation, switch_id, waypoint_ids, exit_id)\n", + " agent_parameters.journey_id = journey_id\n", + " for new_pos in positions:\n", + " agent_parameters.position = new_pos\n", + " simulation.add_agent(agent_parameters)\n", + "\n", + " while simulation.agent_count() > 0:\n", + " simulation.iterate()\n", + "\n", + " return filename, simulation.iteration_count()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d121c98", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "def print_header(scenario_name: str):\n", + " line_length = 50\n", + " header = f\" SIMULATION - {scenario_name} \"\n", + " left_padding = (line_length - len(header)) // 2\n", + " right_padding = line_length - len(header) - left_padding\n", + "\n", + " print(\"=\" * line_length)\n", + " print(\" \" * left_padding + header + \" \" * right_padding)\n", + " print(\"=\" * line_length)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39b63d1e", + "metadata": {}, + "outputs": [], + "source": [ + "for scenario in scenarios:\n", + " print_header(scenario.__name__.upper())\n", + " filename, iteration_count = run_scenario_simulation(\n", + " scenario,\n", + " jps.CollisionFreeSpeedModelAgentParameters(),\n", + " positions,\n", + " walkable_area.polygon,\n", + " )\n", + " print(\n", + " f\"> Simulation completed after {iteration_count} iterations.\\n\"\n", + " f\"> Output File: {filename}\\n\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2beaa468", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "import plotly.graph_objects as go\n", + "import plotly.express as px\n", + "import sqlite3\n", + "\n", + "DUMMY_SPEED = -1000\n", + "\n", + "\n", + "def read_sqlite_file(trajectory_file: str) -> pedpy.TrajectoryData:\n", + " with sqlite3.connect(trajectory_file) as con:\n", + " data = pd.read_sql_query(\n", + " \"select frame, id, pos_x as x, pos_y as y, ori_x as ox, ori_y as oy from trajectory_data\",\n", + " con,\n", + " )\n", + " fps = float(\n", + " con.cursor()\n", + " .execute(\"select value from metadata where key = 'fps'\")\n", + " .fetchone()[0]\n", + " )\n", + " walkable_area = (\n", + " con.cursor().execute(\"select wkt from geometry\").fetchone()[0]\n", + " )\n", + " return (\n", + " pedpy.TrajectoryData(data=data, frame_rate=fps),\n", + " pedpy.WalkableArea(walkable_area),\n", + " )\n", + "\n", + "\n", + "def speed_to_color(speed, min_speed, max_speed):\n", + " \"\"\"Map a speed value to a color using a colormap.\"\"\"\n", + " normalized_speed = (speed - min_speed) / (max_speed - min_speed)\n", + " r, g, b = plt.cm.jet_r(normalized_speed)[:3]\n", + " return f\"rgba({r*255:.0f}, {g*255:.0f}, {b*255:.0f}, 0.5)\"\n", + "\n", + "\n", + "def get_line_color(disk_color):\n", + " r, g, b, _ = [int(float(val)) for val in disk_color[5:-2].split(\",\")]\n", + " brightness = (r * 299 + g * 587 + b * 114) / 1000\n", + " return \"black\" if brightness > 127 else \"white\"\n", + "\n", + "\n", + "def create_orientation_line(row, line_length=0.2, color=\"black\"):\n", + " end_x = row[\"x\"] + line_length * row[\"ox\"]\n", + " end_y = row[\"y\"] + line_length * row[\"oy\"]\n", + "\n", + " orientation_line = go.layout.Shape(\n", + " type=\"line\",\n", + " x0=row[\"x\"],\n", + " y0=row[\"y\"],\n", + " x1=end_x,\n", + " y1=end_y,\n", + " line=dict(color=color, width=3),\n", + " )\n", + " return orientation_line\n", + "\n", + "\n", + "def get_geometry_traces(area):\n", + " geometry_traces = []\n", + " x, y = area.exterior.xy\n", + " geometry_traces.append(\n", + " go.Scatter(\n", + " x=np.array(x),\n", + " y=np.array(y),\n", + " mode=\"lines\",\n", + " line={\"color\": \"grey\"},\n", + " showlegend=False,\n", + " name=\"Exterior\",\n", + " hoverinfo=\"name\",\n", + " )\n", + " )\n", + " for inner in area.interiors:\n", + " xi, yi = zip(*inner.coords[:])\n", + " geometry_traces.append(\n", + " go.Scatter(\n", + " x=np.array(xi),\n", + " y=np.array(yi),\n", + " mode=\"lines\",\n", + " line={\"color\": \"grey\"},\n", + " showlegend=False,\n", + " name=\"Obstacle\",\n", + " hoverinfo=\"name\",\n", + " )\n", + " )\n", + " return geometry_traces\n", + "\n", + "\n", + "def get_colormap(frame_data, max_speed):\n", + " \"\"\"Utilize scatter plots with varying colors for each agent instead of individual shapes.\n", + "\n", + " This trace is only to incorporate a colorbar in the plot.\n", + " \"\"\"\n", + " scatter_trace = go.Scatter(\n", + " x=frame_data[\"x\"],\n", + " y=frame_data[\"y\"],\n", + " mode=\"markers\",\n", + " marker=dict(\n", + " size=frame_data[\"radius\"] * 2,\n", + " color=frame_data[\"speed\"],\n", + " colorscale=\"Jet_r\",\n", + " colorbar=dict(title=\"Speed [m/s]\"),\n", + " cmin=0,\n", + " cmax=max_speed,\n", + " ),\n", + " text=frame_data[\"speed\"],\n", + " showlegend=False,\n", + " hoverinfo=\"none\",\n", + " )\n", + "\n", + " return [scatter_trace]\n", + "\n", + "\n", + "def get_shapes_for_frame(frame_data, min_speed, max_speed):\n", + " def create_shape(row):\n", + " hover_trace = go.Scatter(\n", + " x=[row[\"x\"]],\n", + " y=[row[\"y\"]],\n", + " text=[f\"ID: {row['id']}, Pos({row['x']:.2f},{row['y']:.2f})\"],\n", + " mode=\"markers\",\n", + " marker=dict(size=1, opacity=1),\n", + " hoverinfo=\"text\",\n", + " showlegend=False,\n", + " )\n", + " if row[\"speed\"] == DUMMY_SPEED:\n", + " dummy_trace = go.Scatter(\n", + " x=[row[\"x\"]],\n", + " y=[row[\"y\"]],\n", + " mode=\"markers\",\n", + " marker=dict(size=1, opacity=0),\n", + " hoverinfo=\"none\",\n", + " showlegend=False,\n", + " )\n", + " return (\n", + " go.layout.Shape(\n", + " type=\"circle\",\n", + " xref=\"x\",\n", + " yref=\"y\",\n", + " x0=row[\"x\"] - row[\"radius\"],\n", + " y0=row[\"y\"] - row[\"radius\"],\n", + " x1=row[\"x\"] + row[\"radius\"],\n", + " y1=row[\"y\"] + row[\"radius\"],\n", + " line=dict(width=0),\n", + " fillcolor=\"rgba(255,255,255,0)\", # Transparent fill\n", + " ),\n", + " dummy_trace,\n", + " create_orientation_line(row, color=\"rgba(255,255,255,0)\"),\n", + " )\n", + " color = speed_to_color(row[\"speed\"], min_speed, max_speed)\n", + " return (\n", + " go.layout.Shape(\n", + " type=\"circle\",\n", + " xref=\"x\",\n", + " yref=\"y\",\n", + " x0=row[\"x\"] - row[\"radius\"],\n", + " y0=row[\"y\"] - row[\"radius\"],\n", + " x1=row[\"x\"] + row[\"radius\"],\n", + " y1=row[\"y\"] + row[\"radius\"],\n", + " line_color=color,\n", + " fillcolor=color,\n", + " ),\n", + " hover_trace,\n", + " create_orientation_line(row, color=get_line_color(color)),\n", + " )\n", + "\n", + " results = frame_data.apply(create_shape, axis=1).tolist()\n", + " shapes = [res[0] for res in results]\n", + " hover_traces = [res[1] for res in results]\n", + " arrows = [res[2] for res in results]\n", + " return shapes, hover_traces, arrows\n", + "\n", + "\n", + "def create_fig(\n", + " initial_agent_count,\n", + " initial_shapes,\n", + " initial_arrows,\n", + " initial_hover_trace,\n", + " initial_scatter_trace,\n", + " geometry_traces,\n", + " frames,\n", + " steps,\n", + " area_bounds,\n", + " width=800,\n", + " height=800,\n", + " title_note: str = \"\",\n", + "):\n", + " \"\"\"Creates a Plotly figure with animation capabilities.\n", + "\n", + " Returns:\n", + " go.Figure: A Plotly figure with animation capabilities.\n", + " \"\"\"\n", + "\n", + " minx, miny, maxx, maxy = area_bounds\n", + " title = f\"{title_note + ' | ' if title_note else ''}Number of Agents: {initial_agent_count}\"\n", + " fig = go.Figure(\n", + " data=geometry_traces + initial_scatter_trace\n", + " # + hover_traces\n", + " + initial_hover_trace,\n", + " frames=frames,\n", + " layout=go.Layout(\n", + " shapes=initial_shapes + initial_arrows, title=title, title_x=0.5\n", + " ),\n", + " )\n", + " fig.update_layout(\n", + " updatemenus=[_get_animation_controls()],\n", + " sliders=[_get_slider_controls(steps)],\n", + " autosize=False,\n", + " width=width,\n", + " height=height,\n", + " xaxis=dict(range=[minx - 0.5, maxx + 0.5]),\n", + " yaxis=dict(\n", + " scaleanchor=\"x\", scaleratio=1, range=[miny - 0.5, maxy + 0.5]\n", + " ),\n", + " )\n", + "\n", + " return fig\n", + "\n", + "\n", + "def _get_animation_controls():\n", + " \"\"\"Returns the animation control buttons for the figure.\"\"\"\n", + " return {\n", + " \"buttons\": [\n", + " {\n", + " \"args\": [\n", + " None,\n", + " {\n", + " \"frame\": {\"duration\": 100, \"redraw\": True},\n", + " \"fromcurrent\": True,\n", + " },\n", + " ],\n", + " \"label\": \"Play\",\n", + " \"method\": \"animate\",\n", + " },\n", + " ],\n", + " \"direction\": \"left\",\n", + " \"pad\": {\"r\": 10, \"t\": 87},\n", + " \"showactive\": False,\n", + " \"type\": \"buttons\",\n", + " \"x\": 0.1,\n", + " \"xanchor\": \"right\",\n", + " \"y\": 0,\n", + " \"yanchor\": \"top\",\n", + " }\n", + "\n", + "\n", + "def _get_slider_controls(steps):\n", + " \"\"\"Returns the slider controls for the figure.\"\"\"\n", + " return {\n", + " \"active\": 0,\n", + " \"yanchor\": \"top\",\n", + " \"xanchor\": \"left\",\n", + " \"currentvalue\": {\n", + " \"font\": {\"size\": 20},\n", + " \"prefix\": \"Frame:\",\n", + " \"visible\": True,\n", + " \"xanchor\": \"right\",\n", + " },\n", + " \"transition\": {\"duration\": 100, \"easing\": \"cubic-in-out\"},\n", + " \"pad\": {\"b\": 10, \"t\": 50},\n", + " \"len\": 0.9,\n", + " \"x\": 0.1,\n", + " \"y\": 0,\n", + " \"steps\": steps,\n", + " }\n", + "\n", + "\n", + "def _get_processed_frame_data(data_df, frame_num, max_agents):\n", + " \"\"\"Process frame data and ensure it matches the maximum agent count.\"\"\"\n", + " frame_data = data_df[data_df[\"frame\"] == frame_num]\n", + " agent_count = len(frame_data)\n", + " dummy_agent_data = {\"x\": 0, \"y\": 0, \"radius\": 0, \"speed\": DUMMY_SPEED}\n", + " while len(frame_data) < max_agents:\n", + " dummy_df = pd.DataFrame([dummy_agent_data])\n", + " frame_data = pd.concat([frame_data, dummy_df], ignore_index=True)\n", + " return frame_data, agent_count\n", + "\n", + "\n", + "def animate(\n", + " data: pedpy.TrajectoryData,\n", + " area: pedpy.WalkableArea,\n", + " *,\n", + " every_nth_frame: int = 50,\n", + " width: int = 800,\n", + " height: int = 800,\n", + " radius: float = 0.2,\n", + " title_note: str = \"\",\n", + "):\n", + " data_df = pedpy.compute_individual_speed(\n", + " traj_data=data,\n", + " frame_step=5,\n", + " speed_calculation=pedpy.SpeedCalculation.BORDER_SINGLE_SIDED,\n", + " )\n", + " data_df = data_df.merge(data.data, on=[\"id\", \"frame\"], how=\"left\")\n", + " data_df[\"radius\"] = radius\n", + " min_speed = data_df[\"speed\"].min()\n", + " max_speed = data_df[\"speed\"].max()\n", + " max_agents = data_df.groupby(\"frame\").size().max()\n", + " frames = []\n", + " steps = []\n", + " unique_frames = data_df[\"frame\"].unique()\n", + " selected_frames = unique_frames[::every_nth_frame]\n", + " geometry_traces = get_geometry_traces(area.polygon)\n", + " initial_frame_data = data_df[data_df[\"frame\"] == data_df[\"frame\"].min()]\n", + " initial_agent_count = len(initial_frame_data)\n", + " initial_shapes, initial_hover_trace, initial_arrows = get_shapes_for_frame(\n", + " initial_frame_data, min_speed, max_speed\n", + " )\n", + " color_map_trace = get_colormap(initial_frame_data, max_speed)\n", + " for frame_num in selected_frames:\n", + " frame_data, agent_count = _get_processed_frame_data(\n", + " data_df, frame_num, max_agents\n", + " )\n", + " shapes, hover_traces, arrows = get_shapes_for_frame(\n", + " frame_data, min_speed, max_speed\n", + " )\n", + " title = f\"{title_note + ' | ' if title_note else ''}Number of Agents: {agent_count}\"\n", + " frame = go.Frame(\n", + " data=geometry_traces + hover_traces,\n", + " name=str(frame_num),\n", + " layout=go.Layout(\n", + " shapes=shapes + arrows,\n", + " title=title,\n", + " title_x=0.5,\n", + " ),\n", + " )\n", + " frames.append(frame)\n", + "\n", + " step = {\n", + " \"args\": [\n", + " [str(frame_num)],\n", + " {\n", + " \"frame\": {\"duration\": 100, \"redraw\": True},\n", + " \"mode\": \"immediate\",\n", + " \"transition\": {\"duration\": 500},\n", + " },\n", + " ],\n", + " \"label\": str(frame_num),\n", + " \"method\": \"animate\",\n", + " }\n", + " steps.append(step)\n", + "\n", + " return create_fig(\n", + " initial_agent_count,\n", + " initial_shapes,\n", + " initial_arrows,\n", + " initial_hover_trace,\n", + " color_map_trace,\n", + " geometry_traces,\n", + " frames,\n", + " steps,\n", + " area.bounds,\n", + " width=width,\n", + " height=height,\n", + " title_note=title_note,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "078b5b68", + "metadata": {}, + "source": [ + "## Visualizing the Trajectories\n", + "\n", + "To visualize trajectories, we'll pull simulation data from the SQLite database and then employ a helper function to depict the agent movements. For subsequent analyses, we'll organize these trajectory files within a dictionary for easy access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78299346", + "metadata": {}, + "outputs": [], + "source": [ + "agent_trajectories = {}\n", + "for scenario in scenarios:\n", + " scenario_name = scenario.__name__\n", + " agent_trajectories[scenario_name], walkable_area = read_sqlite_file(\n", + " f\"{scenario_name}.sqlite\"\n", + " )\n", + " animate(\n", + " agent_trajectories[scenario_name],\n", + " walkable_area,\n", + " title_note=f\"Scenario: {scenario_name}\",\n", + " ).show()\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "cfc23599", + "metadata": {}, + "source": [ + "## Analysis of the results\n", + "\n", + "With three distinct evacuation simulations completed, it's time to dive into the outcomes. Let's start by visualizing the trajectories. This will give us an initial insight into the variations among the scenarios:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb1ca7af", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(15, 5))\n", + "for name, ax in zip(agent_trajectories, axes):\n", + " pedpy.plot_trajectories(\n", + " traj=agent_trajectories[name],\n", + " walkable_area=walkable_area,\n", + " axes=ax,\n", + " traj_width=0.2,\n", + " traj_color=\"blue\",\n", + " )\n", + " x, y = Polygon(exit_polygon).exterior.xy\n", + " ax.fill(x, y, alpha=0.1, color=\"red\")\n", + " ax.plot(x, y, color=\"white\")\n", + " centroid = Polygon(exit_polygon).centroid\n", + " ax.text(\n", + " centroid.x, centroid.y, \"Exit\", ha=\"center\", va=\"center\", fontsize=10\n", + " )\n", + " ax.set_title(name)" + ] + }, + { + "cell_type": "markdown", + "id": "087e19f4", + "metadata": {}, + "source": [ + "## Compute density and speed for all three simulations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b51bfcc4", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# densities = {}\n", + "# for name in agent_trajectories:\n", + "# densities[name] = pedpy.compute_individual_voronoi_polygons(\n", + "# traj_data=agent_trajectories[name],\n", + "# walkable_area=walkable_area,\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07166076", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# speeds = {}\n", + "# for name in agent_trajectories:\n", + "# speeds[name] = pedpy.compute_individual_speed(\n", + "# traj_data=agent_trajectories[name],\n", + "# frame_step=5,\n", + "# speed_calculation=pedpy.SpeedCalculation.BORDER_SINGLE_SIDED,\n", + "# )" + ] + }, + { + "cell_type": "markdown", + "id": "547cf352", + "metadata": {}, + "source": [ + "## Calculate profiles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d5a77a0", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# import warnings\n", + "# warnings.filterwarnings(\"ignore\")\n", + "# grid_size = 1 # this is big, and hould be smaller, but I can't wait any longer!\n", + "# density_profiles = {}\n", + "# speed_profiles = {}\n", + "# for name in agent_trajectories:\n", + "# density_profiles[name], speed_profiles[name] = pedpy.compute_profiles(\n", + "# individual_voronoi_speed_data=pd.merge(\n", + "# densities[name],\n", + "# speeds[name],\n", + "# on=[ID_COL, FRAME_COL],\n", + "# ),\n", + "# walkable_area=walkable_area.polygon,\n", + "# grid_size=grid_size,\n", + "# speed_method=pedpy.SpeedMethod.ARITHMETIC,\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "674ba793", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(20, 10))\n", + "# for name, ax in zip(agent_trajectories, axes):\n", + "# cm = pedpy.plot_profiles(\n", + "# walkable_area=walkable_area,\n", + "# profiles=density_profiles[name],\n", + "# axes=ax,\n", + "# label=\"$\\\\rho$ / 1/$m^2$\",\n", + "# vmax=2.,\n", + "# title=name,\n", + "# )\n", + "# fig.tight_layout(pad=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44eff42a", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy import stats\n", + "from typing import Tuple\n", + "import numpy.typing as npt\n", + "\n", + "\n", + "def calculate_density_average_classic(\n", + " bounds: Tuple[float, float, float, float],\n", + " dx: float,\n", + " nframes: int,\n", + " X: npt.NDArray[np.float64],\n", + " Y: npt.NDArray[np.float64],\n", + ") -> npt.NDArray[np.float64]:\n", + " \"\"\"Calculate classical method\n", + "\n", + " Density = mean_time(N/A_i)\n", + " \"\"\"\n", + " geominX, geominY, geomaxX, geomaxY = bounds\n", + " xbins = np.arange(geominX, geomaxX + dx, dx)\n", + " ybins = np.arange(geominY, geomaxY + dx, dx)\n", + " area = dx * dx\n", + " ret = stats.binned_statistic_2d(\n", + " X,\n", + " Y,\n", + " None,\n", + " \"count\",\n", + " bins=[xbins, ybins],\n", + " )\n", + " return np.array(np.nan_to_num(ret.statistic.T)) / nframes / area" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "294e40d5", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "from plotly.subplots import make_subplots\n", + "\n", + "\n", + "def plot_classical_density_profile(data, walkable_area, name, dx, rho_max):\n", + " vmax = rho_max\n", + " geominX, geominY, geomaxX, geomaxY = walkable_area.bounds\n", + " title = f\"{name}\"\n", + " fig = make_subplots(rows=1, cols=1, subplot_titles=([title]))\n", + " xbins = np.arange(geominX, geomaxX + dx, dx)\n", + " ybins = np.arange(geominY, geomaxY + dx, dx)\n", + " x, y = walkable_area.polygon.exterior.xy\n", + " x = list(x)\n", + " y = list(y)\n", + " heatmap = go.Heatmap(\n", + " x=xbins,\n", + " y=ybins,\n", + " z=data,\n", + " zmin=0,\n", + " zmax=rho_max,\n", + " name=title,\n", + " connectgaps=False,\n", + " zsmooth=None,\n", + " hovertemplate=\"Density: %{z:.2f}
\\nPos: (%{x:2f}: %{y:.2f}}\",\n", + " colorbar=dict(title=\"Density\"),\n", + " colorscale=\"Jet\",\n", + " )\n", + " fig.add_trace(heatmap)\n", + " # Geometry walls\n", + " line = go.Scatter(\n", + " x=x,\n", + " y=y,\n", + " mode=\"lines\",\n", + " name=\"wall\",\n", + " showlegend=False,\n", + " line=dict(\n", + " width=3,\n", + " color=\"white\",\n", + " ),\n", + " )\n", + " fig.add_trace(line)\n", + "\n", + " return fig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d1ffa32", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "import math\n", + "\n", + "dx = 0.5\n", + "rho_max = -1\n", + "fig = make_subplots(\n", + " rows=1, cols=3, subplot_titles=(list(agent_trajectories.keys()))\n", + ")\n", + "for count, name in enumerate(agent_trajectories):\n", + " trajectories = agent_trajectories[name]\n", + " data = calculate_density_average_classic(\n", + " walkable_area.bounds,\n", + " dx,\n", + " nframes=trajectories.data[\"frame\"].max(),\n", + " X=trajectories.data[\"x\"],\n", + " Y=trajectories.data[\"y\"],\n", + " )\n", + " rho_max = max(np.max(data), rho_max)\n", + " ind_fig = plot_classical_density_profile(\n", + " data, walkable_area, name, dx, math.ceil(rho_max)\n", + " )\n", + " for trace in ind_fig.data:\n", + " fig.add_trace(trace, row=1, col=count + 1)\n", + "\n", + " fig.update_xaxes(title_text=\"X [m]\", row=1, col=count + 1)\n", + " fig.update_yaxes(title_text=\"Y [m]\", scaleanchor=\"x\", scaleratio=1)\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "id": "5b857d6b", + "metadata": {}, + "source": [ + "## Analyzing Evacuation Duration\n", + "\n", + "To further understand our earlier observations, we compute the $N−t$ diagram, which shows when an agent crosses a designated measurement line. \n", + "We position this line near the exit and evaluate the $N−t$ curves for all three simulations, subsequently determining the respective evacuation durations.\n", + "\n", + "Note: It's essential to position the measurement line inside the simulation area, ensuring that agents **cross** it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d74cb6a1", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "measurement_line = pedpy.MeasurementLine([[-3, 4], [0, 4]])\n", + "fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 10))\n", + "colors = [\"blue\", \"red\", \"green\"]\n", + "for i, name in enumerate(agent_trajectories):\n", + " nt, _ = pedpy.compute_n_t(\n", + " traj_data=agent_trajectories[name],\n", + " measurement_line=measurement_line,\n", + " )\n", + " ax = pedpy.plot_nt(nt=nt, color=colors[i])\n", + " ax.lines[-1].set_label(name)\n", + " Time = np.max(nt[\"time\"])\n", + " print(\n", + " \"Name: {:<20} Evacuation time: {:<15}\".format(\n", + " name, \"{} seconds\".format(Time)\n", + " )\n", + " )\n", + "ax.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0125c8ac", + "metadata": {}, + "source": [ + "## Findings and Conclusions\n", + "\n", + "The exploration of density profiles demonstrates notable variations in agent distribution, contingent upon the algorithm employed. The shortest path algorithm, aligning with our initial predictions, induces higher densities prior to encountering the corner. Conversely, the round-robin algorithm demonstrates a capacity to redistribute the jam, steering agents away from the corner bend and facilitating a more even spread around it.\n", + "\n", + "A vital observation from the simulations underscores the role of waypoint placement, particularly when positioned as circles, along with the discernment of their range. This cruciality not only impacts agent navigation but also influences the effectiveness of the deployed algorithm.\n", + "\n", + "## Future Considerations\n", + "\n", + "As the waypoint placement proves to be instrumental, ensuing studies or simulations might delve into optimizing these placements, exploring a range of scenarios and algorithmic strategies to discern optimal configurations for various contexts. Furthermore, additional research could investigate the scalability of these findings, examining the consistency of agent distribution patterns in scenarios with varying agent quantities, environmental layouts, and navigational complexities." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/motivation.ipynb b/notebooks/motivation.ipynb new file mode 100644 index 0000000000..a107d40edf --- /dev/null +++ b/notebooks/motivation.ipynb @@ -0,0 +1,737 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simulation of a corridor with different motivations\n", + "\n", + "In this demonstration, we model a narrow corridor scenario featuring three distinct groups of agents. Among them, one group exhibits a higher level of motivation compared to the others.\n", + "\n", + "We employ the collision-free speed model to determine the speed of each agent. This speed is influenced by the desired speed, denoted as $v^0$, the agent's radius $r$, and the slope factor $T$.\n", + "\n", + "The varying motivation levels among the groups are represented by different $T$ values. The rationale for using $T$ to depict motivation is that highly motivated pedestrians, who are more aggressive in their movements, will quickly occupy any available space between them, correlating to a lower $T$ value. Conversely, less motivated pedestrians maintain a distance based on their walking speed, aligning with a higher $T$ value.\n", + "\n", + "To accentuate this dynamic, the first group of agents will decelerate a few seconds into the simulation. As a result, we'll notice that the second group, driven by high motivation, will swiftly close distances and overtake the first group as it reduces speed. In contrast, the third group, with average motivation, will decelerate upon nearing the slower agents, without attempting to pass them. \n", + "\n", + "# Setting up the geometry\n", + "\n", + "We will be using the a corridor 40 meters long and 4 meters wide." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "from shapely import Polygon\n", + "import pathlib\n", + "import pandas as pd\n", + "import numpy as np\n", + "import jupedsim as jps\n", + "import pedpy\n", + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"JuPedSim: {jps.__version__}\\nPedPy: {pedpy.__version__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "corridor = [(-1, -1), (60, -1), (60, 5), (-1, 5)]\n", + "\n", + "areas = {}\n", + "areas[\"first\"] = Polygon([[0, 0], [5, 0], [5, 4], [0, 4]])\n", + "areas[\"second\"] = Polygon([[6, 0], [12, 0], [12, 4], [6, 4]])\n", + "areas[\"third\"] = Polygon([[18, 0], [24, 0], [24, 4], [18, 4]])\n", + "areas[\"exit\"] = Polygon([(56, 0), (59, 0), (59, 4), (56, 4)])\n", + "\n", + "walkable_area = pedpy.WalkableArea(corridor)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(nrows=1, ncols=1)\n", + "ax.set_aspect(\"equal\")\n", + "_, ymin, _, ymax = walkable_area.bounds\n", + "ax.set_ylim(ymin - 2, ymax + 2)\n", + "pedpy.plot_walkable_area(walkable_area=walkable_area, axes=ax)\n", + "for name, area in areas.items():\n", + " x, y = area.exterior.xy\n", + " plt.fill(x, y, alpha=0.1)\n", + " plt.plot(x, y, color=\"white\")\n", + " centroid = Polygon(area).centroid\n", + " plt.text(\n", + " centroid.x, centroid.y, name, ha=\"center\", va=\"center\", fontsize=8\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Operational model\n", + "\n", + "Now that the geometry is set, our subsequent task is to specify the model and its associated parameters. \n", + "For this demonstration, we'll employ the \"collision-free\" model. \n", + "However, since we are interested in two different motivation states, we will have to define two different time gaps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "T_normal = 1.3\n", + "T_motivation = 0.1\n", + "v0_normal = 1.5\n", + "v0_slow = 0.5" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note, that in JuPedSim the model parameter $T$ is called `time_gap`. \n", + "\n", + "The values $1.3\\, s$ and $0.1\\, s$ are chosen according to the paper [Rzezonka2022, Fig.5](https://doi.org/10.1098/rsos.211822). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Up the Simulation Object\n", + "\n", + "Having established the model and geometry details, and combined with other parameters such as the time step $dt$, we can proceed to construct our simulation object as illustrated below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "trajectory_file = \"trajectories.sqlite\"\n", + "simulation = jps.Simulation(\n", + " dt=0.05,\n", + " model=jps.CollisionFreeSpeedModel(\n", + " strength_neighbor_repulsion=2.6,\n", + " range_neighbor_repulsion=0.1,\n", + " range_geometry_repulsion=0.05,\n", + " ),\n", + " geometry=walkable_area.polygon,\n", + " trajectory_writer=jps.SqliteTrajectoryWriter(\n", + " output_file=pathlib.Path(trajectory_file)\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Specifying Routing Details\n", + "\n", + "At this point, we'll provide basic routing instructions, guiding the agents to progress towards an exit point, which is in this case at the end of the corridor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exit_id = simulation.add_exit_stage(areas[\"exit\"])\n", + "journey_id = simulation.add_journey(jps.JourneyDescription([exit_id]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining and Distributing Agents\n", + "\n", + "Now, we'll position the agents and establish their attributes, leveraging previously mentioned parameters.\n", + "We will distribute three different groups in three different areas.\n", + "\n", + "- First area contains normally motivated agents. \n", + "- The second area contains motivated agents that are more likely to close gaps to each other.\n", + "- The third area contains normally motivated agents. These agents will reduce their desired speeds after some seconds.\n", + "\n", + "### Distribute normal agents in the first area" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "total_agents_normal = 20\n", + "positions = jps.distribute_by_number(\n", + " polygon=Polygon(areas[\"first\"]),\n", + " number_of_agents=total_agents_normal,\n", + " distance_to_agents=0.4,\n", + " distance_to_polygon=0.4,\n", + " seed=45131502,\n", + ")\n", + "\n", + "for position in positions:\n", + " simulation.add_agent(\n", + " jps.CollisionFreeSpeedModelAgentParameters(\n", + " position=position,\n", + " v0=v0_normal,\n", + " time_gap=T_normal,\n", + " journey_id=journey_id,\n", + " stage_id=exit_id,\n", + " )\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Distribute motivated agents in the second area" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "total_agents_motivated = 20\n", + "positions = jps.distribute_by_number(\n", + " polygon=Polygon(areas[\"second\"]),\n", + " number_of_agents=total_agents_motivated,\n", + " distance_to_agents=0.6,\n", + " distance_to_polygon=0.6,\n", + " seed=45131502,\n", + ")\n", + "for position in positions:\n", + " simulation.add_agent(\n", + " jps.CollisionFreeSpeedModelAgentParameters(\n", + " position=position,\n", + " v0=v0_normal,\n", + " time_gap=T_motivation,\n", + " journey_id=journey_id,\n", + " stage_id=exit_id,\n", + " )\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Distribute normal agents in the third area" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "total_agents_motivated_delay = 20\n", + "positions = jps.distribute_by_number(\n", + " polygon=Polygon(areas[\"third\"]),\n", + " number_of_agents=total_agents_motivated_delay,\n", + " distance_to_agents=0.8,\n", + " distance_to_polygon=0.4,\n", + " seed=45131502,\n", + ")\n", + "ids_third_group = set(\n", + " [\n", + " simulation.add_agent(\n", + " jps.CollisionFreeSpeedModelAgentParameters(\n", + " position=position,\n", + " v0=v0_normal,\n", + " time_gap=T_normal,\n", + " journey_id=journey_id,\n", + " stage_id=exit_id,\n", + " )\n", + " )\n", + " for position in positions\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Executing the Simulation\n", + "\n", + "With all components in place, we're set to initiate the simulation.\n", + "For this demonstration, the trajectories will be recorded in an sqlite database." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "while simulation.agent_count() > 0:\n", + " simulation.iterate()\n", + " if simulation.iteration_count() == 200:\n", + " for id in ids_third_group:\n", + " for agent in simulation.agents():\n", + " if agent.id == id:\n", + " agent.model.v0 = v0_slow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualizing the Trajectories\n", + "\n", + "For trajectory visualization, we'll extract data from the sqlite database. A straightforward method for this is employing the jupedsim-visualizer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "import plotly.graph_objects as go\n", + "import plotly.express as px\n", + "import sqlite3\n", + "\n", + "DUMMY_SPEED = -1000\n", + "\n", + "\n", + "def read_sqlite_file(trajectory_file: str) -> pedpy.TrajectoryData:\n", + " with sqlite3.connect(trajectory_file) as con:\n", + " data = pd.read_sql_query(\n", + " \"select frame, id, pos_x as x, pos_y as y, ori_x as ox, ori_y as oy from trajectory_data\",\n", + " con,\n", + " )\n", + " fps = float(\n", + " con.cursor()\n", + " .execute(\"select value from metadata where key = 'fps'\")\n", + " .fetchone()[0]\n", + " )\n", + " walkable_area = (\n", + " con.cursor().execute(\"select wkt from geometry\").fetchone()[0]\n", + " )\n", + " return (\n", + " pedpy.TrajectoryData(data=data, frame_rate=fps),\n", + " pedpy.WalkableArea(walkable_area),\n", + " )\n", + "\n", + "\n", + "def speed_to_color(speed, min_speed, max_speed):\n", + " \"\"\"Map a speed value to a color using a colormap.\"\"\"\n", + " normalized_speed = (speed - min_speed) / (max_speed - min_speed)\n", + " r, g, b = plt.cm.jet_r(normalized_speed)[:3]\n", + " return f\"rgba({r*255:.0f}, {g*255:.0f}, {b*255:.0f}, 0.5)\"\n", + "\n", + "\n", + "def get_line_color(disk_color):\n", + " r, g, b, _ = [int(float(val)) for val in disk_color[5:-2].split(\",\")]\n", + " brightness = (r * 299 + g * 587 + b * 114) / 1000\n", + " return \"black\" if brightness > 127 else \"white\"\n", + "\n", + "\n", + "def create_orientation_line(row, line_length=0.2, color=\"black\"):\n", + " end_x = row[\"x\"] + line_length * row[\"ox\"]\n", + " end_y = row[\"y\"] + line_length * row[\"oy\"]\n", + "\n", + " orientation_line = go.layout.Shape(\n", + " type=\"line\",\n", + " x0=row[\"x\"],\n", + " y0=row[\"y\"],\n", + " x1=end_x,\n", + " y1=end_y,\n", + " line=dict(color=color, width=3),\n", + " )\n", + " return orientation_line\n", + "\n", + "\n", + "def get_geometry_traces(area):\n", + " geometry_traces = []\n", + " x, y = area.exterior.xy\n", + " geometry_traces.append(\n", + " go.Scatter(\n", + " x=np.array(x),\n", + " y=np.array(y),\n", + " mode=\"lines\",\n", + " line={\"color\": \"grey\"},\n", + " showlegend=False,\n", + " name=\"Exterior\",\n", + " hoverinfo=\"name\",\n", + " )\n", + " )\n", + " for inner in area.interiors:\n", + " xi, yi = zip(*inner.coords[:])\n", + " geometry_traces.append(\n", + " go.Scatter(\n", + " x=np.array(xi),\n", + " y=np.array(yi),\n", + " mode=\"lines\",\n", + " line={\"color\": \"grey\"},\n", + " showlegend=False,\n", + " name=\"Obstacle\",\n", + " hoverinfo=\"name\",\n", + " )\n", + " )\n", + " return geometry_traces\n", + "\n", + "\n", + "def get_colormap(frame_data, max_speed):\n", + " \"\"\"Utilize scatter plots with varying colors for each agent instead of individual shapes.\n", + "\n", + " This trace is only to incorporate a colorbar in the plot.\n", + " \"\"\"\n", + " scatter_trace = go.Scatter(\n", + " x=frame_data[\"x\"],\n", + " y=frame_data[\"y\"],\n", + " mode=\"markers\",\n", + " marker=dict(\n", + " size=frame_data[\"radius\"] * 2,\n", + " color=frame_data[\"speed\"],\n", + " colorscale=\"Jet_r\",\n", + " colorbar=dict(title=\"Speed [m/s]\"),\n", + " cmin=0,\n", + " cmax=max_speed,\n", + " ),\n", + " text=frame_data[\"speed\"],\n", + " showlegend=False,\n", + " hoverinfo=\"none\",\n", + " )\n", + "\n", + " return [scatter_trace]\n", + "\n", + "\n", + "def get_shapes_for_frame(frame_data, min_speed, max_speed):\n", + " def create_shape(row):\n", + " hover_trace = go.Scatter(\n", + " x=[row[\"x\"]],\n", + " y=[row[\"y\"]],\n", + " text=[f\"ID: {row['id']}, Pos({row['x']:.2f},{row['y']:.2f})\"],\n", + " mode=\"markers\",\n", + " marker=dict(size=1, opacity=1),\n", + " hoverinfo=\"text\",\n", + " showlegend=False,\n", + " )\n", + " if row[\"speed\"] == DUMMY_SPEED:\n", + " dummy_trace = go.Scatter(\n", + " x=[row[\"x\"]],\n", + " y=[row[\"y\"]],\n", + " mode=\"markers\",\n", + " marker=dict(size=1, opacity=0),\n", + " hoverinfo=\"none\",\n", + " showlegend=False,\n", + " )\n", + " return (\n", + " go.layout.Shape(\n", + " type=\"circle\",\n", + " xref=\"x\",\n", + " yref=\"y\",\n", + " x0=row[\"x\"] - row[\"radius\"],\n", + " y0=row[\"y\"] - row[\"radius\"],\n", + " x1=row[\"x\"] + row[\"radius\"],\n", + " y1=row[\"y\"] + row[\"radius\"],\n", + " line=dict(width=0),\n", + " fillcolor=\"rgba(255,255,255,0)\", # Transparent fill\n", + " ),\n", + " dummy_trace,\n", + " create_orientation_line(row, color=\"rgba(255,255,255,0)\"),\n", + " )\n", + " color = speed_to_color(row[\"speed\"], min_speed, max_speed)\n", + " return (\n", + " go.layout.Shape(\n", + " type=\"circle\",\n", + " xref=\"x\",\n", + " yref=\"y\",\n", + " x0=row[\"x\"] - row[\"radius\"],\n", + " y0=row[\"y\"] - row[\"radius\"],\n", + " x1=row[\"x\"] + row[\"radius\"],\n", + " y1=row[\"y\"] + row[\"radius\"],\n", + " line_color=color,\n", + " fillcolor=color,\n", + " ),\n", + " hover_trace,\n", + " create_orientation_line(row, color=get_line_color(color)),\n", + " )\n", + "\n", + " results = frame_data.apply(create_shape, axis=1).tolist()\n", + " shapes = [res[0] for res in results]\n", + " hover_traces = [res[1] for res in results]\n", + " arrows = [res[2] for res in results]\n", + " return shapes, hover_traces, arrows\n", + "\n", + "\n", + "def create_fig(\n", + " initial_agent_count,\n", + " initial_shapes,\n", + " initial_arrows,\n", + " initial_hover_trace,\n", + " initial_scatter_trace,\n", + " geometry_traces,\n", + " frames,\n", + " steps,\n", + " area_bounds,\n", + " width=800,\n", + " height=800,\n", + " title_note: str = \"\",\n", + "):\n", + " \"\"\"Creates a Plotly figure with animation capabilities.\n", + "\n", + " Returns:\n", + " go.Figure: A Plotly figure with animation capabilities.\n", + " \"\"\"\n", + "\n", + " minx, miny, maxx, maxy = area_bounds\n", + " title = f\"{title_note + ' | ' if title_note else ''}Number of Agents: {initial_agent_count}\"\n", + " fig = go.Figure(\n", + " data=geometry_traces + initial_scatter_trace\n", + " # + hover_traces\n", + " + initial_hover_trace,\n", + " frames=frames,\n", + " layout=go.Layout(\n", + " shapes=initial_shapes + initial_arrows, title=title, title_x=0.5\n", + " ),\n", + " )\n", + " fig.update_layout(\n", + " updatemenus=[_get_animation_controls()],\n", + " sliders=[_get_slider_controls(steps)],\n", + " autosize=False,\n", + " width=width,\n", + " height=height,\n", + " xaxis=dict(range=[minx - 0.5, maxx + 0.5]),\n", + " yaxis=dict(\n", + " scaleanchor=\"x\", scaleratio=1, range=[miny - 0.5, maxy + 0.5]\n", + " ),\n", + " )\n", + "\n", + " return fig\n", + "\n", + "\n", + "def _get_animation_controls():\n", + " \"\"\"Returns the animation control buttons for the figure.\"\"\"\n", + " return {\n", + " \"buttons\": [\n", + " {\n", + " \"args\": [\n", + " None,\n", + " {\n", + " \"frame\": {\"duration\": 100, \"redraw\": True},\n", + " \"fromcurrent\": True,\n", + " },\n", + " ],\n", + " \"label\": \"Play\",\n", + " \"method\": \"animate\",\n", + " },\n", + " ],\n", + " \"direction\": \"left\",\n", + " \"pad\": {\"r\": 10, \"t\": 87},\n", + " \"showactive\": False,\n", + " \"type\": \"buttons\",\n", + " \"x\": 0.1,\n", + " \"xanchor\": \"right\",\n", + " \"y\": 0,\n", + " \"yanchor\": \"top\",\n", + " }\n", + "\n", + "\n", + "def _get_slider_controls(steps):\n", + " \"\"\"Returns the slider controls for the figure.\"\"\"\n", + " return {\n", + " \"active\": 0,\n", + " \"yanchor\": \"top\",\n", + " \"xanchor\": \"left\",\n", + " \"currentvalue\": {\n", + " \"font\": {\"size\": 20},\n", + " \"prefix\": \"Frame:\",\n", + " \"visible\": True,\n", + " \"xanchor\": \"right\",\n", + " },\n", + " \"transition\": {\"duration\": 100, \"easing\": \"cubic-in-out\"},\n", + " \"pad\": {\"b\": 10, \"t\": 50},\n", + " \"len\": 0.9,\n", + " \"x\": 0.1,\n", + " \"y\": 0,\n", + " \"steps\": steps,\n", + " }\n", + "\n", + "\n", + "def _get_processed_frame_data(data_df, frame_num, max_agents):\n", + " \"\"\"Process frame data and ensure it matches the maximum agent count.\"\"\"\n", + " frame_data = data_df[data_df[\"frame\"] == frame_num]\n", + " agent_count = len(frame_data)\n", + " dummy_agent_data = {\"x\": 0, \"y\": 0, \"radius\": 0, \"speed\": DUMMY_SPEED}\n", + " while len(frame_data) < max_agents:\n", + " dummy_df = pd.DataFrame([dummy_agent_data])\n", + " frame_data = pd.concat([frame_data, dummy_df], ignore_index=True)\n", + " return frame_data, agent_count\n", + "\n", + "\n", + "def animate(\n", + " data: pedpy.TrajectoryData,\n", + " area: pedpy.WalkableArea,\n", + " *,\n", + " every_nth_frame: int = 50,\n", + " width: int = 800,\n", + " height: int = 800,\n", + " radius: float = 0.2,\n", + " title_note: str = \"\",\n", + "):\n", + " data_df = pedpy.compute_individual_speed(\n", + " traj_data=data,\n", + " frame_step=5,\n", + " speed_calculation=pedpy.SpeedCalculation.BORDER_SINGLE_SIDED,\n", + " )\n", + " data_df = data_df.merge(data.data, on=[\"id\", \"frame\"], how=\"left\")\n", + " data_df[\"radius\"] = radius\n", + " min_speed = data_df[\"speed\"].min()\n", + " max_speed = data_df[\"speed\"].max()\n", + " max_agents = data_df.groupby(\"frame\").size().max()\n", + " frames = []\n", + " steps = []\n", + " unique_frames = data_df[\"frame\"].unique()\n", + " selected_frames = unique_frames[::every_nth_frame]\n", + " geometry_traces = get_geometry_traces(area.polygon)\n", + " initial_frame_data = data_df[data_df[\"frame\"] == data_df[\"frame\"].min()]\n", + " initial_agent_count = len(initial_frame_data)\n", + " initial_shapes, initial_hover_trace, initial_arrows = get_shapes_for_frame(\n", + " initial_frame_data, min_speed, max_speed\n", + " )\n", + " color_map_trace = get_colormap(initial_frame_data, max_speed)\n", + " for frame_num in selected_frames:\n", + " frame_data, agent_count = _get_processed_frame_data(\n", + " data_df, frame_num, max_agents\n", + " )\n", + " shapes, hover_traces, arrows = get_shapes_for_frame(\n", + " frame_data, min_speed, max_speed\n", + " )\n", + " title = f\"{title_note + ' | ' if title_note else ''}Number of Agents: {agent_count}\"\n", + " frame = go.Frame(\n", + " data=geometry_traces + hover_traces,\n", + " name=str(frame_num),\n", + " layout=go.Layout(\n", + " shapes=shapes + arrows,\n", + " title=title,\n", + " title_x=0.5,\n", + " ),\n", + " )\n", + " frames.append(frame)\n", + "\n", + " step = {\n", + " \"args\": [\n", + " [str(frame_num)],\n", + " {\n", + " \"frame\": {\"duration\": 100, \"redraw\": True},\n", + " \"mode\": \"immediate\",\n", + " \"transition\": {\"duration\": 500},\n", + " },\n", + " ],\n", + " \"label\": str(frame_num),\n", + " \"method\": \"animate\",\n", + " }\n", + " steps.append(step)\n", + "\n", + " return create_fig(\n", + " initial_agent_count,\n", + " initial_shapes,\n", + " initial_arrows,\n", + " initial_hover_trace,\n", + " color_map_trace,\n", + " geometry_traces,\n", + " frames,\n", + " steps,\n", + " area.bounds,\n", + " width=width,\n", + " height=height,\n", + " title_note=title_note,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "agent_trajectories, walkable_area = read_sqlite_file(trajectory_file)\n", + "animate(\n", + " agent_trajectories,\n", + " walkable_area,\n", + " every_nth_frame=5,\n", + " width=1000,\n", + " height=500,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notes and Comments\n", + "\n", + "It's noticeable that members of the second group tend to draw nearer to each other compared to those in the first group, primarily attributed to their lower $T$ values. As the third group begins to decelerate after a while, due to an adjustment in the target speed $v_0$, the second group seizes this opportunity to bridge the distance and surpass them. \n", + "\n", + "Conversely, the first group maintains a consistent pace and doesn't attempt to overtake the now-lagging third group." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/routing.ipynb b/notebooks/routing.ipynb new file mode 100644 index 0000000000..38d35d6ed8 --- /dev/null +++ b/notebooks/routing.ipynb @@ -0,0 +1,795 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simulation of a room following different routes\n", + "\n", + "In this demonstration, we'll be simulating a room with a single exit. \n", + "We'll place two distinct groups of agents in a designated zone within the room. \n", + "Each group will be assigned a specific route to reach the exit: \n", + "one group will follow the shortest path, while the other will take a longer detour.\n", + "\n", + "To chart these paths, we'll use several waypoints, creating unique journeys for the agents to navigate.\n", + "\n", + "## Configuring the Room Layout\n", + "\n", + "For our simulation, we'll utilize a square-shaped room with dimensions of 20 meters by 20 meters. \n", + "Inside, obstacles will be strategically placed to segment the room and guide both agent groups.\n", + "\n", + "**Note** that the obstacles can not intersect the geometry. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "import pathlib\n", + "import pandas as pd\n", + "import numpy as np\n", + "import jupedsim as jps\n", + "import shapely\n", + "from shapely import Polygon\n", + "import pedpy\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"JuPedSim: {jps.__version__}\\nPedPy: {pedpy.__version__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "complete_area = Polygon(\n", + " [\n", + " (0, 0),\n", + " (0, 20),\n", + " (20, 20),\n", + " (20, 0),\n", + " ]\n", + ")\n", + "obstacles = [\n", + " Polygon(\n", + " [\n", + " (5, 0.0),\n", + " (5, 16),\n", + " (5.2, 16),\n", + " (5.2, 0.0),\n", + " ]\n", + " ),\n", + " Polygon(\n", + " [(15, 19), (15, 5), (7.2, 5), (7.2, 4.8), (15.2, 4.8), (15.2, 19)]\n", + " ),\n", + "]\n", + "\n", + "exit_polygon = [(19, 19), (20, 19), (20, 20), (19, 20)]\n", + "waypoints = [([3, 19], 3), ([7, 19], 2), ([7, 2.5], 2), ([17.5, 2.5], 2)]\n", + "distribution_polygon = Polygon([[0, 0], [5, 0], [5, 10], [0, 10]])\n", + "obstacle = shapely.union_all(obstacles)\n", + "walkable_area = pedpy.WalkableArea(shapely.difference(complete_area, obstacle))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(nrows=1, ncols=1)\n", + "ax.set_aspect(\"equal\")\n", + "pedpy.plot_walkable_area(walkable_area=walkable_area, axes=ax)\n", + "\n", + "for idx, (waypoint, distance) in enumerate(waypoints):\n", + " ax.plot(waypoint[0], waypoint[1], \"ro\")\n", + " ax.annotate(\n", + " f\"WP {idx+1}\",\n", + " (waypoint[0], waypoint[1]),\n", + " textcoords=\"offset points\",\n", + " xytext=(10, -15),\n", + " ha=\"center\",\n", + " )\n", + " circle = Circle(\n", + " (waypoint[0], waypoint[1]), distance, fc=\"red\", ec=\"red\", alpha=0.1\n", + " )\n", + " ax.add_patch(circle)\n", + "\n", + "x, y = Polygon(exit_polygon).exterior.xy\n", + "plt.fill(x, y, alpha=0.1, color=\"orange\")\n", + "centroid = Polygon(exit_polygon).centroid\n", + "plt.text(centroid.x, centroid.y, \"Exit\", ha=\"center\", va=\"center\", fontsize=8)\n", + "\n", + "x, y = distribution_polygon.exterior.xy\n", + "plt.fill(x, y, alpha=0.1, color=\"blue\")\n", + "centroid = distribution_polygon.centroid\n", + "plt.text(\n", + " centroid.x, centroid.y, \"Start\", ha=\"center\", va=\"center\", fontsize=10\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration of Simulation Scenarios\n", + "\n", + "With our room geometry in place, the next step is to define the simulation object, the operational model and its corresponding parameters. In this demonstration, we'll use the \"collision-free\" model.\n", + "\n", + "We'll outline an array of percentage values, allowing us to adjust the sizes of the two groups across multiple simulations. As a result, creating distinct simulation objects for each scenario becomes essential." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "simulations = {}\n", + "percentages = [0, 20, 40, 50, 60, 70, 100]\n", + "total_agents = 100\n", + "for percentage in percentages:\n", + " trajectory_file = f\"trajectories_percentage_{percentage}.sqlite\"\n", + " simulation = jps.Simulation(\n", + " dt=0.05,\n", + " model=jps.CollisionFreeSpeedModel(strength_neighbor_repulsion=2.6, range_neighbor_repulsion=0.1, range_geometry_repulsion=0.05),\n", + " geometry=walkable_area.polygon,\n", + " trajectory_writer=jps.SqliteTrajectoryWriter(\n", + " output_file=pathlib.Path(trajectory_file),\n", + " ),\n", + " )\n", + " simulations[percentage] = simulation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Outlining Agent Journeys\n", + "\n", + "Having established the base configurations, it's time to outline the routes our agents will take. \n", + "We've designated two distinct pathways:\n", + "\n", + "- The first route is a direct path, guiding agents along the shortest distance to the exit.\n", + "- The second route, in contrast, takes agents on a more extended journey, guiding them along the longest distance to reach the same exit.\n", + "\n", + "These variations in routing are designed to showcase how agents navigate and respond under different evacuation strategies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def set_journeys(simulation):\n", + " exit_id = simulation.add_exit_stage(exit_polygon)\n", + " waypoint_ids = []\n", + " for waypoint, distance in waypoints:\n", + " waypoint_ids.append(simulation.add_waypoint_stage(waypoint, distance))\n", + "\n", + " long_journey = jps.JourneyDescription([*waypoint_ids[:], exit_id])\n", + " for idx, waypoint in enumerate(waypoint_ids):\n", + " next_waypoint = (\n", + " exit_id if idx == len(waypoint_ids) - 1 else waypoint_ids[idx + 1]\n", + " )\n", + " long_journey.set_transition_for_stage(\n", + " waypoint, jps.Transition.create_fixed_transition(next_waypoint)\n", + " )\n", + "\n", + " short_journey = jps.JourneyDescription([waypoint_ids[0], exit_id])\n", + " short_journey.set_transition_for_stage(\n", + " waypoint_ids[0], jps.Transition.create_fixed_transition(exit_id)\n", + " )\n", + "\n", + " long_journey_id = simulation.add_journey(long_journey)\n", + " short_journey_id = simulation.add_journey(short_journey)\n", + " return short_journey_id, long_journey_id, waypoint_ids[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Allocation and Configuration of Agents\n", + "\n", + "With our environment set up, it's time to introduce and configure the agents, utilizing the parameters we've previously discussed. We're going to place agents in two distinct groups, the proportion of which will be determined by the specified percentage parameter.\n", + "\n", + "- The first group will be directed to take the longer route to the exit.\n", + "- Conversely, the second group will be guided along the shortest path to reach the exit.\n", + "\n", + "By doing so, we aim to observe and analyze the behaviors and dynamics between these two groups under varying evacuation strategies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "positions = jps.distribute_by_number(\n", + " polygon=distribution_polygon,\n", + " number_of_agents=total_agents,\n", + " distance_to_agents=0.4,\n", + " distance_to_polygon=0.7,\n", + " seed=45131502,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Reminder:**\n", + "\n", + "Given that the journey operates as a graph, it's essential to designate the initial target for the agents by setting the `stage_id`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Launching the Simulations\n", + "\n", + "Having configured our environment, agents, and routes, we are now poised to set the simulation into motion. For the purposes of this demonstration, agent trajectories throughout the simulation will be systematically captured and stored within an SQLite database. This will allow for a detailed post-analysis of agent behaviors and movement patterns.\n", + "\n", + "**Note**\n", + "Given that we've set the time step at $dt=0.05$ seconds and aim to restrict the simulation duration to approximately 2 minutes, we will cap the number of iterations per simulation to 3000." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "def print_header(scenario_name: str):\n", + " line_length = 50\n", + " header = f\" SIMULATION - {scenario_name} \"\n", + " left_padding = (line_length - len(header)) // 2\n", + " right_padding = line_length - len(header) - left_padding\n", + "\n", + " print(\"=\" * line_length)\n", + " print(\" \" * left_padding + header + \" \" * right_padding)\n", + " print(\"=\" * line_length)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "trajectory_files = {}\n", + "for percentage, simulation in simulations.items():\n", + " print_header(f\"percentage {percentage}%\")\n", + " short_journey_id, long_journey_id, first_waypoint_id = set_journeys(\n", + " simulation\n", + " )\n", + "\n", + " num_items = int(len(positions) * (percentage / 100.0))\n", + "\n", + " for position in positions[num_items:]:\n", + " simulation.add_agent(jps.CollisionFreeSpeedModelAgentParameters(position=position, journey_id=short_journey_id, stage_id=first_waypoint_id))\n", + "\n", + " for position in positions[:num_items]:\n", + " simulation.add_agent(jps.CollisionFreeSpeedModelAgentParameters(position=position, journey_id=long_journey_id, stage_id=first_waypoint_id))\n", + "\n", + " while simulation.agent_count() > 0 and simulation.iteration_count() < 3000:\n", + " simulation.iterate()\n", + "\n", + " trajectory_file = f\"trajectories_percentage_{percentage}.sqlite\"\n", + " trajectory_files[percentage] = trajectory_file\n", + " # can I get trajectory_file from the simulation object?\n", + " print(\n", + " f\"> Simulation completed after {simulation.iteration_count()} iterations.\\n\"\n", + " f\"> Output File: {trajectory_file}\\n\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualizing Agent Pathways\n", + "\n", + "To gain insights into the movement patterns of our agents, we'll visualize their trajectories. Data for this endeavor will be pulled directly from the SQLite database we've previously populated. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "import plotly.graph_objects as go\n", + "import plotly.express as px\n", + "import sqlite3\n", + "\n", + "DUMMY_SPEED = -1000\n", + "\n", + "\n", + "def read_sqlite_file(trajectory_file: str) -> pedpy.TrajectoryData:\n", + " with sqlite3.connect(trajectory_file) as con:\n", + " data = pd.read_sql_query(\n", + " \"select frame, id, pos_x as x, pos_y as y, ori_x as ox, ori_y as oy from trajectory_data\",\n", + " con,\n", + " )\n", + " fps = float(\n", + " con.cursor()\n", + " .execute(\"select value from metadata where key = 'fps'\")\n", + " .fetchone()[0]\n", + " )\n", + " walkable_area = (\n", + " con.cursor().execute(\"select wkt from geometry\").fetchone()[0]\n", + " )\n", + " return (\n", + " pedpy.TrajectoryData(data=data, frame_rate=fps),\n", + " pedpy.WalkableArea(walkable_area),\n", + " )\n", + "\n", + "\n", + "def speed_to_color(speed, min_speed, max_speed):\n", + " \"\"\"Map a speed value to a color using a colormap.\"\"\"\n", + " normalized_speed = (speed - min_speed) / (max_speed - min_speed)\n", + " r, g, b = plt.cm.jet_r(normalized_speed)[:3]\n", + " return f\"rgba({r*255:.0f}, {g*255:.0f}, {b*255:.0f}, 0.5)\"\n", + "\n", + "\n", + "def get_line_color(disk_color):\n", + " r, g, b, _ = [int(float(val)) for val in disk_color[5:-2].split(\",\")]\n", + " brightness = (r * 299 + g * 587 + b * 114) / 1000\n", + " return \"black\" if brightness > 127 else \"white\"\n", + "\n", + "\n", + "def create_orientation_line(row, line_length=0.2, color=\"black\"):\n", + " end_x = row[\"x\"] + line_length * row[\"ox\"]\n", + " end_y = row[\"y\"] + line_length * row[\"oy\"]\n", + "\n", + " orientation_line = go.layout.Shape(\n", + " type=\"line\",\n", + " x0=row[\"x\"],\n", + " y0=row[\"y\"],\n", + " x1=end_x,\n", + " y1=end_y,\n", + " line=dict(color=color, width=3),\n", + " )\n", + " return orientation_line\n", + "\n", + "\n", + "def get_geometry_traces(area):\n", + " geometry_traces = []\n", + " x, y = area.exterior.xy\n", + " geometry_traces.append(\n", + " go.Scatter(\n", + " x=np.array(x),\n", + " y=np.array(y),\n", + " mode=\"lines\",\n", + " line={\"color\": \"grey\"},\n", + " showlegend=False,\n", + " name=\"Exterior\",\n", + " hoverinfo=\"name\",\n", + " )\n", + " )\n", + " for inner in area.interiors:\n", + " xi, yi = zip(*inner.coords[:])\n", + " geometry_traces.append(\n", + " go.Scatter(\n", + " x=np.array(xi),\n", + " y=np.array(yi),\n", + " mode=\"lines\",\n", + " line={\"color\": \"grey\"},\n", + " showlegend=False,\n", + " name=\"Obstacle\",\n", + " hoverinfo=\"name\",\n", + " )\n", + " )\n", + " return geometry_traces\n", + "\n", + "\n", + "def get_colormap(frame_data, max_speed):\n", + " \"\"\"Utilize scatter plots with varying colors for each agent instead of individual shapes.\n", + "\n", + " This trace is only to incorporate a colorbar in the plot.\n", + " \"\"\"\n", + " scatter_trace = go.Scatter(\n", + " x=frame_data[\"x\"],\n", + " y=frame_data[\"y\"],\n", + " mode=\"markers\",\n", + " marker=dict(\n", + " size=frame_data[\"radius\"] * 2,\n", + " color=frame_data[\"speed\"],\n", + " colorscale=\"Jet_r\",\n", + " colorbar=dict(title=\"Speed [m/s]\"),\n", + " cmin=0,\n", + " cmax=max_speed,\n", + " ),\n", + " text=frame_data[\"speed\"],\n", + " showlegend=False,\n", + " hoverinfo=\"none\",\n", + " )\n", + "\n", + " return [scatter_trace]\n", + "\n", + "\n", + "def get_shapes_for_frame(frame_data, min_speed, max_speed):\n", + " def create_shape(row):\n", + " hover_trace = go.Scatter(\n", + " x=[row[\"x\"]],\n", + " y=[row[\"y\"]],\n", + " text=[f\"ID: {row['id']}, Pos({row['x']:.2f},{row['y']:.2f})\"],\n", + " mode=\"markers\",\n", + " marker=dict(size=1, opacity=1),\n", + " hoverinfo=\"text\",\n", + " showlegend=False,\n", + " )\n", + " if row[\"speed\"] == DUMMY_SPEED:\n", + " dummy_trace = go.Scatter(\n", + " x=[row[\"x\"]],\n", + " y=[row[\"y\"]],\n", + " mode=\"markers\",\n", + " marker=dict(size=1, opacity=0),\n", + " hoverinfo=\"none\",\n", + " showlegend=False,\n", + " )\n", + " return (\n", + " go.layout.Shape(\n", + " type=\"circle\",\n", + " xref=\"x\",\n", + " yref=\"y\",\n", + " x0=row[\"x\"] - row[\"radius\"],\n", + " y0=row[\"y\"] - row[\"radius\"],\n", + " x1=row[\"x\"] + row[\"radius\"],\n", + " y1=row[\"y\"] + row[\"radius\"],\n", + " line=dict(width=0),\n", + " fillcolor=\"rgba(255,255,255,0)\", # Transparent fill\n", + " ),\n", + " dummy_trace,\n", + " create_orientation_line(row, color=\"rgba(255,255,255,0)\"),\n", + " )\n", + " color = speed_to_color(row[\"speed\"], min_speed, max_speed)\n", + " return (\n", + " go.layout.Shape(\n", + " type=\"circle\",\n", + " xref=\"x\",\n", + " yref=\"y\",\n", + " x0=row[\"x\"] - row[\"radius\"],\n", + " y0=row[\"y\"] - row[\"radius\"],\n", + " x1=row[\"x\"] + row[\"radius\"],\n", + " y1=row[\"y\"] + row[\"radius\"],\n", + " line_color=color,\n", + " fillcolor=color,\n", + " ),\n", + " hover_trace,\n", + " create_orientation_line(row, color=get_line_color(color)),\n", + " )\n", + "\n", + " results = frame_data.apply(create_shape, axis=1).tolist()\n", + " shapes = [res[0] for res in results]\n", + " hover_traces = [res[1] for res in results]\n", + " arrows = [res[2] for res in results]\n", + " return shapes, hover_traces, arrows\n", + "\n", + "\n", + "def create_fig(\n", + " initial_agent_count,\n", + " initial_shapes,\n", + " initial_arrows,\n", + " initial_hover_trace,\n", + " initial_scatter_trace,\n", + " geometry_traces,\n", + " frames,\n", + " steps,\n", + " area_bounds,\n", + " width=800,\n", + " height=800,\n", + " title_note: str = \"\",\n", + "):\n", + " \"\"\"Creates a Plotly figure with animation capabilities.\n", + "\n", + " Returns:\n", + " go.Figure: A Plotly figure with animation capabilities.\n", + " \"\"\"\n", + "\n", + " minx, miny, maxx, maxy = area_bounds\n", + " title = f\"{title_note + ' | ' if title_note else ''}Number of Agents: {initial_agent_count}\"\n", + " fig = go.Figure(\n", + " data=geometry_traces + initial_scatter_trace\n", + " # + hover_traces\n", + " + initial_hover_trace,\n", + " frames=frames,\n", + " layout=go.Layout(\n", + " shapes=initial_shapes + initial_arrows, title=title, title_x=0.5\n", + " ),\n", + " )\n", + " fig.update_layout(\n", + " updatemenus=[_get_animation_controls()],\n", + " sliders=[_get_slider_controls(steps)],\n", + " autosize=False,\n", + " width=width,\n", + " height=height,\n", + " xaxis=dict(range=[minx - 0.5, maxx + 0.5]),\n", + " yaxis=dict(\n", + " scaleanchor=\"x\", scaleratio=1, range=[miny - 0.5, maxy + 0.5]\n", + " ),\n", + " )\n", + "\n", + " return fig\n", + "\n", + "\n", + "def _get_animation_controls():\n", + " \"\"\"Returns the animation control buttons for the figure.\"\"\"\n", + " return {\n", + " \"buttons\": [\n", + " {\n", + " \"args\": [\n", + " None,\n", + " {\n", + " \"frame\": {\"duration\": 100, \"redraw\": True},\n", + " \"fromcurrent\": True,\n", + " },\n", + " ],\n", + " \"label\": \"Play\",\n", + " \"method\": \"animate\",\n", + " },\n", + " ],\n", + " \"direction\": \"left\",\n", + " \"pad\": {\"r\": 10, \"t\": 87},\n", + " \"showactive\": False,\n", + " \"type\": \"buttons\",\n", + " \"x\": 0.1,\n", + " \"xanchor\": \"right\",\n", + " \"y\": 0,\n", + " \"yanchor\": \"top\",\n", + " }\n", + "\n", + "\n", + "def _get_slider_controls(steps):\n", + " \"\"\"Returns the slider controls for the figure.\"\"\"\n", + " return {\n", + " \"active\": 0,\n", + " \"yanchor\": \"top\",\n", + " \"xanchor\": \"left\",\n", + " \"currentvalue\": {\n", + " \"font\": {\"size\": 20},\n", + " \"prefix\": \"Frame:\",\n", + " \"visible\": True,\n", + " \"xanchor\": \"right\",\n", + " },\n", + " \"transition\": {\"duration\": 100, \"easing\": \"cubic-in-out\"},\n", + " \"pad\": {\"b\": 10, \"t\": 50},\n", + " \"len\": 0.9,\n", + " \"x\": 0.1,\n", + " \"y\": 0,\n", + " \"steps\": steps,\n", + " }\n", + "\n", + "\n", + "def _get_processed_frame_data(data_df, frame_num, max_agents):\n", + " \"\"\"Process frame data and ensure it matches the maximum agent count.\"\"\"\n", + " frame_data = data_df[data_df[\"frame\"] == frame_num]\n", + " agent_count = len(frame_data)\n", + " dummy_agent_data = {\"x\": 0, \"y\": 0, \"radius\": 0, \"speed\": DUMMY_SPEED}\n", + " while len(frame_data) < max_agents:\n", + " dummy_df = pd.DataFrame([dummy_agent_data])\n", + " frame_data = pd.concat([frame_data, dummy_df], ignore_index=True)\n", + " return frame_data, agent_count\n", + "\n", + "\n", + "def animate(\n", + " data: pedpy.TrajectoryData,\n", + " area: pedpy.WalkableArea,\n", + " *,\n", + " every_nth_frame: int = 50,\n", + " width: int = 800,\n", + " height: int = 800,\n", + " radius: float = 0.2,\n", + " title_note: str = \"\",\n", + "):\n", + " data_df = pedpy.compute_individual_speed(traj_data=data, frame_step=5)\n", + " data_df = data_df.merge(data.data, on=[\"id\", \"frame\"], how=\"left\")\n", + " data_df[\"radius\"] = radius\n", + " min_speed = data_df[\"speed\"].min()\n", + " max_speed = data_df[\"speed\"].max()\n", + " max_agents = data_df.groupby(\"frame\").size().max()\n", + " frames = []\n", + " steps = []\n", + " unique_frames = data_df[\"frame\"].unique()\n", + " selected_frames = unique_frames[::every_nth_frame]\n", + " geometry_traces = get_geometry_traces(area.polygon)\n", + " initial_frame_data = data_df[data_df[\"frame\"] == data_df[\"frame\"].min()]\n", + " initial_agent_count = len(initial_frame_data)\n", + " initial_shapes, initial_hover_trace, initial_arrows = get_shapes_for_frame(\n", + " initial_frame_data, min_speed, max_speed\n", + " )\n", + " color_map_trace = get_colormap(initial_frame_data, max_speed)\n", + " for frame_num in selected_frames[1:]:\n", + " frame_data, agent_count = _get_processed_frame_data(\n", + " data_df, frame_num, max_agents\n", + " )\n", + " shapes, hover_traces, arrows = get_shapes_for_frame(\n", + " frame_data, min_speed, max_speed\n", + " )\n", + " title = f\"{title_note + ' | ' if title_note else ''}Number of Agents: {agent_count}\"\n", + " frame = go.Frame(\n", + " data=geometry_traces + hover_traces,\n", + " name=str(frame_num),\n", + " layout=go.Layout(\n", + " shapes=shapes + arrows,\n", + " title=title,\n", + " title_x=0.5,\n", + " ),\n", + " )\n", + " frames.append(frame)\n", + "\n", + " step = {\n", + " \"args\": [\n", + " [str(frame_num)],\n", + " {\n", + " \"frame\": {\"duration\": 100, \"redraw\": True},\n", + " \"mode\": \"immediate\",\n", + " \"transition\": {\"duration\": 500},\n", + " },\n", + " ],\n", + " \"label\": str(frame_num),\n", + " \"method\": \"animate\",\n", + " }\n", + " steps.append(step)\n", + "\n", + " return create_fig(\n", + " initial_agent_count,\n", + " initial_shapes,\n", + " initial_arrows,\n", + " initial_hover_trace,\n", + " color_map_trace,\n", + " geometry_traces,\n", + " frames,\n", + " steps,\n", + " area.bounds,\n", + " width=width,\n", + " height=height,\n", + " title_note=title_note,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent_trajectories = {}\n", + "for percentage in percentages:\n", + " trajectory_file = trajectory_files[percentage]\n", + " agent_trajectories[percentage], walkable_area = read_sqlite_file(\n", + " trajectory_file\n", + " )\n", + " animate(\n", + " agent_trajectories[percentage],\n", + " walkable_area,\n", + " title_note=f\"Percentage: {percentage}%\",\n", + " ).show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "evac_times = []\n", + "for percentage, traj in agent_trajectories.items():\n", + " t_evac = traj.data[\"frame\"].max() / traj.frame_rate\n", + " evac_times.append(t_evac)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "fig = go.Figure()\n", + "\n", + "fig.add_trace(\n", + " go.Scatter(\n", + " x=list(agent_trajectories.keys()),\n", + " y=evac_times,\n", + " marker=dict(size=10),\n", + " mode=\"lines+markers\",\n", + " name=\"Evacuation Times\",\n", + " )\n", + ")\n", + "\n", + "fig.update_layout(\n", + " title=\"Evacuation Times vs. Percentages\",\n", + " xaxis_title=\"Percentage %\",\n", + " yaxis_title=\"Evacuation Time (s)\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary and Discussion\n", + "\n", + "In our simulated scenario, agents are presented with two distinct paths: a direct route that is shorter but prone to congestion and a detour. \n", + "Given the high volume of individuals arriving at door 1, relying solely on one door's capacity proves impractical.\n", + "\n", + "Although the alternate path through door 2 may be considerably longer in distance, it becomes crucial to utilize both doors in order to alleviate congestion and reduce waiting times at door 1.\n", + "The findings from our simulation align with this rationale. \n", + "\n", + "To optimize both average and peak arrival times, approximately 40% of individuals should choose the longer journey via door 2, which is in accordance with the results reported in this [paper](https://collective-dynamics.eu/index.php/cod/article/view/A24).\n", + "This strategic distribution ensures smoother flow dynamics and contributes towards enhancing evacuation efficiency.\n", + "\n", + "Note, that in we used a fixed seed number to distribute the agents. To get a reliable result for this specific scenario, one should repeat the simulations many times for the sake of some statistical relevance.\n", + "\n", + "Please note that in the section [Allocation and Configuration of Agents](#distribution), we employed a consistent seed number for agent distribution. For dependable outcomes, it's advised to run the simulations multiple times to ensure statistical significance. Morover, more percentage values between 0 and 100 will enhance the quality of the results.\n", + "\n", + "\n", + "## Troubleshooting\n", + "\n", + "On certain occasions, improper configuration of the simulation, such as setting an agent's desired speed to 0 m/s, can cause the simulation loop to run indefinitely. If this happens, it's recommended to modify the loop condition. \n", + "\n", + "Instead of allowing it to run without constraints, consider limiting its duration using `simulation.iteration_count()`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}