Development

This page explains how to set up Algan from source to work on Algan itself. If you just want to install a released version to make animations, follow Installation instead.

Have questions or want to discuss changes? Head over to our Discord server or the GitHub issue tracker.

System Dependencies

Install these before installing Python dependencies, as two of Algan’s packages compile native extensions from source:

sudo apt update
sudo apt install build-essential python3-dev pkg-config \
                 libpango1.0-dev \
                 texlive-latex-base texlive-latex-extra \
                 texlive-fonts-recommended latexmk \
                 ffmpeg
sudo dnf install gcc gcc-c++ python3-devel pkg-config \
                 pango-devel ffmpeg
# plus a LaTeX installation -- see the installation guide
brew install pkg-config ffmpeg
# plus MacTeX -- see the installation guide

Install MiKTeX and add an FFmpeg build to your PATH. Pango headers are not required because manimpango ships pre-built Windows wheels.

Why each dependency is needed:

  • A C compiler, Python headers, pkg-config and Pango headers — only for manimpango, which the development environment installs because uv sync --all-extras includes the pango extra and the render tests compare against Pango-typeset baselines. It publishes no Linux wheel, so it is compiled during the install; without the Pango headers that fails with Package 'pangocairo' was not found. A plain pip install algan needs none of this – see Installation.

  • LaTeX — required for Tex and Text. See the LaTeX step for what a minimal installation needs.

  • FFmpegnot needed to render video, which uses the binary bundled with imageio-ffmpeg, but the documentation build shells out to a system ffmpeg.

Getting the Source Code

Clone the repository and set up an editable environment using uv:

git clone https://github.com/algorithmicsimplicity/algan
cd algan
uv venv
uv sync --locked --all-extras --dev

uv sync installs Algan in editable mode along with all runtime, optional, and development dependencies pinned to the versions in uv.lock.

Important

Always pass --locked. Resolving without the lockfile may pull in newer releases of upstream dependencies (such as Torch or Manim) that have not been tested and could introduce breaking changes.

If you change a dependency in pyproject.toml, re-run uv lock and commit the updated uv.lock with your change.

PyTorch and Hardware Acceleration

The lockfile installs a CUDA build of PyTorch by default. Algan works without a GPU by falling back to CPU execution (and all tests pass on CPU), but rendering will be significantly slower. For a ROCm or a CPU-only build, install the wheel for your platform from https://pytorch.org/get-started/locally/ over the top, replacing pip3 with uv pip in the command PyTorch gives you.`.

Running the Interpreter

Run commands through uv run (e.g. uv run pytest -q --fast) or use the virtual environment’s Python interpreter directly:

  • Linux / macOS: .venv/bin/python

  • Windows: .venv\Scripts\python.exe

Do not use your system Python, as it will not have the locked virtual environment packages.

Testing

We provide two testing loops:

uv run pytest -q --fast   # Fast development loop (~1 minute)
uv run pytest -q          # Full test suite (~12 minutes)

Run --fast after every code change, and run the full test suite before opening a pull request.

The --fast suite runs a curated set of ~190 tests covering the core animation pipeline, scene management, timeline materialization, and a deterministic pixel-compared render test.

Note

Render tests compare generated frames against baselines committed in expected_outputs_cuda/ or expected_outputs_cpu/. Because CPU and GPU rasterization differences are expected, baseline files are maintained separately for each backend. macOS is keyed separately again (expected_outputs_macos_cpu/) and ships no baselines: the x86-64 CPU set was measured against an Apple Silicon runner and missed by up to 45 channel values, against a tolerance of 2, so a path tracer’s float32 arithmetic does not survive the change of instruction set. On a Mac the render still runs; only the pixel comparison is skipped.

Updating Baseline Videos

When a change you’ve made legitimately and intentionally alters rendered output, regenerate the baselines for your device and look at the result before committing it:

ALGAN_UPDATE_FAST_BASELINE=1 uv run pytest -q tests/fast
ALGAN_UPDATE_FULL_RENDER_BASELINES=1 uv run pytest -q tests/full_renders

If a comparison fails, a diff video will be produced in that suite’s output_errors/ dir. Small deviations (a channel or two) across runs are expected and tolerated; anything larger is a real change and needs an explanation in the pull request.

Documentation

uv run python docs/make_and_open_docs.py

This renders every embedded example video, so it is quite slow. For a structural or autodoc-only check:

uv run python docs/make_and_open_docs.py --skip-examples --no-open

Docstrings on the public API follow DOCSTRINGS.md; read it before writing or editing one.

Documented code is tested

tests/unit_tests/test_doc_examples.py extracts every Python block in docs/source and checks it, so a renamed API cannot quietly leave the tutorials behind. Two of its three tiers run whenever the suite does: a static pass over every block, and an execution pass over the blocks that are complete scripts with rendering stubbed out. The third tier actually renders them and is opt-in behind ALGAN_RUN_DOC_RENDERS=1.

Prefer .. algan:: over .. code-block:: python when an example is a complete script and the video will get the point across faster than text can. Keep code-block for fragments, for anti-examples, and for anything needing an asset the repository does not carry; mark those last two so the test skips them:

.. algan-doc-check: skip -- needs an asset that does not ship with the docs

.. code-block:: python

The marker is an reStructuredText comment, so it never reaches the rendered page. tests/README.md documents the tiers and when to reach for each.

Linting

uv run ruff check --no-fix
uv run ruff format --check

Warning

Ruff is configured with fix = true, so a bare ruff check rewrites your files. Pass --no-fix unless you mean to apply the fixes.

Never let a formatter touch *_taichi.py. The from __future__ import annotations it inserts breaks Taichi kernel compilation. Those files are excluded in the Ruff configuration, which is why every kernel module’s name has to end in _taichi.

Opening a pull request

.github/pull_request_template.md is the layout, and it asks for the things a diff cannot show: what the change is for, whether rendered output moved, which suites you ran and on what hardware, and which documentation pages moved with it.

Versioning and releases

Where the version lives

[project] version in pyproject.toml is the single source of truth. algan.__version__ resolves lazily through importlib.metadata, so it reports what is installed rather than what is in the working tree – in an editable install the two agree only after a re-sync.

What the number promises

Algan is pre-1.0, so the middle number carries the breakage: while the version is 0.x.y, an x bump may change or remove public API and a y bump may not. The public surface is what algan.__all__ exports, the algan.manim compatibility layer, the SETTINGS sections and their public fields, and the documented CLI. Explicitly outside it, and free to move in any release: the experimental settings section, the ALGAN_ kernel and performance gates (see Settings), anything named with a leading underscore, and algan.external_libraries.

Rendered output is not covered by the version at all. A renderer change that moves pixels within the baselines’ tolerance is a patch release like any other; what changes with it is the expected-output set, not the API.

Branch flow

master is where development happens. stable carries the latest released version, and a release is a pull request from master into stable. .github/workflows/test.yaml and docs.yaml both list stable as a trigger branch precisely so that PR is gated: pull_request filters on the base branch, so leaving it out would skip CI at the moment it matters most.

Cutting a release

  1. Pick a commit on master that has its own green run in the Actions tab. A green run on a later commit does not vouch for an earlier one, and the release is cut from a commit, not from a branch tip.

  2. Bump [project] version and run the full suite (uv run -m pytest -q, not --fast) on a machine with a GPU if the release touches the renderer – no CI leg has one.

  3. Open the master -> stable pull request and let both workflows finish.

  4. Dispatch the Release workflow with dry_run on. It re-checks the version, the tag, that green run and the baseline pointer, then builds the distributions and the docs without publishing anything.

  5. Dispatch it again with dry_run off. It fast-forwards stable, tags the released commit v<version>v0.0.0, not BETA_v0.0.63, which is the one legacy tag and is not the pattern to follow – publishes the docs, creates the GitHub release and uploads to PyPI, in that order.

The steps are ordered by how hard each is to undo, and PyPI is last because it is the only one that cannot be taken back. Nothing in the release edits the tree it releases, so the version bump has to land in the pull request above. RELEASE_RUNBOOK.md covers the one-time account setup behind it.

A version that has been uploaded to PyPI cannot be edited, only yanked, so the metadata in pyproject.toml – URLs, classifiers, license files, dependency bounds – is worth re-reading in the release PR rather than after it.