Web GUI
An optional local web app that runs the same model as run.py behind a form:
pick a scenario, set the options, run it, watch the progress, and look at the
results. It is for exploring a scenario; for studies, scripts and run.py
are easier to reproduce. To turn a GUI run into a script, see
Show the run as Python.
Install and launch
uv sync --extra gui
uv run app.pyThen open http://localhost:5001. The extra installs FastHTML and its dependencies. On a screen narrower than 900 px, such as a phone, the form sits above the results instead of beside them.
Run a scenario
The steps use iso_table21_coupled, a bundled scenario that comes with its
FDS output: one agent walks a 100 m corridor filled with smoke of about
K = 1 1/m. The numbered markers in the first screenshot show where each
control is. Click a screenshot to open it at full size.

Pick a scenario
In Core, choose iso_table21_coupled in the scenario picker (1). Leave
the seed blank to use the scenario’s own baseSeed. To use your own
scenario, drop its config JSON and geometry WKT, or a .zip bundle, on the
upload box and click Add to list.
Set the options
Open Smoke (2) and enter assets/iso_table21_coupled/fds in FDS dir,
or pick the folder with Browse…. Leave the other fields at their
defaults. Every run.py option has a field, and labels show the unit where
the value has one, for example Smoke slice height (m). Press ? next
to a field to read its help text. The groups and their fields are listed
under The form.

Run it
Click Run scenario (3). The results area (4) turns into a progress card and a console with the model’s log. The card names the scenario and the run number and shows agents evacuated, simulated time, wall-clock time and percent done. Cancel run stops the run at its next step.
If a value is invalid, the run does not start. An alert above the results
area says “The run was not started.” with the error, and your settings and
any results already shown are kept. The field is named in the message when
the error comes from that field’s value, for example Seed: …; otherwise
the message is shown as it is, with the exception type under
Technical details.
Look at the results
When the run ends, the results replace the progress card. A header names the run (Results, the run number, scenario and start time) and holds Show Python for this run and Clear results. Below it, the outcome is stated in words, “Complete: all agents evacuated” here, followed by tiles for the evacuation time, agents evacuated, agents remaining and the seed used, and any warnings from the run.

Further down, press play in Trajectories to replay the run, or drag the time slider. Scroll over the plan to zoom and drag to pan; ↺ resets the view. With an FDS folder set, the extinction slice is drawn under the agents as a grey smoke layer; Smoke switches it off.

The Smoke chart below the replay plots the mean speed factor and the mean extinction coefficient K over time. Other charts, such as Cognitive map growth, stay empty for this one-agent corridor.
Keep the run as a script
Click Show Python for this run in the results header to get this run as a standalone Python script. Show equivalent Python, in the Parameters header, previews the current form settings instead. See Show the run as Python.
The form
The form is built from the run.py parser, so every option has a field.
Choice fields with no parser default show “default”: for heat_clothing that
means clothed, ISO 13571 Eq. (9), and a blank heat_fed_threshold follows
fed_threshold
(#311). The fields are grouped:
| Group | Fields |
|---|---|
| Core | scenario (pick or upload), seed; a blank seed uses the scenario’s baseSeed, as run.py does, and 42 if the scenario sets none (Usage) |
| Smoke | fds_dir (with a folder browser), constant_extinction, smoke_update_interval, smoke_slice_height |
| FED & Tenability | disable_tenability, incapacitation_mode, susceptibility_sigma, enable_fic_speed, fic_alpha, fic_min_factor, fed_threshold, o2_threshold_percent, enable_heat_fed, heat_incapacitation_mode, heat_susceptibility_sigma, heat_clothing, heat_fed_threshold |
| Rerouting | enable_rerouting, reroute_interval |
| Visibility | vis_cache; blank means no cache |
| Output files | Output folder; the SQLite, the four CSVs and the scenario bundle (<run folder>/bundle) are written to the run’s folder (see Output folders) |
| Other (collapsed) | every remaining option: clear_air_visibility, no_visibility, vis_cell_size, max_sign_distance, and the heat options heat_endpoint, heat_fed_method, heat_emissivity, heat_convective_coefficient, heat_skin_temperature, heat_radiant_source, heat_u_factor, heat_regime, heat_layer_height, heat_view_factor, heat_layer_emissivity |
Four run.py options have no field: --print-summary, --export-only,
--inspect-fds and --cleanup. Options with a fixed set of values are
dropdowns. The option meanings and defaults are on Usage.
The flags under “Other” are not yet next to the group they belong to (#311, #270).
Runs
The GUI calls the same run_scenario() as the command line, through the same
option builder (build_run_kwargs in pyfds_evac/core/run_config.py), so a
run configured in the browser gets the same options as the equivalent
run.py command. Invalid combinations (for example --vis-cache with rerouting off)
are rejected when the form is submitted, with the same message as run.py,
in the alert above the results area.
One run is active at a time. It runs on a background thread and streams its progress to the page. Run scenario and Results only stay locked while a run is active or cancelling. Cancel run stops the run at its next step.
Run states
When a run ends, the panel shows one of these states, each with a header that names the run and its own buttons:
| State | Shown when | Buttons |
|---|---|---|
| Results | the run finished and its results are displayed | Show Python for this run, Clear results |
| Failed | the run raised an error; the message comes first | Show configuration of this run, Clear |
| Cancelled | you cancelled the run; no results were produced | Show configuration of this run, Clear |
| Results not displayed | the run finished, but its results view could not be built | Show Python for this run, Clear |
| Ended | another run started, possibly in another window | Show current run |
Clear results asks for confirmation first: “Clear the results of run #N from this view? The files on disk are kept.” Clear on a failed or cancelled run does not ask. A reload of the page shows the state the server holds, so results survive a refresh. The form itself returns to its defaults, so the Settings changed banner appears until you set the run’s settings again.
The outcome line comes from whether every agent left, not from whether the run raised an error. It reads “Complete: all agents evacuated”, “Incomplete: time limit reached (k of N remaining)” when the run stopped at its time limit, or “Incomplete (k of N remaining)” otherwise.
Settings changed
After a run, editing the form so that it would configure a different run shows a banner: “Settings changed since run #N. The results below show that run’s settings, not the form. Run again to get results for the current settings.” The results header gets a Previous settings tag. If the edited form is invalid, the banner says that the settings cannot currently be run, and why. The comparison uses the same resolution as a submitted run and leaves the output paths out. Restoring the settings removes the banner.
Output folders
Each run writes into a folder of its own, so no run overwrites another:
- Output folder blank (the default): the folder is derived as
<results root>/<scenario>/<mode>/seed<seed>/<start time>, where mode is the incapacitation mode, seed the seed the run uses, and start time the run’s UTC start time, for example20260929T181031Z. - Output folder typed: the run writes into
<typed folder>/<start time>. While a folder is typed, a note under the box shows the resolved path: “Each run writes into folder/<start time>/”. A relative folder is taken under the results root.
The results root is the folder in the environment variable
PYFDS_EVAC_RESULTS_DIR when it is set, and otherwise results/ in the
repository, whatever folder the server was started from. If a run’s folder
already exists, -2, -3, … is added to its name.
Results
- Summary: the outcome in words, tiles for time, evacuated, remaining and seed used, the peak gas and heat FED when the run reported them (with “Incapacitated: not reported by this version”), and the run’s warnings.
- Charts: smoke over time and the growth of the cognitive maps. FED is shown live during a run.
- Trajectory replay: agents move between the stored trajectory samples,
coloured by cumulative gas FED or by assigned exit, with play, pause, a
scrub bar, speeds of 1×, 2×, 5×, 10× and 50× or a custom speed, and
scroll-to-zoom and drag-to-pan. When the run has an
fds_dir, the FDS extinction slice is drawn under the agents as a smoke layer that can be switched off. Without a FED model, for example when the FDS output has no CO, CO₂ or O₂ slices, agents are coloured by exit and the FED/exit switch is not shown. The replay colours by gas FED only, not by heat FED (#232). - FED panel: under the replay, when the run has a FED model. It shows
the highest gas FED of any agent on one continuous scale that runs to 1, or
to the run’s FED threshold if that is higher. A marker labelled
“threshold” (deterministic mode) or “median threshold” (probabilistic
mode) sits at the run’s
fed_threshold, read from the run’s own settings, not the current form. Without a recorded threshold there is no marker.
The files the run writes are described on Outputs.
Show the run as Python
The GUI can write any configuration as a standalone Python script. Use it to rerun a GUI run on another machine or to start a batch study from it, with the settings and the seed traceable to the run. The script reproduces a configuration; it does not validate the model. The GUI never executes it.
Two buttons open the code:
- Show equivalent Python, in the Parameters header: a preview of the current form settings. It is not a run.
- Show Python for this run, in the results header: the code of the most recent run, built from the settings frozen when it was submitted. On a failed or cancelled run the same button reads Show configuration of this run.
Both open a dialog with Copy and Download .py.

Save a run and run it again
In the scenario picker, choose the plain
blind_spawn_discoveryentry, not one of its/ config_….jsonvariants, and run it with the default settings.When the run has finished, click Show Python for this run. The dialog is titled “Code for run #1 · blind_spawn_discovery · start time” and reports “Status: Complete: all agents evacuated (30/30), evacuation time 55.17 s”.
Click Download .py. The file is named after the scenario, the run number and the run’s UTC start time, for example
pyfds_evac_blind_spawn_discovery_run1_20260929T182009Z.py.Run it with the pyfds-evac version named in its header, installed the same way as the GUI, for example the repository’s
uvenvironment. Theguiextra is not needed. From the folder that holds the script:uv run --project /path/to/pyFDS-Evac python pyfds_evac_blind_spawn_discovery_run1_20260929T182009Z.py
On another machine, copy the scenario folder and the FDS results as well: the
script contains code only. Then edit the PATHS block at the top of the
script. A scenario uploaded into the GUI lives in the GUI’s own uploads folder
and will not exist elsewhere.
What to keep in mind
- The snapshot stores settings, not inputs. It keeps the option values and the scenario path, not the scenario JSON, the FDS results or the visibility cache. The script reloads them from disk, so after an edit to the scenario or the FDS folder the code for run #N no longer reproduces run #N.
- Results can differ from the GUI run, even with the same seed, with other versions or on other platforms.
- Outputs. The script writes to a new
OUTPUT_DIRand does not overwrite the GUI run’s files. It does read the sameVIS_CACHEfile, and the cache key includes the resolved FDS directory (_make_metainpyfds_evac/core/visibility.py), so a changedFDS_DIRor setting makes the script rebuild the map and rewrite that file. - Fewer output files. The script writes the trajectory SQLite and its manifest, not the GUI’s CSV histories or app bundle (#328).
Preview and run code
Preview. The form is resolved exactly as a submitted run would be. If the
settings are invalid, the dialog shows the same error message as run.py,
with a Details disclosure, and no code; Copy and Download .py are
disabled. The file is named pyfds_evac_<scenario>_preview.py.
Run code. It is built only from the run’s frozen snapshot, never from the
live form. The status line uses the wording of the results view:
“Complete: all agents evacuated (n/N), evacuation time t s”,
“Incomplete: time limit reached (k of N remaining), simulated time t s”,
“Incomplete (k of N remaining), simulated time t s”, failed,
cancelled, or not recorded. The file is named
pyfds_evac_<scenario>_run<N>_<start time>.py, with the run’s UTC start
time such as 20260929T182009Z.
- Failed and cancelled runs still have code. The dialog is titled “Configuration of the failed run #N” or “Configuration of the cancelled run #N”; the button that opens it reads Show configuration of this run. The code stays until you click Clear.
- Only the most recent run has code. There is none while a run is in progress. After Clear (or Clear results for a finished run) or a new run, the old run’s code is gone and its button disappears. A button left over in another tab opens a dialog titled “No recorded run”. Run numbers restart at 1 with every GUI session, so file and folder names carry the start time as well.
- Changed form. If the current form no longer resolves to the run’s options, a note says that the code reproduces the run, not the form, and suggests Preview. Output paths are left out of this comparison.
What the script contains
The script has six parts, in this order. The excerpt is the run from Save a run and run it again, trimmed where marked.
- Header. The pyfds-evac version and git commit, with “(with uncommitted changes)” when the checkout was dirty; the commit alone then does not identify the code. Then “Run #N, started UTC time” or “Preview”, the scenario name, and the notices shown in the dialog.
PATHS.SCENARIO,FDS_DIRandVIS_CACHEare absolute paths on the machine that ran the GUI.OUTPUT_DIRis a new relative folder,pyfds_evac_<scenario>_run<N>_<start time>_outputorpyfds_evac_<scenario>_preview_output, resolved against the folder the script is launched from.OPTIONS. Every resolved option, one per line, exactly as the GUI hands it tobuild_run_kwargs.- The run. A
Namespaceis rebuilt fromOPTIONSandPATHS; the script then callsload_scenario,build_run_kwargs(pyfds_evac/core/run_config.py) andrun_scenario. There is no second option mapping, so the script cannot drift from the GUI. - After the run. A finished or stopped message. The trajectory SQLite is
copied to
OUTPUT_DIR/trajectory.sqlitewith its manifest, andresult.cleanup()removes the temporary copiesrun_scenariowrote. - CLI-only keys.
cleanup,export_only,inspect_fdsandprint_summaryappear inOPTIONS, butbuild_run_kwargsignores them.'cleanup': Falsenext toresult.cleanup()is expected, not a bug.
# Generated by the pyFDS-Evac GUI.
# pyfds-evac 0.1.0, commit ba4ade3041930519187984ba052ca02f0add0485.
# Run #1, started 2026-09-29T18:20:09+00:00.
# Scenario: blind_spawn_discovery
# ... notices ...
import argparse
import pathlib
import shutil
from pyfds_evac.core import load_scenario, run_scenario
from pyfds_evac.core.manifest import manifest_path_for
from pyfds_evac.core.run_config import build_run_kwargs
# PATHS: from the computer that ran the GUI. Edit them on another machine.
SCENARIO = '/.../pyFDS-Evac/assets/blind_spawn_discovery' # path shortened
FDS_DIR = None
VIS_CACHE = None
# A new folder: the script never overwrites the GUI run's files.
OUTPUT_DIR = pathlib.Path('pyfds_evac_blind_spawn_discovery_run1_20260929T182009Z_output')
# Settings: the resolved configuration the GUI passes to build_run_kwargs.
OPTIONS = {
'seed': 1301, # seed used by run #1
'cleanup': False,
'clear_air_visibility': False,
'collect_route_cost_history': True,
# ... remaining options, sorted by name ...
'output_sqlite': None,
'print_summary': False,
# ...
}
opts = argparse.Namespace(
**OPTIONS,
scenario=SCENARIO,
fds_dir=FDS_DIR,
vis_cache=VIS_CACHE,
)
scenario = load_scenario(SCENARIO)
run_kwargs = build_run_kwargs(scenario, opts, log=print)
result = run_scenario(scenario, **run_kwargs)
# ... finished / stopped message, then copy the trajectory SQLite
# and its manifest to OUTPUT_DIR ...
result.cleanup() # remove the temporary copies run_scenario wroteHow the seed is written
The seed line in OPTIONS carries a comment that says where its value
comes from. None means the scenario’s baseSeed, which is 42 when the
scenario sets none (Usage).
- Run code, the run reported the seed expected at submission: that seed,
with
# seed used by run #N. - Run code, seed box left blank and no seed confirmed (for example a
failed or cancelled run):
None, with# seed not recorded for this run; None = the scenario's baseSeed. - Run code, seed typed in but not confirmed (for example a failed or
cancelled run): the submitted value, with
# seed submitted; the seed used was not recorded. - Preview, seed box blank:
None, with# None = the scenario's baseSeed (<value>), for example(1301)forblind_spawn_discovery.
What the script leaves out, and why
- The GUI’s CSV histories and app bundle. The smoke, FED, route and
route-cost histories and the bundle are written by
apply_outputs, which lives inrun.py, outside the installed package. Their keys (output_smoke_history,output_fed_history,output_route_history,output_route_cost_history,output_sqlite,export_app_bundle) are set toNone(#328). Route-cost history collection stays on (collect_route_cost_history), as in the GUI. - The GUI’s progress callback. It only drives the GUI’s live view. The model’s own progress lines are still printed.
Evidence: the equivalence test
test_exported_script_reproduces_the_gui_run in
tests/test_webapp.py runs a GUI run and its exported script in two fresh
interpreters, so each is the first run of its process. For one scenario,
blind_spawn_discovery (baseSeed 1301), the two match on total, evacuated
and remaining agents, evacuation time and seed. It covers that scenario only.
