Fire Dynamics Simulator (FDS) coupled evacuation modeling with smoke-speed reduction, toxic gas dose (FED), and dynamic route rerouting.
The project includes:
- Smoke-speed model (visibility/extinction-based speed reduction)
- Full ISO 13571 FED model (toxic gas dose accumulation)
- Dynamic smoke-based route rerouting with congestion awareness
- Sign-visibility-gated route rejection (fdsvismap integration)
- Per-agent cognitive maps with
fullanddiscoveryfamiliarity tiers - JuPedSim scenario loading and simulation
This project uses uv for dependency management.
uv syncActivate the virtual environment:
uv shellRun a JSON-first scenario with the CLI runner:
uv run run.py --scenario assets/ISO-table21 --cleanupSee docs/usage.md for the full catalogue of CLI flags,
post-processing scripts, and the scripts/run_and_plot.sh driver that
runs a simulation and produces every plot in one go.
A FastHTML + MonsterUI
web GUI exposes the same model behind a form: pick a scenario, set any
run.py flag (the fds dir field has a folder browser), run it, watch
live progress, and explore the results as interactive
Plotly charts (trajectories coloured by
exit, cumulative FED, smoke, and route cost).
Install the optional GUI dependencies and launch:
uv sync --extra gui
uv run app.pyThen open http://localhost:5001. The GUI calls the same
run_scenario() as the CLI (via the shared
pyfds_evac.core.run_config.build_run_kwargs option builder), so a run
configured in the browser is identical to the equivalent run.py
invocation. Runs execute on a background thread and stream progress over
Server-Sent Events; one run is active at a time.
See docs/smoke-speed-model.md for the full model description, configuration, and API reference.
The smoke-speed model uses extinction coefficient K [1/m] as the primary
input. Two speed laws are available, selected via SmokeSpeedConfig.speed_law:
speed_law |
Model | Reference |
|---|---|---|
"lund" (default) |
Linear: speed_factor = 1 + β·K/α, clamped to [min_speed_factor, 1] |
Frantzich & Nilsson / FDS+Evac |
"fridolf" |
Non-linear: speed_factor = V / (V + 2) where V = C / K (Jin) |
Fridolf et al. (2019) |
The Fridolf law is empirically validated against individual walking-speed
measurements in smoke-filled tunnels and naturally asymptotes to zero without
a hard clamp. Select it with SmokeSpeedConfig(speed_law="fridolf");
visibility_factor_c controls the Jin constant (default 3 for reflective
signs, 8 for light-emitting signs).
For real FDS output, fdsreader provides the local extinction field
via SliceFieldSampler. For verification cases such as ISO 20414 Table 21,
the runner can also apply a constant extinction coefficient directly.
All FDS slice data is read through a single library:
fdsreader— reads raw FDS slice quantities with nearest-neighbor spatial and temporal lookup viaSliceFieldSampler(pyfds_evac/core/fds_sampling.py)- Used by both the smoke-speed model (extinction
K [1/m]) and the FED model (CO, CO2, O2, and optional irritant gases) - When a scenario needs both extinction and FED fields from the same FDS
case, pass a shared
fdsreader.Simulationinstance to avoid parsing the directory twice (see FDS sampling API)
Run the ISO Table 21 corridor with a constant extinction coefficient:
uv run run.py \
--scenario assets/ISO-table21 \
--constant-extinction 1.0 \
--smoke-update-interval 0.1 \
--output-smoke-history /tmp/iso-table21-smoke-history.csv \
--cleanupRun the smoke-speed model against FDS results read through fdsreader:
uv run run.py \
--scenario assets/ISO-table21 \
--fds-dir fds_data \
--smoke-update-interval 0.1 \
--output-smoke-history /tmp/iso-table21-fds-smoke-history.csv \
--cleanupInspect the FDS quantities available through fdsreader:
uv run run.py --inspect-fds --fds-dir fds_data --scenario assets/ISO-table21Plot smoke-speed history for a single agent:
uv run python scripts/plot_smoke_history.py \
--input /tmp/iso-table21-smoke-history.csv \
--output /tmp/iso-table21-smoke-history.png \
--agent-id 1Plot aggregate smoke-speed history:
uv run python scripts/plot_smoke_history.py \
--input /tmp/iso-table21-smoke-history.csv \
--output /tmp/iso-table21-smoke-history-aggregate.pngGenerate a stable ISO Table 21 sweep artifact under artifacts/:
uv run python scripts/generate_iso_table21_sweep.pyGenerate the FDS+Evac smoke-density vs speed verification plot:
uv run python scripts/generate_smoke_density_speed_plot.pyThe FED model implements the full ISO 13571 / Purser formulation as described in Section 3.4 of the FDS+Evac Technical Reference and User's Guide (Korhonen, 2021).
| Term | Guide Eq. | Formula | Input |
|---|---|---|---|
| FED_CO | (13) | CO (ppm) | |
| FED_CN | (14-15) |
|
HCN, NO2 (ppm) |
| FED_NOx | (16) |
|
NO, NO2 (ppm) |
| FLD_irr | (17) | HCl, HBr, HF, SO2, NO2, acrolein, formaldehyde (ppm) | |
| HV_CO2 | (19) | CO2 (vol %) | |
| FED_O2 | (18) | O2 (vol %) |
Irritant Ct values (ppm·min) from guide Table 2:
| Species | HCl | HBr | HF | SO2 | NO2 | acrolein | formaldehyde |
|---|---|---|---|---|---|---|---|
| F_FLD | 114000 | 114000 | 87000 | 12000 | 1900 | 4500 | 22500 |
Gas species are read from FDS slice outputs via fdsreader. Required
species: CO, CO2, O2. Optional species (HCN, NO, NO2, HCl, HBr, HF,
SO2, acrolein, formaldehyde) are loaded when available; missing species
default to 0 and contribute nothing to the FED sum. With only the three
required species, the model reduces to the original FDS+Evac default
pathway: FED_CO * HV_CO2 + FED_O2.
The FED model was extended in March 2026 to include all ISO 13571 terms:
- HCN (hydrogen cyanide) and NO2 (nitrogen dioxide): CN-term for narcosis, where NO2 has a protective effect (C_CN = C_HCN - C_NO2)
- NO (nitric oxide): Added to NOx-term alongside NO2
- Multiple irritant gases: HCl, HBr, HF, SO2, NO2, acrolein, formaldehyde with species-specific Ct thresholds from guide Table 2
- O2 hypoxia guard: The O2 FED term (guide Eq. 18) is suppressed at or above 19.5 % O2 (OSHA safe-air threshold). At ambient conditions (20.9 %) the denominator of Eq. 18 is non-zero, producing a tiny but finite rate that accumulates spuriously over long simulations or when agents sample outside the FDS domain (where O2 defaults to 20.9 %). The guard sets the rate to zero when O2 ≥ 19.5 %, matching the default behaviour in Pathfinder (Thunderhead Engineering).
All new terms are fully tested with constant-exposure unit tests in
tests/test_fed.py.
- Equation-level constant-exposure checks for all ISO 13571 terms are covered in tests/test_fed.py
- An ISO Table 22 style stationary benchmark is covered with
assets/ISO-table22, comparing the runtimeFED=1crossing time against the analytical reference
Generate the ISO Table 22 stationary FED verification figure:
uv run python scripts/generate_iso_table22_stationary_plot.py- Thermal FED terms (radiant heat, convective heat)
- Height-relative FED and smoke sampling: gas concentrations and extinction
are sampled from a single horizontal FDS slice at a fixed height
(
slice_height_m, default 2.0 m), shared by all agents regardless of their individual heights. Pathfinder samples at 90 % of each occupant's height, which is more accurate for scenarios with mixed-height populations (children, wheelchair users). A per-agent sampling height would require either multiple slice outputs at different elevations or 3-D slice data, and is a known approximation of the current model.
See docs/usage.md for the full catalogue of
run.py flags (scenario, FDS coupling, FED, rerouting, tenability)
and the post-processing scripts. Note: if an agent
sample lies outside the FDS domain the implementation falls back to
ambient conditions.
On top of the Frantzich–Nilsson extinction–speed law, pyFDS-Evac applies two Purser/FDS+Evac rules when a FED model is loaded:
- FIC-driven slowdown. Purser's Fractional Irritant
Concentration (HCl, HBr, HF, SO2, NO2, acrolein, formaldehyde; see
pyfds_evac.core.fed.default_fic) multiplies the Frantzich speed bymax(fic_min_factor, 1 − fic_alpha·FIC). Defaults:fic_alpha = 0.7,fic_min_factor = 0.3. - Incapacitation at the FED threshold. Once cumulative FED crosses
an agent's threshold, its target speed is driven to zero for the rest
of the run and the agent remains as a static obstacle. Incapacitation
is a population endpoint (NIST TN 1797 / Purser: ~11 % of occupants
by FED 0.3, 50 % by 1, 89 % by 3), so by default each agent draws its
own threshold from a log-normal
D_incap = D₅₀·exp(σ·Z),Z ~ N(0,1)(medianD₅₀ = --fed-threshold = 1,σ = --susceptibility-sigma = 0.94), sampled from the run's seed for reproducibility. Pass--incapacitation-mode deterministicto make every agent use the same threshold (the legacy uniform rule).
Both rules are enabled by default and can be tuned via CLI flags
--fic-alpha, --fic-min-factor, --fed-threshold,
--incapacitation-mode, --susceptibility-sigma, or turned off
entirely with --disable-tenability. The FED history CSV
(--output-fed-history) gains three extra columns fic,
fic_speed_factor, incapacitated.
See docs/routing.md for the full routing model, cost formulas, and API reference.
The routing system implements smoke-aware path planning with dynamic rerouting:
- StageGraph: Dijkstra-based shortest-path routing on a graph of stages (distributions, checkpoints, exits)
- Route cost evaluation: Samples extinction (K) along candidate paths
to compute smoke exposure (FED terms are supported when a
fed_modelis provided; otherwise only smoke drives ranking) - Dynamic rerouting: Agents recompute routes at configurable intervals, selecting lower-exposure paths when available
- Congestion-aware routing: Optional exit-congestion term (
w_queue) balances load across exits based on current agent counts and capacities - Throughput throttling: Optional exit flux limiting via
enable_throughput_throttlingandmax_throughputin scenario config
See docs/usage.md for the full rerouting CLI
(--enable-rerouting, --reroute-interval, --output-route-history,
--output-route-cost-history, --vis-cache) and the plotting scripts
that consume the generated route-cost CSVs.
Implements Spec 008: sign visibility gates route rejection and per-agent cognitive maps control what knowledge each agent has about the building layout.
Each exit and checkpoint can carry a "sign" descriptor in the scenario
config:
{
"exits": {
"exit_A": {
"sign": {"x": 0.5, "y": 11.5, "alpha": 90, "c": 3}
}
}
}alpha is a compass bearing (degrees from north, clockwise): 90 = sign
visible from the east, 270 = from the west, 180 = from the south.
At each reevaluation tick the VisibilityModel checks whether an agent
can see the next node's sign using a cached fdsvismap
pickle. If the sign is not visible, the route is rejected with
rejection_reason="next_node_not_visible".
# Build or reuse the vismap cache and enable visibility-gated rejection
uv run run.py \
--scenario assets/demo \
--fds-dir fds_data/demo \
--enable-rerouting \
--vis-cache fds_data/demo/vismap_cache.pkl \
--output-route-cost-history route_costs.csv \
--cleanupRejected routes are recorded in the route-cost CSV with
rejected=True, rejection_reason=next_node_not_visible.
# Coverage and ASET maps (sign placement validation)
uv run python scripts/demo_vismap_phase0.py
# With fresh vismap recompute
uv run python scripts/demo_vismap_phase0.py --no-cacheAgents have a familiarity tier that controls how much of the building they know at the start of the simulation:
| Tier | familiarity |
Knowledge at spawn | Expansion |
|---|---|---|---|
| Trained staff | "full" |
Complete stage graph | — |
| Visitors | "discovery" |
Spawn node + visible neighbors | On arrival + at reevaluation |
Set per distribution group in the scenario config:
{
"distributions": {
"visitors": {
"parameters": {
"familiarity": "discovery"
}
}
}
}Default when the key is absent: "full" (backward compatible).
Discovery expansion rules:
- At spawn — agent learns its spawn node plus any adjacent node whose sign is currently visible from the spawn centroid.
- On arrival — when an agent physically reaches a node, all immediate neighbours are added to the cognitive map unconditionally.
- At reevaluation — adjacent nodes whose sign is visible from the agent's current position are added.
Routing (Dijkstra) runs over the agent's known sub-graph only. If no exit is reachable in the cognitive map, no rerouting occurs and the agent continues on its last assigned route until the cognitive map expands via an arrival or reevaluation event.
# 4-panel figure: spawn → junction → reroute → full baseline
uv run python scripts/demo_cognitive_map_vis.py
# Without cached vismap (all neighbours assumed visible at spawn)
uv run python scripts/demo_cognitive_map_vis.py --no-cacheTwo scenario configs differ only in familiarity tier:
| Config | Tier |
|---|---|
assets/demo/config_full.json |
familiarity=full |
assets/demo/config_discovery.json |
familiarity=discovery |
Run both back-to-back and produce a 3-panel comparison (exit split, rejection timeline, evacuation time):
uv run python scripts/run_familiarity_comparison.py \
--fds-dir fds_data/demo \
--vis-cache fds_data/demo/vismap_cache.pklOutputs: results/familiarity_comparison/{full,discovery}_route_costs.csv,
results/familiarity_comparison/comparison.png.
A two-layer verification suite checks each sub-model against a hand-computable reference:
- Tier A pins each model function (FED, smoke-speed, cognitive map, pre-movement) to a closed form, to machine precision.
- Behavioural scenarios drive the coupling inside
run_scenariowith injected synthetic fields (no FDS run): a corridor for FED lethality (S1) and smoke-speed slowdown (S2), and a T-junction for dynamic rerouting (S4). Each uses a control / treatment / null-field design and asserts both exact wiring (from the per-agent history logs) and aggregate behaviour.
uv run pytest tests/verification -m "not slow" # fast suite
uv run pytest tests/verification # incl. ensemble checksSee tests/verification/README.md for the full catalogue and specs/012-model-verification/SPEC.md for the design. The suite already surfaced two engine findings: trajectory-level nondeterminism (only aggregate outcomes reproduce under a fixed seed) and a rerouting bug under by-number placement (issue #21).
When --output-sqlite is combined with FED computation, the SQLite also carries
an optional agent_scalars(frame, id, fed, speed) table. The base JuPedSim
schema is unchanged, so jupedsim replay and Web-Based-JuPedSim still read the
file. fds-viewer reads this
table to colour agents by FED dose or speed in a 3D scene alongside the FDS
smoke.
Agent visualisation is handled by
fds-viewer, which
renders the JuPedSim trajectory SQLite in a 3-D scene alongside the FDS
smoke. Run with --output-sqlite to produce the file fds-viewer loads:
uv run run.py --scenario assets/demo \
--fds-dir fds_data/demo \
--output-sqlite demo.sqliteWhen FED is computed, the SQLite also carries the optional
agent_scalars(frame, id, fed, speed) table (see above), which
fds-viewer uses to colour agents by FED dose or speed.
Reference materials are stored in materials/:
- FDS+Evac Technical Reference and User's Guide — Korhonen (2021). Primary reference for the FED equations (Section 3.4) and smoke-speed model (Section 3.4, Eq. 11).
- Boerger et al. (2024) — Beer-Lambert integrated extinction along line of sight (Eq. 8-9), waypoint-based visibility maps. Fire Safety Journal 150:104269.
- Haensel (2014) — Knowledge-based routing and cognitive map framework for evacuation modelling.
- Schroder et al. (2020) — Waypoint-based visibility and evacuation modeling.
- Ronchi et al. (2013) — FDS+Evac evacuation model validation and verification.
- evac.f90 — Original FDS+Evac Fortran source for cross-referencing implementation details.
Scenario definitions are stored in assets/:
- ISO-table21: ISO 20414 corridor verification case (single exit)
- ISO-table22: ISO 20414 stationary benchmark (single agent, analytical FED=1 time)
- haspel: Multi-exit scenario with three zones and dynamic rerouting
- demo: T-corridor FDS scenario with cable fire, two exits (A open, B smoke-accumulating),
200 visitors spawning in the branch; used for visibility-aware routing and cognitive
map verification (Spec 008). Includes
config_full.jsonandconfig_discovery.jsonfor familiarity-tier comparison. - basic: Minimal scenarios for smoke-speed verification
- HC: Hazard composition cases
- social_force: Social force model test cases
- jupedsim
- pedpy
- fdsreader
- plotly
- nbformat



