Contributing to PyTES#
If you’d like to contribute to PyTES (thanks!), please have a look at the workflow below. If you’re already familiar with it, the checks before you commit are directly below.
Checks before you commit#
All tests pass:
uv run pytestThe pre-commit hooks pass:
uv run pre-commit run --all-filesA changelog fragment describes any user-visible change.
Pre-commit hooks#
PyTES uses pre-commit hooks to check and format the code (ruff, and checks for YAML, TOML, file endings and whitespace). Install them once, after setting up your environment:
uv run pre-commit install
They then run on the files of every commit. To run them on all files:
uv run pre-commit run --all-files
To commit anyway, for example to push work in progress for discussion, use
git commit --no-verify.
Workflow#
We use Git and GitHub to coordinate our work. When making any kind of change, we follow the procedure below.
A. Before you begin#
Open an issue to discuss your proposal before any coding is done.
If the change affects the design (models, variables, parameters, submodel interfaces), read
docs/design/DESIGN.mdfirst. It records what is decided, what is deferred (and the hooks that must stay in place) and what is open. Decided points are not reopened without discussion.Create a branch, ideally on your own fork, where all your changes will be made.
Clone the repository (or your fork) and set up the development environment with uv:
uv syncThis installs PyTES in editable mode with the development and notebook tools, from the lock file
uv.lock.Check that everything works:
uv run pytest.
You now have everything you need to start making changes!
B. Writing your code#
PyTES is written in Python on top of PyBaMM: every model is a PyBaMM model, and PyBaMM is never hidden from the user. If you know PyBaMM, you know most of what you need.
Follow the coding style guidelines.
Commit your changes with useful, descriptive commit messages. They are public and should still make sense a few months from now.
If you change the model contract (variable names, units, signs, parameter names, submodel interfaces), add an entry to
docs/design/findings.mdexplaining what changed and why, and acontractchangelog fragment. If the change alters a design decision, updateDESIGN.mdin the same pull request.If you want to add a dependency or reuse code from elsewhere, read Dependencies and reusing code.
C. Merging your changes into PyTES#
Test your code, including the conformance suite.
Add a changelog fragment for any user-visible change.
Document your change: docstrings, and for a new capability a short example notebook.
When your code is finished, or ready for serious discussion, run the checks and open a pull request (PR).
Your PR will be reviewed, and changes may be suggested, which you make by adding commits to your branch. When everything is ready, a maintainer merges it.
Changelog entries#
PyTES keeps its changelog with towncrier. Instead of
editing CHANGELOG.md, each PR with a user-visible change adds a small
file, a fragment, to changelog.d/, named <PR number>.<type>.md, with
one or two sentences written for users. Fragments never conflict with each other, and at
each release they are assembled into CHANGELOG.md and deleted.
The types, in the order they appear in the changelog:
breaking(Breaking changes): anything existing code or results would notice, such as a renamed or removed variable or parameter, a removal, or a new default that moves results. Say how to adapt.contract(Model contract changes): any change to the model contract (variable names, units, signs, parameter names, submodel interfaces), additions included. Name the entry indocs/design/findings.mdthat logs it (e.g. F-049). A contract change that existing code would notice also has abreakingfragment.deprecated(Deprecations): an API that now warns and will be removed in a later release; say when.feature(New features): a model, an option, a submodel, a toolkit function.improvement(Improvements): better existing behaviour that breaks nothing: speed, clearer errors, more outputs.fix(Bug fixes): wrong behaviour corrected.
For example, for PR 42:
echo 'Experiment steps report which termination ended them.' > changelog.d/42.feature.md
A PR can belong to several sections: add one fragment for each, all named with its number
(42.feature.md and 42.breaking.md). For two entries in the same section, add a counter
(42.feature.md and 42.feature.1.md).
A PR with no user-visible change (a refactor, tests, CI, a typo) needs no fragment: label it
no changelog, and the CI check for a fragment is skipped. To preview the next changelog:
uv run towncrier build --draft --version <next version>
At release, a maintainer runs uv run towncrier build --version <version>, which writes
the new section into CHANGELOG.md and deletes the fragments.
Coding style guidelines#
PyTES follows PEP 8, checked and formatted by
ruff through the pre-commit hooks.
Its configuration is in pyproject.toml; the ruff version in .pre-commit-config.yaml
matches the one in the development environment.
Language#
Write prose in British English: docstrings, comments, documentation, notebooks and the names
of variables and parameters (behaviour, colour, centre, licence, modelling; -ise endings such
as discretise). Identifiers from Python and other libraries keep their own spelling
(matplotlib’s color=, pytest’s parametrize).
Naming#
Naming is hard. Aim for descriptive names, and avoid abbreviations where this does not make
names overly long. Class names are CamelCase (PackedBedModel); functions, methods and
variables are lower case with underscores (energy_between). Short mathematical names are
fine inside equations when they follow the docstring’s notation (T_f, h).
Renaming and deprecating#
A public name is renamed through the registry in
src/pytes/deprecation.py, never by hand, so that every old name
warns and fails in the same way:
Rename the class, function or option everywhere: code, tests, notebooks and documentation. The tests fail if an example notebook or the user documentation still uses the old name, and PyTES’s own tests turn the deprecation warning into an error.
Add a
RenametoRENAMES, with the finding that logs it, the release that renames it (the next one) and the release that removes the alias (before 1.0, the next minor release).For a class or function, add
__getattr__ = deprecation.module_getattr(__name__)to the module that held the old name (once per module), and drop the old name from its__all__.Log the rename in
docs/design/findings.mdand adddeprecatedandcontractchangelog fragments.
When a release reaches an alias’s removal release, a test fails: set alias=False in its
entry, and add a breaking fragment. Keep the entry, so that the old name raises an error
that gives the new one. Variable and parameter names cannot be deprecated yet.
PyTES and PyBaMM conventions#
Parameter names carry their units, as in PyBaMM:
"PCM latent heat [J.kg-1]". Material and fluid properties start with their layer’s prefix (PCM ...,Metal ...,Heat transfer fluid ...); geometry starts with the geometric object (Tube ...,Bed ...,Capsule ...,Storage ...).SI units only in models. Twins in °C, kW, kWh, minutes and hours are generated when a model is built; never write them by hand. Temperature differences contain “temperature difference” in their names and get no twin.
Power is positive on discharge and defined from the fluid’s enthalpy,
mdot (h_out - h_in).Fluid properties come only from the fluid-property object; the heat transfer coefficient belongs to the coupling’s surface flux law.
Workarounds for PyBaMM live only in
src/pytes/compat/, each with the upstream issue and the condition for removing it.Docstrings follow the NumPy style, and models provide PyBaMM’s
default_*properties.
Contributing a model#
PyTES is a curated catalogue: what makes it useful is that every model shares the same interface, so models compare like for like. A model is admitted (DESIGN D1) when it:
is a fixed composition of PyTES submodels (
model.submodels), with variants chosen throughoptions;passes the conformance suite (
tests/conformance/) with every combination of its options, which includes exact energy conservation;uses the shared parameter layers and materials;
has at least one validation case, analytical or experimental;
belongs to a declared family; and
has been reviewed by a maintainer.
Open an issue first to discuss where the model fits. To try ideas before that, build a model
from submodels with pytes.ComposedModel (see examples/composing_models.ipynb).
Physics outside PyTES’s scope (DESIGN.md D2) is best built directly with PyBaMM.
Dependencies and reusing code#
PyTES depends on PyBaMM, NumPy and SciPy. Optional features use extras: plot (matplotlib)
for the standard plots. Import optional dependencies only where they are used, so that
import pytes works without them. Before adding a dependency, open an issue: each one is a
maintenance cost for everyone.
Reusing code from elsewhere is fine if its licence allows it (PyTES is BSD 3-Clause). Say where it comes from in the code and in the PR.
Testing#
All code needs tests. We use pytest:
uv run pytest # the fast tests (the default)
uv run pytest -m slow # the slow tests (e.g. the full-size mush model)
uv run pytest --nbmake examples/ # the example notebooks
uv run pytest -m "" --cov # everything, with coverage
uv run pytest tests/models/test_packed_bed.py::test_charge_melts_the_pcm # one test
The suite is organised as:
tests/conformance/: the model contract, run over every model and every combination of its options. Where the contract’s prose and these tests disagree, the tests win.tests/models/,tests/submodels/,tests/experiments/,tests/toolkit/: each part of PyTES.tests/validation/: comparisons with analytical solutions.
Tests are grouped by what they test, not split into unit and integration tests; the slow
marker separates the expensive ones.
Writing tests#
Every new feature needs its own tests, and coverage stays at 100 % of lines and branches
(uv run pytest -m "" --cov fails below it): if a line cannot be reached, it is probably
dead code to remove rather than test. Look for a test of something similar and start from
it. Check behaviour, not only that the code runs: compare with an exact or analytical value
where there is one, and state the tolerance and why it is what it is. Keep regular tests
fast (coarse meshes, tests/conftest.py), and mark long ones @pytest.mark.slow.
Debugging#
Isolate the problem with the smallest example that shows it. PyTES models are PyBaMM models,
so PyBaMM’s tools apply: inspect model.submodels and model.print_submodel_info(), solve
with a coarse mesh, or tighten the solver tolerances to tell discretisation error from
solver error.
Documentation#
Every public class, method and function has a docstring saying, in plain terms, what it does and what its inputs and outputs are, in the NumPy style.
The design is documented in docs/design/: DESIGN.md (the decisions and
their reasons), contract.md (what every model provides) and findings.md (the log of
changes and discrepancies).
Example notebooks#
Each capability has a short example notebook in examples/. Notebooks are
committed with their outputs, so run them before committing, and keep them short. CI
executes every notebook (uv run pytest --nbmake examples/) and fails if a cell raises.
Add a new notebook to the list in docs/examples/index.md, so
that it appears on the documentation site.
Documentation site#
The documentation site is built with Sphinx from
docs/ and hosted on Read the Docs (configured in
.readthedocs.yaml). It has the API reference, generated from the docstrings
(docs/api/), the example notebooks, rendered from their committed outputs, the design
documents, the changelog and this guide. To build it:
uv run --group docs sphinx-build -W --keep-going docs docs/_build/html
and open docs/_build/html/index.html. Any warning fails the build, here and in CI: a
broken cross-reference, a malformed docstring, or a page missing from every table of
contents. A new public module needs an entry in one of the pages of docs/api/.
The diagrams on the model pages are drawn in TikZ, in
docs/models/diagrams/, with their colours and styles in a shared
preamble. The SVGs built from them are committed, so the site builds without TeX. After
changing a diagram, rebuild its SVG (this needs TeX with TikZ, and dvisvgm):
python docs/models/diagrams/build.py capsule # or no name, for every diagram
Releasing#
For maintainers. PyTES uses semantic versioning, with pre-releases
such as 0.1.0a1 before 1.0.
Make sure CI is green on
main.Set the new version in
pyproject.toml(version) and inCITATION.cff(versionanddate-released). If the tests then fail because an alias reached its removal release, retire it (see Renaming and deprecating).Build the changelog:
uv run towncrier build --version <version>. It writes the new section intoCHANGELOG.mdand deletes the fragments; check it and edit if needed.The README is also the PyPI project page: keep its links absolute (relative links break there), and drop
--prefrom the installation commands at the first stable release.Commit, push, and wait for CI.
Create a GitHub release with the tag
v<version>(e.g.v0.1.0a1), mark pre-releases as such, and paste the new changelog section as its notes.
Publishing the release runs .github/workflows/release.yml: it checks that the tag matches
the version, builds the package, and attaches it to the release. Zenodo archives the release
and mints its DOI. The upload to PyPI runs only when the repository variable
PUBLISH_TO_PYPI is true, with PyPI’s trusted publishing set up for the workflow.
Infrastructure#
Installation#
The environment is managed with uv, configured in
pyproject.toml; the lock file uv.lock makes it reproducible.
Continuous integration#
Every push and pull request runs, with GitHub Actions
(.github/workflows/tests.yml):
the pre-commit hooks;
the tests against the latest PyBaMM release: on Linux with every Python version PyBaMM supports, and on macOS and Windows with the oldest and newest;
coverage: every test, slow ones included, reported to Codecov (configured in
codecov.yml);the example notebooks;
the documentation site, built with warnings treated as errors;
the tests against PyBaMM’s
mainbranch (non-blocking, to catch upstream changes early);on pull requests, the check for a changelog fragment.
Dependabot (.github/dependabot.yml) opens a pull request each month that bumps the
GitHub Actions the workflows use, labelled no changelog.
Publishing a GitHub release runs .github/workflows/release.yml (see
Releasing).
GitHub#
GitHub shows README.md on the repository’s front page, links the licence in
LICENSE, and links this file when new issues and pull requests are opened. New
issues offer the forms in .github/ISSUE_TEMPLATE/ (bug report,
feature request, new model), and new pull requests start from the checklist in
.github/pull_request_template.md.
Acknowledgements#
This file is adapted from PyBaMM’s contributing guidelines, which were in turn copied from the excellent PINTS GitHub repository.