# PyTES model contract (Stage B) Status: **Stage B** (closed 2026-10-16). Changes from here on are logged in `findings.md`; the contract is frozen at v1.0 (Stage E, DESIGN §5). The executable form is `src/pytes/conformance.py`, run over every model by `tests/conformance/`. **Where this page and the tests disagree, the tests win.** ## 1. What a model is - A `pytes.BaseModel` subclass (itself a `pybamm.BaseModel`). PyBaMM tools apply directly: `pybamm.Simulation`, solvers, `QuickPlot`, PyBOP. - A **fixed composition** of submodels in `model.submodels` (an ordered dict), shown by `model.print_submodel_info()`. Users call models; advanced users may build their own from submodels with `pytes.ComposedModel` (same wiring checks; not admitted, DESIGN D1). - It belongs to a family (today: `finned_tube` and `packed_bed`) and passes the conformance suite, with every combination of its options (`allowed_options`; today `"storage along flow path": "resolved" | "averaged"` in every model; `"heat transfer coefficient": "constant" | "Gnielinski"` in the finned tube, `"constant" | "Wakao-Kaguei"` in the packed bed, plus any correlation registered for the model's flow with `register_correlation`; and in the packed bed `"storage element": "resolved" | "lumped" | "lumped with Jeffreson"`). Unknown options or values raise `pytes.OptionError`. ## 2. Submodels A submodel is a small `pybamm.BaseModel` (`pytes.submodels.BaseSubModel`). PyBaMM's own `BaseSubModel` is not used: it assumes battery domains and options. In `build()` a submodel sets its own `variables`, `rhs`, `algebraic`, `boundary_conditions`, `initial_conditions` and `events`, and reads any variable set by another submodel as a `pybamm.CoupledVariable` (through `self.coupled(name, domain)`), never by reaching into that submodel. A storage submodel sets all its own boundary conditions; couplings provide the value it applies (a heat flux into it, or a temperature). Metadata, on every submodel, read from the code rather than declared separately: | Attribute | Meaning | |---|---| | `requires` | Names of its coupled variables (PyBaMM's `coupled_variables`) | | `provides` | Names of its `variables` | | `parameters` | Names of the parameters in its equations, conditions, variables and state scales | | `assumptions` | One sentence each, declared, for model cards (rendering deferred) | On the model: `geometry_parameters`, the parameters of its default geometry (domain lengths), and `print_submodel_info()`, which shows all of the above. Every parameter of a model's default set belongs to a submodel or to the geometry, except declared hooks (tested). The model merges the submodels with `pybamm.BaseModel.update` and checks the wiring (`WiringError` otherwise): each required name is provided by exactly one submodel; no variable depends on itself through coupled variables; after resolution, each provided variable is on the primary domain its coupled variable declares; no two submodels set equations or conditions for the same variable. It then resolves every coupled variable (`pytes.compat.resolve_coupled_variables`), so the finished model contains none and works with a default `pybamm.Simulation`. ### Domains (DESIGN D12) - `"flow path"`, coordinate `z`: along the flow, from the inlet; every family. - `"storage depth"`, coordinate `y`: distance into the storage from the heat-exchange wall, **not along the flow** (finned tube; cylindrical around a tube). - `"fin gap"`, coordinate `x`: across the gap between two resolved fins (mush model). - `"capsule"`, coordinate `r`: from a capsule's centre (packed bed; spherical). A storage resolved along the flow path has `"flow path"` as its last secondary domain; an averaged one (`"storage along flow path": "averaged"`) does not. A lumped storage element (`"storage element": "lumped"` or `"lumped with Jeffreson"`, packed bed) has no domain of its own: its variables are on `"flow path"`, or on none when it is also averaged. ### Slots (DESIGN D5) | Slot | Contract | Current submodels | |---|---|---| | Fluid | State: fluid specific enthalpy `"Heat transfer fluid specific enthalpy [J.kg-1]"` on domain `"flow path"`, coordinate `z`; its temperature is `T(h)` from the fluid-property object. Requires `"Heat transfer fluid heat gain per unit length [W.m-1]"` (per flow path) and the initial temperature. Provides the fluid enthalpy and temperature, the inlet/outlet temperatures and specific enthalpies (outlet temperature `T(h_out)`), the mass flow rate and its thermal energy. Its spatial method on `"flow path"` is `compat.fluid_spatial_method()`. | `AdvectedFluid` | | Storage | Enthalpy method, one submodel for every medium (phase-change or sensible, D5); state list `[H, *material.internal_states]`. Geometry switch = coordinate system of its primary domain. Sets zero flux on its outer side and, on its inner side, applies `"