GUI run: Run pressed to results¶
What the runner dock does from the moment a user presses Run (F5, the Run button) to the moment the result is on screen: the checks that can stop a run before anything starts, the subprocess, how its output reaches the console, the badges and the progress bar, and how Pause, Resume, Cancel and closing the window act on a run in flight. The contracts of every function below are in their docstrings (the standard is Function contracts and flow charts).
One fact shapes every chart. The solver does not run in the GUI. action_run
saves the case to a folder, and QProcessRunner starts
python -m aeolus1d.cli.main run <case.toml> ... as a child process (a
QProcess; no thread, no multiprocessing). Everything the dock shows is parsed
from that child's text output, and Pause, Resume and CFL changes reach it through
a small JSON file it polls. A crash in the solver therefore cannot take the GUI
down, and a run cannot be inspected beyond what it prints. The one place the
solver does run on the GUI thread is the Engine Wizard's "Run 3 cycles": it runs
on the GUI thread but processes events every 50 ms, so the window repaints,
shows progress and can be cancelled (until 2026-10-11 it froze the window for
22 s at the defaults); it is not part of this flow.
Before anything starts¶
What enters: a click or F5 on act_run (the dock's Run button emits
run_clicked, which the window wires to the same slot). What leaves: either a
message and no process, or one launched child.
flowchart TD
IN["F5 / Run button"] --> AR{"action_run"}
AR -- "a run is live" --> M1["status bar: A run is already active (2.5 s)"]
AR -- "no pipes" --> D1["warning dialog: Nothing to run"]
AR --> SC{"_scheme_compatibility_error: validate_case_pre_run, with the resume and step-cap options"}
SC -- "refused" --> D2["critical dialog: Scheme not compatible, the validator's own text"]
SC --> RR{"run root mkdir, then run folder mkdir"}
RR -- "OSError" --> D3["critical dialog: Run output directory unavailable"]
RR --> ST{"resolve_relative_csv_paths_for_staged_run, then save_case to case.toml"}
ST -- "any exception" --> D4["critical dialog: Save failed"]
ST --> PF{"load_case on the staged file"}
PF -- "CaseValidationError" --> H1["red halos on the canvas; console cleared; the issues in red; status bar: Run aborted"]
PF -- "ValueError or OSError" --> H2["_report_staged_load_failure: red console line and status bar"]
PF --> GO["halos cleared; console cleared; badges reset; Running and Output lines"]
GO --> RS{"resume: box ticked AND a previous out.h5 exists?"}
RS -- "ticked, file gone" --> N1["console note: starting from t = 0"]
RS --> LC["fresh live_control.json path, revision 0; run model repointed at the new out.h5"]
LC --> START["QProcessRunner.start"]
Every refusal before start is visible: a dialog, or a console line with a
status-bar message. The run folder is <run root>/<case name>_<timestamp>,
with _2, _3, ... when a second run starts in the same second; the case name
is reduced to [A-Za-z0-9._-], so a name such as ../x cannot leave the root.
A staging or validation failure removes the folder; a parse failure of the
staged file keeps it as evidence (both since 2026-10-11). The pre-run load_case is the same check the subprocess would make,
done in-process on the exact staged file so a case that is going to fail does
not cost a process launch. A second Run while one is live is refused by the
first check; the Run button and act_run are also disabled once the process has
started, but between the click and the started signal only that first check
stands, and it reads the process state, which is already "starting".
The subprocess and the dock¶
flowchart TD
START["QProcessRunner.start"] --> BUSY{"already running?"}
BUSY -- yes --> X1["RuntimeError: cancel first"]
BUSY --> ARGS["argv: run case.toml, --out, --live-control, --progress, plus --trace-dt and --live-flush-interval 0.5 (trace on), --max-steps, --info-every-n, --resume when set"]
ARGS --> QP["QProcess.start"]
QP -- "FailedToStart" --> ER["_on_error: red line, finished(-1, not cancelled)"]
QP -- "started" --> RUN["_on_run_started: set_running_state(True); Run off, Cancel on"]
RUN --> OUT["child stdout and stderr"]
OUT --> DEC["_drain_stdout / _drain_stderr: one incremental UTF-8 decoder per stream"]
DEC -- "stdout chunk" --> AO["RunnerDock.append_stdout"]
DEC -- "stderr chunk" --> AE["RunnerDock.append_stderr (not scanned)"]
AO --> CON["console, 5000-block ring"]
AO --> SP["_scan_for_picard: Picard N/M, red when saturated"]
AO --> SN["_scan_for_newton: sum N"]
AO --> SG{"_scan_for_progress: t = X / Y, whole lines only"}
SG -- "Y not > 0, or X or Y not finite" --> IG["line ignored"]
SG --> BAR["progress bar, thousandths, 100 % at the end"]
OUT --> FIN["child exits: _on_finished drains the rest, finished(exit code, cancelled)"]
ER --> RF
FIN --> RF{"_on_run_finished"}
RF -- "cancelled" --> S1["status: cancelled"]
RF -- "exit 0" --> S2["status: done (exit 0)"]
RF -- "other exit" --> S3["status: failed (exit N)"]
S1 --> AL["_auto_load_last_run"]
S2 --> AL
S3 --> AL
AL --> RES["run model loads out.h5 if it exists; every post-processing dock refreshes"]
The dock holds an unfinished last line until its newline arrives, so the
scanners only see whole lines (until 2026-10-11 a t = X / Y line cut in two by
the pipe was read as its prefix). Two limits are worth knowing before trusting
the dock. The console keeps only its last 5000 lines. And a run that never
reaches the solver (a failed start) reports failed (exit -1), with the reason
in red.
With a trace interval set, the child rewrites out.h5 every 0.5 s of wall time,
so the plot docks update while the run is live. A cancelled or failed run still
loads whatever was written.
Pause, Resume, Cancel and closing the window¶
flowchart TD
PB["Pause button"] --> PR{"RunnerDock._on_pause_resume_clicked"}
PR -- "not paused" --> AP{"action_pause_run"}
PR -- "paused" --> ARS{"action_resume_run"}
AP -- "no live run, or no control file" --> N0["nothing, or a red note"]
AP --> WP{"write_control_update(paused=True, next revision)"}
WP -- "OSError" --> E1["red console line: pause failed, label stays Pause"]
WP --> P1["set_paused_state(True): button reads Resume; status paused"]
ARS --> SC2{"_scheme_compatibility_error on the case CFL"}
SC2 -- "refused" --> D5["critical dialog: Setting not compatible"]
SC2 --> WR{"write_control_update(cfl, adaptive_cfl_max, paused=False)"}
WR -- "OSError" --> E2["red console line: resume failed, label stays Resume"]
WR --> P2["set_paused_state(False): status running"]
CB["Cancel button or Esc"] --> AC{"action_cancel_run"}
AC -- "a run is live" --> CN["QProcessRunner.cancel: SIGTERM, return at once; a 2 s timer kills the child if it is still running"]
CN --> FIN2["child exits: finished(exit, cancelled = True) as in the second chart"]
CL["window closed"] --> CE{"closeEvent: a run is live?"}
CE -- "no" --> UC
CE -- "yes" --> Q{"question: A solve is still running. Stop it and quit?"}
Q -- "No (the default)" --> KEEP["event ignored, window and run stay"]
Q -- "Yes" --> UC{"dirty? Discard unsaved changes?"}
UC -- "No" --> KEEP
UC -- "yes, or clean" --> CN2["runner.cancel() with exceptions suppressed; the window closes"]
write_control_update writes <file>.tmp and renames it, so the solver never
reads half a file. It is the only thing the GUI does to a live run: the child
reads the file every ten iterations, applies a new revision once, and blocks
while paused is true. The GUI catches OSError around the write and shows it
in the console; nothing else can be raised from it for the inputs the window
passes. Cancel is a SIGTERM: the solver installs a handler that makes it
checkpoint the HDF5 and stop at its next poll, and only after 2 s of silence is
it killed, so a cancelled run can normally be resumed. cancel returns at
once; the kill is a timer, so the window stays responsive. When the application
quits inside the grace window, the runner waits for the rest of it (until
2026-10-11 cancel blocked the GUI thread for up to the 2 s).
Known gaps in this flow¶
Each is pinned by a ..._known_gap or ..._known_defect test and listed in the
batch 5 report.
- ~~A menu or toolbar click on Delete, Fit view, Reset view or Probe mode is
dropped while the cursor is in a spin box, text field or table.~~ Fixed
2026-10-09: the guard now drops only the key shortcut (
QEvent.Shortcut), never a click (shell/actions.py,_SwallowShortcutWhileTyping). - ~~A progress line cut between two reads is parsed as its prefix.~~ Fixed
2026-10-11: the dock holds the unfinished line until its newline
(
RunnerDock.append_stdout). - The Engine Wizard's three-cycle run still runs on the GUI thread (since 2026-10-11 it processes events, shows progress and can be cancelled); two open wizards can nest their runs.
Functions¶
aeolus1d_gui/app.py::MainWindow.action_runaeolus1d_gui/app.py::_scheme_compatibility_erroraeolus1d_gui/io/case_io.py::resolve_relative_csv_paths_for_staged_runaeolus1d_gui/io/case_io.py::save_casesrc/aeolus1d/io/parsers.py::load_caseaeolus1d_gui/app.py::MainWindow._set_validation_warningsaeolus1d_gui/app.py::MainWindow._report_staged_load_failureaeolus1d_gui/runners/qprocess_runner.py::QProcessRunner.startaeolus1d_gui/runners/qprocess_runner.py::QProcessRunner._on_erroraeolus1d_gui/runners/qprocess_runner.py::QProcessRunner._drain_stdoutaeolus1d_gui/runners/qprocess_runner.py::QProcessRunner._drain_stderraeolus1d_gui/runners/qprocess_runner.py::QProcessRunner._on_finishedaeolus1d_gui/runners/qprocess_runner.py::QProcessRunner.cancelaeolus1d_gui/shell/runner_dock.py::RunnerDock.append_stdoutaeolus1d_gui/shell/runner_dock.py::RunnerDock.append_stderraeolus1d_gui/shell/runner_dock.py::RunnerDock._scan_for_picardaeolus1d_gui/shell/runner_dock.py::RunnerDock._scan_for_newtonaeolus1d_gui/shell/runner_dock.py::RunnerDock._scan_for_progressaeolus1d_gui/shell/runner_dock.py::RunnerDock.reset_picard_badgeaeolus1d_gui/shell/runner_dock.py::RunnerDock.set_running_stateaeolus1d_gui/shell/runner_dock.py::RunnerDock.set_statusaeolus1d_gui/shell/runner_dock.py::RunnerDock.set_paused_stateaeolus1d_gui/shell/runner_dock.py::RunnerDock._on_pause_resume_clickedaeolus1d_gui/app.py::MainWindow._on_run_startedaeolus1d_gui/app.py::MainWindow._on_run_finishedaeolus1d_gui/app.py::MainWindow._auto_load_last_runaeolus1d_gui/app.py::MainWindow.action_pause_runaeolus1d_gui/app.py::MainWindow.action_resume_runaeolus1d_gui/app.py::MainWindow.action_cancel_runaeolus1d_gui/app.py::MainWindow.closeEventsrc/aeolus1d/runtime/live_control.py::write_control_updateaeolus1d_gui/shell/actions.py::_SwallowShortcutWhileTyping.eventFilter