CFD-U: crowd density in corridor bends
CFD-U (Crowd Flow Dynamics U-Net) predicts the steady-state pedestrian density map (ped/m²) of a one-way corridor bend from its geometry and inflow. It is a surrogate for JuPedSim simulations: one inference replaces a simulation run, which makes interactive exploration possible.
- Demo: https://aporia.pierrelouis.net/demo/ (runs this model in the browser)
- Dataset: pierrelouis-c/aporia-bends
Files
| File | Content |
|---|---|
cfdu-ensemble.onnx |
3 training seeds merged in one graph, float32 (23 MB) |
cfdu-ensemble-fp16.onnx |
the same graph with float16 weight storage, float32 compute (12 MB) |
manifest.json |
shapes, SHA-256 hashes, parity checks |
Input and output
- Input
channels:float32 [1, 5, 96, 96], a 24 m × 24 m window at 0.25 m/pixel centred on the turn. The channels are, in order:walkable: 1 inside the corridor, 0 outsidewall_distance: distance to the nearest wall (m)inlet: mask of the opening where the flow enters the windowexit: mask of the opening where it leavesload: specific flow (ped/(m·s)), constant over the image
- Output
seeds:float32 [3, 96, 96], one density map (ped/m²) per seed, 0 outside the corridor. Use the mean as the prediction and the standard deviation as a measure of disagreement.
Input normalisation, softplus and wall masking are inside the graph: feed raw channels
and get ped/m² back. The channels have to be built exactly as aporia.rasterize builds them.
import numpy as np, onnxruntime as ort
from aporia.generators import generate
from aporia.rasterize import rasterize
params = {"theta_deg": 90, "width": 2.0, "cut": 0.5, "specific_flow": 0.6}
x = rasterize(generate("bend", params)).channels[None].astype(np.float32)
seeds = ort.InferenceSession("cfdu-ensemble.onnx").run(["seeds"], {"channels": x})[0]
density = seeds.mean(axis=0) # (96, 96), ped/m²
Model
U-Net, 4 levels (32-64-128-256 channels), 1.93 M parameters per seed, ONNX opset 18. Three seeds (0, 1, 2) trained with identical settings; the app averages them with equal weight.
Training
- Data: dataset v2: 1,500 simulated bends (angle, width, inner-corner cut, flow), two simulation seeds each, 1,466 accepted. Splits: 896 train / 112 validation / 111 interpolation test / 347 extrapolation test.
- Simulator: JuPedSim 1.4.2, collision-free speed model, configuration calibrated on
experiments (see
docs/validation-report.mdin the repo). - Setup: 300 epochs, AdamW (lr 2e-3, weight decay 1e-4, 5 warm-up epochs), batch 16, float16 autocast, one RTX 2060. Loss: MSE over walkable pixels; checkpoint chosen on validation RMSE.
Results
Mean of the three seeds. RMSE in ped/m² over walkable pixels. Interpolation: θ 30–120°; extrapolation: θ 120–150°, angles never seen in training.
| Method | RMSE (interp. / extrap.) | p99 peak error | Peak Fruin level correct |
|---|---|---|---|
| Continuity baseline | 0.200 / 0.248 | 39 % / 51 % | 23 % / 16 % |
| Nearest neighbour | 0.158 / 0.307 | 10 % / 17 % | 84 % / 73 % |
| CFD-U | 0.008 / 0.077 | 1.2 % / 8.5 % | 99 % / 83 % |
Seed-to-seed simulation noise floor: 0.0023 (interp.) / 0.0079 (extrap.) ped/m².