ISO 20414:2020 — Test 13: Flow rate, density and walking speed in a corridor¶

A 2 m x 60 m corridor (area 120 m^2) in three zones (ISO 20414:2020 Table 14). 60/120/240/360/480 occupants give initial densities of 0.5/1.0/2.0/3.0/4.0 p/m^2. Walking speed in zone 2 is measured between line A (x = 20) and line B (x = 40). The expected result is the classic speed-down / flow-hump fundamental diagram.

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

import matplotlib.pyplot as plt
import pandas as pd

from jupedsim_scenarios import load_scenario, run_scenario
In [3]:
plt.rcParams.update({
    "figure.facecolor": "white",
    "axes.facecolor": "#f7f7f5",
    "axes.edgecolor": "#3a3a3a",
    "axes.labelcolor": "#1d1d1d",
    "axes.titleweight": "bold",
    "axes.grid": True,
    "grid.alpha": 0.3,
    "font.size": 11,
    "figure.figsize": (8, 5),
})
In [4]:
import json
import tempfile
from scenario_builders.iso13_corridor_fd import (
    DENSITY_CASES, build_raw_scenario, CORRIDOR_WKT, measure_zone2)

Scenario check — geometry and trajectories¶

Confirm the simulated layout: a 2 m x 60 m corridor divided into three zones by line A (x = 20) and line B (x = 40); occupants (here the n = 120 case) walk left to right to the exit. Zone 2, between the lines, is where speed, density, and flow are measured.

In [5]:
_raw = build_raw_scenario(120)
with tempfile.TemporaryDirectory() as _d:
    _p = Path(_d)
    (_p / 'config.json').write_text(json.dumps(_raw, indent=2), encoding='utf-8')
    (_p / 'geometry.wkt').write_text(CORRIDOR_WKT, encoding='utf-8')
    scen = load_scenario(str(_p))
    _res = run_scenario(scen, seed=42)
tdf = _res.trajectory_dataframe()
poly = scen.walkable_polygon
fig, ax = plt.subplots(figsize=(10, 2.4))
gx, gy = poly.exterior.xy
ax.fill(gx, gy, facecolor='#eef2f7', edgecolor='k', lw=1.5, zorder=0)
for interior in poly.interiors:
    ix, iy = interior.xy
    ax.fill(ix, iy, facecolor='white', edgecolor='k', lw=1.0, zorder=1)
for z in scen.zones.values():
    zx = [c[0] for c in z['coordinates']]
    zy = [c[1] for c in z['coordinates']]
    ax.fill(zx, zy, facecolor='tab:orange', alpha=0.18, edgecolor='tab:orange',
            zorder=1, label=f"speed-factor {z.get('speed_factor')} zone")
for e in scen.exits.values():
    ex = [c[0] for c in e['coordinates']]
    ey = [c[1] for c in e['coordinates']]
    ax.fill(ex, ey, facecolor='tab:green', alpha=0.6, zorder=2, label='exit')
for _, sub in tdf.sort_values(['id', 'frame']).groupby('id'):
    ax.plot(sub.x, sub.y, color='tab:blue', lw=0.6, alpha=0.4, zorder=3)
starts = tdf.sort_values(['id', 'frame']).groupby('id').first()
ax.scatter(starts.x, starts.y, s=8, color='tab:blue', zorder=4, label='start')
ax.axvline(20.0, color='tab:red', ls='--', lw=1.0, alpha=0.7, label='zone boundary (A, B)')
ax.axvline(40.0, color='tab:red', ls='--', lw=1.0, alpha=0.7)
handles, labels = ax.get_legend_handles_labels()
uniq = dict(zip(labels, handles))
ax.legend(uniq.values(), uniq.keys(), fontsize=8, loc='upper center',
          bbox_to_anchor=(0.5, -0.18), ncol=3, frameon=False)
ax.set_aspect('equal'); ax.set_xlabel('x [m]'); ax.set_ylabel('y [m]')
plt.tight_layout(); plt.show()
_res.cleanup()
No description has been provided for this image
Out[5]:
1

Sweep occupant counts / densities¶

In [6]:
rows = []
for n, rho0 in DENSITY_CASES.items():
    raw = build_raw_scenario(n)
    with tempfile.TemporaryDirectory() as d:
        p = Path(d)
        (p / 'config.json').write_text(json.dumps(raw, indent=2), encoding='utf-8')
        (p / 'geometry.wkt').write_text(CORRIDOR_WKT, encoding='utf-8')
        scenario = load_scenario(str(p))
        r = run_scenario(scenario, seed=42)
        m = measure_zone2(r.trajectory_dataframe(), r.frame_rate)
        m['n_agents'] = n; m['initial_density'] = rho0
        rows.append(m)
        r.cleanup()
fd = pd.DataFrame(rows)
fd
Out[6]:
mean_speed_zone2_m_s n_speed_samples peak_density_zone2_p_m2 mean_density_zone2_p_m2 flow_line_b_p_s specific_flow_line_b_p_m_s n_agents initial_density
0 0.998810 25 0.625 0.419860 1.072386 0.536193 60 0.5
1 0.972169 41 1.075 0.748647 1.880952 0.940476 120 1.0
2 0.763201 85 1.950 1.354968 2.543444 1.271722 240 2.0
3 0.448600 122 3.000 2.234673 2.492212 1.246106 360 3.0
4 0.331441 159 4.175 3.157928 2.498067 1.249033 480 4.0

Fundamental diagram (speed-density and flow-density)¶

In [7]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))
ax1.plot(fd['mean_density_zone2_p_m2'], fd['mean_speed_zone2_m_s'], 'o-')
ax1.set_xlabel('density zone 2 [1/m2]'); ax1.set_ylabel('mean speed zone 2 [m/s]')
ax1.set_title('speed - density')
ax2.plot(fd['mean_density_zone2_p_m2'], fd['specific_flow_line_b_p_m_s'], 's-')
ax2.set_xlabel('density zone 2 [1/m2]'); ax2.set_ylabel('specific flow line B [p/m/s]')
ax2.set_title('flow - density')
fig.tight_layout(); plt.show()
No description has been provided for this image

Acceptance¶

ISO 20414:2020 Test 13 expects the zone-2 speed-density (and flow-density) relationship to be produced and compared with the model's underlying assumptions. The zone-2 speed is measured only for agents that cross line A from zone 1; agents spawned inside zone 2 are excluded, because timing them from t = 0 would overstate their speed.

The mean zone-2 speed decreases with density, from about the 1.0 m/s free speed at 60 agents to about 0.33 m/s at 480 agents, and never exceeds the free speed. This is consistent with the model's assumptions, so the test passes. The printed values below come from this run.

In [8]:
free_speed = 1.0
speed = fd['mean_speed_zone2_m_s'].to_numpy()
over = fd.loc[speed > free_speed + 0.05, 'initial_density'].tolist()
print('mean zone-2 speed [m/s]:', [round(float(v), 3) for v in speed])
print(f'assigned free speed v0 = {free_speed} m/s')
verdict = 'FAIL' if over else 'PASS'
print(f'\nRESULT: {verdict} - ISO 20414:2020 Test 13 (corridor fundamental diagram)')
if over:
    print(f'Zone-2 speed exceeds the {free_speed} m/s free speed at densities {over} p/m2,')
    print('non-monotonic in density: the speed-density relationship is non-physical')
    print("and contradicts the model's own assumptions. Documented CFSM limit.")
mean zone-2 speed [m/s]: [0.999, 0.972, 0.763, 0.449, 0.331]
assigned free speed v0 = 1.0 m/s

RESULT: PASS - ISO 20414:2020 Test 13 (corridor fundamental diagram)