Outputs
The integrator does not return objects. Hosts read packed f32 buffers. Layouts below are the contract; the byte order is in the technical host note.
Walker frame
One record per agent, including done and removed bodies.
| Field | Unit | Meaning |
|---|---|---|
| time | s | Simulation clock |
| count | — | Number of records |
| x, y, z | m | Position. z is the nearest floor height |
| state | — | 0 walk, 1 teleport, 2 done, 3 removed, 4 in a store |
| vx, vy | m/s | Velocity |
| density | ped/m² | Headcount inside a 1.5 m radius, divided by that disk’s area, same floor only |
| stair progress | 0–1 | Along the connector axis, 0 at the entry end |
| stair id | — | 1-based zone index while on a connector; 0 otherwise. On an escalator only a rider counts; someone on foot in the footprint reads 0 |
| desired speed | m/s | The speed sampled at spawn, before zone factors and headway |
| trip | — | 0 boarding, 1 alighting |
| group | — | Shared by everyone in one visitor group; 0 for someone alone |
| stamina | 0–1 | A visitor's energy (see behaviours). 1 for everyone else |
| activity | — | 0 walking, 1 in a store, 2 resting (seated or standing aside), 3 heading out, 4 browsing a shopfront, 5 waiting for the group, 6 held at a crossing curb, 7 waiting for a pick-up |
Per-walker density is a local crowd measure for drawing. It is not the occupancy-grid density and it does not enter the force sum.
Inspect
For one selected walker: the last acceleration split into driving, wall, and person terms, the remaining waypoints, and a crumb trail of positions about 0.4 m apart (last 80 crumbs).
Crossings
One record per crossing, in scenario order.
| Field | Unit | Meaning |
|---|---|---|
| signal | — | 0 walk, 1 flashing, 2 don't walk; for a zebra, 3 open or 4 shut (a vehicle coming could not stop) |
| seconds left | s | Until the phase changes; 0 for a zebra |
| waiting | — | Walkers held at the curb whose path enters this crossing. 0 during walk or while a zebra is open |
Vehicles
The number of records that follow, then the run's count of vehicles that have reached their sink, then one record per vehicle on the road or standing in a bay. The order is not stable between frames. Match records by id.
| Field | Unit | Meaning |
|---|---|---|
| x, y, z | m | Middle of the body on its lane centreline, or in its bay. While crawling in or out it is drawn part way between the two. z is the lane's floor height |
| heading | rad | Direction of travel, from +x toward +y. In a bay, the way the bay faces |
| mode | — | 0 car, 1 motorcycle, 2 bicycle |
| state | — | 0 driving, 1 queued, 2 standing at a kerb, 3 at a lot gate (being served), 4 parked, 5 yielding (stopped at a give-way point, including one held for walkers at a crossing), 6 crawling into or out of a bay |
| speed | m/s | |
| length | m | Body length |
| occupants | — | People aboard |
| id | — | Unique for the run and fixed for the vehicle's life, starting at 1 |
Traffic stats
A header, then one record per kerb, per lot and per vehicle signal, each in scenario order. The rules behind them are in vehicles.
| Header field | Unit | Meaning |
|---|---|---|
| kerbs, lots, signals | — | Records that follow, of each kind |
| trips | — | Vehicles that reached a sink this run |
| mean delay | s | Mean time lost against free flow over those trips. 0 before the first |
| Kerb field | Unit | Meaning |
|---|---|---|
| bays | — | |
| in use | — | Bays with a vehicle crawling in, standing, or crawling out |
| queue | — | Vehicles queued for the kerb without a bay yet |
| spilling | — | 1 while that queue reaches back past the start of the kerb's lane |
| spill share | — | Share of the run spent spilling, 0 to 1 |
| mean dwell | s | Mean stand, from the start of the crawl in to pulling out, over finished stands |
| Lot field | Unit | Meaning |
|---|---|---|
| bays | — | |
| parked | — | Bays with a vehicle parked in them |
| taken | — | Bays parked in or claimed by a vehicle on its way in or out |
| gate queue | — | Vehicles being served at the gate or queued for it |
| mean search | s | Mean time from the end of gate service (or getting a bay, with no gate) to being parked. Gate service is not included |
| Signal field | Unit | Meaning |
|---|---|---|
| aspect | — | 0 green, 1 amber, 2 red |
| seconds left | s | Until the aspect changes |
Stores
One record per store, in scenario order.
| Field | Unit | Meaning |
|---|---|---|
| visits | — | Times a visitor has stepped in this run |
| inside | — | Visitors inside now |
A store's colour on the mall page is its share of the busiest store's visits. It measures pull, not crowding.
Occupancy grid
Per floor, cells of grid_cell metres (default 1 m, minimum 0.25 m). Each tick a walking body adds dt of occupancy-time to its cell. If they are actually moving (speed above 0.05 m/s and not in a hold), that dt also enters the speed average. Peak is the highest simultaneous headcount the cell has seen. Traffic is the number of times a walker entered the cell. A step straight back into the cell the walker just left is not counted, so someone standing on a cell border does not add up.
Reported mean density smooths occupancy-time over a window of about 2.5 m, then divides by window area times elapsed time. A 1 m cell with one standing body would otherwise read as a crowd. Mean speed is the speed-sum divided by walking-time. Someone standing at a gate or an attractor occupies the cell and does not enter the speed mean.
The vector is the mean walking velocity: the same moving walkers add velocity × dt, divided by the same walking-time. Mean speed averages lengths, so it does not cancel. The vector averages directions, so two equal streams in opposite directions cancel to a short or zero arrow. Its length is never more than the mean speed. The ratio of the two shows how one-way a cell is. The vector is exported as a two-band float GeoTIFF:
| Band | Meaning |
|---|---|
| 1 orientation | Bearing of the mean velocity in degrees clockwise from north (+y), 0–360 |
| 2 speed | Length of the mean velocity, m/s |
Both bands are NaN where nobody walked.
All of these can cover the whole run or a recent window. The grid keeps the last 40 slices of 15 s each, which is 10 minutes. A window uses the slice being filled plus as many closed slices as it takes to reach the window length, so it covers between the window and 15 s more. Density divides by that covered time, speed and the vector are the means over it, peak is the highest in it, and traffic is the count in it. A window longer than the run so far is the whole run.
Level of service
Fruin’s 1971 walkway table in metres. The original is in square feet per pedestrian. These are LTA's rounded metre bands from the walkway column in Architectural Design Criteria §2.2.3 (an exact conversion of 5 ft² is 0.46 m², not 0.5). Do not invent a second scale.
| LOS | Space (m²/ped) | Density (ped/m²) |
|---|---|---|
| A | > 3.3 | ≤ 0.303 |
| B | 2.3 – 3.3 | 0.303 – 0.435 |
| C | 1.4 – 2.3 | 0.435 – 0.714 |
| D | 0.9 – 1.4 | 0.714 – 1.111 |
| E | 0.5 – 0.9 | 1.111 – 2.0 |
| F | 0.25 – 0.5 | 2.0 – 4.0 |
| G | ≤ 0.25 | ≥ 4.0 |
G is not Fruin's. Fruin's F has no upper bound. Piper splits off G at 4 ped/m² (los::CRUSH_DENSITY), the UK Green Guide's safety limit for a moving crowd, above which Still notes the risk of crushing rises sharply (G. K. Still, Introduction to Crowd Science, 2014; see references). Bodies are hard discs (forces). With 0.25 m bodies, hexagonal packing tops out near 4.6 ped/m², so G means bodies in contact all round. The 1.5 m disk can read up to about 5, from disk edges and the centimetre a packed jam may still be squeezed.
The harness colours both the LOS layer and the congestion layer with these bins. Speed is a separate red-to-green scale. Stairs and queues are still coloured with the walkway table; Fruin’s stair and queue tables are not implemented.
Crush hotspots
A crush is LOS G that holds. Each tick every walker counts the bodies on its floor within 1 m, itself included, and divides by the disk area. The disk is tighter than the 1.5 m display density so a packed knot is not diluted by open floor beside it. A walker whose count stays at or above 4 ped/m² for 2 s is flagged. Riders on a belt and walkers captured by an attractor are not; a belt spaces its riders by design.
Flagged walkers within 3 m of each other on one floor are one hotspot. While any walker there stays flagged, the spot is live: it reports the highest density among them now and how many there are, and it moves to follow them. With nobody flagged for 3 s it is cleared but kept, with its peak density, the time its first walker reached G (the start of the hold, 2 s before the flag) and the time it was last flagged, so a run leaves a record of where crushes happened. A new flag at the same place after that starts a new hotspot. The run keeps at most 64; when full, the oldest cleared one makes room.
This is a density criterion only. It does not model body pressure, falls or asphyxia. Social-force bodies compress further than real ones, so densities well above 4.6 ped/m² mean the model is overpacked at that spot, which is itself the warning.
Flow graph
The harness graph is built from the walker frames, every 0.5 s of sim time. It plots people walking, their mean speed, and the exit rate. The exit rate is walkers who reached their target per minute, over the last 60 s; it reads 0 for the first 10 s.
Each minute of sim time gets one crowd state. It compares that minute's mean walking count, mean speed and exits (walkers who reached their target during the minute) with the minute before. The walker count is rising or falling when it moves by 5 % (of at least 20 people). Speed is falling or recovering when it moves by 3 % of the desired speed. Exits are climbing when they rise by 5 % (of at least 10 per minute).
| State | Rule |
|---|---|
| Growing | The first minute; walkers rising while speed holds; or walkers rising or holding while speed falls but exits still climb |
| Overflow | Walkers rising while speed falls or is already under 75 % of desired, or walkers holding while speed falls, and exits not climbing |
| Settling | Walkers falling; or walkers holding while speed recovers |
| Stable | Neither walkers nor speed moves |
Overflow is the congested branch of the fundamental diagram: more people on the floor, each moving slower, and throughput no longer rising, so it is not keeping up with arrivals. While exits still climb, the crowd is slowing but the network is still filling up to its capacity, so the minute is growing. The thresholds are in packages/viewer/src/view.ts (PHASE_*).
Batch metrics
piper-batch reads the same walker frame, occupancy grid and crush buffer as the harness. It does not add fields to those buffers. How a plan is written is in the technical batch note.
Average LOS is walker-time weighted. Each sample, every record in state walk (0) adds sample_s to a duration total and density × sample_s to a density total, and the same duration to the Fruin band of that density (los_from_density). Reported: mean density and its LOS letter; the mean of the per-walker LOS index (A=0 … G=6); and each band's share of walker-time. Teleport, done, removed and in-store records are skipped.
The whole run keeps that sum. The scatter and the per-set means use the last window_s when the run is stable, otherwise the time after warmup_s (or the whole run if it never reached warmup). Warmup on an empty floor would otherwise pull the letter toward A.
Measured traffic is arrivals and exits. Arrivals are new records in the frame since the last sample (the count field includes done and removed bodies, so a spawn is a rising count). Exits are new records in state done (2). Rates are people per hour over the same window as the LOS report. Nominal input pax/h is the plan's entrance total plus each track's alight / headway_s × 3600.
Stability. After warmup_s, compare mean walking count, mean density and exit rate over the last window_s to the window before it. Stable when every relative change is below tolerance. The windows are meant to cover whole train cycles so headway periodicity is not read as drift. Relative change divides by max(|previous|, floor) with floors 5 people, 0.02 ped/m² and 10 pax/h.
Crush fail. A live hotspot from crush() fails the run when walkers is at least fail_walkers or it has been live for fail_hold_s (now minus first_s). That is a substantial crush, not a single flagged body. The reported letter is G and the LOS index is 6: the run stopped because it is not viable, even if the time-weighted mean had not yet reached G. A run that hits max_s without settling is unstable.
results.csv is one row per run and one row of set means (stable runs only). That is the LOS–traffic scatter.
What is deliberately not an output
Individual force terms are only on the selected walker (inspect), not in the crowd frame. Wall geometry, waypoint graphs, and the random stream are internal. Origin longitude and latitude are stored on the scenario and are not in the frame.