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:

  1. Write a test for the functionality you want to add.

  2. 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.ops API (see the Keras ops documentation), instead of using backend-specific functions. For example, use keras.ops.squeeze instead of jax.numpy.squeeze or torch.squeeze. Also, when converting tensors to numpy arrays, use the keras.ops.convert_to_numpy function (instead of my_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:

  1. Add the paper to docs/source/references.bib, using a <surname><year><first title word> key (paper/paper.bib is only for zea’s own citation and the papers-using-zea list).

  2. 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.

  3. 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.

  1. 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 zea as the first cell, and all cells run in order.

  2. 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, intermediate or advanced.

  3. 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 with fill_value=np.nan so everything outside the cone becomes transparent.

  4. Check it: pre-commit run --files <notebook>, then make docs-serve in docs/ 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.