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 becauseuv sync --all-extrasincludes thepangoextra 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 withPackage 'pangocairo' was not found. A plainpip install alganneeds none of this – see Installation.LaTeX — required for
TexandText. See the LaTeX step for what a minimal installation needs.FFmpeg — not needed to render video, which uses the binary bundled with
imageio-ffmpeg, but the documentation build shells out to a systemffmpeg.
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/pythonWindows:
.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¶
Pick a commit on
masterthat 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.Bump
[project] versionand 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.Open the
master->stablepull request and let both workflows finish.Dispatch the Release workflow with
dry_runon. It re-checks the version, the tag, that green run and the baseline pointer, then builds the distributions and the docs without publishing anything.Dispatch it again with
dry_runoff. It fast-forwardsstable, tags the released commitv<version>–v0.0.0, notBETA_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.