Dynamic route rerouting

Dynamic route rerouting

Note

This page is the specification of the routing model. For worked cases on the assets (runs, figures, numbers), see Routing in practice and Gate model in practice.

Based on: Extinction coefficient, Visibility through smoke and Exit choice and familiarity.

See docs/routing.md for the routing machinery, cost formulas, and API reference, and docs/routing-and-signs-notes.md for working notes on exit choice and where the exit-choice research papers disagree with each other.

Symbols follow the notation table.

Background: the Concepts page and the talk A Modular Workflow for Visibility-Aware Evacuation Modelling.

How smoke enters route choice

Two models, selected per deck with routing.cost_model.

"gate" (default). One quantity does the whole of the smoke reasoning: the route’s optical depth

tau = K_ave * L

the soot column the agent walks through, with K_ave the mean extinction along the route polyline and L the distance still to walk. On this page tau is always this dimensionless optical depth, not the relaxation time τ of FDS+Evac’s movement model. A route is refused when tau exceeds tau_max (see Parameters), Dijkstra weights every edge by its own tau, and tau orders the routes that survive, with travel time breaking ties. Path choice and exit choice are therefore one objective. In clear air every tau is zero, nothing is refused, and routes order by travel time (the Dijkstra tie-break is 1e-6 × length), which is nearest-exit; this equivalence has not been re-measured since routes were first ordered on tau (Known limitations).

Top: plan view with three routes from one agent to exits A, B and C around a smoke plume. Bottom: bar chart of optical depth per route against the budget 6

Schematic with a prescribed toy plume, not a simulation. Top: three candidate routes, sampled along each walk, the samples shaded by extinction K [1/m]. Bottom: optical depth tau [-] per route against tau_max = 6 (any exit). Route C is refused; route B ranks first although it is the longest walk. Script: scripts/figures/exposure_gate.py.

tau is an exposure statement, not a sighting distance; where its budget comes from is under Deviations from the literature.

Refusals are not remembered. The criterion is relative to the distance still to walk, so it relaxes on approach: smoke that refuses a door at 40 m accepts it at 2 m. When every route is refused the agent still has to move. It never takes a route whose predicted dose is lethal while one that is not remains; among the rest it takes the one with least smoke to walk through and holds it unless a rival’s tau is clearly lower (fallback_switch_margin) (#128). Churn is held down by the exit-switch anchor, by a discount on the current exit’s tau in the sort (current_exit_discount, FDS+Evac’s FAC_DOOR_OLD2). FDS+Evac applies FAC_DOOR_OLD2 only when no door is smoke-free (its tier 4, evac.f90:16803); the gate applies the discount in every ordering.

Measured, with the limitation stated. When ranking on tau landed (0d9bf79, 9f55f6e), it sent 39 of world100’s agents to the far clean exit without returns, while on l_corridor agents returned to abandoned exits (34 at 9f55f6e, 29 of them following a field that reversed); what is missing is commitment (#124). Those counts come from the commit messages; the deck and seed were not recorded. Re-measured at c2c2081f on the scripts/margin_audit.py decks, l_corridor gives 1 return in 20 seeds and world100 none; the history and the re-measurement are in docs/route-cost-gate.md and Known limitations.

This is a departure from FDS+Evac, not a reproduction of it. The threshold tau > 6 is borrowed with a citation; the place it is used is not. In the reference’s first three tiers the rank is a time or distance norm and smoke is only a boolean admission test (evac.f90:16601, :16690, :16737), so smoke can move a door between tiers but cannot reorder candidates. It ranks on smoke only in the tier-4 last resort, over known-or-visible doors, on a bee line, with a strike-out that lasts one call (evac.f90:16800-16801, reset at :16170-16171). Its only lasting smoke memory is a weak mark on a lone agent’s previous target once K_ave >= 0.3 /m (:16628-16637), described in docs/model-comparison.md. Here tau is the ordering everywhere, with no memory. On l_corridor that diverts 18 of 100 agents where the reference criterion would send essentially everyone to the near exit – a prediction reasoned from evac.f90, not a measured run of it. A L/d geometric bias (1.41 near against 1.27 far on that deck) tilts the criterion about 11 % further in the same direction. Both are written up in docs/route-cost-gate.md.

An optional clean-exit tier (clean_extinction_threshold, off by default) prefers exits below an absolute smoke criterion outright, however far. It is FDS+Evac’s primary door rule, and measured on both reference decks it did not redirect anyone while costing monotonicity – see docs/gate-model-review-notes.md.

Every exit is priced from the agent’s position. Dijkstra starts where the agent stands. Its first legs are the walks from the agent to each successor of its origin node, the node it last left, and to the node it is heading for. Each walk goes through the walkable area and is weighted by the smoke on that walk; all other edges are weighted by the smoke present at decision time (_generate_candidates, _first_hops). The search never routes back through the origin node, so every path starts at the agent and its first node is one of those successors or the current target. A walk that passes the origin on its way to a successor is still priced, on the smoke along it.

The first leg of each route is then measured on that walk: its mean extinction, length and travel time enter K_ave, tau and the travel time, so the agent pays for the smoke ahead of it and none of the smoke behind it (_measure_route). The agent’s current exit is also measured on the path it is walking. If that path orders ahead of the searched one, it becomes the current exit’s entry, so the current exit is never ranked behind the path the agent walks (rank_routes, current_path). Without a position, or when the source is an exit, the search starts at the source node (_search_from_position).

FDS+Evac (Change_Target_Door) also evaluates every door, the current one included, from the agent’s position (x1_old = xx, :16178): smoke on the straight sight line to the door (See_door, :16486) and the Euclidean distance (:16593, :16606). It has no graph, so the walked path through the stage graph is a pyFDS-Evac extension.

Anticipation (anticipate, foresight_horizon_s) applies only to the path the search returns for each exit: every smoke sample on that path, the walk to the next node included, is read at the time the agent would reach it, t_dec + min(s / base_speed_m_per_s, foresight_horizon_s), where s is the walk from the agent’s position to the sample along the path and the speed is unimpeded. tau, travel time and projected FED are measured from those samples (_measure_route, _ForesightClock). The FED rate of each edge is one sample, at its midpoint, read when the agent reaches the edge’s start. Reading the whole edge at its start instead made the cost of a route jump when the agent passed a node (#650). A time past the earliest last frame of the routing FDS slices reads that frame, with one warning per run (#666). Anticipation is a pyFDS-Evac extension with no FDS+Evac counterpart: FDS+Evac extrapolates from the conditions it observes when the agent decides (docs/model-comparison.md).

"additive". The original model: smoke is a toll per metre walked, effective_length * (1 + w_smoke * k_ave) + w_fed * fed_max. Both terms scale with route length, so a long clean detour pays for its length twice and can never win – which is why the gate exists. Pin it with {"cost_model": "additive", "anticipate": false}; anticipate is independent of the model, so the pin needs both.

FIC does not route under either model. It drives the Purser slowdown and incapacitation only. FIC and the optical-depth gate are driven by the same smoke, so routing on both would double-count.

What this model does not do. It is not hazard avoidance. On the fires measured here the dose veto never comes close to firing – the largest projected FED over a whole l_corridor run is 0.0016 against a threshold of 1.0 – and impassable_extinction_threshold cannot fire under the gate at all, because it is reached only from the additive all segments non-visible rejection and the gate refuses on dose and tau only. So no smoke rejection bypasses the exit-switch anchor at any density. What the gate does is exposure-gated wayfinding.

The gate at work, with its evidence, is in docs/route-cost-gate.md; provenance and the open questions are in docs/gate-model-review-notes.md. assets/l_corridor is the deck the model is judged on – a near exit behind the fire and a clean 58 m way round – and its results are in the sciebo case folder.

Components

  • 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. When a fed_model is provided, projected FED vetoes routes under both models and enters the ranking only under "additive"
  • Dynamic rerouting: Agents recompute routes at configurable intervals, selecting lower-exposure paths when available
  • Cognitive-map history: run_scenario(collect_cognitive_map_history=True) records what each agent knows, every time it changes — see docs/testing-familiarity.md and scripts/animate_cognitive_map.py
  • Discovery-world generator: scripts/generate_discovery_world.py produces random test decks (open room, convex obstacles, signed checkpoints, exits optionally hidden from the spawn) for exercising discovery routing; the same decks drive the invariant tests in tests/test_generated_worlds.py
  • Congestion-aware routing: Optional exit-congestion term (w_queue), off by default — it scales with a global agent count, so no constant suits every scenario. assets/station_fahy opts in at 0.024, calibrated against Fahy Table 2 — see docs/routing.md and scripts/sweep_queue_weight.py
  • Throughput throttling: Optional exit flux limiting via enable_throughput_throttling and max_throughput in scenario config

Parameters

Defaults in the code, as a scenario’s routing block reads them (RouteCostConfig.from_routing_params, pyfds_evac/core/route_graph.py). This is the complete list of keys. Which keys act under which cost model is tabulated in docs/route-cost-gate.md.

routing keyDefaultMeaning
cost_model"gate""gate" or "additive", matched exactly; any other value raises ValueError
tau_max6.0Budget \(\tau_{\max}\) on the optical depth of a route
current_exit_discount0.9Factor on the current exit’s tau in the sort
tau_deadband0.1Anchor deadband, as a fraction of tau_max
clean_extinction_threshold0.0 (off)Extinction [1/m] of the smokiest leg at or below which an exit is in the clean tier
clean_exit_margin0.1Hysteresis: the current exit stays clean up to clean_extinction_threshold / clean_exit_margin (FDS+Evac FAC_DOOR_OLD)
fed_rejection_threshold1.0Projected FED above which a route is refused
anticipatetrueRead every smoke sample on the path to each exit at the time the agent would reach it; the path search uses the smoke at decision time. No FDS+Evac counterpart
foresight_horizon_sinfHow far ahead [s] anticipation reads the FDS record
fallback_switch_margin0.2Between two refused routes, a rival’s tau must be more than this fraction below the current exit’s; differences up to 1e-9 are ties, which hold. Before 0.4.0 it compared worst extinction k_max_route (#458)
exit_switch_anchor0.9A rival exit needs rank_cost below this fraction of the current exit’s; an explicit RerouteConfig.exit_switch_anchor overrides it
w_smoke1.0Smoke weight; additive model only, inert under the gate
w_fed10.0Dose weight; additive model only, inert under the gate
w_queue0.0Weight on queue time in the ranking cost (off)
visibility_extinction_threshold0.5Extinction [1/m] at or above which a segment counts as not visible; additive model only, inert under the gate
impassable_extinction_threshold3.0Route-average K [1/m] above which a visibility rejection must be fled, bypassing the anchor; additive model only, never reached under the gate
sampling_step_m2.0\(\Delta s\), spacing of smoke samples where the field has no grid (a constant or test field) or the line leaves every FDS subslice, and of the FED-rate samples of the walk dose [m]. An FDS field is read once per grid cell the line crosses (#653)
base_speed_m_per_s1.3Router’s clear-air speed [m/s], not the agent’s \(v_0\)
alpha0.706Router’s copy of \(\alpha\) [m/s], for travel time only
beta-0.057Router’s copy of \(\beta\) [m²/s], for travel time only
min_speed_factor0.1Router’s copy of \(f_{\min}\), for travel time only
default_exit_capacity1.3Exit capacity [agents/s] for queue time, when an exit sets no capacity_agents_per_s
fed_rate_memotrueRemember route-side FED-rate reads per frame and point; run time only, results unchanged (#243)

Each switching margin must be a number in its range, or the run stops with a ValueError naming the key, also with --no-enable-rerouting: exit_switch_anchor, current_exit_discount and clean_exit_margin in (0, 1]; tau_deadband and fallback_switch_margin in [0, 1]. The value that switches a margin off is in range: 1, or 0 for tau_deadband and fallback_switch_margin. fallback_switch_margin = 1 keeps the current exit between two refused routes unless it must be fled, the scenario-file way to the hold rule.

The router always uses the linear speed law, whatever SmokeSpeedConfig the agents walk with. Each agent re-decides every RerouteConfig.reevaluation_interval_s:

Where the run startsRe-decision interval
RerouteConfig() built in Python10.0 s
run.py --reroute-interval1.0 s

An interval below 1 s acts as 1 s: run_scenario runs the reroute pass at most once per simulated second (#660).

The other defaults that differ between run_scenario() and run.py are listed in Python API and command line.

Assumptions

ValueSource
tau_max = 6FDS+Evac tier-4 door rule, \(\bar K d \le 6\) (evac.f90:16794, :16799), used here as an exposure budget; not calibrated (see Deviations)
current_exit_discount = 0.9FDS+Evac FAC_DOOR_OLD2, which FDS+Evac applies only when no door is smoke-free (tier 4, evac.f90:16803); the gate applies it in every ordering
clean_exit_margin = 0.1FDS+Evac FAC_DOOR_OLD
exit_switch_anchor = 0.9value of FDS+Evac FAC_DOOR_WAIT, applied to a different cost; not calibrated
visibility_extinction_threshold = 0.5Jin’s allowable smoke density for occupants familiar with the building (Jin 1997, Table 1, p. 11), applied to every agent; see Limitations
tau_deadband, fallback_switch_margin, impassable_extinction_threshold, w_smoke, w_fedpyFDS-Evac assumptions, not calibrated
w_queue = 0off; assets/station_fahy sets 0.024, calibrated against Fahy Table 2 (routing in practice)
base_speed_m_per_s = 1.3, default_exit_capacity = 1.3pyFDS-Evac assumptions

evac.f90 line numbers on this page refer to the copy in materials/evac.f90, FDS commit c9da70d7a.

Switching rule

At each re-decision the candidates ranked above the agent’s current exit are tried in order, and the first one the exit-switch anchor accepts is taken. Under the gate, two tau values at most EPS_TAU = 1e-9 apart rank as equal (after the current-exit discount), so travel time orders them; neighbouring ties chain into one group (taus_tie, _order_routes). The anchor (_AnchoredPolicy.anchor_allows) decides in this order:

  1. No current route: accept.
  2. Must flee (_must_flee_rejection): the current route breaks the FED limit or, under additive only, is refused as all segments non-visible with a route-average K above impassable_extinction_threshold; under the gate only the FED limit qualifies. Accept, whatever the cost. The test reads which limits the route breaks (RouteCost.violation_kinds), not its rejection reason, so a route over both the FED and the tau limit is fled although its reason names tau, and so is a route the fallback promoted.
  3. Two refused routes (both feasible false, which includes a route the fallback promoted): the candidate is accepted only if its tau is strictly below the current route’s × (1 − fallback_switch_margin) and the two differ by more than EPS_TAU (_fallback_rival_wins). Neither travel time, k_max_route nor the clean bypass of step 4 decides, so the agent is not held on a route with three times the smoke because it is quicker, and a tie (both tau 0 included) holds. The fallback order uses the same rule (#458), after one step: a route the agent must flee never displaces one it need not flee, and always yields to one, whatever their tau (#128). A 10 s return lockout (fallback_return_lockout_s, #458) that held a switch straight back between two refused routes was removed in #187: it was added while route smoke was sampled at steps that could pass over a narrow plume core, which 0.5.0 reads per grid cell (#653); audited at c2c2081f it bound only on t_junction with pre-movement, suppressed about 34 fallback switches per run between the two refused routes and delayed about 100 returns per run from within 10 s to 11–12 s, with exit shares, FED and RSET unchanged in every seed at 60 s pre-movement and shifted within seed noise at 30 s. No behavioural evidence supports a time lockout.
  4. Otherwise the cost model decides (improvement):
    • Gate (GatePolicy.improvement): a clean candidate leaves a dirty exit; an infeasible candidate needs rank_cost below exit_switch_anchor × the current one; a feasible candidate whose tau is lower by more than tau_max × tau_deadband (0.6 by default) is accepted, one higher by more than that is refused, and a tie inside the band falls through to the same rank_cost ratio.
    • Additive (AdditivePolicy.improvement): only the top-ranked route is tried, and it needs rank_cost below exit_switch_anchor × the current one.

A new path to the same exit is taken when its rank_cost is below the walked path’s (path_switch_anchor, which required 0.9 of it, was removed in #187), or when the walked path is refused and the new one is not (reason better_path). Every route is priced from the node the agent last left, so an applied route sends the agent to the node after that one, even when its current target lies further along the route (#445). When every route is refused, the least-smoky one is promoted as a fallback; it stays rejected, marked fallback:. The full decision table is in Routing in practice.

Code structure

For maintainers, one re-decision runs through: _measure_route (samples the smoke and dose along a candidate) → policy_for(config), which returns the GatePolicy or AdditivePolicy that supplies edge_weight, feasibility, rank_cost, order_key and improvement → rank_routes and _apply_fallback → evaluate_and_reroute, which returns the switch records written to the route history.

How agents are steered

pyFDS-Evac decides where each agent goes, and JuPedSim moves the agent there. The link between the two is JuPedSim’s direct-steering stage: an agent on such a stage walks towards the point stored in its target, and the caller sets that point. JuPedSim 1.4.2 is the locked version (uv.lock; pyproject.toml requires jupedsim>=1.4.2).

What each side does

JuPedSim, inside simulation.iterate():

  • computes the next waypoint from the agent’s position to its target on the navigation mesh of the walkable area, every step (TacticalDecisionSystem.hpp);
  • moves the agent towards that waypoint with the operational model set in the deck (model_type; CollisionFreeSpeedModel when unset).

pyFDS-Evac, in Python, in the main loop of run_scenario (scenario.py):

  • sets an agent’s target when the agent starts a new stage or changes exit (direct_steering_runtime.py, assign_agent_target);
  • reads every agent’s position each step and tests whether the agent has reached its stage (direct_steering_runtime.py, reached_stage). An exit is reached when the agent’s centre is inside the exit polygon or within EXIT_REACH_TOLERANCE_M = 0.03 m of it. A checkpoint or waypoint is reached within the agent radius plus TARGET_REACH_MARGIN_M = 0.5 m of its target point, or when the agent’s centre is inside the checkpoint polygon (#726). A spawn area walked to on a patrol, a zone and an exit without a polygon are reached only within the agent radius plus 0.5 m of the target point;
  • applies checkpoint waiting times, throughput caps, exit schedules and zone speed factors;
  • removes an agent that reaches an exit with simulation.mark_agent_for_removal.

Which agents are steered directly

  • Deck without journeys (no journeys and no transitions, or no distributions). Every exit gets a direct-steering stage, and all agents share one journey made of a single direct-steering stage (simulation_init.py, _initialize_with_fallback). This is the default mechanism.
  • Deck with journeys. An exit without throughput throttling becomes a native JuPedSim exit stage (add_exit_stage); a throttled exit becomes a direct-steering stage (simulation_init.py, _add_stages). Every exit is also kept in pyFDS-Evac’s direct-steering table, so routed agents can be sent to it. JuPedSim accepts a direct-steering stage only as the sole stage of a journey. An agent whose journey contains a checkpoint or an exit from that table, which is every journey that ends at an exit, is therefore placed on the shared direct-steering journey, and pyFDS-Evac walks it through the journey’s stages.
  • Distribution without a journey, in a deck with journeys. Its agents get a JuPedSim journey to the nearest exit (simulation_init.py, find_nearest_exit_journey). At an unthrottled exit, JuPedSim removes them itself. A throttled exit is a direct-steering stage, so pyFDS-Evac steers these agents to it and removes them there under its cap (#434). A deck in which such an exit also has a schedule (open_from_s or closed_after_s) is refused at start.

Why direct steering

The routing and wayfinding models need a target per agent that can change during the run:

  • Each agent re-evaluates its route at a fixed interval, every 1.0 s with run.py (--reroute-interval; RerouteConfig.reevaluation_interval_s is 10 s when the config is built directly). Two agents in the same room can pick different exits, and an agent can switch exit mid-way.
  • Each agent ranks only the routes on its own cognitive map (Wayfinding).
  • Exits open and close on a schedule (#396).
  • Exits and checkpoints can cap their throughput, and checkpoints can hold agents for a waiting time.
  • Zone speed factors are applied per agent in the same pass.

A JuPedSim journey is a stage graph shared by every agent assigned to it. Its transitions follow fixed rules (fixed, round robin, least targeted) set when the journey is built, and they do not read smoke or an agent’s knowledge.

FDS+Evac also picks each agent’s target itself. Each agent walks along the flow field of its own target door (FIND_PREFERRED_DIRECTION, evac.f90:11403–11404). At random times, on average every TAU_CHANGE_DOOR = 1.0 s (:1531, :7998), the agent calls CHANGE_TARGET_DOOR to choose its door again.

Cost

Direct steering moves per-agent work out of JuPedSim’s C++ core into Python. Each step, pyFDS-Evac iterates over the agents through JuPedSim’s Python API, reads positions, tests stages and updates speeds. With native journeys, stage reaching and removal run inside simulation.iterate().

In two small clear-air runs at commit 4f859bf5, before 0.2.0, the per-step steering pass took about four times as long as simulation.iterate(). The shares were not measured again for 0.4.0 (#665).

CaseSteering passiterate()Rest of loop
familiarity_test_no_journey: 20 agents, no journeys, 3489 steps79–80 %19–20 %1–2 %
t_junction: 200 agents flow-spawned over 400 s, deck with journeys (steered directly), 30000 steps76–78 %18–20 %4–5 %

Shares are of the wall time of the main loop. The largest part of the steering pass is pyFDS-Evac’s own geometry: the exit test builds a shapely point per agent per step. The cost of the same runs with native JuPedSim journeys is not measured. These agent counts are small, so the shares can differ at higher density.

How the shares were measured
  • Commit 4f859bf5, jupedsim 1.4.2, Python 3.12, Apple M3 Pro (12 cores), macOS 27.0.
  • run_scenario(load_scenario("assets/<case>"), seed=1): no smoke, no rerouting, JuPedSim’s default time step of 0.01 s.
  • Timers (time.perf_counter) around the main loop of run_scenario and around the steering pass (the block that runs when the direct-steering table is not empty, up to the cognitive-map history), in a local, uncommitted copy of scenario.py; jupedsim.Simulation.iterate wrapped with the same timer. “Rest of loop” is spawning and the other per-step passes.
  • Four runs of each case; the table gives the range.
  • familiarity_test_no_journey uses SocialForceModel and t_junction uses CollisionFreeSpeedModel, as set in their decks.
  • At that commit, familiarity_test_no_journey ended when every agent was out, at 34.9 s, and t_junction stopped at its 300 s limit with 143 of 200 agents out. Both outcomes predate #610 and #250.
  • Since #610 the call raises for familiarity_test_no_journey: the deck sets no_known_exit: explore, which needs rerouting.
  • A cProfile run of t_junction attributes the largest part of the steering pass to reached_stage → distance_to_polygon (shapely Point construction and distance), more than to JuPedSim’s agent iterator.

What this means for correctness

Stage reaching, removal at exits, waiting times, throughput caps and exit schedules are pyFDS-Evac code, so their tests live in this repository:

Exit throughput throttling has no general test yet (#355).

Limitations

  • Switching can oscillate where two routes cross in cost (#124). Pricing the first leg on the walk (#451) made this more frequent near a smoky door: on the l_corridor_gate reference deck, switches went from 28 to 58 and exit reversals from 16 to 40, 28 of them within 2 s. Reading each smoke sample at the time the agent reaches it (#650) brought this down to 17 switches and 8 reversals, 5 within 2 s; the oscillation is reduced, not removed. Each switch follows the switching rule on correct prices.
  • Routes are priced with smoke the agent cannot perceive, including stretches it has never seen (#125).
  • One path is priced per exit (#185).
  • The path to each exit is chosen on the smoke at decision time. Anticipation applies only to that path, each sample at the time the agent reaches it (_generate_candidates, _measure_route). Under anticipate, the search ranks on decision-time smoke, so its path can carry a higher tau on arrival than another path to the same exit.
  • The search never offers “walk back to the origin node, then on”. When that is the only way round the smoke, the direct walk is priced on its own smoke.
  • Node-to-node legs take their FED growth from one FED-rate sample at the polyline midpoint. The first leg is charged by its own rule (#171): an agent on or ahead of the origin node is charged the share of the first segment’s FED growth that is left of it; an agent behind it is charged the whole first segment plus the stretch by which the walk is longer than the segment, at the mean FED rate sampled along that stretch every sampling_step_m at decision time, over the walk’s pace (_first_leg_dose). The candidate search charges each first hop the same way. Anticipated times are counted from the agent’s position (#650).
  • The ordering treats tau differences up to 1e-9 as ties, so round-off no longer decides it (#452). The anchor treats a difference within tau_max × tau_deadband as a tie, but a real difference inside that band still keeps the current exit ranked first, and the anchor is then never consulted (#187, open).
  • The queue term counts agents globally, not those an agent can perceive (#89).
  • Heat does not enter route choice (#81).
  • The smoke terms are the same for every familiarity setting (#362).
  • visibility_extinction_threshold (0.5 1/m) is Jin’s allowable smoke density for occupants familiar with the building; for unfamiliar occupants Jin gives 0.15 1/m (Familiar and unfamiliar occupants). Jin took it from the density at which most of the Fire Research Institute researchers among his 49 subjects began to lose their steadiness on a seated hand-steadiness test in white wood smoke, and read it as the threshold where escape becomes difficult for familiar occupants (Jin 1997, p. 11). It was not derived from a wayfinding or incapacitation test. pyFDS-Evac applies it to every agent, whatever its familiarity (#516, #362). It acts under the additive model only.
  • impassable_extinction_threshold (3.0 1/m) is a modelling assumption with no empirical source (#371). It acts under the additive model only, on a route refused because the mean K of every segment is at or above visibility_extinction_threshold while some other unrejected route has a visible segment, and then only when the route-average K exceeds 3.0 1/m. The field surveys (Wood 1972; Bryan 1977) give only visibilities estimated by respondents, and most people who entered smoke did not turn back. Two studies measured K. Jin and Yamada (1989, p. 513): in a dead-end corridor of irritant smoke at K = 0.92 ± 0.21 1/m, 14 of 31 subjects turned back; they had been told they could return whenever they wished, wore a towel over mouth and nose, and radiant heaters stood at the far end. Six of the seven who stopped first turned back again in a second trial at 0.66 1/m, and two of them in a third at 0.62 1/m. Frantzich and Nilsson (2003, §3.2 and Table 6) had 45 subjects leave a 37 m tunnel through artificial smoke made irritant with acetic acid, at measured K of about 2–7 1/m; 44 continued to an exit, one left by the entrance. Neither supports a fixed density at which people turn back.
  • Under the default gate, smoke on the current route never bypasses the exit-switch anchor as a lethal FED does: however dense it is, the agent leaves only for a rival the anchor accepts (clearly lower τ, or clean where the current route is not) (#750). A dose-based comparator is researched in #751; the wider question is #696.
  • Anticipation assumes unimpeded speed and, by default, perfect foresight of the finished FDS record.

The measurements behind these are in Gate model in practice › Known limitations.

Verification

See the Verification index for their status.

Usage

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.

Deviations from the literature

The published laws are on Visibility through smoke and Exit choice and familiarity.

  • Where the budget comes from. FDS+Evac’s tier-4 door rule requires the visibility of a reflective sign at the door, \(3/\bar K\), to be at least half the distance d to the door, which rearranges to \(\bar K d \le 6\) (evac.f90:16794, :16799). That is the default tau_max (route_graph.py, RouteCostConfig.tau_max). Jin’s law describes a straight line of sight to a sign in uniform smoke; here the same number bounds the integral of K along a walked polyline, which measures exposure, not sight. The two agree only on a straight corridor, and the budget has not been calibrated against a smoke-exposure or FED limit. The line-level comparison with evac.f90 is in docs/route-cost-gate.md.
  • Exit choice. Routes are ranked on optical depth and travel time. Familiarity and social influence, which the exit-choice literature finds significant, enter only through the cognitive map, not through the route cost, and herding is not modelled.
  • Smoke in a cognitive-map router, earlier work. Schröder et al. (2015) priced routes by smoke in the cognitive-map router of JuPedSim. They integrate the FDS optical density D along the straight line of sight from the agent to each exit available to the agent, and weight the integral by the maximum D on that line over the maximum on all such lines (Eq. 4, p. 331). A door whose line of sight is obstructed is excluded from the smoke edge-factor calculation. The smoke raises the edge weight by the factor \(1 + 2 f_{\mathrm{smoke}} (1 - f_{\mathrm{risk}})\) (Eq. 5, p. 332), where the factor 2 and the individual risk tolerance \(f_{\mathrm{risk}}\) are chosen, not fitted to data (p. 332); the authors call the model a proof of concept. D is read at an extraction height in the upper layer (2.80 m in their example, p. 330; varied over 2–3 m, Table 2, p. 334), and the sensor does not evaluate visibility (p. 330). The height is an artificial parameter and the most influential one in their sensitivity study (p. 337). pyFDS-Evac differs: it integrates K along the walked route polyline, not a line of sight, and samples smoke the agent cannot perceive (#125). It implements neither Eq. 4, Eq. 5, \(f_{\mathrm{risk}}\) nor the obstruction test. Route choice in smoke after Schröder et al. (2015) runs pyFDS-Evac’s own routing on their geometry.

Sources

  • Bryan, J. L. (1977). Smoke as a Determinant of Human Behavior in Fire Situations (Project People). NBS-GCR-77-94, University of Maryland, for the National Bureau of Standards. NTIS PB271755, ntrl.ntis.gov.
  • Frantzich, H., & Nilsson, D. (2003). Utrymning genom tät rök: beteende och förflyttning. Report TVBB-3126, Department of Fire Safety Engineering, Lund University. No DOI; lup.lub.lu.se.
  • Jin, T. (1997). Studies on human behavior and tenability in fire smoke. Fire Safety Science, 5, 3–21. doi:10.3801/iafss.fss.5-3
  • Jin, T., & Yamada, T. (1989). Experimental study of human behavior in smoke filled corridors. Fire Safety Science, 2, 511–519. doi:10.3801/iafss.fss.2-511
  • Schröder, B., Haensel, D., Chraibi, M., Arnold, L., Seyfried, A., & Andresen, E. (2015). Knowledge- and perception-based route choice modelling in case of fire. Proceedings of the 6th International Symposium on Human Behaviour in Fire, Cambridge, UK, 28–30 September 2015, pp. 327–338. Interscience Communications. ISBN 978-0-9933933-0-3. No DOI; juser.fz-juelich.de/record/255940.
  • Wood, P. G. (1972). The Behaviour of People in Fires. Fire Research Note No. 953, Fire Research Station (study at Loughborough University of Technology). No DOI; publications.iafss.org.
Last updated on