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 —
Uis(N+2·Ng, 3). Nojitclass. - Every kernel is a free
@njitfunction; 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:
- 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.mutationsso the controller stays thin. - Unique-id synthesis (
_fresh_id). - 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-rootconftest.pywalks every collected test and tags itfastif its node-id starts withtests/unit/oraeolus1d_gui/tests/. Plus any test already carryingsmoke(added by the smoke-curation pass) getsfasttoo, sopytest -m fastis a strict superset ofpytest -m smoke.smoke—tests/conftest.pywalks every collected test and tags itsmokeif its node-id appears intests/conftest_smoke_marker.py'sSMOKE_TEST_IDStuple. 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
fastinstead. - 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/oraeolus1d_gui/tests/. Anything that imports a fixture fromtests/fixtures/or runs more than ~5 s should go undertests/integration/ortests/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.