Resources Numerical accuracy Calculation policy

People movement and travel distance

Works out how far the farthest point of each room is from an exit and simulates people walking out of or into the building, with times, queues and walked distances.

How the precision is kept

Rooms are covered by a grid of 250 mm cells (coarser for very large rooms) and distances are shortest paths over that grid in 64-bit numbers. Walking speed falls with crowd density by a published linear rule and openings pass people at a fixed rate per meter of effective width. Time advances in steps of 0.25 s. The grid, the time step and the typed assumptions set the accuracy; results are rounded to 0.1 s and 0.01 m.

Calculations under this policy

12 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

people-sim.js BimCraft 0.8.1 64-bit 10 calculations Reviewed

Tests tools/bimcraft-test/people-test.mjs

Room grid

64-bit

Covers a room with square cells and marks the ones whose center is inside it.

cell × 1.5 while (W ÷ cell + 1) × (H ÷ cell + 1) > 60000

The cell size starts at the given value and grows by 1.5 until the room's bounding box holds at most 60000 cells. A cell is free when its center passes an even-odd ray test against the outline and is outside every hole.

  • people-sim.js
  • lines 93 to 106
  • BimCraft 0.8.1
/** The grid of one room: free[i] = 1 where the cell's centre is inside the outline and outside the holes. */
function makeGrid(room, cell0) {
  const xs = room.poly.map((p) => p[0]), ys = room.poly.map((p) => p[1]);
  const x0 = Math.min(...xs), y0 = Math.min(...ys), x1 = Math.max(...xs), y1 = Math.max(...ys);
  let cell = cell0;
  while (((x1 - x0) / cell + 1) * ((y1 - y0) / cell + 1) > 60000) cell *= 1.5;   // very big rooms: coarser
  const nx = Math.max(1, Math.ceil((x1 - x0) / cell)), ny = Math.max(1, Math.ceil((y1 - y0) / cell));
  const free = new Uint8Array(nx * ny);
  for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
    const x = x0 + (i + 0.5) * cell, y = y0 + (j + 0.5) * cell;
    if (inRing(room.poly, x, y) && !(room.holes || []).some((h) => inRing(h, x, y))) free[j * nx + i] = 1;
  }
  return { x0, y0, nx, ny, cell, free, fields: new Map() };
}

Distance field over a room

64-bit

The walked distance from one cell to every other free cell of a room.

d(n) = min over 16 neighbors of d(c) + √(Δi² + Δj²) × cell

Distances are 64-bit sums of step lengths kept in a Float64Array. Paths are restricted to 16 directions, which the file's comment puts within about 2.5 % of a straight line. Diagonal and knight moves are allowed only when the cells they pass are free.

  • people-sim.js
  • lines 131 to 150
  • BimCraft 0.8.1
/** Distance field (mm) from a cell over the free cells: 16 neighbours with true lengths, within about 2.5 % of the straight-line truth. */
function field(g, src) {
  const d = new Float64Array(g.nx * g.ny).fill(Infinity), h = new Heap(), nx = g.nx, ny = g.ny, free = g.free, cell = g.cell;
  d[src] = 0; h.push(0, src);
  while (h.size) {
    const [dd, c] = h.pop();
    if (dd > d[c]) continue;
    const ci = c % nx, cj = (c - ci) / nx;
    for (const [di, dj] of NB) {
      const i = ci + di, j = cj + dj;
      if (i < 0 || j < 0 || i >= nx || j >= ny || !free[j * nx + i]) continue;
      if (Math.abs(di) + Math.abs(dj) === 2 && (!free[cj * nx + i] || !free[j * nx + ci])) continue;   // no cutting a corner
      if (Math.abs(di) === 2 && (!free[cj * nx + ci + di / 2] || !free[j * nx + ci + di / 2])) continue;   // knight move: both middle cells free
      if (Math.abs(dj) === 2 && (!free[(cj + dj / 2) * nx + ci] || !free[(cj + dj / 2) * nx + i])) continue;
      const n = j * nx + i, nd = dd + Math.hypot(di, dj) * cell;
      if (nd < d[n]) { d[n] = nd; h.push(nd, n); }
    }
  }
  return d;
}

Route between two places in a room

64-bit

The distance in meters between two points of a room and the path walked.

straight: √(Δx² + Δy²) ÷ 1000; else Σ segment lengths of the string-pulled path ÷ 1000

When every sampled cell on the straight line is free the distance is one Math.hypot. Otherwise the path follows the distance field downhill and is shortened by jumping to the farthest visible vertex; when only the distance is needed the grid distance is returned without that shortening.

  • people-sim.js
  • lines 179 to 214
  • BimCraft 0.8.1
/** Distance in metres between two standing places of one room, and the walked route. */
function inRoomRoute(scene, room, p, q, needPath) {
  const g = scene.rooms[room].g;
  if (los(g, p, q)) return { d: Math.hypot(q[0] - p[0], q[1] - p[1]) / 1000, path: needPath ? [p, q] : null };
  const cq = nearFree(g, q[0], q[1]), cp = nearFree(g, p[0], p[1]);
  if (cq < 0 || cp < 0) return { d: Infinity, path: null };
  const f = fieldOf(g, cq), d = f[cp];
  if (!Number.isFinite(d)) return { d: Infinity, path: null };
  if (!needPath) return { d: d / 1000, path: null };
  // walk down the field, then string-pull: from each vertex jump to the farthest one it can see
  const raw = [p];
  let c = cp;
  for (let guard = 0; guard < 100000 && f[c] > 0; guard++) {
    const ci = c % g.nx, cj = (c - ci) / g.nx;
    let best = c, bv = f[c];
    for (const [di, dj] of NB.slice(0, 8)) {
      const i = ci + di, j = cj + dj;
      if (i < 0 || j < 0 || i >= g.nx || j >= g.ny) continue;
      const n = j * g.nx + i;
      if (f[n] < bv) { bv = f[n]; best = n; }
    }
    if (best === c) break;
    c = best; raw.push(centre(g, c));
  }
  raw.push(q);
  const path = [raw[0]];
  let at = 0;
  while (at < raw.length - 1) {
    let far = at + 1;
    for (let k = raw.length - 1; k > at + 1; k--) if (los(g, raw[at], raw[k])) { far = k; break; }
    path.push(raw[far]); at = far;
  }
  let len = 0;
  for (let i = 1; i < path.length; i++) len += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]);
  return { d: len / 1000, path };
}

Distance to the goal from every opening

64-bit

The remaining walking distance to an exit, or to a room, from each side of each door and stair.

d(far side of j) = min(d(i) + walk(j → i) + length of opening j)

A shortest-path search over the openings with 64-bit sums in a Float64Array. A door adds no length; a stair adds its length in meters.

  • people-sim.js
  • lines 216 to 247
  • BimCraft 0.8.1
/**
 * Cost (m) to a goal from every standing place, counted from standing at the place to the goal: crossing its opening (a stair's length)
 * and everything after. goal { exit: true }: an exit's inside place costs 0 (the door itself takes no length). goal { room }: crossing
 * into that room is the end. Shut openings are not used.
 */
function costToGoal(scene, goal, shut, exitSet) {
  const E = scene.ends, dist = new Float64Array(E.length).fill(Infinity), h = new Heap();
  const seed = (i, d) => { if (d < dist[i]) { dist[i] = d; h.push(d, i); } };
  E.forEach((e, i) => {
    const o = scene.openings[e.o];
    if (shut.has(o.id)) return;
    if (goal.exit) { if (exitSet.has(o.id) && o.end[1 - e.side].room < 0) seed(i, 0); }
    else if (e.room === goal.room && scene.far[i] >= 0) seed(scene.far[i], o.len);   // the place on the other side of j crosses into the room
  });
  while (h.size) {
    const [dd, i] = h.pop();
    if (dd > dist[i]) continue;
    const e = E[i];
    for (const j of scene.inRoom[e.room]) {   // arriving at j (by crossing its opening from its far side), then walking to i
      if (j === i) continue;
      const oj = scene.openings[E[j].o];
      if (shut.has(oj.id)) continue;
      const far = scene.far[j];
      if (far < 0) continue;
      const walk = inRoomRoute(scene, e.room, E[j].p, e.p, false).d;
      if (!Number.isFinite(walk)) continue;
      const nd = dd + walk + oj.len;
      if (nd < dist[far]) { dist[far] = nd; h.push(nd, far); }
    }
  }
  return dist;
}

Travel distance of each room

64-bit

The farthest point of each room from an exit, in meters along the walls.

max over cells and corners of min over openings (distance to opening + distance beyond it); rounded to 0.01 m

Every free cell center is tried, then every corner of the outline as its nearest cell plus the straight step to the corner. The result is rounded to 0.01 m and the reported point to a whole millimeter.

  • people-sim.js
  • lines 264 to 302
  • BimCraft 0.8.1
/** The farthest point of every room from an exit, walked (m), with the opening that way leads through. */
export function travelDistances(scene, o = {}) {
  const shut = shutSet(o), dist = costToGoal(scene, { exit: true }, shut, exitsOf(scene, o));
  return scene.rooms.map((r) => {
    const ends = scene.inRoom[r.index].filter((i) => Number.isFinite(dist[i]) && !shut.has(scene.openings[scene.ends[i].o].id));
    const res = { room: r.index, id: r.id, name: r.name || '', maxM: null, at: null, via: null, reachable: false };
    if (!ends.length) return res;
    const fields = ends.map((i) => fieldOf(r.g, scene.ends[i].cellId));
    const g = r.g;
    let worst = -1;
    const consider = (v, at) => { if (v > worst) { worst = v; res.at = at.map(Math.round); } };
    // from a place to the nearest way out: straight when it can see the opening, else along the room's distance field
    const toExit = (c, pt, extra) => {
      let best = Infinity, via = -1;
      for (let k = 0; k < ends.length; k++) {
        const e = scene.ends[ends[k]], straight = los(g, pt, e.p) ? Math.hypot(pt[0] - e.p[0], pt[1] - e.p[1]) : fields[k][c];
        const v = straight / 1000 + dist[ends[k]];
        if (v < best) { best = v; via = k; }
      }
      return [best + extra / 1000, via];
    };
    let viaAt = null;
    for (let c = 0; c < g.free.length; c++) {
      if (!g.free[c]) continue;
      const pt = centre(g, c), [v, via] = toExit(c, pt, 0);
      if (v > worst) { consider(v, pt); viaAt = via; }
    }
    // the corners themselves (the cell centres stop half a cell short of the wall): a corner is its nearest cell plus the step to it
    for (const ring of [r.poly, ...(r.holes || [])]) for (const vx of ring) {
      const c = nearFree(g, vx[0], vx[1]);
      if (c < 0) continue;
      const pt = centre(g, c), [v, via] = toExit(c, pt, Math.hypot(vx[0] - pt[0], vx[1] - pt[1]));
      if (v > worst) { consider(v, vx); viaAt = via; }
    }
    if (worst >= 0) res.via = scene.openings[scene.ends[ends[viaAt]].o].id;
    if (worst >= 0) { res.maxM = Math.round(worst * 100) / 100; res.reachable = true; }
    return res;
  });
}

Speed factor from crowd density

64-bit

How much the crowd in a room slows a walker.

D = people ÷ max(area, 0.5); factor = 1 for D ≤ 0.54, else max(min factor, (1 − 0.266·D) ÷ (1 − 0.266 × 0.54))

The constants 0.266 and 0.54 persons per m² are the published values named in the file's header. The room area is floored at 0.5 m², which guards the division.

  • people-sim.js
  • line 304
  • BimCraft 0.8.1
const dens = (count, area, minF) => { const D = count / Math.max(area, 0.5); return D <= D_FREE ? 1 : Math.max(minF, (1 - A * D) / (1 - A * D_FREE)); };

Flow rate of an opening

64-bit

How many people per second a door or stair lets through.

rate = flow per meter × max(0.2, width ÷ 1000 − 2 × boundary)

The width is converted from millimeters with ÷ 1000 and reduced by a boundary layer on each side (0.15 m by default). The capacity shown is this rate rounded to 0.01; the simulation uses the unrounded rate.

  • people-sim.js
  • lines 366 to 368
  • BimCraft 0.8.1
const rate = OP.map((x) => (x.kind === 'stair' ? o.flow.stair : o.flow.door) * Math.max(0.2, x.w / 1000 - 2 * o.boundary));
const ostat = OP.map((x) => ({ id: x.id, kind: x.kind, passed: 0, switches: 0, lastDir: null, lastPassT: -Infinity, peakQueue: 0, totalWait: 0, maxWait: 0, first: null, last: null, capacity: 0 }));
ostat.forEach((s, i) => { s.capacity = Math.round(rate[i] * 100) / 100; });

Walking step

64-bit

How far a walker moves in one time step and the distance added to their total.

Δs = v₀ × factor × Δt; walked += min(Δs, remaining length of the leg)

Positions along a leg are accumulated by repeated addition of Δs. The walked total adds only the part of the step that lies on the leg, so overshoot at the end of a leg is not counted.

  • people-sim.js
  • lines 427 to 428
  • BimCraft 0.8.1
const f = dens(inRoomCount[a.room], Rm[a.room].area_m2, o.minDensityFactor), ds = a.v0 * f * o.dt;
a.s += ds; a.travel += Math.min(ds, Math.max(0, a.leg.len - (a.s - ds)));

Passing an opening

64-bit

The moment each queued person passes a door or stair, and their wait.

pass = max(next free, arrival, t) + counter-flow penalty; next free = pass + 1 ÷ rate; wait = pass − arrival

Pass times are 64-bit and are not tied to the time step. The counter-flow penalty is 1 ÷ (rate × share) − 1 ÷ rate when the previous pass went the other way within the window. Waits are summed in order.

  • people-sim.js
  • lines 442 to 449
  • BimCraft 0.8.1
const a = q.shift(), base = Math.max(nextFree[oi], a.qT, t);
// counter-flow (0.8 EG.48): a pass in the other direction from the last one, close behind it, costs the opening part of its rate
const dir = a.cross.from >= 0 ? E[a.cross.from].side : 1 - E[a.cross.to].side, sw = S.lastDir !== null && S.lastDir !== dir && base - S.lastPassT <= o.counterWindow;
const passT = base + (sw ? 1 / (rate[oi] * o.counterFlow) - 1 / rate[oi] : 0);
if (sw) S.switches++;
S.lastDir = dir; S.lastPassT = passT;
nextFree[oi] = passT + 1 / rate[oi];
const wait = passT - a.qT;

Reported times and distances

64-bit

Total time, median and 90th percentile times, walked distances and waits.

percentile p = times[ceil(n·p) − 1]; times rounded to 0.1 s, distances to 0.01 m

Percentiles pick one of the sorted exit times, with no interpolation. Every reported time is rounded with Math.round(v × 10) ÷ 10 and every distance with Math.round(v × 100) ÷ 100 after the 64-bit sums and maxima are taken.

  • people-sim.js
  • lines 464 to 477
  • BimCraft 0.8.1
const pct = (p) => (times.length ? Math.round(times[Math.max(0, Math.ceil(times.length * p) - 1)] * 10) / 10 : null);
const roomRes = Rm.map((r, ri) => {
  const mine = P.filter((a) => exchange ? a.home === ri && a.destRoom === null : (a.home === ri && !arrive || arrive && a.destRoom === ri));
  const left = mine.filter((a) => a.exitT !== null);
  return { id: r.id, name: r.name || '', people: mine.length, stuck: mine.filter((a) => a.st === ST.stuck).length,
    clearT: left.length === mine.length && mine.length ? Math.round(Math.max(...left.map((a) => a.exitT)) * 10) / 10 : null,
    maxWalked: mine.length ? Math.round(Math.max(...mine.map((a) => a.travel)) * 100) / 100 : 0 };
});
const bottleneck = ostat.filter((s) => s.totalWait > 0).sort((x, y) => y.totalWait - x.totalWait)[0] || null;
const results = {
  scenario: o.scenario, people: N, finished: done.length, stuck: stuck.length, unfinished: still.length,
  totalTime: times.length ? Math.round(times[times.length - 1] * 10) / 10 : null, t50: pct(0.5), t90: pct(0.9),
  meanWalked: N ? Math.round(P.reduce((s, a) => s + a.travel, 0) / N * 100) / 100 : 0, maxWalked: N ? Math.round(Math.max(...P.map((a) => a.travel)) * 100) / 100 : 0,
  meanWait: N ? Math.round(P.reduce((s, a) => s + a.waited, 0) / N * 10) / 10 : 0,
people-model.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests tools/bimcraft-test/people-test.mjs

Stair length

64-bit

The walked length of a stair from its plan walk line and its height.

length = √(plan length² + height²)

The plan length is a sum of Math.hypot segment lengths in millimeters, and the slope length is one Math.hypot of that sum and the stair's height. Landings are counted, not measured.

  • people-model.js
  • lines 82 to 84
  • BimCraft 0.8.1
let plan = 0;
for (let i = 1; i < lay.walk.length; i++) plan += Math.hypot(lay.walk[i][0] - lay.walk[i - 1][0], lay.walk[i][1] - lay.walk[i - 1][1]);
stairs.push({ id: st.guid, a: { room: ra.id, x: a[0], y: a[1] }, b: { room: rb.id, x: b[0], y: b[1] }, length: Math.hypot(plan, height), w: st.width, landings: (lay.pieces || []).filter((pc) => pc.kind === 'landing').length });

People per room

64-bit

How many people each room holds before a run.

count = floor(area in m² ÷ m² per person)

The room area in mm² is divided by 1e6 elsewhere in the file and the count is truncated with Math.floor. The area shown beside it is rounded to 0.1 m²; the count uses the unrounded area. A typed count is truncated to a whole number.

  • people-model.js
  • lines 107 to 115
  • BimCraft 0.8.1
/**
 * 0.8 EG.48 (bim-sim_PART_3): the people each room holds before a run, so the person can set a count per room.
 *   roomCounts(ed, { m2PerPerson, loads }) → [{ id, name, level, levelName, area_m2, occupied, auto, count }]     auto = the count the factor gives (0 for a corridor, stair, lobby …);
 *   count = the person's number when `loads` has the room, else auto. The same `loads` map goes to simulate().
 */
export function roomCounts(ed, { m2PerPerson = 10, loads = {} } = {}) {
  const { rooms } = namedRooms(ed);
  return rooms.map((r) => { const auto = r.occupied ? Math.floor(r.area_m2 / m2PerPerson) : 0, own = loads[r.id]; return { id: r.id, name: r.name, level: r.level, levelName: r.levelName, area_m2: Math.round(r.area_m2 * 10) / 10, occupied: r.occupied, auto, count: own !== undefined && own !== '' && own !== null ? Math.max(0, Math.floor(Number(own)) || 0) : auto }; });
}

In detail

Method
Dijkstra distance fields over 16 neighbors per cell, string-pulled paths, a shortest-path search over openings, a fixed-step simulation with first-come queues. Speed rule S = k(1 − a·D) with k = 1.4 m/s and a = 0.266, cited in the file as Nelson and MacLennan.
Number types
64-bit numbers and Float64Array for distances; Uint8Array for the free-cell grid; Float32Array only for the playback frames.
Tolerances
Grid cell 250 mm, multiplied by 1.5 until a room has at most 60000 cells. Time step 0.25 s. Comparisons of times use 1e-9 s. Door to room matching: half the wall thickness + 60 mm. Effective width at least 0.2 m.
Limits
The file's own comment puts the 16-neighbor distance within about 2.5 % of the straight-line value. Cell centers stop half a cell short of the walls, so room corners are added separately. Arrival and exit times fall on multiples of the time step for walkers and on the queue's pass times for openings.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach