Skip to content

Architecture

This document is the high-level module tree + design conventions snapshot. For a step-by-step narrative of how a run flows end-to-end (TOML → Case → network → timeloop → HDF5 → resume → GUI), read concepts.md — that's the 30-minute onboarding doc.

src/aeolus1d/
├── __init__.py            # __version__ + (lazy) load_case / build_network / run_case
├── _types.py              # End, Limiter, RKScheme, BCKind, *Kind enums
├── _compat.py             # py39 shims (zip_strict, …)
├── _numba_flags.py        # central @njit flag policy
├── _resources.py          # bundled-example / data-file resolver
├── constants.py           # defaults
├── audio.py               # audio probe / WAV export
├── engine_post.py         # cycle accumulator post-processing
├── engine_oil.py          # engine-oil thermal model + FMEP scaling
├── coolant.py             # lumped single-T coolant-network model
├── fmi.py                 # FMU export scaffolding
├── fuels.py               # fuel database
├── fuel_map.py            # brake-specific-fuel-consumption (BSFC) map
├── logging.py             # structured run logging
├── notebook.py            # Jupyter / notebook integration helpers
├── homotopy.py            # homotopy / continuation operator
├── optimize.py            # parameter-calibration driver
├── parallel.py            # parallel case runner
├── sensitivity.py         # parameter sweeps
├── doe.py                 # design-of-experiments sampling
├── uq.py                  # uncertainty-quantification Monte Carlo
├── surrogates.py          # unified ML-surrogate API
├── validation.py          # validation-suite utilities
│   # --- vehicle / driveline / hybrid stack (v0.7–v0.8) ---
├── vehicle.py             # longitudinal vehicle dynamics
├── driver.py              # closed-loop longitudinal driver model
├── drive_cycles.py        # speed-vs-time drive-cycle profiles
├── driveline.py           # driveline / transmission
├── transmission_shift.py  # AMT shift sequencing + elastic flange
├── planetary.py           # planetary power-split (THS-style e-CVT)
├── hybrid.py              # hybrid powertrain
├── energy_management.py   # hybrid energy management + series hybrid
├── emotor.py              # map-based e-motor + inverter losses
├── battery_ecm.py         # multi-RC equivalent-circuit battery (Randles)
├── regen.py               # regenerative-braking split
├── solver/
│   ├── hllc.py                # @njit HLLC flux (ideal gas)
│   ├── hllc_eos.py            # real-gas HLLC (PR / RK / tabulated)
│   ├── hllc_eos_njit.py       # JIT'd real-gas HLLC kernels
│   ├── hllc_lm.py             # Thornber low-Mach correction
│   ├── hllc_jac.py            # closed-form analytic HLLC Jacobian
│   ├── reconstruct.py         # MUSCL slopes (van-Leer / minmod)
│   ├── kernels.py             # 1st-order + MUSCL + quasi-1D pipe residuals
│   ├── kernels_mp5.py         # MP5 5th-order monotonicity-preserving
│   ├── species_advection_njit.py  # JIT'd species advection on HLLC mass flux
│   ├── packed_pipes.py        # parallel/serial multi-pipe HLLC dispatcher
│   ├── chem_bs_jit.py         # @njit Bader-Deuflhard semi-implicit BS
│   ├── chem_bs_brinkmeier.py  # @njit semi-implicit BS kernel for TWC Brinkmeier
│   ├── chem_numbalsoda.py     # opt-in nogil per-cell stiff chem (numbalsoda seam)
│   ├── chem_diagnostics.py    # stiff-chem telemetry + failure reporting
│   ├── solver_ops.py          # operator-split source terms shared by all drivers
│   ├── rk.py                  # SSP-RK2/RK3 stage combinators
│   ├── timeloop.py            # single-pipe explicit driver
│   ├── network_timeloop.py    # coupled pipe/junction explicit driver
│   ├── quasi_steady.py        # low-Mach / QS pressure-projection core (single pipe)
│   ├── quasi_steady_network.py # QS network coupling (junction Picard/GGA)
│   ├── loop_flow_steady.py    # QS loop-flow / null-space steady-flow solver
│   ├── implicit_euler.py      # implicit-Euler + BDF2 single-pipe driver
│   ├── implicit_kernels.py    # block-Thomas, line search, augmenters
│   ├── implicit_network.py    # implicit network driver + Picard outer loop
│   ├── implicit_radau.py      # Radau IIA(3) stencil
│   └── startup_banner.py      # one-shot resolved-config + calibration printout
├── components/
│   ├── pipe.py                # finite-volume grid + ghosts + variable area
│   ├── volume.py              # 0D plenum (m, E_int, optional m_species)
│   ├── junction.py            # plenum + multi-port + species transport
│   ├── junction_cp.py         # Corberán CP+h N-branch junction
│   ├── junction_ehr.py        # Extended Helmholtz Resonator (per-port u_n)
│   ├── junction_passthrough.py # transparent area-preserving connector
│   ├── cylinder.py            # slider-crank V(θ) + Wiebe + 2-zone + ports
│   ├── valve_flow.py          # compressible isentropic-orifice valve flow
│   ├── turbo.py               # Turbine + Compressor + Shaft (1-DOF ODE)
│   ├── turbo_csv.py           # CSV / NPZ TurboMap loader
│   ├── helmholtz_resonator.py # 1-D neck + cavity component
│   ├── coolant_pipe.py        # 1-D coolant pipe + per-cell solid-wall thermal mass
│   ├── species_coupling.py    # shared fuel/species port helpers
│   └── protocols.py           # Component protocol + isinstance dispatch
├── bc/
│   ├── base.py                # wall, transmissive, compose helpers
│   ├── stagnation_inlet.py    # static + transient
│   ├── pressure_outlet.py     # static + transient
│   ├── mass_flow_inlet.py     # static + transient
│   ├── velocity_inlet.py      # static + transient
│   ├── throttle.py            # controllable throttle-body inlet BC
│   ├── valve.py               # Chalet-Chesse-Hetet effective-area
│   ├── valve_orifice.py       # Winterbone-Pearson orifice
│   ├── orifice_flow.py        # shared discharge-coefficient orifice physics
│   ├── egr_valve.py           # controllable EGR-valve orifice BC
│   ├── nonreflecting.py       # LODI / Poinsot-Lele characteristic BC
│   └── transient.py           # Clock + schedule helpers
├── closures/
│   ├── gas.py                 # ideal-gas EOS, Euler flux, max wave speed
│   ├── friction.py            # NONE, DARCY_CONST, COLEBROOK
│   ├── heat.py                # NONE, CONSTANT_TWALL
│   ├── porous.py              # NONE, DARCY_FORCHHEIMER (μ const + Sutherland)
│   ├── bend_loss.py           # Idelchik smooth-bend loss coefficient
│   ├── wall_thermal.py        # standalone dynamic pipe wall (convection + radiation)
│   ├── wall_coolant.py        # wall + coolant ODE rate functions + step driver
│   ├── catalyst.py            # NONE, TWO_PHASE_HEAT (+ var-Nu), MECHANISM
│   ├── catalyst_eos.py        # real-gas catalyst T-inverse helpers
│   ├── film_transfer.py       # external gas↔surface film mass-transfer resistance
│   ├── filter.py              # DPF: trap + cake Δp + active regen + thermal mass
│   ├── monolith.py            # square-channel monolith parameter resolver
│   ├── thiele.py              # Thiele modulus diffusion limit
│   ├── eos.py + eos_njit.py   # real-gas EOS (RK / PR + Newton T-solve;
│   │                          #   sound speed uses cv_real = ∂e/∂T, #12)
│   ├── eos_tabulated*.py      # CoolProp-backed tabulated EOS (+ JIT companion)
│   ├── reaction_demo.py       # single Arrhenius (+ reaction_demo_njit companion)
│   ├── mechanism.py           # generic spec-driven reaction-mechanism builder
│   ├── scr_kinetics.py        # Cu-zeolite NH3 ads/des + standard SCR
│   ├── scr_storage.py         # surface coverage state evolution (Temkin des.)
│   ├── hnco_hydrolysis.py     # urea-water HNCO → NH3
│   ├── urea_decomposition.py  # urea (AdBlue) decomposition kinetics
│   ├── hc_oxidation.py        # HC + DOC HC/CO branch
│   ├── doc_sulfur.py          # DOC sulfate poisoning + reduction (Phase 7c-doc-S)
│   ├── twc.py + twc_brinkmeier.py # 3-pathway VML and 11-reaction TWC
│   ├── pollutants.py          # NO/NO2 EGR pollutant module (Zeldovich)
│   ├── multi_stage_wiebe.py   # multi-stage Wiebe heat-release (HCCI / RCCI)
│   ├── species_contract.py    # declarative reaction↔pipe species channel
│   ├── _extras_layout.py      # sparse-pipe ⇄ dense-kernel extras remap
│   ├── pinn.py                # PINN closure adapter
│   ├── chem_linop.py          # two-stage linearized chemistry surrogate runtime
│   ├── chem_surrogate.py      # runtime ML chemistry surrogate (pure numpy)
│   ├── isat_chem.py           # in-situ adaptive tabulation (ISAT) mapping cache
│   ├── learned_turbo_map.py   # NN turbomap surrogate
│   ├── learned_junction_bc.py # NN port-state surrogate
│   └── registry.py            # closure-kind registry (single source of truth)
├── control/
│   ├── boost_controller.py    # VGT vane → manifold pressure PI
│   ├── wastegate_controller.py # bypass-fraction → manifold pressure PI
│   ├── egr_valve_controller.py # velocity-form PI EGR-valve controller
│   ├── fuel_injector_controller.py # velocity-form PI fuel-injection controller
│   ├── spark_timing_controller.py  # velocity-form PI spark-timing controller
│   ├── epw_closed_loop.py     # AVL EPW Diesel closed-loop controller helpers
│   ├── pid_tune.py            # PID auto-tuning from a step response
│   ├── step_response.py       # closed-loop step-response harness
│   ├── signal_toolbox.py      # general control / signal-processing blocks
│   └── dpf_heater_controller.py # heater power → cake T PI
│       (DOCSulfurController lives in closures/doc_sulfur.py
│        because it co-mutates that closure's state)
├── chemistry/
│   ├── species.py             # SpeciesThermo (NASA-7 polynomials)
│   ├── reaction.py            # Reaction + Mechanism dataclasses
│   ├── kinetics.py + kinetics_njit.py  # Mechanism kinetics + JIT companion
│   ├── surface_kinetics.py    # surface-coverage kinetics
│   ├── mixture.py             # multi-species γ averaging
│   ├── db.py                  # native NPZ-per-species DB layer
│   ├── mech_db.py             # native JSON-per-mechanism DB layer
│   └── importers/             # CHEMKIN (.inp), Cantera YAML, JANAF, NASA-7
├── runtime/
│   ├── live_control.py        # GUI ↔ runtime poll-file controller
│   └── cfl_ramp.py            # CFL ramp profiles
├── analysis/
│   ├── cfl_inspector.py       # CFL margin inspector
│   ├── engine_metrics.py      # Cycle accumulator (EngineMetrics)
│   ├── nox_post.py            # Zeldovich NOx post-processor
│   ├── acoustics.py           # acoustic post-processing (order-tracking, FFT)
│   ├── acoustics_tm.py        # transfer-matrix (four-pole) acoustic solver
│   ├── linearize.py           # state-space linearization about a steady point
│   ├── sensitivities.py       # time-domain forward sensitivities
│   ├── random_cycle.py        # stochastic drive-cycle generator
│   ├── system_reports.py      # system-level post-processing reports
│   ├── turbo_map_viz.py       # compressor + turbine map visualizer
│   └── vehicle_tasks.py       # vehicle-level analysis tasks (Cruise-M style)
├── modes/
│   ├── network.py             # FEM eigensolver assembly
│   ├── elements.py            # 1-D Helmholtz element matrices
│   ├── assemble.py            # global assembly + BC application
│   └── solve.py               # eigenvalue solve
├── bench/                     # canonical benches with analytical references
│   ├── sod.py, smooth_advection.py, lax.py, shock_reflection.py
│   ├── shu_osher.py, woodward_colella.py, abgrall_interface.py  # var-γ benches
│   ├── nozzle.py, helmholtz*.py, porous_plug.py, moody.py
│   ├── colebrook.py, tjunction.py, tjunction_cp.py
├── io/
│   ├── schema.py              # dataclasses for a parsed TOML case
│   ├── parsers.py             # TOML reader
│   ├── validate.py            # Phase B10 two-pass validator
│   ├── build.py               # case → live network materialiser
│   ├── case.py                # load_case, build_network, run_case
│   ├── vehicle_case.py        # vehicle-system drive-cycle case (TOML → run)
│   ├── writer.py              # HDF5 output
│   ├── manifest.py            # run-provenance manifest beside every HDF5
│   ├── snapshot.py + resume.py # checkpoint + restart
│   ├── tracer.py              # probe time-series
│   ├── cgns_writer.py         # CGNS export (Tecplot / Paraview)
│   ├── xdmf.py                # XDMF sidecar writer (ParaView)
│   ├── schema_help.py         # GUI inspector blurbs
│   ├── schema_upgrades.py     # back-compat schema migrations
│   ├── case_upgrades.py       # versioned TOML case-format upgrades
│   ├── expand_case.py         # `aeolus1d expand` — lenient → strict TOML
│   ├── _kind_codec.py         # registry-driven flat-spec codec
│   ├── source_pos.py          # TOML source-position lookup
│   └── interop/               # AVL Boost/Cruise-M (.proj/.ucp/.btc),
│                              #   GT-Power/GT-Suite (.gtm), Ricardo WAVE,
│                              #   FMI 2.0 FMU export/import
└── cli/
    ├── main.py                # top-level `aeolus1d` dispatcher
    ├── registry.py            # subcommand registry
    ├── _shared.py             # shared CLI helpers
    ├── doctor.py              # `aeolus1d doctor` environment check
    └── commands/              # run, bench, examples, optimize, gui, expand,
                               #   export, import, modes, sensitivity, drive-cycle,
                               #   cycle-settle, turbo-map, acoustics, audio,
                               #   analyze-cfl, warmup, export-fmu, …

Hot-path conventions

  • Struct-of-arrays only — U is (N+2·Ng, 3). No jitclass.
  • Every kernel is a free @njit function; closures dispatch on small integer codes so the JIT can inline cheaply.
  • Physics knobs live on the Pipe / Volume dataclasses (not inside the kernels). The timeloop reads them once per stage.

Python vs JIT split

  • Python: case loading, BC resolution, RK stage orchestration, CFL dt, HDF5 IO, CLI.
  • @njit: HLLC, MUSCL reconstruction, pipe residuals (first order, MUSCL, quasi-1D), pipe_min_dt, friction / heat source evaluation.

ML seam

See docs/closures.md. Two Protocol-shaped extension points will be plumbed once the relevant learned models are wired in:

  • ClosureProtocol — friction, heat transfer, EOS. Physics impl is @njit; an ML impl is plain Python, and the solver routes its pipes through a NumPy-mode residual when any non-JIT closure is attached.
  • JunctionModel — port coupling with fixed-width tensors of port states. The default is characteristic / plenum-ghost; a learned variant is a drop-in replacement.

GUI architecture (Model-View-Controller)

aeolus1d_gui follows a textbook MVC layout. The split landed in an 8-phase rewrite (see docs/changelog.md — GUI MVC rewrite).

aeolus1d_gui/
├── model.py              # M — CaseModel + RunModel (QObject)
├── app.py                # C — MainWindow (Qt menus, slot handlers)
├── __main__.py           # `python -m aeolus1d_gui` entry point
├── vehicle_panel.py      # V — vehicle-case TOML editor panel
├── controller/           # C — spec-construction factories (mutations.py)
├── editor/               # V — canvas + history snapshots
├── shell/                # V — palette, inspector, runner, dock widgets
│   ├── actions.py        # C — the QAction table (MainWindow's menu/toolbar set)
│   ├── docks.py          # C — the DockSpec table (which panels exist, where,
│   │                     #     and which are reachable from View → Panels)
│   └── window_state.py   # C — dock layout persistence + panel recovery
├── post/                 # V — plot, network view, modes, optimize docks
├── runners/              # V — QProcess runner + progress streaming
└── io/                   # M-side helpers — case_io, layout_io, recent,
                          #   autosave, layout_heuristic

Model

aeolus1d_gui.model.CaseModel(QObject) owns the authoritative case, layout, on-disk path, dirty flag, and undo / redo history. RunModel(QObject) owns the most-recent run's HDF5 path and cached RunResult. Both expose narrow APIs:

Concern API
Replace state replace(case, layout, path, dirty=…)
File I/O new_untitled, load(path), save(path), import_from(path, tool)
Mutations add_pipe, add_junction, add_bc, add_turbo, add_shaft, add_cylinder, add_probe, remove_probe, delete_component, set_layout_position
History push_undo, undo, redo, clear_history, revert_pending_checkpoint
Run state RunModel.set_path, load_now, adopt, clear

Mutations emit Qt signals (caseReplaced, caseMutated, pathChanged, dirtyChanged, canUndoChanged, canRedoChanged, runLoaded) so views subscribe instead of being manually re-synced.

View

The view layer is pure Qt + PyVista widgets. Each dock takes its data via constructor args or method calls — none reach back into MainWindow.case or MainWindow.model. The canvas (editor/canvas.py) renders nodes; the inspector (shell/inspector.py) edits a spec via a generated form; the plot / network / modes / optimize docks render run data; the palette / component-list / runner docks emit user-action signals.

User-action signals from views (palette.component_requested, inspector.spec_edited, canvas.node_moved, runner_dock.run_clicked, …) flow to the controller, which translates them into model API calls. Model signals flow back to view-side slots (network_view.show_run, plot_dock.set_run, act_undo.setEnabled, …) to refresh.

Controller

aeolus1d_gui.app.MainWindow is a controller. It builds the menus / toolbars / docks, instantiates the two models, wires user-action signals → model API, and wires model signals → view refresh slots. It does NOT own state — self.case, self.layout, self.current_path, self.dirty are read-only @property shims that forward to self.model. The handful of auxiliary attributes that remain (self._counters, self._max_steps, self._import_warned) are controller-side view bookkeeping, not domain data.

The structural-mutation handlers (_on_palette_request, action_delete_selected, _on_probe_requested, …) keep three view-side concerns the model doesn't:

  1. Spec construction with dynamic defaults (e.g. a new BC's default target pipe = first pipe in the case). The spec factories live in aeolus1d_gui.controller.mutations so the controller stays thin.
  2. Unique-id synthesis (_fresh_id).
  3. Canvas-side echo (canvas.add_node / remove_node) and status-bar messages.

After those, the handler hands the spec + position to the appropriate model.add_*(...) call, which mutates state and emits caseMutated.

Why this split

  • Testability — the model can be unit-tested without a Qt display. The view can be smoke-tested with a stub model.
  • Reuse — a future second view (web, headless TUI) plugs into the same model. Today: ~930 GUI test functions (a subset skip without a display).
  • Local reasoning — every state mutation goes through the model's API. Reading model.py (~600 lines) gives a complete picture of what state exists and how it changes.

Test tiers

Phase-A7 of the architecture refactor (see docs/architecture-refactor-plan.md). The full suite — pytest tests/ aeolus1d_gui/tests/ — runs ~6 minutes including all integration + validation benches; that's the right cadence for pre-commit / CI but too slow for a tight refactor loop. Two nested marker tiers give faster feedback:

Tier Marker Budget Coverage
smoke @pytest.mark.smoke ≤ 10 s wall ~8 canonical happy-path tests (Sod shock-tube signature, mass-flow inlet steady-state, two-pipes plenum init, CLI run-and-write-HDF5, closure-registry consistency, Component protocol, checkpoint versioning, mypy strict gate)
fast @pytest.mark.fast ≤ 60 s wall every test under tests/unit/ and aeolus1d_gui/tests/, plus the smoke tier (smoke ⊆ fast) — a few thousand test functions
(none) (default) ~6 min full repo + GUI suite

Run with pytest -m smoke or pytest -m fast. The default pytest (no -m) keeps running everything, so CI behaviour is unchanged.

How the markers get applied

Auto-application via two pytest_collection_modifyitems hooks in conftests at the repo root. No per-test @pytest.mark.* decorators are needed — the curation lives in one place each:

  • fast — repo-root conftest.py walks every collected test and tags it fast if its node-id starts with tests/unit/ or aeolus1d_gui/tests/. Plus any test already carrying smoke (added by the smoke-curation pass) gets fast too, so pytest -m fast is a strict superset of pytest -m smoke.
  • smoke — tests/conftest.py walks every collected test and tags it smoke if its node-id appears in tests/conftest_smoke_marker.py's SMOKE_TEST_IDS tuple. That tuple is the smoke set's source of truth — adding a smoke test is one line there.

Curation criteria

When adding a test to the smoke tier:

  • ≤ 1 s in isolation. The smoke budget is the sum.
  • Covers a canonical happy-path. Targeted regression tests belong in fast instead.
  • Self-contained (no fixture that loads a multi-MB asset, no integration test that exercises a 10-component network).

When adding a test to the fast tier:

  • File lives under tests/unit/ or aeolus1d_gui/tests/. Anything that imports a fixture from tests/fixtures/ or runs more than ~5 s should go under tests/integration/ or tests/validation/ instead.
  • The auto-marker covers all such tests automatically — no decorator needed.

Markers registered in pyproject.toml

[tool.pytest.ini_options]
markers = [
    "slow:       long-running benchmarks (deselect with '-m \"not slow\"') ...",
    "slow_mr:    curated slow subset that ALSO runs on MR pipelines ...",
    "regression: golden full-case snapshot net (tests/regression/) ...",
    "smoke:      ≤ 10 s of canonical happy-path coverage ...",
    "fast:       ≤ 60 s unit + integration coverage ...",
]

--strict-markers is enabled, so a typo on a @pytest.mark.foo fails collection. Add new markers to pyproject.toml first.