← notes / 2026.07.22
build log: fire egress sim
the tools that define my field, like pathfinder and simulex, are black boxes to most of the people who use them. you draw a building, set an occupant load, press run, and trust the number that comes out. that number can end up supporting a real life-safety decision. that bothered me enough to build the model underneath those tools myself, from scratch, in python with the mesa framework.
the repo is github.com/njlagana/fire_egress_sim.
the idea
occupants are agents. each one waits out its own randomized pre-movement delay (people don’t move the instant an alarm sounds), then heads for an exit, and queues when a door is at capacity. nothing about crowd behavior is scripted. door bottlenecks, exit imbalance, and the spread of evacuation times all emerge from simple per-occupant rules. a single run of a random model proves nothing, so everything is reported as a monte carlo distribution across dozens of runs.
v1, a room with two exits
deliberately simple, just a walled room, two doors, and nearest-exit logic. its job was to prove the framework before pointing it at anything real. the most important lesson came from one buried assumption, the door flow rate. the theoretical max (~1.3 persons per meter per second) produced almost no bottleneck. the honest number for genuinely crowded flow is closer to 0.5, and with it the bottleneck forms and drives the whole result.
| scenario | mean evac time | spread | worst case |
|---|---|---|---|
| frictionless | ~34 s | ~1.2 s | ≈ mean |
| realistic congestion | ~58 s | ~4.0 s | ~68.5 s |
congestion didn’t just slow the average, it tripled the variance. the tail is the risk, which is exactly why life-safety analysis is built around worst cases instead of averages.
v2, a real building
in a prior course (enes-250) i worked through a full nfpa 101 life-safety analysis of a real four-story building by hand, and found its fourth floor egress-deficient, with an occupant load of 421 against a combined stair-door capacity of 320. v2 rebuilds that floor cell by cell (58×67 grid, 1 m cells) and asks what the evacuation actually looks like when those same two 32-inch stair doors are the constraint.
real geometry breaks straight-line exit logic (it routes people through walls), so each stairwell gets its own shortest-path distance field computed by bfs, and occupants walk downhill on their assigned field. pre-movement times come from a right-skewed weibull fit. across 30 monte carlo runs at the real occupant load i get a ~5.08 min mean fourth-floor evacuation, with a near-linear evacuation curve. that’s the signature of a capacity-limited drain, not a walking-distance problem.
the result i care about most is that switching the pre-movement distribution changed the total time by only ~2%. the floor is bottleneck-dominated. the model was built independently of my hand analysis and landed on the same controlling constraint, the stair doors, which is the kind of cross-check a life-safety analysis depends on.
v3, the floor catches fire
v3 is pushed. it keeps the validated v2 floor and occupants and adds a hazard that grows over time. there’s a fire that spreads cell by cell (8-directional, only through walkable space, never across walls), and a smoke front that moves about three times faster than flame, since smoke, not fire, is what reaches and incapacitates people first.
the real modeling shift is that the floor is no longer static. in v2 the shortest-path routes are computed once, because walls never move. fire breaks that, since a corridor that was the fastest way out at ignition can be impassable two minutes later. so distance fields get recomputed as the hazard advances, an occupant whose stair becomes unreachable switches to the other one, and if fire reaches a stair’s entry that stair leaves the egress system entirely. the new output metric is casualties, meaning an occupant caught in a burning cell.
the question changes too. v2 asked “how long does evacuation take.” v3 asks “where, and how badly, does this floor fail when a fire starts in it.”
the calibration lesson
fire spread is governed by one per-neighbor ignition probability. my first attempt set it to 0.05 and produced fire covering 99% of the floor in five minutes, with ~50% casualties every run. that’s roughly three times worse than the deadliest assembly-occupancy fire on record, so the result flagged the model as wrong, not the building.
the bug was compounding. a cell on the fire front has about three burning
neighbors, so its real per-step ignition chance isn’t p, it’s 1 − (1 − p)³,
which is about three times higher. the raw parameter had to be calibrated
against the emergent front speed, not assumed. solving for a target of
~0.05 m/s (a serious compartment fire, ~3 m/min) gives a per-neighbor
probability of ~0.017, and the notebook now measures the emergent front speed
empirically (~0.045 m/s against the 0.05 target) instead of trusting the
algebra. the takeaway matters more than the number. in a cellular spread
model, per-neighbor probabilities compound at the front, so you have to
calibrate to a measured physical speed and check it against a real-world
anchor instead of tuning by feel.
the result
across 30 monte carlo runs at the real occupant load (391 occupants, with a random assembly-room fire origin each run), here’s what comes out.
| fire origin | casualties |
|---|---|
| far from the south stair | often 0 to a few |
| adjacent to the south stair | ~90 to 110 |
the correlation between how close the fire starts to the south stair and how many people die is −0.85, strong and negative. this is the fire-dynamics analog of v2’s bottleneck finding. v2 showed the floor’s egress capacity is structurally deficient (load 421 vs capacity 320). v3 shows when that deficiency turns lethal, which is when a fire removes one of the two stairs and halves an already-insufficient capacity. the engineering implication is pretty concrete. hardening the south stair’s entry (rated separation from the assembly room next to it) would cut the dominant casualty mode more than any change to how the occupants behave.
everything above is deliberately conservative. no sprinkler suppression (the design question is what happens if it fails), fire tuned to the aggressive end of plausible, flame contact immediately lethal, and occupants re-planning only every 10 seconds. a “casualty” here is someone who loses every egress path, so it’s an upper envelope on how completely a fire severs egress, not a literal death prediction.
next
- v4, tenability. the honest limitation in v3 is that smoke is a flat 2d proxy. it doesn’t model smoke rising into a ceiling layer, that layer descending, and spilling through doorways. doing it right needs a two-zone (upper/lower layer) treatment with layer height, temperature, and visibility, which is exactly what tenability analysis needs anyway. so it’s documented and deferred rather than faked. this is the long-term aim, whether a path stays survivable for the time it takes to use it.
- capacity-aware routing, so the stair split emerges from agents reacting to congestion instead of being imposed.