jupedsim-network 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. jupedsim_network-0.1.0/.github/workflows/ci.yml +66 -0
  2. jupedsim_network-0.1.0/.gitignore +11 -0
  3. jupedsim_network-0.1.0/LICENSE +14 -0
  4. jupedsim_network-0.1.0/PKG-INFO +66 -0
  5. jupedsim_network-0.1.0/README.md +53 -0
  6. jupedsim_network-0.1.0/pyproject.toml +41 -0
  7. jupedsim_network-0.1.0/scripts/figures/make_figures.py +442 -0
  8. jupedsim_network-0.1.0/scripts/figures/make_gifs.py +291 -0
  9. jupedsim_network-0.1.0/scripts/figures/scenarios.py +100 -0
  10. jupedsim_network-0.1.0/site/assets/css/custom.css +6 -0
  11. jupedsim_network-0.1.0/site/content/_index.md +31 -0
  12. jupedsim_network-0.1.0/site/content/docs/_index.md +53 -0
  13. jupedsim_network-0.1.0/site/content/docs/evacuationz.md +39 -0
  14. jupedsim_network-0.1.0/site/content/docs/getting-started.md +169 -0
  15. jupedsim_network-0.1.0/site/content/docs/limitations.md +152 -0
  16. jupedsim_network-0.1.0/site/content/docs/model/_index.md +43 -0
  17. jupedsim_network-0.1.0/site/content/docs/model/elements.md +169 -0
  18. jupedsim_network-0.1.0/site/content/docs/model/update-scheme.md +153 -0
  19. jupedsim_network-0.1.0/site/content/docs/references.md +77 -0
  20. jupedsim_network-0.1.0/site/content/docs/ten-storey.md +50 -0
  21. jupedsim_network-0.1.0/site/content/docs/using/_index.md +14 -0
  22. jupedsim_network-0.1.0/site/content/docs/using/networks.md +146 -0
  23. jupedsim_network-0.1.0/site/content/docs/using/populations.md +111 -0
  24. jupedsim_network-0.1.0/site/content/docs/using/results.md +83 -0
  25. jupedsim_network-0.1.0/site/content/docs/using/running.md +186 -0
  26. jupedsim_network-0.1.0/site/content/docs/verification.md +137 -0
  27. jupedsim_network-0.1.0/site/go.mod +5 -0
  28. jupedsim_network-0.1.0/site/go.sum +2 -0
  29. jupedsim_network-0.1.0/site/hugo.yaml +55 -0
  30. jupedsim_network-0.1.0/site/layouts/alias.html +29 -0
  31. jupedsim_network-0.1.0/site/layouts/shortcodes/cite.html +20 -0
  32. jupedsim_network-0.1.0/src/jupedsim_network/__init__.py +42 -0
  33. jupedsim_network-0.1.0/src/jupedsim_network/hydraulic.py +62 -0
  34. jupedsim_network-0.1.0/src/jupedsim_network/network.py +267 -0
  35. jupedsim_network-0.1.0/src/jupedsim_network/sampling.py +109 -0
  36. jupedsim_network-0.1.0/src/jupedsim_network/simulation.py +482 -0
  37. jupedsim_network-0.1.0/tests/test_network.py +310 -0
  38. jupedsim_network-0.1.0/uv.lock +1183 -0
@@ -0,0 +1,66 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v6
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: uv run ruff check .
20
+ - run: uv run ruff format --check .
21
+ - run: uv run pytest
22
+
23
+ docs:
24
+ runs-on: ubuntu-latest
25
+ env:
26
+ HUGO_VERSION: 0.144.0
27
+ steps:
28
+ - name: Install Hugo CLI
29
+ run: |
30
+ wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
31
+ && sudo dpkg -i ${{ runner.temp }}/hugo.deb
32
+ - uses: actions/setup-go@v5
33
+ with:
34
+ go-version: "1.22"
35
+ - uses: actions/checkout@v4
36
+ with:
37
+ fetch-depth: 0 # full history for enableGitInfo (last-updated dates)
38
+ - uses: astral-sh/setup-uv@v6
39
+ # The figures are not in git: regenerate them from the current code,
40
+ # so the site never shows a figure the code can no longer produce.
41
+ - name: Generate figures
42
+ run: |
43
+ uv run --group docs python scripts/figures/make_figures.py
44
+ uv run --group docs python scripts/figures/make_gifs.py
45
+ - name: Build with Hugo
46
+ env:
47
+ HUGO_ENVIRONMENT: production
48
+ working-directory: site
49
+ run: hugo --minify --panicOnWarning
50
+ - uses: actions/upload-pages-artifact@v3
51
+ with:
52
+ path: site/public
53
+
54
+ deploy:
55
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
56
+ needs: [test, docs]
57
+ runs-on: ubuntu-latest
58
+ permissions:
59
+ pages: write
60
+ id-token: write
61
+ environment:
62
+ name: github-pages
63
+ url: ${{ steps.deployment.outputs.page_url }}
64
+ steps:
65
+ - id: deployment
66
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ build/
5
+ dist/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ site/public/
9
+ site/resources/
10
+ .hugo_build.lock
11
+ site/static/images/network/
@@ -0,0 +1,14 @@
1
+ License:
2
+ ========
3
+ jupedsim-network is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU Lesser General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ any later version.
7
+
8
+ jupedsim-network is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU Lesser General Public License
14
+ along with jupedsim-network. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.5
2
+ Name: jupedsim-network
3
+ Version: 0.1.0
4
+ Summary: Coarse network egress model for fast evacuation-time estimates, companion to JuPedSim
5
+ Project-URL: Documentation, https://pedestriandynamics.org/jupedsim-network/
6
+ Project-URL: Source, https://github.com/PedestrianDynamics/jupedsim-network
7
+ Project-URL: JuPedSim, https://www.jupedsim.org
8
+ License-Expression: LGPL-3.0-or-later
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: numpy>=1.26
12
+ Description-Content-Type: text/markdown
13
+
14
+ # jupedsim-network
15
+
16
+ A coarse network egress model for fast estimates of evacuation times
17
+ (pre-movement plus movement). Rooms, corridors and stair flights are
18
+ nodes with an area; doors and stair entries are links with a flow
19
+ capacity. Agents move
20
+ through the graph by relations adapted from the SFPE hydraulic model. A run of a
21
+ ten-storey building takes about 0.1 s, so scenarios can be sampled many
22
+ times.
23
+
24
+ The package is a companion to [JuPedSim](https://www.jupedsim.org): use
25
+ the network model to screen scenarios and find critical cases, and the
26
+ microscopic models of JuPedSim to check geometry details. It depends only
27
+ on NumPy.
28
+
29
+ The model is a prototype. It is verified against hand calculations but
30
+ not validated against experiments, and it is not intended for
31
+ regulatory or design use; see the
32
+ [limitations](https://pedestriandynamics.org/jupedsim-network/docs/limitations/).
33
+
34
+ ## Install
35
+
36
+ Python 3.10 or later:
37
+
38
+ ```
39
+ pip install git+https://github.com/PedestrianDynamics/jupedsim-network
40
+ ```
41
+
42
+ ## Development
43
+
44
+ ```
45
+ uv sync
46
+ uv run pytest
47
+ uv run ruff check . && uv run ruff format --check .
48
+ ```
49
+
50
+ The documentation is a [Hugo](https://gohugo.io) site with the
51
+ [Hextra](https://imfing.github.io/hextra/) theme in `site/`. Hugo
52
+ (extended) and Go are needed to build it:
53
+
54
+ ```
55
+ uv run --group docs python scripts/figures/make_figures.py
56
+ uv run --group docs python scripts/figures/make_gifs.py
57
+ cd site && hugo server
58
+ ```
59
+
60
+ The figures and animations are not stored in git. The two scripts write
61
+ them to `site/static/images/network/` (about 2 minutes); CI runs them
62
+ before every build.
63
+
64
+ ## License
65
+
66
+ LGPL-3.0-or-later, as JuPedSim.
@@ -0,0 +1,53 @@
1
+ # jupedsim-network
2
+
3
+ A coarse network egress model for fast estimates of evacuation times
4
+ (pre-movement plus movement). Rooms, corridors and stair flights are
5
+ nodes with an area; doors and stair entries are links with a flow
6
+ capacity. Agents move
7
+ through the graph by relations adapted from the SFPE hydraulic model. A run of a
8
+ ten-storey building takes about 0.1 s, so scenarios can be sampled many
9
+ times.
10
+
11
+ The package is a companion to [JuPedSim](https://www.jupedsim.org): use
12
+ the network model to screen scenarios and find critical cases, and the
13
+ microscopic models of JuPedSim to check geometry details. It depends only
14
+ on NumPy.
15
+
16
+ The model is a prototype. It is verified against hand calculations but
17
+ not validated against experiments, and it is not intended for
18
+ regulatory or design use; see the
19
+ [limitations](https://pedestriandynamics.org/jupedsim-network/docs/limitations/).
20
+
21
+ ## Install
22
+
23
+ Python 3.10 or later:
24
+
25
+ ```
26
+ pip install git+https://github.com/PedestrianDynamics/jupedsim-network
27
+ ```
28
+
29
+ ## Development
30
+
31
+ ```
32
+ uv sync
33
+ uv run pytest
34
+ uv run ruff check . && uv run ruff format --check .
35
+ ```
36
+
37
+ The documentation is a [Hugo](https://gohugo.io) site with the
38
+ [Hextra](https://imfing.github.io/hextra/) theme in `site/`. Hugo
39
+ (extended) and Go are needed to build it:
40
+
41
+ ```
42
+ uv run --group docs python scripts/figures/make_figures.py
43
+ uv run --group docs python scripts/figures/make_gifs.py
44
+ cd site && hugo server
45
+ ```
46
+
47
+ The figures and animations are not stored in git. The two scripts write
48
+ them to `site/static/images/network/` (about 2 minutes); CI runs them
49
+ before every build.
50
+
51
+ ## License
52
+
53
+ LGPL-3.0-or-later, as JuPedSim.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "jupedsim-network"
7
+ version = "0.1.0"
8
+ description = "Coarse network egress model for fast evacuation-time estimates, companion to JuPedSim"
9
+ readme = "README.md"
10
+ license = "LGPL-3.0-or-later"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ dependencies = ["numpy>=1.26"]
14
+
15
+ [project.urls]
16
+ Documentation = "https://pedestriandynamics.org/jupedsim-network/"
17
+ Source = "https://github.com/PedestrianDynamics/jupedsim-network"
18
+ JuPedSim = "https://www.jupedsim.org"
19
+
20
+ [dependency-groups]
21
+ dev = ["pytest>=8", "ruff>=0.6"]
22
+ docs = ["matplotlib==3.10.8"]
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/jupedsim_network"]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+
30
+ [tool.ruff]
31
+ line-length = 80
32
+ indent-width = 4
33
+ target-version = "py310"
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I", "N"]
37
+ ignore = ["E501"]
38
+
39
+ [tool.ruff.format]
40
+ quote-style = "double"
41
+ indent-style = "space"
@@ -0,0 +1,442 @@
1
+ """Static figures for the network model documentation.
2
+
3
+ Writes PNGs to ``site/static/images/network`` or to the directory given as
4
+ the first argument. ``jupedsim_network`` must be importable::
5
+
6
+ uv run --group docs python scripts/figures/make_figures.py
7
+ uv run --group docs python scripts/figures/make_gifs.py
8
+ """
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import matplotlib
14
+
15
+ matplotlib.use("Agg")
16
+ import matplotlib.pyplot as plt
17
+ import numpy as np
18
+ from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch
19
+ from scenarios import building, building_populations, merge_corridor
20
+
21
+ from jupedsim_network import (
22
+ LogNormal,
23
+ Network,
24
+ NetworkSimulation,
25
+ Population,
26
+ hydraulic,
27
+ )
28
+
29
+ ROOT = Path(__file__).resolve().parents[2]
30
+ DEFAULT_OUT = ROOT / "site" / "static" / "images" / "network"
31
+ PAL = ["#90c1c6", "#72a5b4", "#58849f", "#446485", "#324465", "#1f253f"]
32
+ RED = "#bd0c0c"
33
+ ORANGE = "#fc8d59"
34
+ GREY = "dimgrey"
35
+
36
+
37
+ def style():
38
+ plt.rcParams.update(
39
+ {
40
+ "font.family": "DejaVu Sans",
41
+ "font.size": 10,
42
+ "axes.spines.top": False,
43
+ "axes.spines.right": False,
44
+ "axes.spines.left": False,
45
+ "axes.spines.bottom": False,
46
+ "axes.grid": False,
47
+ "axes.labelcolor": "#333333",
48
+ "axes.titlesize": 11,
49
+ "axes.titleweight": "semibold",
50
+ "xtick.major.size": 0,
51
+ "ytick.major.size": 0,
52
+ "xtick.color": GREY,
53
+ "ytick.color": GREY,
54
+ "legend.frameon": True,
55
+ "legend.facecolor": "white",
56
+ "legend.framealpha": 0.8,
57
+ "legend.edgecolor": "lightgrey",
58
+ "legend.labelcolor": GREY,
59
+ "savefig.dpi": 150,
60
+ "savefig.bbox": "tight",
61
+ }
62
+ )
63
+
64
+
65
+ def frame(*axes):
66
+ for ax in axes:
67
+ ax.patch.set_edgecolor("lightgrey")
68
+ ax.patch.set_linewidth(0.8)
69
+ ax.grid(alpha=0.4, linewidth=0.6)
70
+
71
+
72
+ def box(ax, xy, w, h, text, color):
73
+ patch = FancyBboxPatch(
74
+ xy,
75
+ w,
76
+ h,
77
+ boxstyle="round,pad=0.02,rounding_size=0.08",
78
+ fc=color,
79
+ ec="#324465",
80
+ lw=1.0,
81
+ )
82
+ ax.add_patch(patch)
83
+ ax.text(
84
+ xy[0] + w / 2,
85
+ xy[1] + h / 2,
86
+ text,
87
+ ha="center",
88
+ va="center",
89
+ fontsize=9,
90
+ color="#1f253f",
91
+ )
92
+
93
+
94
+ def arrow(ax, a, b, text=None, offset=(0, 0.12)):
95
+ ax.add_patch(
96
+ FancyArrowPatch(
97
+ a, b, arrowstyle="-|>", mutation_scale=12, color="#446485", lw=1.4
98
+ )
99
+ )
100
+ if text:
101
+ mid = ((a[0] + b[0]) / 2 + offset[0], (a[1] + b[1]) / 2 + offset[1])
102
+ ax.text(*mid, text, ha="center", va="bottom", fontsize=8, color=GREY)
103
+
104
+
105
+ def schematic(out):
106
+ fig, ax = plt.subplots(figsize=(10, 3.6))
107
+ box(ax, (0.0, 1.6), 2.2, 1.1, "", "#e8f1f2")
108
+ ax.text(
109
+ 1.1,
110
+ 2.48,
111
+ "room a, area $A_a$",
112
+ ha="center",
113
+ fontsize=9,
114
+ color="#1f253f",
115
+ )
116
+ box(ax, (0.0, 0.0), 2.2, 1.1, "room b\narea $A_b$", "#e8f1f2")
117
+ box(ax, (3.6, 0.8), 1.8, 1.1, "corridor\narea $A_c$", "#e8f1f2")
118
+ box(ax, (6.8, 0.8), 1.8, 1.1, "stair flight\n$k(R,T)$, area", "#d5e5ea")
119
+ box(ax, (9.8, 0.8), 1.4, 1.1, "safe", "#e9f4e4")
120
+ arrow(ax, (2.2, 2.15), (3.6, 1.55), "door, $w_a$")
121
+ arrow(ax, (2.2, 0.55), (3.6, 1.15), "door, $w_b$", offset=(0, -0.35))
122
+ arrow(ax, (5.4, 1.35), (6.8, 1.35), "stair entry")
123
+ arrow(ax, (8.6, 1.35), (9.8, 1.35), "door")
124
+ for x in range(5):
125
+ ax.add_patch(Circle((1.75 - 0.18 * x, 2.15), 0.06, color=RED))
126
+ ax.annotate(
127
+ "",
128
+ xy=(0.15, 1.92),
129
+ xytext=(2.05, 1.92),
130
+ arrowprops=dict(arrowstyle="<->", color=GREY, lw=0.8),
131
+ )
132
+ ax.text(
133
+ 1.1,
134
+ 1.7,
135
+ "link length $L$ (walked in source)",
136
+ ha="center",
137
+ fontsize=7.5,
138
+ color=GREY,
139
+ )
140
+ ax.text(
141
+ 1.0, 2.15, "queue →", fontsize=7.5, color=RED, ha="right", va="center"
142
+ )
143
+ ax.text(
144
+ 5.6,
145
+ -0.25,
146
+ r"link capacity $C = F_s\,(w - 2b)$ · node density "
147
+ r"$D = \sum_i a_i / A$",
148
+ ha="center",
149
+ fontsize=9,
150
+ color=GREY,
151
+ )
152
+ ax.set_xlim(-0.2, 11.4)
153
+ ax.set_ylim(-0.5, 2.9)
154
+ ax.set_aspect("equal")
155
+ ax.axis("off")
156
+ fig.savefig(out / "schematic.png")
157
+ plt.close(fig)
158
+
159
+
160
+ def mark_densities(ax, marks):
161
+ top = ax.get_ylim()[1] * 0.98
162
+ for x, text in marks:
163
+ ax.axvline(x, color="lightgrey", lw=0.8, zorder=0)
164
+ ax.text(
165
+ x,
166
+ top,
167
+ text,
168
+ fontsize=7.5,
169
+ color=GREY,
170
+ ha="center",
171
+ va="top",
172
+ bbox=dict(fc="white", ec="none", pad=1),
173
+ )
174
+
175
+
176
+ def fundamental(out):
177
+ d = np.linspace(0, hydraulic.JAM_DENSITY, 400)
178
+ k_level = hydraulic.LEVEL_SPEED_CONSTANT
179
+ k_stair = hydraulic.stair_speed_constant(0.18, 0.28)
180
+ fig, axes = plt.subplots(1, 3, figsize=(13, 3.8))
181
+ for k, label, color in [
182
+ (k_level, "level, k = 1.40 m/s", PAL[3]),
183
+ (k_stair, "stair 18/28 cm, k = 1.08 m/s", ORANGE),
184
+ ]:
185
+ s = hydraulic.speed(d, k)
186
+ axes[0].plot(d, s, color=color, lw=2, label=label)
187
+ axes[1].plot(d, s * d, color=color, lw=2, label=label)
188
+ marks = [
189
+ (hydraulic.MIN_DENSITY, "0.54"),
190
+ (hydraulic.PEAK_FLOW_DENSITY, "1.88"),
191
+ (2.75, "2.75"),
192
+ (hydraulic.JAM_DENSITY, "3.76"),
193
+ ]
194
+ for ax in axes[:2]:
195
+ mark_densities(ax, marks)
196
+ axes[0].set(
197
+ xlabel="density D (1/m²)",
198
+ ylabel="speed S (m/s)",
199
+ title="(a) speed, S = k (1 − 0.266 D)",
200
+ )
201
+ axes[1].set(
202
+ xlabel="density D (1/m²)",
203
+ ylabel="specific flow S·D (1/m/s)",
204
+ title="(b) specific flow",
205
+ )
206
+ axes[0].legend(loc="lower left", fontsize=8)
207
+ phi = np.clip((2.75 - d) / (2.75 - hydraulic.PEAK_FLOW_DENSITY), 0, 1)
208
+ axes[2].plot(d, phi, color=PAL[4], lw=2, label="supply reduction on")
209
+ axes[2].plot(
210
+ d,
211
+ np.where(d < 2.75, 1, 0),
212
+ color=RED,
213
+ lw=1.2,
214
+ ls="--",
215
+ label="hard limit only",
216
+ )
217
+ axes[2].set(
218
+ xlabel="density of receiving node D (1/m²)",
219
+ ylabel="accepted share of inflow φ",
220
+ title="(c) supply factor",
221
+ xlim=(0, hydraulic.JAM_DENSITY),
222
+ )
223
+ axes[2].legend(loc="lower left", fontsize=8)
224
+ axes[2].annotate(
225
+ "$D_{peak}$",
226
+ (1.88, 1.0),
227
+ (1.2, 0.7),
228
+ color=GREY,
229
+ arrowprops=dict(arrowstyle="-", color=GREY, lw=0.6),
230
+ )
231
+ axes[2].annotate(
232
+ "$D_{max}$",
233
+ (2.75, 0.0),
234
+ (3.0, 0.35),
235
+ color=GREY,
236
+ arrowprops=dict(arrowstyle="-", color=GREY, lw=0.6),
237
+ )
238
+ frame(*axes)
239
+ fig.tight_layout()
240
+ fig.savefig(out / "fundamental_diagram.png")
241
+ plt.close(fig)
242
+
243
+
244
+ def door_and_merge(out):
245
+ fig, axes = plt.subplots(1, 2, figsize=(11, 4))
246
+ net = Network()
247
+ net.add_room("room", area=40.0)
248
+ net.add_safe("exit")
249
+ net.connect("room", "exit", width=1.0, specific_flow=1.33)
250
+ r = NetworkSimulation(net, [Population("room", 100, speed=1.0)]).run(1)
251
+ exits = np.sort(r.exit_times)
252
+ axes[0].step(
253
+ exits,
254
+ np.arange(1, 101),
255
+ where="post",
256
+ color=PAL[3],
257
+ lw=2,
258
+ label="network model, Δt = 0.5 s",
259
+ )
260
+ t = np.linspace(0, 99 / (1.33 * 0.7), 50)
261
+ axes[0].plot(
262
+ t,
263
+ 1 + 1.33 * 0.7 * t,
264
+ color=RED,
265
+ ls="--",
266
+ lw=1.2,
267
+ label="model convention 1 + C t",
268
+ )
269
+ t = np.linspace(0, 100 / (1.33 * 0.7), 50)
270
+ axes[0].plot(
271
+ t,
272
+ 1.33 * 0.7 * t,
273
+ color=ORANGE,
274
+ ls=":",
275
+ lw=1.6,
276
+ label="SFPE hand calculation C t",
277
+ )
278
+ axes[0].set(
279
+ xlabel="time (s)",
280
+ ylabel="agents through door",
281
+ title="(a) IMO test 4 geometry: 100 agents, 1 m door",
282
+ )
283
+ axes[0].text(
284
+ 60,
285
+ 12,
286
+ f"last agent: {r.evacuation_time:.1f} s\n"
287
+ f"(N − 1)/C = {99 / 0.931:.1f} s\n"
288
+ f"N/C = {100 / 0.931:.1f} s",
289
+ color=GREY,
290
+ fontsize=9,
291
+ )
292
+ axes[0].legend(loc="upper left", fontsize=8)
293
+
294
+ pops = [Population("a", 300), Population("b", 300)]
295
+ r = NetworkSimulation(merge_corridor(), pops, t_max=150.0).run(seed=2)
296
+ for name, color, w, ls in [
297
+ ("a->corridor", PAL[2], 1, "-"),
298
+ ("b->corridor", ORANGE, 3, "--"),
299
+ ]:
300
+ flow = r.link_flow[:, r.link_names.index(name)]
301
+ axes[1].plot(
302
+ r.times,
303
+ np.cumsum(flow),
304
+ color=color,
305
+ ls=ls,
306
+ lw=2,
307
+ label=f"{name.split('-')[0]} (merge weight {w})",
308
+ )
309
+ axes[1].set(
310
+ xlabel="time (s)",
311
+ ylabel="agents into corridor",
312
+ title="(b) merging into a full corridor, weights 1 : 3",
313
+ )
314
+ axes[1].legend(loc="upper left", fontsize=8)
315
+ frame(*axes)
316
+ fig.tight_layout()
317
+ fig.savefig(out / "door_and_merge.png")
318
+ plt.close(fig)
319
+
320
+
321
+ def rset_quantiles(max_density, supply_reduction, runs):
322
+ sim = NetworkSimulation(
323
+ building(stairs=("A",)),
324
+ building_populations(),
325
+ max_density=max_density,
326
+ supply_reduction=supply_reduction,
327
+ )
328
+ times = sim.run_many(runs, seed=1).evacuation_times
329
+ return np.quantile(times, [0.05, 0.5, 0.95])
330
+
331
+
332
+ def density_sweep(out, runs=30):
333
+ limits = np.array([2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.7])
334
+ fig, ax = plt.subplots(figsize=(7, 4.2))
335
+ for on, color, label, marker in [
336
+ (True, PAL[3], "supply reduction on", "o"),
337
+ (False, RED, "hard limit only", "s"),
338
+ ]:
339
+ q = np.array([rset_quantiles(m, on, runs) for m in limits])
340
+ ax.fill_between(limits, q[:, 0], q[:, 2], color=color, alpha=0.15)
341
+ ax.plot(limits, q[:, 1], color=color, marker=marker, lw=2, label=label)
342
+ ax.set(
343
+ xlabel="max_density (1/m²)",
344
+ ylabel="evacuation time (s), median and 5–95 %",
345
+ title="10 floors × 60 agents, one stair",
346
+ )
347
+ ax.legend(loc="upper left", fontsize=8)
348
+ frame(ax)
349
+ fig.savefig(out / "max_density_sweep.png")
350
+ plt.close(fig)
351
+
352
+
353
+ def route_tie(out):
354
+ cases = [
355
+ (
356
+ "two stairs, nearest exit (tie → one stair)",
357
+ building(),
358
+ building_populations(),
359
+ RED,
360
+ "-",
361
+ ),
362
+ (
363
+ "two stairs, targets split 50/50",
364
+ building(),
365
+ building_populations(split="AB"),
366
+ PAL[3],
367
+ "--",
368
+ ),
369
+ (
370
+ "one stair",
371
+ building(stairs=("A",)),
372
+ building_populations(),
373
+ GREY,
374
+ ":",
375
+ ),
376
+ ]
377
+ fig, ax = plt.subplots(figsize=(7, 4.2))
378
+ for label, net, pops, color, ls in cases:
379
+ r = NetworkSimulation(net, pops).run(seed=1)
380
+ e = np.sort(r.exit_times)
381
+ ax.step(
382
+ e,
383
+ np.arange(1, len(e) + 1),
384
+ where="post",
385
+ color=color,
386
+ ls=ls,
387
+ lw=2,
388
+ label=f"{label}: {r.evacuation_time:.1f} s",
389
+ )
390
+ ax.set(
391
+ xlabel="time (s)",
392
+ ylabel="agents evacuated",
393
+ title="Equal-distance exits are not split",
394
+ )
395
+ ax.legend(loc="lower right", fontsize=8)
396
+ frame(ax)
397
+ fig.savefig(out / "route_tie.png")
398
+ plt.close(fig)
399
+
400
+
401
+ def monte_carlo(out, runs=300):
402
+ pre = LogNormal(120.0, 60.0, upper=600.0)
403
+ pops = building_populations(pre=pre, split="AB")
404
+ mc = NetworkSimulation(building(), pops).run_many(runs, seed=11)
405
+ t = mc.complete
406
+ fig, ax = plt.subplots(figsize=(7, 4.2))
407
+ ax.hist(t, bins=30, color=PAL[2], edgecolor="white")
408
+ for q, ls in [(0.5, "--"), (0.95, "-")]:
409
+ v = mc.quantile(q)
410
+ ax.axvline(v, color=RED, ls=ls, lw=1.2)
411
+ ax.text(
412
+ v,
413
+ ax.get_ylim()[1] * 0.95,
414
+ f" {int(q * 100)} %: {v:.0f} s",
415
+ color=RED,
416
+ fontsize=8.5,
417
+ va="top",
418
+ )
419
+ ax.set(
420
+ xlabel="evacuation time (s)",
421
+ ylabel="runs",
422
+ title=f"{runs} runs, pre-movement LogNormal(120 s, 60 s)",
423
+ )
424
+ ax.grid(alpha=0.4, axis="y")
425
+ fig.savefig(out / "monte_carlo.png")
426
+ plt.close(fig)
427
+
428
+
429
+ def main():
430
+ out = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_OUT
431
+ out.mkdir(parents=True, exist_ok=True)
432
+ style()
433
+ schematic(out)
434
+ fundamental(out)
435
+ door_and_merge(out)
436
+ density_sweep(out)
437
+ route_tie(out)
438
+ monte_carlo(out)
439
+
440
+
441
+ if __name__ == "__main__":
442
+ main()