From 9bca0fc5cdbca0ac27147a00bcf65f8e084dd38b Mon Sep 17 00:00:00 2001 From: Ozaq Date: Thu, 12 Oct 2023 14:40:06 +0200 Subject: [PATCH] Add two additional notebooks * single file movement * lane-formation --- docs/source/notebooks/index.rst | 2 + docs/source/notebooks/lane-formation.ipynb | 1 + docs/source/notebooks/single-file.ipynb | 1 + notebooks/bottleneck.ipynb | 12 - notebooks/corner.ipynb | 2 +- notebooks/double-bottleneck.ipynb | 2 +- notebooks/journey.ipynb | 2 +- notebooks/lane-formation.ipynb | 415 +++++++++++++++++ notebooks/motivation.ipynb | 20 +- notebooks/routing.ipynb | 6 +- notebooks/single-file.ipynb | 433 ++++++++++++++++++ .../jupedsim/internal/notebook_utils.py | 15 +- 12 files changed, 879 insertions(+), 32 deletions(-) create mode 120000 docs/source/notebooks/lane-formation.ipynb create mode 120000 docs/source/notebooks/single-file.ipynb create mode 100644 notebooks/lane-formation.ipynb create mode 100644 notebooks/single-file.ipynb diff --git a/docs/source/notebooks/index.rst b/docs/source/notebooks/index.rst index f3850278ff..fa69d9bad0 100644 --- a/docs/source/notebooks/index.rst +++ b/docs/source/notebooks/index.rst @@ -11,3 +11,5 @@ Notebooks How to work with Journeys Modelling Motivation How routing can influence evacuation times + Single file movement + Lane Formation diff --git a/docs/source/notebooks/lane-formation.ipynb b/docs/source/notebooks/lane-formation.ipynb new file mode 120000 index 0000000000..4022ffcf8d --- /dev/null +++ b/docs/source/notebooks/lane-formation.ipynb @@ -0,0 +1 @@ +../../../notebooks/lane-formation.ipynb \ No newline at end of file diff --git a/docs/source/notebooks/single-file.ipynb b/docs/source/notebooks/single-file.ipynb new file mode 120000 index 0000000000..7aae0920e7 --- /dev/null +++ b/docs/source/notebooks/single-file.ipynb @@ -0,0 +1 @@ +../../../notebooks/single-file.ipynb \ No newline at end of file diff --git a/notebooks/bottleneck.ipynb b/notebooks/bottleneck.ipynb index 5598c77fe3..f2013633c4 100644 --- a/notebooks/bottleneck.ipynb +++ b/notebooks/bottleneck.ipynb @@ -47,7 +47,6 @@ "execution_count": null, "id": "9dba16d9", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false }, @@ -92,7 +91,6 @@ "execution_count": null, "id": "a45d0955-7092-4dda-bc44-707893e4449b", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -141,7 +139,6 @@ "execution_count": null, "id": "ceef2b20-2aeb-44f0-a8e5-06902bb43dee", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false }, @@ -190,7 +187,6 @@ "execution_count": null, "id": "36627194", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -227,7 +223,6 @@ "execution_count": null, "id": "c1cfdadc", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -260,7 +255,6 @@ "execution_count": null, "id": "bad06382", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -304,7 +298,6 @@ "execution_count": null, "id": "2a413666", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -365,7 +358,6 @@ "execution_count": null, "id": "27175d6a", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -418,7 +410,6 @@ "execution_count": null, "id": "febd29a9", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -447,7 +438,6 @@ "execution_count": null, "id": "bbffba65", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -467,7 +457,6 @@ "execution_count": null, "id": "71a20de9", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } @@ -484,7 +473,6 @@ "execution_count": null, "id": "6adbab15", "metadata": { - "collapsed": false, "jupyter": { "outputs_hidden": false } diff --git a/notebooks/corner.ipynb b/notebooks/corner.ipynb index 8514008d6f..a4d4397afe 100644 --- a/notebooks/corner.ipynb +++ b/notebooks/corner.ipynb @@ -9,7 +9,7 @@ } }, "source": [ - "# Movement around Corners\n", + "# Simple Corner Simulation\n", "\n", "In the following we'll investigate the movement of pedestrians around corners. When pedestrians walk around corners they are expected to slow down and take a path that is close to the corner. According to RiMEA Test 6 **[TODO REF]** a scenario is configured where **20 agents** move towards a **corner** at which they should turn to the left.\n", "\n", diff --git a/notebooks/double-bottleneck.ipynb b/notebooks/double-bottleneck.ipynb index 9835f68c91..a4b3688f84 100644 --- a/notebooks/double-bottleneck.ipynb +++ b/notebooks/double-bottleneck.ipynb @@ -5,7 +5,7 @@ "id": "ae798e28-45c8-401a-891d-fdfa71c6516a", "metadata": {}, "source": [ - "# Double bottleneck simulation \n", + "# Double Bottleneck Simulation \n", "\n", "In this demonstration, we'll construct a **double bottleneck** situation and simulate the evacuation of **10 agents** positioned on a grid.\n", "\n", diff --git a/notebooks/journey.ipynb b/notebooks/journey.ipynb index cb9af6d1cb..378553005f 100644 --- a/notebooks/journey.ipynb +++ b/notebooks/journey.ipynb @@ -5,7 +5,7 @@ "id": "ae798e28-45c8-401a-891d-fdfa71c6516a", "metadata": {}, "source": [ - "# Journey demonstration\n", + "# How to work with Journeys\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", diff --git a/notebooks/lane-formation.ipynb b/notebooks/lane-formation.ipynb new file mode 100644 index 0000000000..bf99d79e30 --- /dev/null +++ b/notebooks/lane-formation.ipynb @@ -0,0 +1,415 @@ +{ + "cells": [ + { + "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", + "from shapely import Polygon\n", + "import pedpy\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Lane formation in bi-directional flow\n", + "Lane formation is a quantitative phenomenon, that is observed in bidirectional pedestrian flows. It involves pedestrians automatically forming a number of lanes with varying widths, where individuals within each lane move in the same direction. This self-organizing behavior of pedestrians can have a significant impact on overall evacuation time. \n", + "\n", + "In this example, we will replicate a simple experiment performed by [Feliciani et al 2016](https://doi.org/10.1103/PhysRevE.94.032304).\n", + "In their experiment, Feliciani et al observed bidirectional pedestrian flow in a corridor with two comparative lanes in each flow direction. Thereby, they changed the ratio of both groups of pedestrians Flow ratio is changed by changing each group size while maintaining comparable total flow and density. \n", + "\n", + "See following figure from the abovementioned paper:\n", + "\n", + "TODO ADD MISSING PICTURE\n", + "\n", + "The following is the implementation of the experiment setup in JuPedSim:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "length = 38\n", + "width = 3\n", + "area = [[0, 0], [length, 0], [length, width], [0, width]]\n", + "exit_polygon_left = [(0, 0), (1, 0), (1, width), (0, width)]\n", + "exit_polygon_right = [\n", + " (length - 1, 0),\n", + " (length, 0),\n", + " (length, width),\n", + " (length - 1, width),\n", + "]\n", + "distribution_polygon_left = Polygon([[0, 0], [12, 0], [12, width], [0, width]])\n", + "distribution_polygon_right = Polygon(\n", + " [[length - 12, 0], [length, 0], [length, width], [26, width]]\n", + ")\n", + "measurement_area = pedpy.MeasurementArea([[14, 0], [24, 0], [24, 3], [14, 3]])\n", + "measurement_line_left = pedpy.MeasurementLine([[14, 0], [14, width]])\n", + "measurement_line_right = pedpy.MeasurementLine([[24, 0], [24, width]])\n", + "walkable_area = pedpy.WalkableArea(area)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(nrows=1, ncols=1)\n", + "ax.set_aspect(\"equal\")\n", + "pedpy.plot_measurement_setup(\n", + " walkable_area=walkable_area,\n", + " measurement_areas=[measurement_area],\n", + " measurement_lines=[measurement_line_left, measurement_line_right],\n", + " ml_color=\"red\",\n", + " ml_width=2,\n", + " axes=ax,\n", + ")\n", + "for id, polygon in enumerate(\n", + " [distribution_polygon_left, distribution_polygon_right]\n", + "):\n", + " x, y = polygon.exterior.xy\n", + " plt.fill(x, y, alpha=0.1, color=\"gray\")\n", + " centroid = polygon.centroid\n", + " plt.text(\n", + " centroid.x,\n", + " centroid.y,\n", + " f\"Start {id+1}\",\n", + " ha=\"center\",\n", + " va=\"center\",\n", + " fontsize=10,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Definition of the simulation scenarios\n", + "Since the main focus of the abovementioned experiment was on investigating the influence of the ratio, we will set up different scenarios to investigate the variation of the density in the measurement area with varying ratios. \n", + "\n", + "In order to compare the formation of lanes and evacuation times, we will replicate the setup used in the experiment. This involves creating a corridor with two lanes in each direction of flow. We will use different simulation scenarios by initializing various combinations of ratios and densities. These scenarios include unidirectional flow, unbalanced bidirectional flows, and a balanced bidirectional flow scenario. \n", + "\n", + "This replication study aims to investigate the impact of lane formation on evacuation time in different scenarios of bidirectional pedestrian flows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "simulations = {}\n", + "COLUMNS = 9\n", + "number_agents = [\n", + " (6*COLUMNS, 0*COLUMNS),\n", + " (5*COLUMNS, 1*COLUMNS),\n", + " (4*COLUMNS, 2*COLUMNS),\n", + " (3*COLUMNS, 3*COLUMNS)\n", + "]\n", + "for number in number_agents:\n", + " trajectory_file = f\"trajectories_number_agents_{number}.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[number] = simulation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Initialisation of the simulation and distribution of agents\n", + "\n", + "The simulation will commence by assigning a specific number of pedestrian agents. \n", + "These agents will be distributed randomly across the corridor using two distinct distribution polygons, deviating from Feliciani's paper where participants were positioned on predetermined grid points. \n", + "\n", + "The simulation will then proceed with initializing the journeys of each agent. \n", + "Left-facing groups will opt to exit through the right door, while right-facing groups will choose to exit through the left door. \n", + "\n", + "For further analysis, it is essential to keep record of the identification numbers of agents belonging to different groups throughout their distribution process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "right_wing = {}\n", + "left_wing = {}\n", + "for number, simulation in simulations.items():\n", + " exits = [simulation.add_exit_stage(exit_polygon_left),simulation.add_exit_stage(exit_polygon_right)]\n", + " journeys = [\n", + " simulation.add_journey(jps.JourneyDescription([exit])) for exit in exits\n", + " ] \n", + " \n", + "\n", + " # first group\n", + " positions = jps.distribute_by_number(polygon=distribution_polygon_right,\n", + " number_of_agents=number[1],\n", + " distance_to_agents=0.4,\n", + " distance_to_polygon=0.7,\n", + " seed=45131502,\n", + " )\n", + " group1 = set([simulation.add_agent(jps.CollisionFreeSpeedModelAgentParameters(position=position, journey_id=journeys[0], stage_id=exits[0])) for position in positions])\n", + "\n", + " # second group\n", + " positions = jps.distribute_by_number(polygon=distribution_polygon_left,\n", + " number_of_agents=number[0],\n", + " distance_to_agents=0.4,\n", + " distance_to_polygon=0.7,\n", + " seed=45131502,\n", + " )\n", + "\n", + " group2 = set([simulation.add_agent(jps.CollisionFreeSpeedModelAgentParameters(position=position, journey_id=journeys[1], stage_id=exits[1])) for position in positions])\n", + "\n", + " right_wing[number] = group1\n", + " left_wing[number] = group2" + ] + }, + { + "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": "markdown", + "metadata": {}, + "source": [ + "## Running simulations \n", + "\n", + "Now we will run series of simulations, for different ratio values (here defined by numbers of agents in both groups). \n", + "\n", + "For each simulation, it runs the simulation until either all agents have finished or a maximum iteration count is reached. \n", + "\n", + "Once a simulation completes, its results are saved to a uniquely named file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "trajectory_files = {}\n", + "for number, simulation in simulations.items():\n", + " print_header(f\"number {number}\")\n", + " while simulation.agent_count() > 0 and simulation.iteration_count() < 3000:\n", + " simulation.iterate()\n", + " \n", + " trajectory_file = f\"trajectories_number_agents_{number}.sqlite\"\n", + " trajectory_files[number] = trajectory_file\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": [ + "## Visualisation of the simulation results\n", + "\n", + "Here we visualize the movement of the agents in every simulation along with plots of the trajectories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from jupedsim.internal.notebook_utils import animate, read_sqlite_file\n", + "\n", + "agent_trajectories = {}\n", + "for number in number_agents:\n", + " trajectory_file = trajectory_files[number]\n", + " agent_trajectories[number], walkable_area = read_sqlite_file(trajectory_file)\n", + " animate(agent_trajectories[number], walkable_area, every_nth_frame=5, width=1200, height=400, title_note=f\"Ratio: {min(number)/sum(number):0.2f}\").show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(nrows=len(number_agents), ncols=1, height_ratios=[5,5,5,5])\n", + "axes = np.atleast_1d(axes)\n", + "colors = [\"red\", \"blue\"]\n", + "for ax, number in zip(axes, number_agents):\n", + " trajectories = agent_trajectories[number].data\n", + " for ig, group in enumerate([left_wing[number], right_wing[number]]):\n", + " traj = pedpy.TrajectoryData(\n", + " trajectories[trajectories[\"id\"].isin(group)],\n", + " frame_rate=agent_trajectories[number].frame_rate,\n", + " )\n", + " pedpy.plot_trajectories(\n", + " traj=traj,\n", + " walkable_area=walkable_area,\n", + " axes=ax,\n", + " traj_color=colors[ig],\n", + " traj_width=0.3,\n", + " traj_start_marker=\".\"\n", + " )\n", + " ax.set_title(f\"Ratio: {min(number)/sum(number):.2f}\")\n", + "plt.tight_layout()\n", + "fig.set_size_inches((10,12))\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Density measurements\n", + "\n", + "Although the same total number of agents is simulated in all scenarios, the density in the middle of the corridor (within the measurement area) can still vary depending on the ratio of the distribution of agents within the simulation.\n", + "\n", + "\n", + "Therefore, here will will be calculating the density within the measurement are using the Voronoi method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "individual = {}\n", + "density_voronoi = {}\n", + "passing_density_left = {}\n", + "passing_density_right = {}\n", + "\n", + "for number in number_agents:\n", + " individual[number] = pedpy.compute_individual_voronoi_polygons( \n", + " traj_data=agent_trajectories[number], walkable_area=walkable_area\n", + " )\n", + " density_voronoi[number], intersecting = pedpy.compute_voronoi_density(\n", + " individual_voronoi_data=individual[number], measurement_area=measurement_area\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discussion of results\n", + "\n", + "As we might expect, the is highest for balanced ratio, which is an indicator of high number of unsolved conflicts.\n", + "\n", + "This is expected because the used model is known for not handling lane simulations very well dure to poor conflict resolution. See [Xu2021](https://doi.org/10.1016/j.trc.2021.103464), where a simplified collision-free velocity model that anticipates collisions is presented. This updated model diminishes gridlock events and offers a more accurate depiction of pedestrian movements compared to the previous version." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig0, ax0 = plt.subplots(nrows=1, ncols=1);\n", + "labels = []\n", + "colors = plt.rcParams['axes.prop_cycle'].by_key()['color']\n", + "for i, number in enumerate(number_agents):\n", + " pedpy.plot_density(\n", + " density=density_voronoi[number], axes=ax0, color=colors[i]\n", + " )\n", + " labels.append(f\"Ratio: {min(number)/sum(number):.3f}\")\n", + "\n", + "ax0.legend(labels)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Evacuation time vs Ratios\n", + "\n", + "From the observed increase of the density with increasing ratio, we expect that the evacuation time will increase as well. Again, due to the poor handling of conflicts in narrow space, the agents from both groups tend to clog in the middle of the corridor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "evac_times = {}\n", + "for i, number in enumerate(number_agents):\n", + " trajectories = agent_trajectories[number].data\n", + " fps = agent_trajectories[number].frame_rate\n", + " evac_time = trajectories['frame'].max()/fps\n", + " ratio = min(number)/sum(number)\n", + " evac_times[ratio] = evac_time\n", + "\n", + "plt.plot(list(evac_times.keys()), list(evac_times.values()),\"o-\")\n", + "plt.xlabel(\"Ratio\") \n", + "plt.ylabel(\"Evactime [s]\") \n", + "plt.show()" + ] + } + ], + "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/motivation.ipynb b/notebooks/motivation.ipynb index 5e5b0bec17..019429fc7f 100644 --- a/notebooks/motivation.ipynb +++ b/notebooks/motivation.ipynb @@ -1,10 +1,13 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "# Simulation of a corridor with different motivations\n", + "# Modelling Motivation\n", + "\n", + "## 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", @@ -14,7 +17,7 @@ "\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", + "## Setting up the geometry\n", "\n", "We will be using the a corridor 40 meters long and 4 meters wide." ] @@ -301,13 +304,14 @@ "metadata": {}, "outputs": [], "source": [ + "simulation.iterate(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\n", + "\n", "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" + " simulation.iterate()\n" ] }, { diff --git a/notebooks/routing.ipynb b/notebooks/routing.ipynb index 90a855ffc4..003be26d2e 100644 --- a/notebooks/routing.ipynb +++ b/notebooks/routing.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Simulation of a room following different routes\n", + "# How Route Choice can Impact Evacuation Times\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", @@ -16,9 +16,7 @@ "## 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. " + "Inside, obstacles will be strategically placed to segment the room and guide both agent groups." ] }, { diff --git a/notebooks/single-file.ipynb b/notebooks/single-file.ipynb new file mode 100644 index 0000000000..7f41984765 --- /dev/null +++ b/notebooks/single-file.ipynb @@ -0,0 +1,433 @@ +{ + "cells": [ + { + "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", + "from shapely import Polygon\n", + "import shapely\n", + "import pedpy\n", + "import matplotlib.pyplot as plt\n", + "#from matplotlib.patches import Circle\n", + "from typing import List, Tuple, Dict\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simulating Single-File Movement\n", + "\n", + "Pedestrian dynamics often focuses on the relationship between two key variables: density and either flow or velocity. The relationship is often visualized in what's termed as the fundamental diagram. By quantifying the capacity of pedestrian facilities, it serves as an essential tool for evaluating escape routes in emergencies and determining the effectiveness of various pedestrian movement models when depicting pedestrian streams.\n", + "\n", + "A fundamental question arises: how does one investigate the intricacies of the fundamental diagram without the interference of external factors? \n", + "\n", + "Single-file movement presents the simplest possible system in pedestrian dynamics. It is envisioned as the unidirectional motion of individuals moving along a straight line, thereby considerably reducing the degrees of freedom. In essence, by stripping down the system to this basic representation, it limits the external factors that might affect the outcomes. As a result, any observations or patterns detected can be attributed with higher confidence to the direct interplay between density and flow, providing a clearer understanding of the fundamental diagram.\n", + "\n", + "In this tutorial, we'll simulate with JuPedSim a simplified geometry with closed boundary conditions acording to [Paetzke](https://doi.org/10.3390/app13095450).\n", + "\n", + "![](./demo-data/single-file/single-file.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "def generate_oval_shape_points(\n", + " num_points:int,\n", + " length: float = 2.3,\n", + " radius: float = 1.65,\n", + " start: Tuple[float, float] = (0.0, 0.0),\n", + " dx: float = 0.2,\n", + " threshold: float = 0.5,\n", + " \n", + "):\n", + " \"\"\"Generate points on a closed setup with two segments and two half circles.\"\"\"\n", + " points = [start]\n", + " selected_points = [start]\n", + " last_selected = start # keep track of the last selected point\n", + "\n", + " # Define the points' generating functions\n", + " def dist(p1, p2):\n", + " return ((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)**0.5\n", + "\n", + " # Calculate dphi based on the dx and radius\n", + " dphi = dx / radius\n", + "\n", + " center2 = (start[0] + length, start[1] + radius)\n", + " center1 = (start[0], start[1] + radius)\n", + "\n", + " npoint_on_segment = int(length / dx)\n", + "\n", + " # first segment\n", + " for i in range(1, npoint_on_segment + 1):\n", + " tmp_point = (start[0] + i * dx, start[1])\n", + " points.append(tmp_point)\n", + " if dist(tmp_point, last_selected) >= threshold:\n", + " selected_points.append(tmp_point)\n", + " last_selected = tmp_point\n", + "\n", + " # first half circle\n", + " for phi in np.arange(-np.pi / 2, np.pi / 2, dphi):\n", + " x = center2[0] + radius * np.cos(phi)\n", + " y = center2[1] + radius * np.sin(phi)\n", + " tmp_point = (x, y)\n", + " points.append(tmp_point)\n", + " if dist(tmp_point, last_selected) >= threshold:\n", + " selected_points.append(tmp_point)\n", + " last_selected = tmp_point\n", + "\n", + " # second segment\n", + " for i in range(1, npoint_on_segment + 1):\n", + " tmp_point = (\n", + " start[0] + (npoint_on_segment + 1) * dx - i * dx,\n", + " start[1] + 2 * radius,\n", + " )\n", + " points.append(tmp_point)\n", + " if dist(tmp_point, last_selected) >= threshold:\n", + " selected_points.append(tmp_point)\n", + " last_selected = tmp_point\n", + "\n", + " # second half circle\n", + " for phi in np.arange(np.pi / 2, 3 * np.pi / 2 - dphi, dphi):\n", + " x = center1[0] + radius * np.cos(phi)\n", + " y = center1[1] + radius * np.sin(phi)\n", + " tmp_point = (x, y)\n", + " points.append(tmp_point)\n", + " if dist(tmp_point, last_selected) >= threshold:\n", + " selected_points.append(tmp_point)\n", + " last_selected = tmp_point\n", + "\n", + " if dist(selected_points[-1], start) < threshold:\n", + " selected_points.pop()\n", + " if num_points > len(selected_points):\n", + " print(f\"warning: {num_points} > Allowed: {len(selected_points)}\")\n", + " \n", + " selected_points = selected_points[:num_points]\n", + " return points, selected_points\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_, exterior = generate_oval_shape_points(48, radius=1.65+0.4, start=(0, -0.4), threshold=0.2)\n", + "_, interior = generate_oval_shape_points(35, radius=1.65-0.4, start=(0,0.4), threshold=0.2)\n", + "walkable_area = pedpy.WalkableArea( shapely.difference(Polygon(exterior), Polygon(interior)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-inpput" + ] + }, + "outputs": [], + "source": [ + "def find_nearest_clockwise_waypoint(position, all_points, waypoints):\n", + " idx = all_points.index(position)\n", + " while True:\n", + " idx = (idx + 1) % len(all_points)\n", + " if all_points[idx] in waypoints:\n", + " return all_points[idx]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scenario Overview\n", + "\n", + "Let's begin by positioning $N$ agents along the central path within our specified geometry. This particular geometry presents an interesting challenge; though our goal is to simulate a movement akin to 1D, where agents traverse a straight line, the line here is bent.\n", + "\n", + "We will set up the waypoints and agent positions based on the circuit illustrated by the dashed line in the preceding figure.\n", + "\n", + "Each agent will be assigned its initial waypoint to aim for.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def distribute_agents_and_waypoints(total_agents:int):\n", + " all_waypoints, positions = generate_oval_shape_points(num_points=total_agents)\n", + " first_waypoints = []\n", + " for position in positions:\n", + " first_waypoints.append(\n", + " find_nearest_clockwise_waypoint(position, all_waypoints, all_waypoints)\n", + " )\n", + " \n", + " return all_waypoints, first_waypoints, positions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "def plot_points_and_polygons(all_waypoints, choosen_waypoints, exterior, interior, positions):\n", + " __file__, ax = plt.subplots(ncols=1, nrows=1)\n", + " ax.set_aspect(\"equal\")\n", + " x_exterior, y_exterior = Polygon(exterior).exterior.xy\n", + " plt.plot(x_exterior, y_exterior, \"-k\", label=\"exterior\")\n", + " plt.fill(x_exterior, y_exterior, alpha=0.3)\n", + "\n", + " x_interior, y_interior = Polygon(interior).exterior.xy\n", + " plt.plot(x_interior, y_interior, \"--k\", label=\"interior\")\n", + " plt.fill(x_interior, y_interior, alpha=0.3)\n", + "\n", + " x_awp, y_awp = Polygon(all_waypoints).exterior.xy\n", + " plt.plot(x_awp, y_awp, \"-r\")\n", + " plt.fill(x_awp, y_awp, alpha=0.3)\n", + " plt.scatter(*zip(*all_waypoints), marker=\".\", label=\"waypoints\")\n", + "\n", + " \n", + " x_agents, y_agents = Polygon(positions).exterior.xy\n", + "\n", + " plt.plot(x_agents, y_agents, \"ob\", ms=8, label=\"agents\")\n", + "\n", + " \n", + " x_wp, y_wp = Polygon(choosen_waypoints).exterior.xy\n", + "\n", + " plt.plot(x_wp, y_wp, \"xk\", ms=5, label=\"first goals\")\n", + "\n", + " plt.legend()\n", + " plt.title(f\"N={len(positions)}\")\n", + " plt.show() " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "def compute_unit_vector(new_pos, wp):\n", + " dx = wp[0] - new_pos[0]\n", + " dy = wp[1] - new_pos[1]\n", + " \n", + " magnitude = (dx**2 + dy**2)**0.5\n", + " \n", + " if magnitude == 0:\n", + " return (0,0)\n", + " \n", + " ux = dx / magnitude\n", + " uy = dy / magnitude\n", + " \n", + " return (ux, uy)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining the Journey\n", + "\n", + "For our simulation, we're aiming for movement within a closed system. To achieve this, we'll establish a cyclical transition between waypoints, allowing agents to seamlessly move from one waypoint to the next. Given this structure, agents are confined within the system, eliminating the need for exit points.\n", + "\n", + "While the starting orientation of our agents might not play a pivotal role, we'll still configure it for precision. This means that, from the outset, each agent will be oriented towards its initially selected waypoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def init_waypoints(\n", + " simulation, all_waypoints: List[Tuple[float, float]]\n", + ") -> Dict[Tuple[float, float], id]:\n", + " waypoint_ids = []\n", + " waypoints: Dict[Tuple[float, float], id] = {}\n", + " distance = 0.1\n", + " for waypoint in all_waypoints:\n", + " wp_id = simulation.add_waypoint_stage(waypoint, distance)\n", + " waypoint_ids.append(wp_id)\n", + " waypoints[waypoint] = wp_id\n", + "\n", + " return waypoints" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def init_journey(\n", + " simulation: jps.Simulation, waypoints: Dict[Tuple[float, float], id]\n", + ") -> int:\n", + " waypoint_ids = list(waypoints.values())\n", + " journey = jps.JourneyDescription(waypoint_ids)\n", + " # create a circular transition from wp to next wp\n", + " for idx, waypoint in enumerate(waypoint_ids):\n", + " next_waypoint = (\n", + " waypoint_ids[0]\n", + " if idx == len(waypoint_ids) - 1\n", + " else waypoint_ids[idx + 1]\n", + " )\n", + " journey.set_transition_for_stage(\n", + " waypoint, jps.Transition.create_fixed_transition(next_waypoint)\n", + " )\n", + "\n", + " journey_id = simulation.add_journey(journey)\n", + " return journey_id" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def add_model_to_simulation(\n", + " simulation: jps.Simulation,\n", + " positions,\n", + " choosen_waypoints,\n", + " waypoints: Dict[Tuple[float, float], id],\n", + " journey_id: int,\n", + ") -> None:\n", + " agent_parameters = jps.CollisionFreeSpeedModelAgentParameters()\n", + " agent_parameters.journey_id = journey_id\n", + " for wp, new_pos in zip(choosen_waypoints, positions):\n", + "\n", + " agent_parameters.position = new_pos\n", + " agent_parameters.stage_id = waypoints[wp]\n", + " simulation.add_agent(agent_parameters)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Executing the Simulation\n", + "\n", + "Now, we'll proceed to run our simulation up to a predefined maximum iteration count. This approach lets the internal dynamics stabilize, potentially reaching a steady state. However, it's worth noting that certain dynamics, like stop-and-go waves, might prevent the system from settling into such a state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def run_simulation(total_agents:int, number_iterations:int = 2000):\n", + " all_waypoints, first_waypoints, positions = distribute_agents_and_waypoints(total_agents)\n", + " plot_points_and_polygons(all_waypoints, first_waypoints,exterior, interior, positions) \n", + " trajectory_file = f\"single-file-{total_agents}.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=exterior, excluded_areas=interior,\n", + " trajectory_writer=jps.SqliteTrajectoryWriter(\n", + " output_file=pathlib.Path(trajectory_file),\n", + " ),\n", + " )\n", + " waypoints = init_waypoints(simulation, all_waypoints)\n", + " journey_id = init_journey(simulation, waypoints)\n", + " add_model_to_simulation(simulation, positions, first_waypoints, waypoints, journey_id)\n", + " \n", + " while simulation.iteration_count() < number_iterations:\n", + " simulation.iterate()\n", + "\n", + " return trajectory_file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "trajectory_files = [run_simulation(N) for N in [5, 15, 20, 24]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from jupedsim.internal.notebook_utils import animate, read_sqlite_file\n", + "\n", + "for trajectory_file in trajectory_files:\n", + " trajectories, walkable_area_sqlite = read_sqlite_file(trajectory_file)\n", + " animate(trajectories, walkable_area, every_nth_frame=5).show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discussion\n", + "\n", + "The behavior of agents closely following a central path can be attributed to the dense distribution of waypoints with a small radius. Essentially, the dense waypoints give agents minimal options for deviation, making them adhere closely to the specified path. This can be advantageous in scenarios where precise path-following is necessary.\n", + "\n", + "However, the observed behavior of stop-and-go waves disappearing over time indicates an imperfection in the model, especially if such dynamics are expected or seen in real-world scenarios that the model aims to emulate. The fading of these waves suggests that the agents stabilize into a flow that doesn't have the intermittent stop-and-go behavior.\n", + "\n", + "Velocity-based models often have this limitation. In such models, agents typically adjust their velocities based on surrounding conditions. Once the initial disturbances (like starting from rest or initial congestions) are overcome, the agents find a sort of \"equilibrium\" velocity, leading to a smoother flow. This smooth flow may not always accurately represent real-world dynamics, especially in situations with frequent perturbations or inherent unpredictabilities, like human-driven traffic.\n", + "\n", + "## Summary\n", + "\n", + "The model effectively simulates agents that adhere closely to a central path due to the dense distribution of waypoints with small radii. However, the disappearance of stop-and-go waves over time highlights a limitation inherent to velocity-based models. Such models may not be suitable for scenarios where persistent stop-and-go dynamics are expected or required. Future iterations of the model may benefit from incorporating additional factors or behaviors to better simulate such dynamics." + ] + } + ], + "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/python_modules/jupedsim/jupedsim/internal/notebook_utils.py b/python_modules/jupedsim/jupedsim/internal/notebook_utils.py index 8dec7a5cbe..55f4193d3e 100644 --- a/python_modules/jupedsim/jupedsim/internal/notebook_utils.py +++ b/python_modules/jupedsim/jupedsim/internal/notebook_utils.py @@ -299,7 +299,11 @@ def animate( radius: float = 0.2, title_note: str = "", ): - data_df = pedpy.compute_individual_speed(traj_data=data, frame_step=5) + data_df = pedpy.compute_individual_speed( + traj_data=data, + frame_step=5, + speed_calculation=pedpy.SpeedCalculation.BORDER_SINGLE_SIDED, + ) data_df = data_df.merge(data.data, on=["id", "frame"], how="left") data_df["radius"] = radius min_speed = data_df["speed"].min() @@ -318,7 +322,7 @@ def animate( initial_arrows, ) = _get_shapes_for_frame(initial_frame_data, min_speed, max_speed) color_map_trace = _get_colormap(initial_frame_data, max_speed) - for frame_num in selected_frames[1:]: + for frame_num in selected_frames: frame_data, agent_count = _get_processed_frame_data( data_df, frame_num, max_agents ) @@ -326,9 +330,10 @@ def animate( frame_data, min_speed, max_speed ) title = f"{title_note + ' | ' if title_note else ''}Number of Agents: {agent_count}" + frame_name = str(int(frame_num)) frame = go.Frame( data=geometry_traces + hover_traces, - name=str(frame_num), + name=frame_name, layout=go.Layout( shapes=shapes + arrows, title=title, @@ -339,14 +344,14 @@ def animate( step = { "args": [ - [str(frame_num)], + [frame_name], { "frame": {"duration": 100, "redraw": True}, "mode": "immediate", "transition": {"duration": 500}, }, ], - "label": str(frame_num), + "label": frame_name, "method": "animate", } steps.append(step)