GUI¶
aeolus1d_gui is a PySide6 + PyVista visual editor + post-processor
for aeolus1d cases. It reuses the same aeolus1d.io.schema dataclasses
as the solver so the editor never holds a separate model — what
you see on the canvas IS the spec the CLI runs.
Launch¶
./aeolus1d_gui/run_gui.sh
# or
python -m aeolus1d_gui
# or, if installed with [gui] extra:
aeolus1d-gui
On first launch a Welcome dialog walks through the four panels. Help → Quick Start (F1) reopens it later.
The four panels¶
Palette (left dock) — every component aeolus1d supports, grouped
by category (aeolus1d_gui/shell/palette.py):
- Geometry: Pipe, Plenum Junction, Corberán Junction, EHR Junction
- Boundaries: a single generic Boundary Condition button. Drop
it on (or near) a pipe end and pick the kind in the Inspector — the
schema accepts ~13 kinds (wall / transmissive / stagnation / mass-
flow / velocity inlets, pressure outlet, transient variants,
non-reflecting LODI, and the two valve kinds); the drop defaults to
walland every kind is editable to any other after drop, so a button-per-kind was collapsed into one. - Diagnostics: Probe — a time-series probe at a fractional position
along a pipe. Drag it onto a pipe block and the drop's x-fraction is
captured automatically; the Plot dock turns it into a
var(t)curve once the run is traced (--trace-dt). - Engine: Cylinder, Compressor, Turbine, Shaft
Click a tile to add the component at the canvas centre, or drag it onto the canvas to drop at the cursor.
Canvas (centre) — every component renders as an extruded 2D silhouette specific to its kind (pipe = horizontal capsule, junction = diamond, cylinder = vertical pill, BC = chevron, shaft = thin bar, compressor / turbine = bowtie), coloured by family and centre-stamped with a single-letter glyph (P/J/C/B/S/K/T) so the kind reads at a glance even when zoomed out. Lighter-tinted port spheres on each block edge are the connect points; a soft drop shadow sits below every block for depth, and a dark outline traces the silhouette boundary for crisp definition against the canvas background.
Several decorations encode spec values directly on the block:
- Pipe
n_cells→ vertical tick marks across the top of the capsule (capped at 10). - Pipe with a
[catalyst]block → green corner badge (top-right). - Pipe with a
[filter]/[dpf]block → grey corner badge (top-left).
Connection lines are smooth cubic beziers in the source node's
family colour, with a cone arrowhead at the midpoint pointing
along physical flow direction (aeolus1d convention: pipe left =
upstream, right = downstream). An opt-in flow animation
(Preferences → Canvas → "Animate flow on edges", default off)
walks a small cream / amber dot along each connection at ~30 fps
when you want a live indication of which way gas is going.
Hover any node to pop a small tooltip with the component id +
2–3 of its key spec values (pipe length + n_cells + catalyst /
filter presence, junction kind + volume, cylinder bore × stroke +
ω, BC kind + end, turbo kind + vane).
The view is locked to top-down 2D with a subtle gradient
background (theme-tinted), a minor/major XY grid overlay, and an
aeolus1d · 2D editor watermark in the lower-left corner;
middle-drag pans, right-drag and the mouse wheel zoom, rotation
is disabled.
Inspector (right dock) — when a component is selected, a Qt
form auto-generates from its dataclass schema (Literal → combo,
nested dataclass → group, list[float] → comma-separated text). Edits
write back into the live spec; the case is marked dirty and
connections re-render to track field changes (e.g. swapping a
pipe's length). The right column also tabs in a stack of docks: a
Component-list dock for select-by-name, a Network View, a 2D Plot
dock with Fix-Y lock + probe overlay + PNG/CSV export, a Modes dock
(FEM eigenfrequencies), a Phase-44 Optimize dock (described below), a
Sensitivity dock (parameter ranking that pairs with Optimize), a
CFL analyzer dock, a Case Settings dock (the full run-settings
form, mirrored with the runner-dock quick-strip; raise it with
Ctrl+E), a Turbo-map dock (renders the [[turbo]] compressor /
turbine performance maps), a read-only TOML preview dock, and
a Solver Internals dock (Phase B17-gui / B17-gui-live) showing
a per-pipe table of closure-Jacobian routing (analytic ✓ vs FD)
across friction / heat / porous. The dock has a "Refresh with
built network" button for opt-in live mode that scans
pipe.py_friction / py_heat / py_porous overrides post-
build_network and forces those cells to FD; structural edits
reset the dock to spec mode automatically.
Basic vs Advanced settings¶
Several schema dataclasses carry dozens of knobs the typical
case never touches (chemistry tolerances, implicit-solver inner
loops, QS-driver tuning, adaptive-Δt deadbands, …). Rendering
every field flat overwhelms the Inspector on a fresh case, so
the schema declares a BASIC_FIELDS class-var on each adopter
and the form factory routes anything outside that set into a
collapsed "▶ Advanced settings…" expander below the basic
rows. Clicking the expander reveals the rest; the arrow flips
to ▼ so the state reads at a glance. The split is purely a
presentation layer — see aeolus1d_gui/shell/form_factory.py
(lines 486–532, 671–768) for the implementation and
src/aeolus1d/io/schema.py for the per-dataclass BASIC_FIELDS
frozensets.
Important: the TOML on disk is untouched. Every CaseHeader
field still round-trips through the strict TOML loader and
aeolus1d expand writes every field at the top level regardless
of whether it's marked basic or advanced. The split only
changes which rows the GUI shows up-front; loading a case
written by hand, by the CLI, or by an older GUI build works
exactly as before.
Adopters as of MR !132 / !135 / !136:
| Dataclass | Basic fields | Advanced (hidden) |
|---|---|---|
CaseHeader |
10 | ~56 |
PipeSpec |
5 | 11 |
JunctionSpec |
(kind + core) | rest |
BCSpec |
(kind + core) | rest |
FrictionSpec |
(kind + core) | rest |
HeatSpec |
(kind + core) | rest |
PorousSpec |
(kind + core) | rest |
CatalystSpec |
(kind + core) | rest |
FilterSpec |
(kind + core) | rest |
UreaSpec |
(kind + core) | rest |
SpeciesSpec |
(kind + core) | rest |
EOSSpec |
(kind + core) | rest |
ThermalWallSpec |
(kind + core) | rest |
CaseHeader's 10 basic knobs are the ones that change run to
run on a typical case: name, t_end, cfl, scheme,
limiter, rk, solver, adaptive_dt, dt_fixed,
chem_solver (see CaseHeader.BASIC_FIELDS in
src/aeolus1d/io/schema.py lines 35–41). PipeSpec's 5 are the
ones that define a pipe at all: id, length, n_cells,
diameter, init. The 9 nested closure specs each expose
their kind discriminator + the handful of routine knobs
that go with that kind; everything else (model-specific
correlation coefficients, alternative formulations, debug
toggles) lives under Advanced. The form factory's kind
dispatch — re-rendering when you change friction.kind from
darcy to colebrook, for instance — replays the
BASIC/Advanced split for the newly selected kind on every
swap.
For the actual semantics of each field — defaults, units,
which combinations are legal, when to reach for an advanced
knob — see the run-settings reference (the case_settings
chapter in this doc tree, plus implicit-solver-reference.md
for the Newton / Picard knobs and cli.md for the
chemistry / QS-driver flags). The Inspector and the reference
docs share src/aeolus1d/io/schema.py as the single source of
truth.
Runner (bottom dock) — Run / Cancel plus Pause / Resume buttons,
a "Resume from latest saved time step" checkbox (passes
--resume <last_h5> to the next run when a prior HDF5 exists),
a run-settings strip (t_end / cfl / trace-dt / scheme /
limiter / RK integrator combos, plus adaptive-Δt / CFL-ramp / HLLC
low-Mach / chem-interval knobs that write back into case.case on
edit), and a dark console. Pressing F5 (or Run) saves the case to a
temp directory, launches aeolus1d run as a subprocess, and streams
stdout (grey) / stderr (red) into the console. A pre-flight check
(_scheme_compatibility_error → validate_case_pre_run) rejects
known incompatible combinations up front instead of letting the
subprocess crash — e.g. an explicit RK scheme with cfl > 1, which
exceeds the ~1.0 acoustic-CFL ceiling and would produce a vacuum
cell after a few hundred steps. (MP5 now covers every closure family
and the network timeloop, so it is no longer flagged.) On a clean
exit (code 0), the
post-processing tab automatically loads the output HDF5 and renders
each pipe as a coloured tube, sharing a single colour scale across
all pipes.
View → Panels — the togglable right-pane docks (Inspector,
Network View, 2D Plot, Modes, Optimize, Sensitivity, CFL, Solver
Internals, Case Settings, Turbo-map, TOML — see _right_pane_panels
in aeolus1d_gui/app.py) can each be shown or hidden individually from
the View → Panels submenu, one checkable entry per dock. Each entry
is the dock's own toggleViewAction(). Closing a dock with its window ×
unchecks it here, and re-checking brings it back. The hidden set
persists across sessions, so a layout you pare down to just Inspector +
Plot reopens the same way next launch.
View → Show canvas — unchecked, it hides the editor canvas (the
central view) and the docks take the whole window, which gives a plot
or the network view the full screen. The case, the selection and the
camera are kept while the canvas is hidden; re-check the entry to bring it
back. The choice persists across sessions (canvas_visible in the GUI's
QSettings), like Snap to grid. The shortcut is Ctrl+Shift+C, and the
toolbar's Canvas check box does the same. While the canvas is hidden,
Fit view, Reset view, Probe mode and Organize are greyed out.
Id fields are drop-downs. A field that names another component is picked from a list, not typed: the pipe of a BC, port, cylinder port or probe; a turbo's inlet and outlet pipe and its shaft; and a controller's pipe fields, its turbine (turbines only) and its cylinder. An optional reference (a turbo's shaft, the controller fields) has a blank entry that unsets it. A reference to a component that no longer exists is shown and kept, so an old case loads unchanged.
The three docks this submenu does not list — the Palette (left), the
Component list, and the Runner (bottom) — have no × on their title bar.
A panel is closable only if something can re-open it; those had no menu
entry, so closing one was a one-way door that saveState then carried
into every later session. They are still movable and floatable, just not
closable. _lock_unrecoverable_panels derives that set from
_right_pane_panels, so a dock added later stays un-closable until it is
registered in the submenu.
Interactions¶
| Action | Shortcut |
|---|---|
| New case | Ctrl+N |
| Open / Save / Save As | Ctrl+O / Ctrl+S / Ctrl+Shift+S |
| Open Recent → submenu | (lazy) |
| Add Pipe | Ctrl+P |
| Delete selected | Del |
| Undo / Redo | Ctrl+Z / Ctrl+Shift+Z |
| Run / Cancel | F5 / Esc |
| Fit view | F |
| Quick Start | F1 |
| Preferences | Ctrl+, |
| Quit | Ctrl+Q |
Drag a yellow port sphere to a compatible port on another component to wire them; the rubber-band line turns red on illegal targets. The connection rules:
- Pipe ends connect to BC
target, junctionin, turboinlet/outlet, cylinder ports. - Shaft
portconnects to compressor / turbineshaft. - A pipe end can host at most one consumer (single-consumer rule).
Importers¶
File → Import wraps the third-party case importers as menu actions, so you can bring a foreign model onto the canvas without touching the CLI:
- AVL Cruise M (
.proj) — aftertreatment-only models map cleanly; vehicle / hybrid models report what they dropped. - AVL Boost (
.bws) — best-effort structural import. - GT-Power SIF (
.xml) — GT-Power Standard Input File. - GT-Suite (
.gtm) — the native project file (distinct from the legacy SIF export); resolves sibling.gtsubsub-assemblies recursively so engine templates (cylinder / compressor / turbine / shaft / injector) materialise as aeolus1d components. - Ricardo WAVE (
.wvm).
Opening a [vehicle] drive-cycle TOML (e.g. 85_vehicle_drive_cycle)
is a special case: instead of loading it onto the pipe-network canvas
the GUI detects the vehicle schema (aeolus1d.io.vehicle_case) and pops
a dedicated Vehicle case dialog for editing the [vehicle] /
[driver] / driveline blocks.
Schedule fields and CSV / Parquet import¶
Several boundary conditions carry piecewise-linear schedules
— (t, value) pairs the solver interpolates at runtime. Examples:
mdot_t / mdot_v on a transient mass-flow inlet,
p_back_t / p_back_v on a transient pressure outlet, and
Y_t / Y_v for a time-varying inlet species concentration.
The Inspector renders each detected pair as a single two-column
table (t, value) with row +/− buttons — no need to type
comma-separated lists. Two import options live above the table:
- Load CSV / Parquet… on each schedule widget — pick one file and one column for the time + value. The picker dialog shows a preview of the first 5 rows so you can sanity-check the parse. Bad rows (lines that don't parse as numbers) are dropped with a warning that lists the offending line numbers.
- Load all schedules from CSV / Parquet… appears at the top of the inspector panel when a BC has 2+ schedule pairs (e.g. the transient mass-flow inlet with NH3 + NO species schedules). One click maps every schedule to a column in the same multi-column file — useful for fitting against measured experimental data where ṁ, T₀, Y_NH3, Y_NO arrive as a single CSV / Parquet log.
Parquet (.parquet / .pq) is read via pyarrow — gas-
analyser exports often use Parquet for efficient compression of
long time-series. Install pyarrow (or the [gui] extras
group) to enable it.
Optimize dock (Phase 44)¶
The right column has an Optimize tab that drives the
aeolus1d optimize subprocess. Workflow:
- Load TOML — pick a case file with an
[optimize]block (see the CLI reference for the schema). The dock populates two tables: parameters (path/theta0/lo/hi) and probes (pipe/x_frac/var/metric/reference_csv). Both are inline-editable. - Edit — type in the cells,
+ Add/− Removerows, Browse CSV… to pick a reference file (stored relative to the loaded TOML when possible). Save edits rewrites the[optimize]block in place; everything else in the file is left byte-for-byte unchanged. - Run — auto-saves the edits, launches
aeolus1d optimizein a subprocess, streams progress into the convergence plot (log-y when J spans more than two decades) and a status line showing the current(theta, J). On success, the optimum is written to a sibling*.tuned.tomlnext to the input. - Cancel — terminates the subprocess; the dock returns to idle without modifying the file.
The dock pairs with the 2D Plot dock's CSV export — drop probes on a pipe, run, "CSV" to export the time-series, then point the optimizer at that file as a reference. That's the closed-loop calibration path: measure → tune → re-measure.
Theme¶
Edit → Preferences (Ctrl+,) opens a multi-page dialog whose
Appearance page has a Theme dropdown with seven modes (see
ThemeMode / _THEME_REGISTRY in aeolus1d_gui/shell/theme.py):
System default, Light, Dark, Neumorphism, Neumorphism Dark,
Glassmorphism, and Glassmorphism Dark. System default restores
Qt's platform palette; Light / Dark force a Fusion-styled
palette; the neumorphism modes are soft, low-contrast, extruded-
control themes and the glassmorphism modes are frosted / translucent
(both approximated with QSS qlineargradient fills and bevel borders,
since Qt widget-QSS has no true blur / box-shadow). The choice
persists in $AEOLUS_GUI_CONFIG_DIR/theme.json (default
~/.config/aeolus1d_gui/theme.json) and is applied on startup. The
canvas, console, labels, grid lines, drop shadows, and flow-dot
accents all pick contrast-appropriate colours from the selected
palette. The canvas background is a subtle vertical gradient
biased toward indigo at the top on dark mode (light at the top
on light mode) — picked so the silhouettes pop without the bg
competing for attention.
The Appearance group also has a Canvas: Animate flow on edges checkbox (default off). When enabled, a small cream yellow (dark theme) or deep amber (light theme) dot walks every connection at ~30 fps in the direction of physical flow.
Layout sidecar¶
When you Save the case, the GUI writes the TOML AND a sibling
<case>.layout.json describing component positions, camera state,
and probe points (layout_version 2 — see
aeolus1d_gui/io/layout_io.py). The theme is not stored here; it
lives in the separate theme.json GUI-prefs file. The CLI ignores
the sidecar — it's GUI-only metadata so the visual layout you crafted
survives across sessions.
Recent files¶
File → Open Recent shows the last 10 cases you opened or saved.
Persisted in ~/.config/aeolus1d_gui/recent.json. Missing files
prompt a clean dialog instead of silently failing.
Documentation browser¶
Help → Documentation (Ctrl+H) opens the bundled docs inside the GUI
— the same docs/*.md pages as the published site, rendered Qt-native
(LaTeX math included) so you can read the tutorials, theory, and CLI
reference without leaving the app or running mkdocs serve. A file
tree on the left mirrors the docs/ layout; "Open in browser…" hands
the current page to your system browser when you'd rather use the
styled site.
A search box sits above the tree (Ctrl+F focuses it). Type two
or more characters and it runs a full-text search across every page,
showing ranked results with a one-line context snippet under each
title. The query is tokenised the same way the published site indexes
it — splitting on whitespace, -, ., _, / — so a symbol-heavy
identifier like flow_nu or gas_property_correction is found by its
parts, and multiple terms are AND-matched (every term must appear on
the page). Click a result to jump to that page with all matches
highlighted; clearing the box restores the tree.
Implementation notes (for contributors)¶
The editor canvas is split across a small set of modules that together own everything you see on the canvas:
| Module | What it owns |
|---|---|
aeolus1d_gui/editor/canvas.py |
CanvasController — the PyVista widget, the 2D-lock interactor style, drag / drop / pick / select / connect interactions, the gradient background + grid + watermark, the connection-line bezier + arrowhead build, the hover-tooltip pick path, the flow-animation QTimer. The integration layer; doesn't own visual constants. |
aeolus1d_gui/editor/nodes.py |
PvNode — one node's actor + outline + shadow + symbol + label + port markers + decoration actors. Per-node lifecycle (add_to_plotter / remove_from_plotter / move_to / refresh_*). |
aeolus1d_gui/editor/visual_style.py |
Every visual constant — kind palette, halo geometry, shadow + flow-dot accents, label typography, edge colours, the orient_polyline_for_flow direction-from-endpoint-strings helper. Single source of truth; the other editor modules import from here. |
aeolus1d_gui/editor/silhouettes.py |
Per-kind 2D outline factories + extruded PolyData (silhouette_for_kind), outline polylines (outline_polyline_for_kind), shadow planes (shadow_for_kind). Pure-Python outline → triangulated → extruded; no Qt dependency. |
aeolus1d_gui/editor/roles.py |
What a pipe contains, from its spec (classify(kind, spec, ctx)). Pure — no Qt, no pyvista. The single owner of that question, shared by the canvas decorations, the hover tooltip and the docs generator, so they cannot disagree. Pipe-only: non-pipe kinds return no roles. |
aeolus1d_gui/editor/decorations.py |
Spec-driven annotation glyphs (decorations_for_spec(kind, spec)), drawn by the active icon style in icon_styles.py — the default schematic style: role tint, family pattern, rims, urea band, n_cells tick marks (the [catalyst] / [filter] corner badges are the legacy style). Returns (suffix, polydata, colour, mode) tuples; PvNode owns the actor lifecycle. |
aeolus1d_gui/post/schematic_style.py |
Separate post-run schematic canvas (SchematicCanvas, QPainter-based) plus the Qt-side reference iconography that inspired the editor silhouettes. Not on the editor canvas; opt-in via its own menu item. |
If you're adding a new component kind, the touch points are:
register the silhouette outline in silhouettes._OUTLINES, add a
family colour + glyph symbol to visual_style._GLYPH_COLOURS /
_GLYPH_SYMBOLS, add a hover-tooltip branch to
canvas._format_node_hover, and (optionally) add spec-driven
decorations to decorations.decorations_for_spec.
If instead you are adding a new closure kind that a pipe can carry
(a reaction, catalyst, filter or porous kind), the touch point is
editor/roles.py: map it to a role, or add it to roles.UNMAPPED with a
reason. test_roles_cover_registry fails otherwise — deliberately, because
an unmapped kind renders as a blank pipe and nothing else would notice.
If the new kind should also get its own canvas pattern or tint, add it to
editor/icon_styles.py, then re-run
python scripts/generate_icon_reference.py --write — docs/icon-reference.md
is generated and tests/unit/test_icon_reference_current.py fails when it
is stale. The PvNode
lifecycle picks up the new kind automatically — it dispatches via
self.kind strings, no per-kind branching.
If you're tuning visuals, almost everything ships as a constant
in visual_style.py (block half-extent / thickness lives on
nodes.py to avoid the circular import). Move there first; the
modules that need a value re-export it for backwards compat.
Troubleshooting¶
The canvas is sensitive to the underlying X / OpenGL stack. If you're on WSL and components don't render or the canvas crashes, see the Installation → WSL / Windows section for the standard fixes.