Skip to content

GUI post-processing: a result file to the plot, the schematic and the live plot

What happens to a result HDF5 between the solver writing it and a curve or a coloured pipe appearing: h5_reader turns the file into a RunResult, the run model hands it to the main window, and the main window gives it to the plot dock and the network view. Each step has its own answer to a missing group, an empty file and a run that is still writing. The contracts of every function below are in their docstrings (the standard is Function contracts and flow charts).

Units are the file's, nothing is converted on the way: Pa, K, kg/m^3, m/s, kg/kg, W/m^3, metres, seconds. The plot dock converts only the x axis of a time series, seconds to crank angle in degrees (theta0 + omega t in rad, shown in degrees, minus the case's TDC offset). Temperature is the solver's own Pipe.temperature_cells() where the file stores it (written since 2026-09-28). A traced row with no stored T is shown as p / (rho R) with each cell's R taken from the stored final temperature (traced_gas_constant, since 2026-10-11): exact for a constant R and at the final frame, approximate at earlier times on a mixture or real-gas pipe. An older file has no stored T at all; both views then use the file's single R, which is wrong for a real-gas or mixture pipe. A cell with rho <= 0 shows NaN.

From the file to a RunResult

What enters: a path (the run's output, or Load Results). What leaves: a RunResult, or an exception the caller turns into a message. The live view re-reads the same file while the solver appends to it: write_hdf5 resizes the datasets and writes time_series/t last, so t is the commit marker.

flowchart TD
    F["result HDF5, written by write_hdf5"] --> L{"load_now or _poll_tick"}
    L -- "Load Results, Reload, auto-load after a run" --> LN["load_now: set path, load_run"]
    L -- "timer, file size or mtime moved" --> PT["_poll_tick: load_run_incremental"]
    LN --> LR{"load_run opens the file"}
    LR -- "missing, folder, empty, truncated, locked" --> X1["FileNotFoundError or OSError"]
    LR --> FS["_read_final_state: pipes and junctions"]
    FS -- "pipe without x, rho, u or p" --> X2["KeyError naming the dataset"]
    FS --> TS{"time_series group with a t dataset?"}
    TS -- "no group, or no t: still being created" --> OUT0["RunResult, time_series None: final state only"]
    TS -- yes --> PS["_pipe_series and _junction_series, partitioned by _junction_partition"]
    PS -- "traced pipe without rho, u or p" --> X2
    PS --> OUT["RunResult with a trace"]
    X1 --> SHOW["Load Results and Reload: a box naming the file; auto-load: a console line"]
    X2 --> SHOW
    PT --> INC{"load_run_incremental: previous run, length of t, _cached_tail_matches"}
    INC -- "no previous run, shorter t, or a different tail value: a new run overwrote the file" --> LR
    INC -- "file locked or half written" --> SWALLOW["swallowed: the next tick tries again"]
    INC --> TAIL["cached arrays plus rows n_read onward"]
    TAIL -- "a pipe, junction or field appeared or vanished, or a length disagrees with t" --> LR
    TAIL --> OUT
    OUT0 --> H["RunModel emits runLoaded"]
    OUT --> H
    H --> W["_on_run_loaded, one guard per dock"]

A trace caught between the dataset resize and the t resize comes back with pipe arrays longer than t. Nothing clips them: the profile still draws (it indexes by the slider), the time-series plot raises, and the next tick repairs it. A trace with no samples is treated as untraced by both views.

What the plot dock draws

What enters: the run, the case, the layout (probes), the selection and the mode buttons. What leaves: one matplotlib figure and the data behind it for the CSV, PNG and audio exports. Every branch that cannot draw says why on the canvas instead of raising.

flowchart TD
    S["set_run, select_component, set_case, a toolbar signal"] --> R{"_refresh"}
    R -- "a BC is selected" --> BC["_render_bc: schedules before any run, end cell after"]
    R -- "no run or nothing selected" --> M1["message: Run a case + select a component to plot"]
    R -- "pipe" --> P{"_render_pipe"}
    R -- "junction" --> J["_render_junction: trace, else the final value as text"]
    R -- "cylinder" --> C["_render_cylinder: trace, else the final value as text"]
    R -- "another kind" --> M2["message: No plot for kind"]
    P -- "pipe not in the run" --> M3["message: Pipe not in run output"]
    P -- "profile mode" --> PR["_render_pipe_profile: var(x) at the slider time"]
    P -- "time series, untraced" --> M4["message: Time series requires --trace-dt"]
    P -- "time series" --> TSR["_render_pipe_timeseries: var(t) or var(theta) per probe"]
    PR --> Y{"_pipe_profile_y"}
    Y -- "variable absent" --> M5["message: Variable missing for this pipe"]
    Y --> AX["axes: pad a constant range, species at least 0, Fix Y lock, no offset notation"]
    TSR --> TA{"_timeseries_at_frac"}
    TA -- "no cells or variable not traced" --> M5
    TA --> XK{"_x_axis_kind"}
    XK -- "crank angle and a crank" --> CAD["_t_to_cad, then _apply_cad_range: a reversed range, or one bound past the data, is dropped: axis back to auto, log warning"]
    XK --> AX
    CAD --> AX
    AX --> EX["_stash_payload: PNG, CSV and audio exports on"]
    M1 --> D["_draw_message: no ticks, exports off"]
    M2 --> D
    M3 --> D
    M4 --> D
    M5 --> D

An exception from a renderer is not caught in the dock. From a Qt signal Qt prints it and the plot keeps its last picture; from set_run it reaches _on_run_loaded, which logs it as a warning and goes on to the network view.

What the network view does with a run

What enters: a run and the case Layout, on every live poll (about every half second during a run). What leaves: actors in a PyVista plotter. The actors are rebuilt only when the topology key changes; a poll with new samples only rewrites the colour scalars.

flowchart TD
    A["show_run: run, layout"] --> G{"plotter torn down and dock hidden?"}
    G -- yes --> KEEP["keep the run, build nothing: showEvent replays it"]
    G -- no --> K{"_topology_key_for changed, or no actors?"}
    K -- yes --> B["_clear_actors, _compute_chain_packed_positions, _build_pipe_actors, _build_cylinder_actors"]
    K -- "no: same pipes, cells, cylinders, layout revision" --> V["_refresh_var_combo"]
    B --> V
    V --> SL{"_sync_slider_and_colours"}
    SL -- "run is None" --> BL["slider off, readout blank, legend removed"]
    SL -- "trace of two or more samples" --> ON["_on_slider: readout, sample_changed, refresh_colours, pistons"]
    SL -- "no trace, one sample, or a trace with no samples" --> UT["readout t_final, refresh_colours from the final state"]
    ON --> RC["refresh_colours"]
    UT --> RC
    RC --> SV["_sample_variable per pipe: trace row at the slider, else the final state"]
    SV -- "species the pipe lacks" --> NAN["NaN, drawn in the no-data colour"]
    SV --> RNG{"colour range"}
    RNG -- "Fix Y" --> FR["_fixed_range_for: finite values over the whole trace"]
    RNG -- "per frame" --> PF["finite min and max, a constant widened by 1"]
    FR --> W["_write_scalars into the kept meshes, one legend for all pipes"]
    PF --> W

The chain packer (_pack_rows_horizontally, _propagate_x_topologically, _rank_rows_vertically, _place_cylinders) positions pipes by length, not by the editor's slots: a pipe with no length in the file takes no width, a pipe missing from the layout falls back to a row stack, and an empty case places nothing and draws nothing.

The live-plot dock (LivePlotDock) is not part of this flow. It does not read a file: a caller registers a probe function and passes on_step to the solver loop, and nothing in the main window does that today.

Functions

  • aeolus1d_gui/model.py::RunModel.load_now
  • aeolus1d_gui/model.py::RunModel._poll_tick
  • aeolus1d_gui/app.py::MainWindow._on_run_loaded
  • aeolus1d_gui/app.py::MainWindow._auto_load_last_run
  • aeolus1d_gui/app.py::MainWindow._reload_last_run
  • aeolus1d_gui/post/h5_reader.py::load_run
  • aeolus1d_gui/post/h5_reader.py::load_run_incremental
  • aeolus1d_gui/post/h5_reader.py::_read_final_state
  • aeolus1d_gui/post/h5_reader.py::_pipe_series
  • aeolus1d_gui/post/h5_reader.py::_junction_series
  • aeolus1d_gui/post/h5_reader.py::_junction_partition
  • aeolus1d_gui/post/h5_reader.py::_cached_tail_matches
  • aeolus1d_gui/post/h5_reader.py::variable_names
  • aeolus1d_gui/post/h5_reader.py::derived_T
  • aeolus1d_gui/post/h5_reader.py::pipe_T
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock.set_run
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._refresh
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._render_bc
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._render_pipe
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._render_pipe_profile
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._render_pipe_timeseries
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._render_junction
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._render_cylinder
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._pipe_profile_y
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._timeseries_at_frac
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._x_axis_kind
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._t_to_cad
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._apply_cad_range
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._stash_payload
  • aeolus1d_gui/post/plot_dock.py::Plot2DDock._draw_message
  • aeolus1d_gui/post/network_view.py::NetworkViewDock.show_run
  • aeolus1d_gui/post/network_view.py::NetworkViewDock.showEvent
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._topology_key_for
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._clear_actors
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._compute_chain_packed_positions
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._pack_rows_horizontally
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._propagate_x_topologically
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._rank_rows_vertically
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._place_cylinders
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._build_pipe_actors
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._build_cylinder_actors
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._refresh_var_combo
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._sync_slider_and_colours
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._on_slider
  • aeolus1d_gui/post/network_view.py::NetworkViewDock.refresh_colours
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._sample_variable
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._fixed_range_for
  • aeolus1d_gui/post/network_view.py::NetworkViewDock._write_scalars
  • aeolus1d_gui/post/live_plot_dock.py::LivePlotDock.on_step