Development¶
Contributions are welcome. The repository is matthiaskoenig/sbml2cellml; development happens against the develop branch via pull requests.
Branch model¶
Two branches are permanent:
developis the default branch and the branch everything is integrated into. The documentation on matthiaskoenig.github.io/sbml2cellml is published from it.maintracks the latest published release. It is fast-forwarded to the released commit by thesync-mainjob of theCI-CDworkflow after the package went to pypi, somainand the newest version on pypi always agree. Nothing is developed onmainand nothing is merged into it by hand.
Work happens on short lived branches off develop, which GitHub deletes after
the merge. Releases are tagged on develop, see Release.
Pull requests¶
Neither branch accepts a direct push, every change goes through a pull request
against develop. This includes the maintainer, there is no bypass.
A pull request can only be merged once the four required checks are green:
| check | workflow | content |
|---|---|---|
tests |
ci-cd.yml |
the test matrix, linux and macos with python 3.13 and 3.14 |
ruff |
ruff.yml |
ruff check and ruff format --check |
ty |
ty.yml |
tox r -e ty |
docs |
docs.yml |
the zensical build including the api reference and the agent files |
tests aggregates the test matrix into a single job, so the name of the
required check stays the same when the matrix changes. On linux the CI
installs the python dev files before uv, because the roadrunner extension
links against libpython.
Further rules of a pull request:
- conversations have to be resolved before the merge
- an approval is dismissed when new commits are pushed
- the history stays linear, i.e., a pull request is merged with squash or rebase; merge commits are disabled
- the maintainer is the code owner of the repository (
.github/CODEOWNERS) and is requested for review on every pull request. A pull request of a contributor is therefore reviewed and merged by the maintainer, who has the only write access. The rulesets themselves do not require an approval: on a personal repository a ruleset cannot ask for an approval only from somebody else, and requiring one would block the pull requests of the maintainer, who cannot approve their own. Once a second person has write access, a ruleset requiring an approving review of a code owner can be added
Auto-merge is enabled for the repository, so a pull request can be queued and is merged as soon as the checks pass and the required approval is there.
Repository policies¶
The protection is implemented with
repository rulesets.
They are part of the repository in .github/rulesets/ instead of only living in
the web interface, so a change to a policy is reviewed like any other change:
| ruleset | applies to | rules |
|---|---|---|
develop.json |
develop |
pull request required, the four checks above, resolved conversations, linear history, no force push, no deletion. No bypass, for anybody. |
main.json |
main |
linear history, no force push, no deletion, no bypass. The fast-forward of the release workflow needs none, only a force push or a merge commit would be rejected |
tags.json |
all tags | a tag cannot be deleted or moved, so a release tag keeps pointing at what was released |
Changing a policy means changing the json and applying it:
The script is idempotent: it updates the rulesets which exist and creates the
missing ones. It also sets the merge settings of the repository, i.e.,
auto-merge, delete branch on merge, and squash and rebase as the only merge
methods, and it allows develop to deploy to the github-pages environment:
enabling GitHub Pages creates that environment with a deployment policy for the
default branch of that moment only, which rejects the deployments of the
documentation workflow from develop. It needs the
github cli authenticated as a user with admin
permission on the repository.
Setup development environment¶
Development needs uv and a checkout of the repository:
A single sync creates the virtual environment in .venv, installs sbml2cellml into it in editable mode and adds the complete tooling:
The dev extra contains everything used below, i.e., pytest, ruff, ty, tox, pre-commit, zensical and bump-my-version, together with the simulate extra (libopencor, pandas, matplotlib) and libroadrunner for the roundtrip tests. The python version is taken from .python-version (3.14, the newest supported version; 3.13 is supported as well).
The tools are then run either with uv run <command>, which uses the environment without activating it, or from the activated environment:
The commands in this document are written without the uv run prefix; prepend it if the environment is not activated.
The last step installs the git hook:
uv run pre-commit install # install the hook, once per checkout
uv run pre-commit run --all-files # check the current state of the repository
From now on every commit is checked with ruff (lint and format) and ty, i.e., the same checks that run in continuous integration. On a commit only the changed files are looked at, --all-files checks the whole repository and is what a newly added hook should be tried with.
Testing¶
The tests are written with pytest, tox runs them against the supported python versions.
The tox environments are py3.13, py3.14 and ty (see envlist in tox.ini); a test environment is run with
The tox environments are created from uv.lock by tox-uv (runner = uv-venv-lock-runner in tox.ini).
This needs the interpreters to be available, which uv installs with uv python install 3.13 3.14. Continuous integration runs the same environments as uvx --with tox-uv tox -e py3.13 and -e py3.14.
To run the tests directly against the development environment use
pytest # the full suite
pytest tests/test_cellml.py # a single module
pytest tests/test_cellml.py::test_read_model # a single test
The simulation tests and the examples need libopencor and are skipped without it; with uv sync --extra dev it is installed.
The roundtrip tests (tests/test_roundtrip.py) need roadrunner and are skipped without it. roadrunner and libopencor bundle different LLVM versions and crash once both have JIT-compiled in one process, so roadrunner runs in a subprocess (tests/simulators.py); the same rule shapes the process model of the SBML test suite harness.
Linting and formatting¶
Linting and formatting use ruff:
Type checking¶
Type checking is performed with ty:
Or directly in the working tree:The configuration lives in [tool.ty] in pyproject.toml. Warnings are treated as errors, so the codebase is kept free of diagnostics. Suppress an unavoidable diagnostic with a rule specific # ty: ignore[rule-name] rather than a blanket comment.
Documentation¶
The documentation is built with Zensical, the static site generator of the Material for MkDocs authors. The sources are markdown files in docs/, the site is configured in zensical.toml in the repository root. Nothing rendered is committed: the site is built by the documentation workflow on every push and published to matthiaskoenig.github.io/sbml2cellml from the develop branch. The workflow builds with --strict, so a warning such as a broken link fails the docs check.
Build the site into site/:
For writing, the preview rebuilds on save:
The API reference is rendered from the docstrings by mkdocstrings; a page in docs/api/ only contains the module directive:
Docstrings are therefore the place to document functions and classes, the markdown files provide the narrative around them. Adding a module to the reference means adding such a page and an entry to nav in zensical.toml.
Files for agents¶
Agents and language models read markdown, not rendered html. scripts/llms_txt.py writes the files of the llms.txt convention into the built site, i.e., llms.txt as an annotated index of all pages, llms-full.txt with the complete documentation in a single file, and the markdown of every page next to its html (/conversion-issues.md for /conversion-issues/). The markdown of the API reference is generated from the docstrings with inspect, since the pages themselves only contain the mkdocstrings directive.
The documentation workflow runs both steps, so the files are regenerated with every push. docs/robots.txt points crawlers at the sitemap and at these files. Zensical will provide agent context files itself at some point, then this script can go.
Repository setup¶
The one-time setup of the GitHub repository, for the record:
developis created frommainand made the default branch:gh repo edit matthiaskoenig/sbml2cellml --default-branch develop- the GitHub Pages source is set to GitHub Actions:
gh api -X POST repos/matthiaskoenig/sbml2cellml/pages -f build_type=workflow - the merge settings, the rulesets and the deployment branch of the documentation are applied:
.github/rulesets/apply.sh - the PyPI trusted publisher is registered on pypi.org for the project
sbml2cellml, ownermatthiaskoenig, repositorysbml2cellml, workflowci-cd.yml, environmentpypi(as a pending publisher before the first release) - the repository is enabled in the Zenodo GitHub integration, so that a GitHub release is archived with a DOI
SBML test suite¶
sbml2cellml.testsuite runs the SBML test suite through both converters and both simulators, so that every conversion gap is measured against a real corpus instead of a handful of examples. Each runnable case goes through five stages: the original SBML is simulated with roadrunner (roadrunner), converted to CellML (sbml2cellml), the CellML is simulated with libopencor (libopencor), converted back to SBML (cellml2sbml) and the roundtrip SBML is simulated again with roadrunner (roundtrip); every simulation is compared with the expected results of the case. A value passes when |value - expected| <= absolute + relative * |expected| at every time point, with the absolute and the relative tolerance of the settings of the case (NNNNN-settings.txt; the relative tolerance is 1e-4 for most cases, the absolute tolerance between 1e-9 and 0.15). Cases with an SBML package the converters do not support, without a level 3 version 2 file or of a test type other than TimeCourse are skipped.
downloads the suite into ~/.cache/sbml2cellml on first use (SBML2CELLML_CACHE overrides the cache root), runs the pipeline and writes testsuite/results.json, docs/testsuite.md and its bar diagram docs/images/testsuite.svg (testsuite_dark.svg for dark backgrounds, also shown in README.md). --cases 00001,00002 restricts the run to a subset of case ids and --suite-dir points at a local copy of the semantic/ directory instead of downloading. The tolerances of the comparison are not the tolerances of the solver: every roadrunner and libopencor simulation integrates with tight solver tolerances (1e-9 relative, 1e-12 absolute), so the comparison measures the conversion rather than the default integrator tolerances, an amendment to the original harness design. CVODE gives up on some models with tolerances this tight (CV_TOO_MUCH_WORK, CV_CONV_FAILURE, CV_ERR_FAILURE); only then the simulation is repeated with 1e-8/1e-10 and 1e-7/1e-9 (SOLVER_SETTINGS of sbml2cellml.testsuite.runner), and the failure with the tight tolerances is reported when the model integrates with none. Looser tolerances for every simulation are no alternative: with 1e-8/1e-10 4 cases of the test suite and 22 stages of the BioModels check which pass would fail, with 1e-6/1e-8 78 and 123. Both simulators may take 100000 internal steps between two time points; the 500 steps of libopencor ended the integration of 147 curated models of BioModels.
testsuite/results.json, docs/testsuite.md and the figures docs/images/testsuite*.svg are generated and committed. tests/test_testsuite_full.py (enabled with SBML2CELLML_TESTSUITE=1, run with tox r -e testsuite and in the linux CI job of python 3.14; the pipeline runs with python 3.14 only, the unit tests with every supported python) reruns the full suite and fails if any case regresses against the committed results or if the rendered report no longer matches docs/testsuite.md. The failure reasons of the report list every failing case with its complete error: the message of a stage in testsuite/results.json is the full text of the exception with all its lines, only absolute paths are reduced to the file name and the long decimals of a CVODE diagnostic rounded, so that the committed files do not depend on the machine. To accept an improvement, rerun uv run sbml2cellml-testsuite run and commit the updated testsuite/results.json, docs/testsuite.md and figures together in the same pull request. uv run sbml2cellml-testsuite report rerenders the report and the figures from the committed results, e.g., after a change of the report itself.
roadrunner and libopencor bundle different LLVM versions and crash once both have JIT-compiled in one process (see Testing); the harness therefore runs each simulator in its own worker process (sbml2cellml.testsuite.worker) for the whole run, instead of starting a subprocess per call.
BioModels¶
sbml2cellml.biomodels runs the manually curated SBML models of BioModels (about 1075) through the pipeline of the SBML test suite, reusing sbml2cellml.testsuite, so the converters are measured against published models in addition to the test cases. These models have no expected results: the roadrunner stage simulates the original SBML with roadrunner over a generic timecourse (0 to 100 time units, 100 steps), and its result is what the libopencor and roundtrip simulations are compared with, using the comparison of the test suite with a relative tolerance of 1e-3 and an absolute tolerance of 1e-6 (RELATIVE and ABSOLUTE of sbml2cellml.biomodels.cases); the solver settings are the ones of the test suite (1e-9/1e-12, relaxed only when CVODE fails). A roadrunner failure means roadrunner cannot simulate the model, it says nothing about the converters; models with an SBML package or without a variable (no species and no target of a rate rule or assignment rule) are skipped.
downloads every model into ~/.cache/sbml2cellml/biomodels on first use (cached for later runs), runs the pipeline, prints the regressions and improvements against the committed results and writes biomodels/results.json, the page BioModels (docs/biomodels.md) and its bar diagram docs/images/biomodels.svg (biomodels_dark.svg for dark backgrounds, also shown in README.md). --ids BIOMD0000000001,BIOMD0000000012 and --count N restrict the run to a subset or the first N ids of the selection, for a quick check; give such a run its own --results and --report. uv run sbml2cellml-biomodels report rerenders the report and the figures from the results file.
The check is run locally and not in continuous integration: it takes about 20 minutes and depends on the BioModels web service. tox r -e biomodels runs it in the locked environment with python 3.14, the only python the pipeline runs with; arguments follow --, e.g., tox r -e biomodels -- --count 10 --results /tmp/results.json --report /tmp/report.md. The generated files are committed; rerun the check after changes of the converters, review the regressions and commit the regenerated files with the change.
uv run sbml2cellml-biomodels update refreshes the committed selection biomodels/models.json (the date, the search query and the sorted ids) from the current BioModels search; it is run occasionally, not with every check.
Release¶
A release is made from develop. Since develop only accepts pull requests,
the release is prepared on a branch and tagged once that pull request is merged:
- branch off
develop:git switch -c release/x.y.z develop - write the release notes for the version in
docs/release-notes/x.y.z.mdand add the page to theRelease notessection ofnavinzensical.tomland to the overviewdocs/release-notes/index.md, newest first. The notes are part of the documentation and the body of the GitHub release;tests/test_package.pyfails when the current version has no notes or a page is missing in the navigation or the overview - make sure everything passes:
tox run-parallel,ruff check,tox r -e ty - check the version bump:
uvx bump-my-version bump [major|minor|patch] --dry-run -vv - bump the version:
uvx bump-my-version bump [major|minor|patch], which updatessrc/sbml2cellml/__init__.pyandCITATION.cffand commits. It does not create the tag; a squash or rebase merge would rewrite the commit and leave the tag behind on a commit which is not part ofdevelop - push the branch, open the pull request against
developand merge it once the checks are green -
tag the merged commit on
developand push the tag:This starts the
CI-CDworkflow, which runs the test matrix, publishes to pypi, creates the GitHub release fromdocs/release-notes/x.y.z.mdand fast-forwardsmainto the tagged commit. Check the version before pushing, a tag cannot be moved or deleted afterwards. -
test the installation from pypi in a fresh environment:
-
once Zenodo has archived the release, update the citation information, i.e.,
date-releasedinCITATION.cffand the version, date and version DOI of the release in the citation ofREADME.mdanddocs/index.md.bump-my-versiononly updates the version, not the date and the DOI, which are only known after the release. These changes go in through a pull request like everything else