Skip to content

feat: add the model zoo for contributed models - #5727

Open
BradyPlanden wants to merge 7 commits into
mainfrom
feat/model-zoo
Open

feat: add the model zoo for contributed models#5727
BradyPlanden wants to merge 7 commits into
mainfrom
feat/model-zoo

Conversation

@BradyPlanden

@BradyPlanden BradyPlanden commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Adds packages/pybamm-model-zoo/, a uv workspace member importable as pybamm_model_zoo, as the home for community- and partner-contributed models. One self-contained folder per model holds its code, tests, examples, citation, and a declarative model.toml manifest. An SPM linearised about its starting stoichiometry, for GITT fitting is provided as a reference entry at core tier, so the gating path is exercised from the start.

Nothing under packages/pybamm/src/pybamm/ changes; everything outside the zoo is config, CI, or docs.

Addresses #5511

Using a zoo model

Not on PyPI yet, so pip install pybamm does not provide it:

pip install "pybamm-model-zoo @ git+https://github.com/pybamm-team/PyBaMM.git#subdirectory=packages/pybamm-model-zoo"
import pybamm
import pybamm_model_zoo as zoo

model = zoo.load("LinearisedSPM")()
solution = pybamm.Simulation(model).solve([0, 300])

Models come from the registry rather than the pybamm namespace: zoo.list_models() for what is installed, zoo.info(name) for maintainer, tier, and supported PyBaMM range. A core model is in PyBaMM's merge gate. A community model is tested on every pull request but advisory, so its badge and the compatibility table in the model zoo docs are where to see what it currently passes on. Adding one is nox -s zoo-new plus a filled-in model.toml.

Reviewing the implementation

The manifest is the only boilerplate: registry, contract suite, docs, CI routing, and badges all derive from it, and manifests are parsed, never imported (_registry.py), so a model with missing dependencies still lists and fails cleanly instead of taking the zoo down.

Weekly, model_zoo_status.yml pairs each model with the newest releases its own pybamm_requires admits, then opens a reviewable pull request with status.json, badges, and the docs table. Contributor and reviewer policy is the package README.md, which the docs include verbatim.

Type of change

Add an entry under # [Unreleased] in CHANGELOG.md, in one of ## Breaking changes, ## Deprecated, ## Features, or ## Bug fixes (include PR number; breaking and deprecation entries need a one-line migration note). Internal-only PRs — refactor, docs, CI, tests — can skip this. See RELEASE.md for the full policy.

Important checks:

Please confirm the following before marking the PR as ready for review:

  • No style issues: nox -s pre-commit
  • All tests pass: nox -s tests
  • The documentation builds: nox -s doctests
  • Code is commented for hard-to-understand areas
  • Tests added that prove fix is effective or that feature works

@BradyPlanden
BradyPlanden requested a review from a team as a code owner August 20, 2026 11:08
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.20%. Comparing base (b0de77b) to head (ac805fb).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5727   +/-   ##
=======================================
  Coverage   98.20%   98.20%           
=======================================
  Files         340      340           
  Lines       32743    32743           
=======================================
  Hits        32156    32156           
  Misses        587      587           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adds `packages/pybamm-model-zoo/`, a uv workspace member importable as
`pybamm_model_zoo`, as the home for community- and partner-contributed
models. One self-contained folder per model holds its code, tests,
examples, citation, and a declarative `model.toml` manifest.

The manifest is the only boilerplate a contributor writes. The registry,
the contract test suite, the docs pages, the CI routing, and the status
badges are all derived from it, and manifests are parsed rather than
imported, so a model whose dependencies are missing still appears in the
registry and reports a clean failure instead of taking the zoo down.

`pybamm_model_zoo.testing.contract.CHECKS` is the single definition of
the contract: ten checks, each scoped as portable (`model`), in-tree
wiring (`packaging`), or repository hygiene (`repo`). The test suite,
the manifest's `skip_contract` validation, and the documented table all
derive from that registry, and the scope is what lets the same shipped
module hold a third-party collection to only the portable rules.

Models are `community` tier (advisory CI) or `core` tier (in the merge
gate). Advisory-ness lives in the CI job, never in an `xfail` marker, so
a red community model reports red without blocking a PyBaMM merge.
`spm_series_resistance` is the reference entry, at `core` tier so the
gating path is exercised from the start.

`nox -s zoo-new` renders the template and appends the CODEOWNERS line;
`tests/test_template.py` renders that same template and runs the whole
portable contract plus Ruff against the result, so "follow the template
and CI is green on day one" is tested rather than hoped.

The weekly `model_zoo_status` workflow matrixes on PyBaMM version and
pairs a model only with releases its `pybamm_requires` admits, then
opens a reviewable pull request with `status.json`, refreshed badges,
and the docs compatibility table.

Nothing under `packages/pybamm/src/pybamm/` changes; everything outside
the zoo is config, CI, or docs.

Addresses #5511
The zoo's manifest contract compared `pybamm_requires` against
`pybamm.__version__`, which hatch-vcs derives from `git describe --match
"pybamm-v*"`. The CI checkout is shallow — `_nox.yml` defaults to
`fetch-depth: 1` — so no tag is in reach, setuptools_scm falls back to
guessing `0.0.1.dev1+g<sha>`, and `>=26.0` went unsatisfied, taking the
merge gate with it. It passed locally only because a full clone has the
tags.

`names_a_release` recognises that guess (PyBaMM has never published a
`0.0` series) and skips the range comparison, so an sdist, a fork, or a
shallow clone no longer reports a model as incompatible with the PyBaMM
it is actually running against. This is the carve-out `scripts/matrix.py`
already makes for its `main` cell, which the contract check was missing.
Specifier *validity* stays unconditional — that half never needed git.

The zoo CI jobs then pass `fetch_depth: 0` so the version resolves and
the comparison is exercised rather than silently skipped, and the weekly
status workflow does the same for the `main` cell, which installs PyBaMM
from the checkout.
BradyPlanden and others added 4 commits August 20, 2026 13:03
Codacy's quality gate allows zero new issues and the zoo commit added
five, all from Bandit. Reproduced locally with the repo's own
`bandit.yml`, which reports exactly the same five, and none is a real
vulnerability:

- B105 in `_docs.py`: Bandit reads a dict key of `"pass"` as a password
  field, so the shields.io colour `"brightgreen"` looks like a hardcoded
  credential.
- B404, B603 and B607 in `test_template.py`: importing and calling
  `subprocess` to run the repo's own ruff over a rendered template. The
  argv is a literal list; no external input reaches it.
- B310 in `scripts/matrix.py`: `urlopen` on a module-level https
  constant.

Suppressed inline as `# nosec <code> - <reason>`, the convention already
used in `src/pybamm/codegen/compilation.py` and the unit tests, rather
than widening `bandit.yml`, which would disable these rules repo-wide.
Bandit now reports zero over the zoo. Nothing changes behaviour: the one
code edit is splitting `BADGE_COLORS` across lines so the suppression
sits on the key that triggers it.
Replace the reference entry, and close the gaps the review found in the
zoo's own machinery.

`spm_series_resistance` applied its `I R` drop in `set_voltage_variables`,
which `build_model` calls *after* `_build_model` has built the external
circuit's equations, so every control law that reads the terminal voltage
back was holding the pre-drop one. Measured at the `R = 0.05` its own
tests used: a requested 4 W delivered 3.94 W, 3.5 Ohm held 3.49 Ohm, 3.6 V
held 3.59 V. Experiments were unaffected, since `Simulation` attaches the
experiment controller to an already-built model. PyBaMM has this physics
already as `{"contact resistance": "true"}`, which applies the drop in
`get_coupled_variables` and so gets all of that right, plus the `I^2 R`
heating and `"voltage as a state"`.

The reference entry is now `linearised_spm`, from #3187: SPM with both
porous electrodes' open-circuit potential replaced by its tangent at the
starting stoichiometry, for fitting a diffusivity to a GITT pulse. The
gradient is PyBaMM's symbolic derivative of whichever `U` the parameter
set supplies, so the model adds no parameters, and it reports the `dE/dd`
a Weppner-Huggins fit otherwise has to read off a titration curve. It
substitutes a submodel instead of rewriting variables after the build, so
no control law can reach a stale voltage, and it duplicates nothing in
core -- #3187 was declined for core as too narrow, which is the case for
a zoo entry. A 5 s pulse recovers an imposed 1e-14 m2.s-1 to within 5%,
and the residual shrinks monotonically as the pulse shortens (-28.5% at
80 s, -11.4% at 20 s, -2.9% at 5 s), which is the sqrt(t) approximation's
own spherical-geometry bias rather than model error.

Then the machinery:

- Gate the zoo's own tests. `gating` was derived from a `core` slug
  alone, so the registry, template, contract-infrastructure and
  generated-file tests -- which belong to no model -- were advisory,
  including the one asserting the contract suite is not vacuous. A test
  with no slug now gates however the models happen to be tiered, and
  `zoo-gating` no longer excludes a core model's examples.
- Reject an external model whose *slug* shadows an in-tree one, not just
  its name. `by_slug` is what resolves citations and the generated
  per-model files, so a differently-named entry with a colliding slug
  displaced a built-in model and left `register_citation` reading the
  wrong folder.
- Record a matrix cell that never reported as `missing` instead of
  dropping it, and colour the badge for it. A leg that died before
  uploading its artifact used to vanish from the table with the badge
  left green; the collector now passes the discovered matrix through
  `--expect`.
- Hold a model's manifest and the extra behind it to the same
  requirements, compared both ways and on the specifier rather than the
  distribution name. CI installs every model's extra at once, so a
  one-way name-only comparison let a model lean on a package it never
  declared.
- Keep the previous `status.json` timestamp when no result changed, so
  the weekly workflow stops opening a pull request that says nothing.
- Say what the compatibility matrix covers: the newest releases a
  manifest admits, which is what `scripts/matrix.py --releases` defaults
  to, not every release it admits.

The status pipeline had no tests at all; `tests/test_status.py` now
covers collection, badge precedence and stamping. 55 zoo tests to 73.
Correctness:

- `LinearisedSPM` no longer raises `TypeError` under
  `{"particle size": "distribution"}`: `dUdT` is broadcast onto the
  potential's domains, which is what the base class size-averages against.
- The tangent's slope is taken at `T_ref`, so it is a constant and the
  published entropic change is the exact temperature derivative of the
  linearised potential. Previously the slope carried `T`, leaving the
  reversible heat inconsistent once the stoichiometry moved.
- `--zoo-tier` prunes a model's folder before pytest imports anything in it,
  so a community model that fails to import can no longer abort the merge
  gate or poison every weekly compatibility cell. Marker deselection alone
  ran too late.
- The compatibility matrix filters releases per model before taking each
  model's newest N, so a model with an upper bound keeps the released cells
  it still supports.
- `missing_dependencies` evaluates environment markers, so a
  platform-specific dependency no longer skips every check elsewhere.
- Examples run as `__main__`, so a main-guarded example is executed.
- The scaffold and the `manifest` check reject Python keywords, which are
  identifier-shaped but render invalid code.
- The generated floor pins major.minor rather than the bare major.

Test suite and CI:

- Warning filters are strict: the three blanket class ignores were unused.
  The weekly job downgrades deprecations on released legs only.
- The advisory job runs only what the gate does not, rather than every core
  model twice.
- The zoo filter routes `.github/CODEOWNERS`, `model_zoo_status.yml`, and
  the generated docs, all of which the contract checks cover.
- New tests: an inherited-options matrix and a non-isothermal pair for
  `LinearisedSPM`, collection selection, the release window, environment
  markers, keyword rejection, and the main-guard.

Cleanups:

- `_versions.py` owns the release ordering and window that `_docs.py` and
  `matrix.py` had duplicated.
- The registry cache is `_registry_instance`, so it stops shadowing the
  `_registry` submodule.
- The docs landing page and README give a supported install path.
- Both changelog bullets end with the pull request link.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant