Skip to content

Implement Manim 2D backend for CBFAnimator - #367

Merged
bardhh merged 6 commits into
bardhh:mainfrom
Eduard-Zippenfenig:feature/manim-2d-backend
Jul 28, 2026
Merged

Implement Manim 2D backend for CBFAnimator#367
bardhh merged 6 commits into
bardhh:mainfrom
Eduard-Zippenfenig:feature/manim-2d-backend

Conversation

@Eduard-Zippenfenig

@Eduard-Zippenfenig Eduard-Zippenfenig commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the 2D Manim backend for CBFAnimator, filling the gap where backend="manim" was accepted but raised NotImplementedError ("Manim 2D backend not yet implemented").

CBFAnimator(states, backend="manim") (or "manim-<low|medium|high|production>") now renders the full declarative element API to MP4 (or GIF) via Manim — reaching parity with the existing matplotlib and plotly backends while adding render-quality tiers.

Demo

Rendered from the README quick-start CBF example (unicycle reach-avoid) with backend="manim-medium":

Manim 2D animator demo

The existing high-level path works as a drop-in: visualize_crowd(..., backend="manim") renders pedestrians with safety zones and fading predictions with no other changes.

Backend parity

The new backend renders the same declarative element API as the existing ones — nothing new to learn, just a new backend= value:

Element / feature matplotlib plotly manim (new)
Goals
Obstacles (circular + elliptical)
Trajectories (line + scatter)
Agents (body + safety zone)
Trails (solid + dashed)
Predictions (linear + data)
Time overlay
Output format MP4 / GIF interactive HTML MP4 / GIF
Render-quality tiers low / medium / high / production

Benchmark

Same 500-frame CBF scene rendered through each backend with identical content (Python 3.11, manim 0.20.1, single machine — indicative, your hardware will differ). Reproduce with python benchmarks/animator_backend_comparison.py:

Backend Render time Output size Format
plotly 0.4 s 6.0 MiB HTML (interactive)
manim-low 1.4 s 37 KiB MP4
manim-medium 2.7 s 74 KiB MP4
matplotlib 4.1 s 223 KiB MP4
manim-high 5.5 s 156 KiB MP4 (1080p60)

Every Manim tier produces a dramatically smaller file than matplotlib (37–156 KiB vs 223 KiB); manim-low and manim-medium also render faster, while manim-high trades render time for full 1080p60 output. plotly stays fastest for interactive HTML, at a much larger payload.

Design

Mirrors the existing 3D module (manim_3d_multi_robot.py) throughout:

  • Guarded try: import manim with Scene = object fallback — the module imports cleanly when the manim extra is absent (as in CI); the constructor raises the standard cbfkit[manim] ImportError via _require_manim().
  • Data injected into CBFAnimator2DScene via class attributes before scene.render(); animation driven by a ValueTracker + updaters.
  • Reuses the established manim-<quality> backend-string convention and _parse_manim_backend — no new config fields. Invalid quality strings raise ValueError before the dependency check.
  • White canvas so matplotlib-convention colors (e.g. black obstacles) stay visible; matplotlib-style color names translated to hex.
  • Render intermediates go to a temp dir (scoped with tempconfig + disable_caching, so repeated renders in one process stay isolated); the finished video is copied to the requested path.
  • _ManimMixin added to CBFAnimator alongside the matplotlib/plotly mixins; dispatch added to build/animate/save/show. Also adds a [mypy-manim.*] stanza to mypy.ini.

Tests

  • Dependency-free (run in CI without manim): quality parsing for all five backend strings, import-guard ImportError, invalid-quality ValueError, save() dispatch with a mocked render, and TestPredictionComputation — the shared _compute_prediction core (linear constant-velocity math, degenerate zero-velocity, data row selection, out-of-range fallback).
  • A real-render smoke test (skips without manim) covering goals / obstacles / agents / trail / time / predictions.
  • Full suite: 480 passed, 4 skipped locally (Python 3.11, manim 0.20.1); ruff check src clean; changed files formatted with black 25.11.0 (pre-commit pin).

Reproduce

The demo GIF regenerates from a committed example:

pip install cbfkit[manim]      # + ffmpeg; on macOS also: brew install cairo pango
python examples/unicycle/reach_goal/manim_2d_animation.py

Runs the README quick-start CBF simulation (unicycle reaches a goal while a CBF filter keeps it clear of an obstacle) and renders it with backend="manim-medium" — the exact script behind media/showcase/manim_2d_animator.gif.

Eduard-Zippenfenig and others added 3 commits July 18, 2026 18:08
Fills the declared gap where backend='manim' raised NotImplementedError.
Adds _ManimMixin + CBFAnimator2DScene rendering the full declarative
element API (goals, obstacles, trajectories, agents, predictions, time
overlay) on a white canvas, with quality selected via the existing
manim-<quality> backend-string convention. Renders MP4 or GIF; module
imports cleanly without manim installed (guarded like the 3D backend).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ender

Replaces the NotImplementedError pin with quality-parsing, import-guard,
and save-dispatch tests (dependency-free) plus a real-render smoke test
that skips when manim is not installed (as in CI).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Demo renders the README quick-start CBF example (unicycle reach-avoid)
via backend='manim-medium'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Eduard-Zippenfenig
Eduard-Zippenfenig marked this pull request as draft July 18, 2026 15:30
…mple

Repeated renders in one process crashed because Manim's global config and
animation cache outlived each render's temp media dir. Scope overrides with
tempconfig and disable caching; add a regression test.

examples/unicycle/reach_goal/manim_2d_animation.py reproduces the showcase
GIF from the README quick-start simulation (CBFKIT_TEST_MODE-gated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bardhh
bardhh marked this pull request as ready for review July 21, 2026 17:35

@bardhh bardhh left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Eduard-Zippenfenig .

One small change first:

  1. Please add a prediction case to the smoke test. Right now test_manim_real_render_smoke covers goals/obstacles/agents/trail/time, but the per-frame prediction path (_prediction_group + compute_prediction) isn't tested. Something like this would test that as well (placed before a.save():

a.add_prediction(
source="linear",
agent_x_idx=0, agent_y_idx=1,
agent_vx_idx=2, agent_vy_idx=3,
horizon=5,
)

Thanks for your contributions!

Add the maintainer-requested add_prediction() call to
test_manim_real_render_smoke so the per-frame prediction path
(_prediction_group + compute_prediction) is exercised during a real render.

Also add TestPredictionComputation, a dependency-free unit test of the
backend-shared CBFAnimator._compute_prediction, so the prediction logic is
verified in CI (the smoke test skips when the manim extra is absent). Covers
linear constant-velocity math, the degenerate zero-velocity case, "data"
source row selection, and the out-of-range fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Eduard-Zippenfenig

Copy link
Copy Markdown
Contributor Author

Thanks so much for taking a look, and for the quick turnaround! Made the change in fa1854f.

The smoke test. Dropped your add_prediction(...) snippet in exactly as suggested, right before a.save(), so the prediction path (_prediction_group + _compute_prediction) now gets exercised in a real render.

One small thing I noticed while wiring it up. The simple_states fixture is laid out as (x, y, v, theta), so agent_vx_idx=2, agent_vy_idx=3 actually point at the speed column (all zeros here) and the heading angle rather than real vx/vy. At frame 0 those are both 0, so the five predicted points all land on the agent's current position — effectively four zero-length segments. I was a little worried that'd upset Manim, but I checked the render and it handles it gracefully (no crash, no degenerate-mobject warnings), so I left it without a guard — the path still gets fully exercised, which is the important part. That said, if you'd rather see an actual prediction fan in the rendered output, I'm happy to point the fixture at real velocity columns. Just let me know which you'd prefer.

One extra I hope is useful. I noticed the smoke test skips whenever the manim extra isn't installed — which is the case on CI — so on its own the snippet wouldn't really add coverage there, and _compute_prediction didn't have any tests elsewhere. So I also added TestPredictionComputation: a dependency-free unit test of the shared _compute_prediction core (the same one matplotlib and plotly go through), placed before the module's importorskip so it always runs. It covers the linear constant-velocity math, the degenerate zero-velocity case (pinning that frame-0 behaviour so it can't silently change), the data trajectory-row path, and the out-of-range fallback. Totally happy to trim it back if it feels like more than you wanted here.

Quick check: the four new tests pass, the smoke test renders (rather than skips) with the prediction in place, and the full test_animator.py comes in at 49 passed locally (Python 3.11, manim 0.20.1).

Thanks again for the review — really appreciate you making time for it.

Renders one CBF scene (unicycle reach-avoid) through plotly, matplotlib,
and the manim-low/medium/high tiers, reporting wall-clock render time and
output file size. Reproducible source for the PR benchmark table. Skips
rows whose optional deps are absent; honors CBFKIT_TEST_MODE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@bardhh bardhh left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Eduard-Zippenfenig — this addresses it exactly, and TestPredictionComputation was a good call given the smoke test skips on CI. On your question: let's keep the degenerate frame-0 prediction as-is — the smoke test's job is exercising the render path, and your unit test already pins the real math where CI can see it. Merging once the checks finish. Appreciate the careful work on this one, including the repeated-render fix and the benchmark script.

@bardhh
bardhh merged commit 2bac40b into bardhh:main Jul 28, 2026
4 checks passed
@Eduard-Zippenfenig
Eduard-Zippenfenig deleted the feature/manim-2d-backend branch July 30, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants