# PyTES design document Status legend: **Decided** (do not reopen unless asked), **Deferred** (not built now, but the listed hook must exist), **Open** (to be settled during the stage indicated). Each decision carries its rationale. A decision without its reason gets "improved away". ## 1. Purpose and positioning PyTES is a library of models and auxiliary tools for heat batteries, built on PyBaMM. Audience: practitioners (design studies) and researchers, who both use the provided models. Anyone needing a custom model either builds one from PyTES submodels (D3), for example to develop a new submodel locally, or builds it with PyBaMM directly. Either way they may opt into PyTES's materials, experiments and conformance tests, which are importable tools. Two public layers: - **Models (strict):** curated, admitted by passing the conformance suite and the maintainer's review (D1). - **Toolkit (permissive):** materials, experiments, KPIs, plots, conformance tests. Origin: the finned-tube code prototype (composite finned-tube family, then called finned pipe: sharp-front and mush models) and a separate packed-bed repo, [encapsulated-LTES](https://github.com/mmsg-warwick/encapsulated-LTES) (encapsulated family, enthalpy method, resolved radial conduction in the capsule, constant film coefficient, temperature as fluid state). Both are by the maintainer and are PyBaMM models with the same physics (enthalpy method, upwinded fluid), but they share neither code nor the conservation approach: the material classes are separate copies that differ (F-003), and only the finned tube conserves energy exactly (F-004). They are starting points, not something to preserve as they are. ## 2. Decided ### D1. Curated catalogue with an admission standard Rationale: cross-compatibility is the product. A model is admitted when it (a) passes the conformance suite, (b) uses the shared parameter layering and materials library, (c) has at least one validation case, (d) sits in a declared family, and (e) the maintainer has reviewed it. Passing the suite is necessary, not sufficient. - **Parameter layering:** a parameter set is composed of independent layers (operation: reference and initial temperatures and controls; coupling; HTF; PCM; other materials such as the fin metal; family geometry), so that e.g. a PCM can be swapped without touching the rest. The family geometry is in two layers (maintainer, 2026-10-18): the *flow path*, which sets the size of the device (number, radius and length of the tubes; length and cross-section of the bed), and the *storage element*, the repeating unit, whose names are all per unit length or per tube (fin thickness, fin pitch and storage depth, i.e. the fin height; capsule radius and porosity). So a standard fin or capsule design is chosen once and the device is sized around it. Operation is apart from the hardware, so that one device can be run under several operations (charge, discharge). The tube radius stays in the flow path, although a fin design fits one tube size; a compatibility check between the two can come later. An operation only makes sense relative to the PCM, so besides the registered operations of the named sets, `operation.charge` and `operation.discharge` build one from a PCM layer's melting range, margins and a mass flow rate, all given by the user; their energy reference is the colder end (the initial state of a charge, the inlet of a discharge). Every layer states its sources: a layer with cited values links each source's DOI in its docstring, and one without says that its values are illustrative, an estimate or representative (tested). The maintainer types in every value, to trace its source; `template(kind)` gives an empty layer to fill in. Each material and fluid layer has its own name prefix so layers merge without clashes. `pytes.parameter_layers` (issue #3) defines the kinds and the names each holds, and composes layers into a plain `pybamm.ParameterValues`; named sets list one layer per kind. The families' default parameter values are the named sets "Finned tube default", "Nallusamy2007" and "Raul2018" (maintainer, 2026-10-21, F-048): their values were checked against the literature and replace the earlier flat sets, and `swap` adds a new layer's sources to the set's `"citations"`. Docstrings cite sources as reStructuredText links to their DOI; bibliography roles (sphinxcontrib-bibtex, as PyBaMM uses) can follow with the documentation site (#4). Decided with it (maintainer, 2026-10-18): `"Heat transfer coefficient [W.m-2.K-1]"` is in its own *coupling* layer, which will hold the parameters of other surface flux laws (the correlations of #6 have none: F-049); `"PCM smoothing width [K]"` stays in the PCM layer, although it is numerical; one `Metal` layer per device, with a prefix argument added when a second metal appears. - **Parameter names** (maintainer, 2026-10-16): material and fluid properties start with their layer's prefix (`PCM ...`, `Metal ...`, `Heat transfer fluid ...`); geometry starts with the geometric object (`Tube ...`, `Fin ...`, `Bed ...`, `Capsule ...`, `Storage ...`), never with a material prefix. Geometry names are distinct across families without a family prefix, and stay family-specific (D4). - **Validation case:** an analytical benchmark (Stefan, Schumann) or a comparison with measurements. Experimental data is often unavailable, so an analytical case suffices. ### D2. Scope is defined by structure, not a technology list "HTF passes through or around a separate storage medium." In scope: finned or tubed PCM blocks (including PCM tanks with an immersed coil), encapsulated PCM beds, and sensible-only storage such as packed beds. Out of scope: stratified tanks (the HTF is the storage medium), thermochemical storage, system-level models (heat pump, building), and a general framework for arbitrary physics (composing PyTES submodels through the submodel interface is in, D3). The structural rule makes these "no"s easy to justify. - **PCM slurries** (microencapsulated or emulsified PCM suspended in the carrier fluid): as storage, out (no separate medium, like a stratified tank). As the HTF of an in-scope device, in: a slurry is a fluid-property object whose `h(T)` has a latent bump (D8). Nothing to build now. - **Direct contact** (immiscible HTF bubbling through the PCM, no wall): out for now. It fits the three slots only through an empirical volumetric coefficient; its physics (droplet dynamics, channelling on solidification) is in the flow, which no slot captures. If it returns, it is a new family, not a variant of an existing one. ### D3. Submodel structure, visible and inspectable Models are fixed compositions **for a given set of options**. `model.submodels` lists them. Each submodel exposes the variables it requires and provides and the parameters it uses, all read from its code (F-028, F-045), and declares its assumptions; the model exposes the parameters of its geometry (domain lengths). So wiring can be checked automatically and model cards can be generated later. Submodels are testable in isolation. - **Options** (PyBaMM style): a model's `options` dict selects among submodels, e.g. the fluid (dynamic or quasi-steady, D5) or the film coefficient. `operating_mode` and `initial_state` are removed (D11, D14). This is the preferred route for variants. *Introduced with the first real variant* (maintainer, 2026-10-14), which is `"storage along flow path": "resolved" | "averaged"` (D4; maintainer, 2026-10-15; named `"storage"` until 2026-10-22, F-050). Each model declares `allowed_options`, the first value of each being the default; unknown options or values raise `pytes.OptionError`, and `check_options` rejects incompatible combinations. Values are lower case unless they are names (e.g. `"heat transfer coefficient": "Gnielinski"`). An option is named after what it varies, not after a slot (maintainer, 2026-10-22, F-050): `"storage"` was also the name of a slot in `model.submodels`. A renamed option is an entry of the rename registry (D17), so that its old name warns or raises with the new one. - **No silent swaps.** An option combination that is incompatible raises an error naming the conflict; a model never replaces a submodel the user did not ask for. Rationale: PyBaMM's options grew complicated and it swapped submodels silently to keep combinations working, which hid what was being solved. Every admitted combination passes the conformance suite. - **Public composition API** (advanced users): `model.print_submodel_info()` displays the submodels (class, requires, provides, assumptions), and `pytes.ComposedModel(submodels, param, defaults_from=..., geometry=..., ...)` builds a model from submodels, from scratch or by replacing slots of an existing model (whose submodels are reused, not modified). It runs the same wiring checks as the built-in models, and raises if the geometry, submesh types or spatial methods miss a domain. Rationale: it lets people develop their own submodels locally against the real interface. A composed model is not admitted (D1) until it goes through the admission standard. ### D4. Two-tier compatibility - **Within a family** (one topology, several fidelities): same parameter set, nested outputs, same experiments; one-to-one comparison. - *Parameters:* the most detailed model in the family defines the family's parameter set. Every lower-fidelity model runs on that set, leaving some parameters unused and deriving what it needs from them (e.g. the sharp-front composite properties from the fin and PCM properties). A reduced model that needs a parameter it cannot derive (e.g. a fitted effective coefficient) is a logged exception. - *Outputs:* a higher-fidelity model provides every variable of the simpler models in its family and may add more (e.g. the mush model's fin variables). - *Averaged storage* (maintainer, 2026-10-15): every model has the option `"storage along flow path": "averaged"`, in which one storage cross-section (a capsule, an annulus, a fin-PCM unit) stands for the average along the flow path and exchanges heat with the fluid averaged along it. Averaging is a fidelity choice that applies across families, so it is an option, not a model class per family (it replaces the planned `AverageCapsuleModel`, F-019). In the mush model the fin-PCM unit is averaged, with the fins and the PCM still separate submodels. - *Lumped storage element* (maintainer, 2026-10-22, issue #5, F-051): the option `"storage element": "resolved" | "lumped"`, separate from `"storage along flow path"` as it is about the variation inside one storage element, not along the flow; the two combine (a lumped, averaged capsule is one well-mixed capsule for the whole bed). Only the packed bed has it for now. A lumped storage (`LumpedStorage`, on a `LumpedGeometry`) has a uniform temperature in each element and provides the same variables as `EnthalpyStorage`, so the couplings, the device energy and the ledger work unchanged. The internal resistance it neglects can be put back in the flux law (D6): `"lumped with Jeffreson"` wraps whichever coefficient `"heat transfer coefficient"` gives. It is a value of `"storage element"`, not a separate option (maintainer, 2026-10-23): it only applies to a lumped element (with a resolved one it would count that resistance twice), so as a value it admits no invalid combination. - *Averaged coupling* (maintainer, 2026-10-15): one flux from the fluid averaged along the flow path, taken uniformly from the fluid, as in the maintainer's published reduced model. Rationale: reproduces the published model. Known artefact: early in a charge the fluid near the outlet can be driven outside its inlet and initial temperatures (the default bed's outlet dips to 27 °C from 32 °C); energy stays exact. Recorded alternative, if ever needed: the local flux `q(z) = h (T_f(z) - T_s)` against the averaged storage surface, the storage receiving its mean and the fluid the exact ledger total plus `-S (q(z) - mean q)`, with `S` the storage surface per unit length of flow path; a prototype removed the dip and kept conservation exact (F-041). - **Across families:** same device-level interface: the contract's required outputs, experiments, and a small shared device description (reference and initial temperature, HTF, controls: mass flow rate and inlet temperature). Geometry parameters stay in family-specific namespaces. - *Per device:* the shared interface is always whole-device, in J and W, so experiments, power controls and measurements mean the same in every family. Families may add normalised twins as extra outputs (e.g. per unit bed cross-section [J.m-2], per tube), so the bed needs a cross-section parameter (F-002). The bed provides them with the `PerAreaTotals` submodel ("Power per unit area [W.m-2]" and so on). ### D5. Three slots: fluid, storage, coupling - **Fluid** is written once. Geometry-neutral form, with `q'` the heat exchanged per unit length of flow path: `A_flow * rho * dh/dt + mdot * dh/dz = q'`. Porosity enters only through `A_flow`. Axial dispersion is an optional extra term (zero for tubes). The contract must not define the fluid slot as "an advection PDE", because a quasi-steady fluid (`mdot * dh/dz = q'`) is a planned option (refrigerant, high-flow water). - *State is the specific enthalpy* `h` (maintainer, 2026-10-14). The temperature is `T(h)` from the fluid-property object. It is an internal state, and enthalpy handles temperature-dependent properties (e.g. `c_p(T)`) and a two-phase refrigerant, where `T` is flat across the phase change, without a change of state variable. `h_out` is then the advected value itself and `T_out = T(h_out)`. - *Dynamic form is for incompressible liquids.* Strictly the transient term is `rho u`, not `rho h`, and `mdot` uniform along `z` needs constant density; both are exact for a liquid at constant density. Compressible or two-phase fluids use the quasi-steady form, where `mdot` is uniform by mass conservation. A fully dynamic two-phase fluid is out unless needed. - **Storage** is the enthalpy method with one submodel and a geometry switch (annular fin-PCM, planar gap, sphere). It differs only by a geometric factor in the conduction term. A model that resolves several media (the mush model: PCM and fins) has one storage submodel per medium, all of this one kind; a sensible medium (fin metal, Schumann bed) is the same submodel with a material without latent heat (maintainer, 2026-10-14). Every storage state is therefore an enthalpy, like the fluid's. - **Coupling** is where families differ. Couplings join the fluid to the storage and, where a model resolves several media, storage to storage (the mush model: fluid to fin root, fin to PCM). Contract: given the states on both sides, a coupling provides `q'` to the fluid (if one side is the fluid) and a boundary heat flux or temperature to each storage side (`"