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 * Lthe 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).

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_modelis 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 andscripts/animate_cognitive_map.py - Discovery-world generator:
scripts/generate_discovery_world.pyproduces 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 intests/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_fahyopts in at 0.024, calibrated against Fahy Table 2 — see docs/routing.md andscripts/sweep_queue_weight.py - Throughput throttling: Optional exit flux limiting via
enable_throughput_throttlingandmax_throughputin 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 key | Default | Meaning |
|---|---|---|
cost_model | "gate" | "gate" or "additive", matched exactly; any other value raises ValueError |
tau_max | 6.0 | Budget \(\tau_{\max}\) on the optical depth of a route |
current_exit_discount | 0.9 | Factor on the current exit’s tau in the sort |
tau_deadband | 0.1 | Anchor deadband, as a fraction of tau_max |
clean_extinction_threshold | 0.0 (off) | Extinction [1/m] of the smokiest leg at or below which an exit is in the clean tier |
clean_exit_margin | 0.1 | Hysteresis: the current exit stays clean up to clean_extinction_threshold / clean_exit_margin (FDS+Evac FAC_DOOR_OLD) |
fed_rejection_threshold | 1.0 | Projected FED above which a route is refused |
anticipate | true | Read 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_s | inf | How far ahead [s] anticipation reads the FDS record |
fallback_switch_margin | 0.2 | Between 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_anchor | 0.9 | A rival exit needs rank_cost below this fraction of the current exit’s; an explicit RerouteConfig.exit_switch_anchor overrides it |
w_smoke | 1.0 | Smoke weight; additive model only, inert under the gate |
w_fed | 10.0 | Dose weight; additive model only, inert under the gate |
w_queue | 0.0 | Weight on queue time in the ranking cost (off) |
visibility_extinction_threshold | 0.5 | Extinction [1/m] at or above which a segment counts as not visible; additive model only, inert under the gate |
impassable_extinction_threshold | 3.0 | Route-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_m | 2.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_s | 1.3 | Router’s clear-air speed [m/s], not the agent’s \(v_0\) |
alpha | 0.706 | Router’s copy of \(\alpha\) [m/s], for travel time only |
beta | -0.057 | Router’s copy of \(\beta\) [m²/s], for travel time only |
min_speed_factor | 0.1 | Router’s copy of \(f_{\min}\), for travel time only |
default_exit_capacity | 1.3 | Exit capacity [agents/s] for queue time, when an exit sets no capacity_agents_per_s |
fed_rate_memo | true | Remember 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 starts | Re-decision interval |
|---|---|
RerouteConfig() built in Python | 10.0 s |
run.py --reroute-interval | 1.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
| Value | Source |
|---|---|
tau_max = 6 | FDS+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.9 | FDS+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.1 | FDS+Evac FAC_DOOR_OLD |
exit_switch_anchor = 0.9 | value of FDS+Evac FAC_DOOR_WAIT, applied to a different cost; not calibrated |
visibility_extinction_threshold = 0.5 | Jin’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_fed | pyFDS-Evac assumptions, not calibrated |
w_queue = 0 | off; 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.3 | pyFDS-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:
- No current route: accept.
- Must flee (
_must_flee_rejection): the current route breaks the FED limit or, under additive only, is refused asall segments non-visiblewith a route-average K aboveimpassable_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 thetaulimit is fled although its reason namestau, and so is a route the fallback promoted. - Two refused routes (both
feasiblefalse, which includes a route the fallback promoted): the candidate is accepted only if itstauis strictly below the current route’s × (1 −fallback_switch_margin) and the two differ by more thanEPS_TAU(_fallback_rival_wins). Neither travel time,k_max_routenor 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 (bothtau0 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 theirtau(#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 atc2c2081fit bound only ont_junctionwith 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. - Otherwise the cost model decides (
improvement):- Gate (
GatePolicy.improvement): a clean candidate leaves a dirty exit; an infeasible candidate needsrank_costbelowexit_switch_anchor× the current one; a feasible candidate whosetauis lower by more thantau_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 samerank_costratio. - Additive (
AdditivePolicy.improvement): only the top-ranked route is tried, and it needsrank_costbelowexit_switch_anchor× the current one.
- Gate (
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;CollisionFreeSpeedModelwhen 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 withinEXIT_REACH_TOLERANCE_M= 0.03 m of it. A checkpoint or waypoint is reached within the agent radius plusTARGET_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
journeysand notransitions, or nodistributions). 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_sorclosed_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_sis 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).
| Case | Steering pass | iterate() | Rest of loop |
|---|---|---|---|
familiarity_test_no_journey: 20 agents, no journeys, 3489 steps | 79–80 % | 19–20 % | 1–2 % |
t_junction: 200 agents flow-spawned over 400 s, deck with journeys (steered directly), 30000 steps | 76–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 ofrun_scenarioand 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 ofscenario.py;jupedsim.Simulation.iteratewrapped 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_journeyusesSocialForceModelandt_junctionusesCollisionFreeSpeedModel, as set in their decks.- At that commit,
familiarity_test_no_journeyended when every agent was out, at 34.9 s, andt_junctionstopped 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 setsno_known_exit: explore, which needs rerouting. - A
cProfilerun oft_junctionattributes the largest part of the steering pass toreached_stage→distance_to_polygon(shapelyPointconstruction 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:
tests/test_exit_door_width.py: agents leave only through the exit polygon, and the flow scales with the door width (#349).tests/test_thin_exit.py: agents held a few centimetres short of a thin exit still count as out, and agents beside the door do not (#401).tests/test_scheduled_exits.py: a closed exit removes nobody, and agents heading for it reroute (#396).tests/test_throttled_exit_without_journey.py: agents without a journey whose nearest exit is throttled reach that exit and leave it at the cap spacing (#434).
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_gatereference 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). Underanticipate, the search ranks on decision-time smoke, so its path can carry a highertauon 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_mat 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
taudifferences up to 1e-9 as ties, so round-off no longer decides it (#452). The anchor treats a difference withintau_max×tau_deadbandas 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 abovevisibility_extinction_thresholdwhile 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
- S4 T-junction
(
tests/verification/test_s4_tjunction_reroute.py): without anticipation, agents leave a smoke-blocked exit for the clear one; with it, they choose the clear one at spawn. - Golden rerouting
(
tests/test_rerouting_golden.py): a regression check that decisions do not change unnoticed, not a verification against an independent value.
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 defaulttau_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 withevac.f90is 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.