Contributing¶
zea is an open-source project and we welcome contributions from the ultrasound community. Whether you want to report a bug, suggest a new feature, or contribute code, your input is valuable. This document outlines how you can contribute to the project. In case you have a general question or want to discuss an idea before working on it, please feel free to open an issue on GitHub or join the discussion in the GitHub Discussions.
How to contribute¶
First, take some time to read through this whole guide. Then follow the following steps to contribute:
1. Make an issue¶
Take a look at the current issues on GitHub to see if there are any open issues that you can help with. If your issue is not listed, please create a new issue describing the motivation behind the problem or feature you want to work on. That way we can discuss it and make sure that your contribution is aligned with the project’s goals.
Example Issue Template
**Title:** [Bug/Feature/Question]: Brief summary
**Description:**
- For problems or bugs
- What is the problem?
- Steps to reproduce
- Expected behavior
- Actual behavior
- For feature requests
- What is the feature and why is it needed?
- Possible interface or implementation details
- References (if applicable)
- Link to relevant documentation, discussions, or related issues.
2. Fork the repository¶
Most contributors will not have write access to the main repository. Instead, you will need to fork the repository to your own GitHub account. You can fork the repository on GitHub by clicking the “Fork” button at the top right of the page. This will create a copy of the repository under your GitHub account. One can also use:
gh repo fork tue-bmd/zea --clone
3. Setup environment¶
Check out a new branch for your changes.
cd zea
git checkout -b <your_feature_branch_name>
We recommend using Docker for development as it ensures consistency and avoids
dependency issues — the images ship with the backends pre-installed. If you prefer
a local environment, use uv or pip.
See the Docker section of the installation guide
for build and run instructions. Images built with DEV=true (the default)
already include the dev dependency-group (tests, docs and lint tools).
Every shell prints a message of the day ending in the uv sync command for
that image. Use that one rather than copying from here: it names the backend
groups the image was built with, and uv sync removes any group you leave
out. Run it whenever uv.lock changes, after a rebase or a backend bump, to
update a running container without rebuilding it.
On a DEV=false image the command also has --no-default-groups. Drop that
flag to add the dev group. For zeahub/all it becomes:
uv sync --group jax-gpu --group torch-gpu --group tf-gpu
pre-commit install
From the repository root run:
uv sync
uv run pre-commit install
This creates a .venv with the exact locked dependencies and installs
zea itself in editable mode, so your changes to the source take
effect immediately without reinstalling. This will also install the dev
dependency-group, i.e. the tests, docs and lint groups. Prefix commands
with uv run (e.g. uv run pytest) or activate the environment with
source .venv/bin/activate.
Backends are groups too, so add the ones you want — cpu or gpu, and a
single backend is enough to run the test suite:
uv sync --group jax-cpu # or jax-gpu, torch-cpu, tf-gpu, ...
Install into any existing environment (venv, conda, …) with plain
pip (requires pip >= 25.1 for --group support). The -e flag makes
the install editable:
pip install -e . --group dev
pre-commit install
The Docker images already come with a machine learning backend. For the uv and pip
routes you need one of JAX, PyTorch or TensorFlow: with uv use the backend groups shown
above, with pip install it yourself — see the
backend installation guide.
4. Make your changes¶
In general, we recommend a test-driven development approach:
Write a test for the functionality you want to add.
Write the code to make the test pass.
Tests are divided into three groups:
Group |
Description |
|---|---|
light |
Fast unit tests. Run these on every change. |
heavy |
Resource-intensive tests, requires a GPU. |
notebook |
Runs notebooks end-to-end, requires a GPU. |
Run the light tests (what CI runs on every push):
pytest -m 'not heavy and not notebook'
When working locally you may not have all ML backends installed. Skip tests that require a missing backend with:
pytest -m 'not heavy and not notebook' --skip-unavailable-backends
Choosing the backend and device
Tests run on TensorFlow by default. They use one free GPU when one is available and
fall back to CPU otherwise. KERAS_BACKEND is ignored; use these environment
variables instead:
# Backend: tensorflow, jax or torch
ZEA_TEST_BACKEND=jax pytest -m 'not heavy and not notebook'
# Device: auto:1 (default), cpu, gpu:0, auto:-1 (all GPUs), ...
ZEA_TEST_DEVICE=cpu pytest -m 'not heavy and not notebook'
# or a specific test file or function, e.g.:
ZEA_TEST_BACKEND=jax ZEA_TEST_DEVICE=gpu:0 pytest tests/test_io_lib.py::test_load_video_gif
Parts of tests that only apply to other backends are skipped and counted in the summary. Cross-backend comparisons always run every installed backend on CPU, whichever backend you pick.
Heavy tests
Heavy tests require significant compute and are best run on a machine with a GPU. They are skipped in the light test run automatically.
pytest -m 'heavy'
Notebook tests
Notebook tests execute every .ipynb under docs/source/notebooks/ end-to-end
using papermill. They are split into groups
matching the CI matrix:
# All notebooks
pytest -s -m 'notebook'
# One subfolder only (agent / models / pipeline / data / metrics)
pytest -s -m 'notebook' --notebook-dir agent
pytest -s -m 'notebook' --notebook-dir data --notebook-dir metrics
# A single notebook by name
pytest -s -m 'notebook' --notebook dbua_example.ipynb
Running a single test or test file
Pass any pytest node-id to run just one test:
# Single test function
pytest tests/test_tensor_ops.py::test_flatten
# All tests in a file
pytest tests/test_tensor_ops.py
# Match by keyword
pytest -k "beamform"
A few things to keep in mind when making changes:
Make sure to write backend-agnostic code. This means that your code should work with all supported backends. This can be achieved by using the
keras.opsAPI (see the Keras ops documentation), instead of using backend-specific functions. For example, usekeras.ops.squeezeinstead ofjax.numpy.squeezeortorch.squeeze. Also, when converting tensors to numpy arrays, use thekeras.ops.convert_to_numpyfunction (instead ofmy_tensor.numpy()) to ensure compatibility with all backends.The code is autoformatted using ruff and type-checked with ty. You can run the pre-commit hooks to automatically format, lint, and type-check your code using:
pre-commit run --all-files
5. Document your changes¶
The documentation uses Sphinx and generally is written in reStructuredText format. You can find the documentation files in the docs/source directory. Docstrings are written in Google style, which you can see examples of in the example_google_docstrings.py file. If you add new functionality, please make sure to document it in the documentation files. An example of a well structured docstring below:
Example docstring
def example_function(param1: int, param2: str) -> bool:
"""This is an example function that demonstrates a well-structured docstring.
Note the spacing and indentation between the sections, which is important
for the automatic docs generation. Also it is recommended to add an example
section with doctests, which will automatically test the code snippets in
the docs generation.
Args:
param1 (int): Description of the first parameter.
param2 (str): Description of the second parameter.
Returns:
bool: Description of the return value.
Raises:
ValueError: If param1 is negative.
Example:
Use ``.. doctest::`` with ``>>>`` prompts when you want to show
interactive input/output inline:
.. doctest::
>>> example_function(5, "test")
True
Alternatively use ``.. testcode::`` for blocks that don't fit
the interactive format:
.. testcode::
print(example_function(5, "test"))
.. testoutput::
True
"""
if param1 < 0:
raise ValueError("param1 must be non-negative")
return True
Citing papers. If your contribution implements a method, model, algorithm or
dataset from a paper, attribute the authors with the .. citation:: directive
instead of writing the reference out by hand:
Add the paper to docs/source/references.bib, using a
<surname><year><first title word>key (paper/paper.bibis only forzea’s own citation and the papers-using-zealist).Cite it from the docstring:
.. citation:: luijten2020adaptive
Pass several keys at once with
.. citation:: key1, key2, and add a note – a link to the reference implementation, say – as an indented block below.Build the docs; an unknown key is reported as a warning.
Keep any “this is a zea implementation” remark in its own .. important::
block, so the citation block holds nothing but the reference.
The overall structure of the documentation is manually designed, but the API documentation is auto-generated based on the docstrings in the code. To generate the docs locally you can run:
# if you didn't install the dependencies earlier
pip install -e . --group docs
cd docs
make docs-clean && make docs-build
# you can also serve the docs locally
make docs-clean && make docs-serve
# and open them in your browser at http://localhost:8000
See the README.md for more information.
6. Log your changes¶
Add a line about your change to the Changelog in docs/source/changelog.rst. Put it under the Current section, in the subsection that fits: New Features, Changed or Fixed. The release notes are generated from these entries, so write them for users: what changed and how it affects them, in one or two sentences.
Reference the API with roles, such as
:class:`zea.ops.Normalize`or:func:`zea.func.vmap`, so the entry links to its documentation.End with the pull request number and your GitHub handle:
(:gh:`681` by :user:`your-handle`). You can add the number once the pull request is open.Start breaking changes with “(Breaking)”. Deprecations go under Changed, starting with “Deprecate”.
Fixed
^^^^^
- :class:`zea.ops.Normalize` clips its output instead of its input, so results stay within ``output_range`` (:gh:`681` by :user:`tristan-deep`)
A check on each pull request looks for a changelog entry that ends with the pull request number and your handle. Changes that users will not notice,
such as CI, tooling or refactors, need none: a maintainer labels the pull request
skip changelog (dev and documentation work as well).
7. Make a pull request¶
Once your changes are ready and all tests pass, push your branch to your forked repository:
git add .
git commit -m "Description of your changes"
git push origin <your_feature_branch_name>
Then, go to the original repository on GitHub and open a Pull Request (PR) from your branch. The description starts from a template: explain what you changed and why as short bullets, reference any related issues, and go through the checklist.
The maintainers will review your PR and may request changes or ask questions. Please respond to feedback and update your PR as needed. Once everything looks good, your changes will be merged!
Note
Anyone can review pull requests, we encourage others to review each other’s work, however, only the maintainers can approve a pull request. Pull Requests require at least one approval and all tests passing before being able to merge it.
Thank you for contributing to zea!
Contributing topics¶
Beyond the general contribution workflow above, there are a few specific areas where we especially welcome contributions. Browse the open issues to see what is being worked on, or open a new one to propose something new.
Adding an example notebook¶
New example notebooks are always welcome! Each one shows up on the Examples page as a card with a thumbnail and tags.
Add the notebook to one of the groups in docs/source/notebooks. Copy the structure of an existing notebook: a title, the Colab and GitHub badges,
%pip install zeaas the first cell, and all cells run in order.Add tags and a level to the notebook metadata (Edit → Edit Notebook Metadata in Jupyter):
"zea": {"tags": ["beamforming", "cardiac"], "level": "beginner"}
Use lowercase tags, and reuse the ones already on the examples page where possible. The level is
beginner,intermediateoradvanced.Add a thumbnail at
docs/_static/thumbnails/<notebook name>.webp: the image only, without axes, titles or colorbars. Make it from the image array in a scratch cell (don’t commit it):import sys sys.path.insert(0, "../..") # docs/source from make_thumbnail import make_thumbnail make_thumbnail(image, "my_example", mode="fill") # rectangular image, cut to 4:3 make_thumbnail(image, "my_example", mode="cone") # sector scan, transparent background
For
mode="cone", scan convert withfill_value=np.nanso everything outside the cone becomes transparent.Check it:
pre-commit run --files <notebook>, thenmake docs-serveindocs/to see the card.
To link to an example outside zea (a blog post or another library’s docs),
add an external-example to docs/source/examples.rst with a thumbnail in
the same folder:
.. external-example:: Title of the example
:link: https://example.org/the-example
:thumbnail: my_external_example.webp
:source: Name of the library or blog
:tags: beamforming, inverse problems
:level: intermediate
Adding to zea.models¶
Please see the Adding a new model section for more information on how to add new models to zea.
Adding to zea.ops¶
Please see the Adding a new operation section for more information on how to add new ops to zea.