Aporia

Measurements

25–27 October 2026 · JuPedSim 1.4.2 · Python 3.12 · each number reproducible with one command

The numbers behind each decision, from the cost of one simulation to the speed of a map on a phone. For whether the simulator matches real crowds, see the validation report. The weights and the simulated bends are open: cfdu and aporia-bends on Hugging Face.

22 sMedian compute per simulated bend, 5,400 s of crowd each
98.9 %In-domain bends accepted, 1,466 of 1,483
0.008RMSE in ped/m² on held-out bends, 30–120°
13 msPer map, warm, laptop GPU in the browser

Simulation cost and corridor capacity · 25–26 Oct

One bend takes a fraction of a second at the default settings (90°, 2 m, q = 0.8: 0.24 s; straight 4 m, q = 1.3: 0.46 s), far below the 30 s budget. The final dataset's runs are longer and denser: median 22 s per run, p99 15 min, max 50 min.

Capacity is the flow a straight corridor carries before arrivals are refused. With JuPedSim's default time gap of 1 s it is about 1.0 ped/(m·s), below real corridors (≈ 1.2 in Weidmann's compilation, higher at Jülich). The calibrated T = 0.7 s brings it into range.

WidthFree flow up to q, defaultOverload outflow, defaultFree flow up to q, calibratedOverload outflow, calibrated
1.5 m0.8≈ 0.961.31.32
2.0 m0.9≈ 1.041.41.49
4.0 m0.9≈ 1.041.31.46
ped/(m·s). Free flow: zero refusals and outflow within 5 % of demand at every rate up to that point, 2 seeds, 0.1 steps. Overload outflow: default, mean at q = 2.4; calibrated, mean of runs refusing ≥ 10 % of arrivals.

Capacity against time gap · 2 m corridor drained from a 3 ped/m² reservoir, exploratory

T = 1.0 s (default)
1.02
T = 0.8 s
1.13
T = 0.6 s
1.43
T = 0.5 s
1.63
Two corrections on the way. The first sweep was biased: JuPedSim steers every agent to the centre of its exit, so a 4 m crowd funnelled onto the axis (71 % within ±0.5 m). Exits are now split into 0.5 m lanes, each walker keeping its side. Second, the injector was suspected of setting the plateau; a jam released without it reaches the same throughput within 3–5 % (T = 1 s: 1.037 vs 0.993), so it does not.

Decision. Calibrate the time gap on the measured speed–density relation, not on capacity alone (done, see the report), and accept a bend only if its own run shows zero refusals and passes every check: a straight corridor's capacity does not guarantee a bend's.

The training dataset · 27 Oct

1,500 bends drawn by Latin hypercube: angle 30–150°, width 1.5–4 m, chamfer 0–3.9 m, flow 0.1–0.9 ped/(m·s). Two seeds each, 3,000 runs. A bend is accepted when both seeds pass every check. A first version stopped each run at its first pass; the second observes every run for a fixed 5,400 s.

v1 (superseded)v2 (training set)
Observation160 → 1,200 s, first pass5,400 s fixed, from 600 s
Accepted1,444 / 1,500 (96.3 %)1,466 / 1,483 in domain (98.9 %)
Noise floor, median0.0075 ped/m²0.0021 ped/m²
Peak density, median / max0.65 / 1.900.63 / 1.75
Compute, size0.5 h on 10 cores, 3.4 GB4 h 52 on 9 workers, 18 GB
Noise floor: half the RMS difference between a bend's two seeds, the error of their mean. v2 excludes a box of 17 bends (θ > 120°, w > 3.3 m, q > 0.8) that broke down in v1.
Short runs certified breakdowns. All 17 in-domain bends rejected by v2 had been accepted by v1: stationary over 160–1,200 s, they break down when watched for 5,400 s. They all have q ≥ 0.76, in wide (≥ 2.8 m) or sharp (≥ 96°) bends. Every bend with q ≤ 0.7 is accepted. That is why the app's range stops at 0.7.
  • The 48 bends v1 could not resolve (noisy inner-corner regions) are now 47 accepted, 1 excluded.
  • Tolerances were not loosened after seeing rejections. Two narrow bends (q ≈ 0.89) fail on a single refused arrival in 5,400 s; left as is.
  • Three bends (θ 119–129°, w 3.1–3.8 m, q ≈ 0.8) are bistable: both seeds pass but settle in different states (peaks 1.4 vs 1.9–2.0). Their target is the mean of two regimes, and their noise floor says so.

Splits, accepted bends · by geometry, angles above 120° held out entirely · download

Train
896
Validation
112
Test, 30–120°
111
Test, 120–150°
347

CFD-U accuracy · 27 Oct

A U-Net (32-64-128-256 channels, 1.9 M parameters), 300 epochs, 6.5 min on an RTX 2060. Trained three times with different seeds; the app uses the pixel-wise mean of the three. Compared with two shortcuts: a continuity formula (flow / speed, v = 1.35 m/s) and the nearest training bend.

MethodRMSE, ped/m²Peak errorFruin level rightIoU, level ≥ D
Continuity formula0.200 / 0.24839 % / 51 %23 % / 16 %0.00 / 0.00
Nearest training bend0.158 / 0.30710 % / 17 %84 % / 73 %0.41 / 0.11
CFD-U, seed 0 only0.0081 / 0.1081.3 % / 18.7 %98 % / 72 %0.85 / 0.48
CFD-U, mean of 3 seeds (app)0.0080 / 0.0771.2 % / 8.5 %99 % / 83 %0.87 / 0.58
Simulator noise floor0.0023 / 0.0079–––
Each cell: 30–120° test (111 bends) / 120–150° test (347). Peak: the p99 density. IoU: overlap of the predicted and simulated areas at Fruin level D or worse.

Within the training range, the map is solved to near the noise: 3.5× the floor, the Fruin level right in 99 % of bends. The error is highest at 90–120° (RMSE 0.011) against 0.005–0.006 below 90°.

Four test bends, each as simulation, prediction, error and model disagreement: the two interpolation cases are near-perfect; the worst extrapolation case at 148 degrees moves the queue upstream.
Median and worst test bend of each split. In the worst (148°, 3.7 m, q = 0.74, peak 1.48 → 1.19), the network moves the queue upstream of the corner and spreads it out; the disagreement between seeds is highest exactly there.

Beyond 120°: where it stops being right · 27 Oct

Angles above 120° were never seen in training. The error grows with the angle, and with the load within each angle. Up to about 130° the network stays reliable; beyond 135° its maps are still twice as close as the nearest bend, but it underestimates the peak, systematically, on every seed.

Map RMSE by angle · ped/m², 120–150° test

Median peak bias · CFD-U, below 0 = too low

Fruin level right · share of bends

Over the whole 120–150° test the Fruin level is off by one or two in about one bend in six. The app flags every bend above 120° as out of domain and quotes these numbers; low disagreement between seeds never clears the warning. Angles above 135° are not shown to be unrealistic, only beyond the validation's evidence (Hannun et al. stop at 135°).

More data, more seeds · 27 Oct

The same model trained on random subsets of the training set, and the full model retrained with three seeds. Every run converged (best epoch 189–277 of 300).

RMSE, 30–120° test · ped/m²

RMSE, 120–150° test · ped/m²

  • Within range, more data still helps. 600 → 896 bends cut the error by a third. More training bends at 30–120° would still pay.
  • Beyond 130°, it does not. The smaller runs sit within the full run's seed spread (0.082–0.108): the training seed matters more than the amount of data there.
  • Averaging three seeds gives the lowest pooled error (0.0080 / 0.077) and removes the dependence on one training draw, without beating the best seed on every metric. It was chosen after seeing test results; later confirmatory claims need fresh bends.
  • Seed disagreement predicts the error (correlation 0.90 within range, 0.92 beyond, 0.80–0.95 within each 5° bin), but it is not calibrated: the error is typically 1.5× the spread. The app shows it as an experimental “model disagreement”, without error bars.

The chamfer · 27 Oct

Cutting the inner corner, from 0 to 3 m, at width 3 m and q = 0.7 ped/(m·s). The network swept in 0.1 m steps; 24 check runs simulated with the dataset's protocol, all passing.

Peak density against chamfer from 0 to 3 m for 60, 90 and 120 degree bends: the angle sets the level, around 0.83, 0.95 and 1.08; the chamfer moves each curve by a few percent.
Peak density against chamfer. Lines: CFD-U (band: range of the three seeds). Dots: simulation, mean of two seeds.
AngleChamfer 0 m1 m2 m3 mEffect, simulation
60°0.850 / 0.8380.828 / 0.8200.823 / 0.8170.834 / 0.821−3 % (0 → 2 m)
90°0.965 / 0.9480.967 / 0.9600.941 / 0.9390.944 / 0.930−2.5 % (0 → 2 m)
120°1.064 / 1.0801.130 / 1.0921.106 / 1.0911.105 / 1.067+4 to +6 %
Peak density, ped/m²: simulation / CFD-U. Half the seed range of the simulation: 0.000–0.007.
  • The angle sets the peak, the chamfer barely moves it, and not always down: at 120°, 1–3 m of chamfer raise it by 4–6 %. CFD-U matches the check points to 1.5 % on average (max 3.4 %) and misses that rise, which is at the limit of its error.
  • Across the 1,119 in-range simulations, adding the chamfer to a fit of log(peak) on angle, width and load lowers the residual only from 0.0604 to 0.0599.
  • For the peak alone, a five-term regression is nearly as good: 1.85 % mean error against 1.46 % for CFD-U, on the 47 test bends of the app's range. The network's added value is the map: where the load concentrates.
Decision. No shape optimiser: no shape has a validated effect worth searching. The app compares variants at a fixed total flow, where flow per metre and angle set the peak, and shows the chamfer as the null result it is. How real walkers use a chamfer is not validated.

In the browser · 27 Oct

The three seeds are merged into one ONNX graph (float32, 22.2 MB) run by onnxruntime-web: WebGPU when it passes a self-check against a reference map, WebAssembly otherwise. Warm median of 20 runs, all three seeds per run. The same weights are on Hugging Face.

Time per map · warm median, ms, lower is better

Laptop, WebGPU
13 ms
Laptop, WASM × 4
45 ms
Laptop, WASM × 1
147 ms
iPhone 15 Pro, WebGPU
31 ms
iPhone, WASM × 3
143 ms
iPhone, WASM × 1
185 ms
Galaxy S24, WASM × 4
129 ms
Galaxy S24, WASM × 1
274 ms

Bold: what the app uses on each device. Laptop: Apple Silicon, Chromium 152. iPhone: Safari 27. Galaxy: Chrome 156, which offers no WebGPU there. × n: WebAssembly threads, which need cross-origin isolation.

DownloadRawgzipbrotli
Model, float32 (default)22.2 MB20.5 MB~20 MB
Model, float16 weights11.2 MB10.2 MB~9 MB
Runtime, WebGPU + WASM fallback27.4 MB6.8 MB~3 MB
Downloaded once, then served from the browser's cache.
  • Same maps everywhere. On every device and backend, the browser's raster is bit-identical to Python's and the outputs are within 7 × 10⁻⁷ ped/m² of Python onnxruntime.
  • Float16 stays off. It halves the download but is not faster (the arithmetic stays float32), sometimes slower, and differs by up to 3.5 × 10⁻⁴, above the 10⁻⁴ parity tolerance.
  • Two overlapping runs stall onnxruntime-web on WebGPU; the worker now serialises them.

Geometry parity · 26 Oct

The app rebuilds each bend in JavaScript, independently of Python, and the network sees five raster channels of it. Distances are measured to the full wall boundary, including walls outside the 24 m crop, and window snapping uses the same rounding in both languages. 144 cases are compared as float32 bytes: identical.

uv run pytest tests/test_rasterize.py tests/test_rasterize_js.py