Clojure · Emmy · Manim

desargues

Symbolic mathematics in, mathematical animation out. Emmy computes, LaTeX carries, and a backend-neutral scene facade renders — to Manim video, or to pure data a browser can play.

Every scene on this page was recorded by desargues when the page was built.

A scene is a function of the stage100200300play! / hold! against a protocol, never against Manim

One construct

(defn construct [stage]
  (let [circles (mapv (fn [[x c]] (-> (s/circle :radius 1.0 :color c)
                                      (s/move-to [x 0])
                                      (s/fill! c :opacity 0.25)))
                      [[-3.6 :teal] [0 :gold] [3.6 :green]])
        nums    (mapv #(s/next-to (s/decimal 0) % :down) circles)]
    (s/play! stage (s/stagger (map s/draw circles) :lag-ratio 0.3))
    (s/play! stage (s/together
                     (map-indexed #(s/count-to %2 (* 100 (inc %1))) nums)))
    (s/play! stage (s/together (map #(s/glide % [0 0]) circles)))
    (s/hold! stage 1)))

Two backends

;; Manim: writes scene.mp4 (the lazy default backend)
(s/render! "scene" construct)

;; Pure data, no Python: the scene on the previous slide
(s/with-backend (rec/recording-backend)
  (s/render! "scene" construct))
;; => {:scene "scene" :nodes {1 {:node :circle ...} ...}
;;     :steps [{:step :play :anims [...]} ... {:step :hold :seconds 1}]}

The facade is desargues.scene; the seam is desargues.scene.protocols. Swap the backend, keep the scene.

Layout algebraBank AΔM = 1000Reservesr · Dmeasure → resolve → realize, on any backend

Layout algebra

(v/render-layout! "balance"
  (v/column {:padding 0.8 :spacing 0.5 :align :center-x}
    (v/text "Layout algebra" {:font-size 44 :color :gold})
    (v/row {:width :fill :spacing 0.6}
      (v/box {:width [:portion 1] :border :white}
        (v/text "Bank A")
        (v/text "ΔM = 1000" {:color :teal}))
      (v/box {:width [:portion 1] :border [:teal 3]}
        (v/text "Reserves")
        (v/text "r · D" {:color :gold})))))

column / row / box / text / math / image with :fill, [:portion n] and padding: measure → resolve → realize, on either backend.

Install

A git coordinate. The pure half runs on the JVM alone.

;; deps.edn
{:deps {io.github.mentat-collective/desargues
        {:git/tag "v0.1.1" :git/sha "6d26609"}}}

Or from a clone: git clone https://github.com/mentat-collective/desargues && cd desargues && bb doctor

The Manim backend

One conda env. desargues derives every path from CONDA_PREFIX; nothing is hardcoded.

conda create -n manim -c conda-forge python=3.12 manim
conda activate manim          # sets CONDA_PREFIX; desargues.config picks it up
clojure -M:doctor             # JVM ok? Manim env ok? LaTeX? ffmpeg?
Not the active env? Set DESARGUES_CONDA_PREFIX. Any single path can be overridden with DESARGUES_MANIM_PYTHON / _LIBPYTHON / _SITEPACKAGES.

Sixty seconds, no Python

(require '[desargues.scene :as s]
         '[desargues.scene.data :as rec]
         '[desargues.layout.core :as l])

(rec/layout->scene-graph "hello"
  (l/column {:padding 0.6 :spacing 0.4 :align :center-x}
    (l/text "Reserves")
    (l/math "\\Delta M = 1000"))
  {})
;; => {:scene "hello" :nodes {1 {:node :text ...} 2 {:node :math ...}}
;;     :steps [{:step :play ...} {:step :hold :seconds 3}] ...}

The Emmy conveyor

Collect the symbolic facts, promote them to LaTeX. This line was computed by the build:

\(\frac{d}{dx}\,\mathsf{exp}\left(\sin\left(x\right)\right) = \mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right)\)

(pipe/derivative-spec (fn [x] (e/exp (e/sin x))))
;; => {:expr            (exp (sin x))
;;     :derivative-expr (* (cos x) (exp (sin x)))
;;     :func-latex      "e^{\\sin\\left(x\\right)}"
;;     :deriv-latex     "\\cos\\left(x\\right)\\,e^{\\sin\\left(x\\right)}"}

Differentiate until it stops being obvious

Four derivatives of \(e^{\sin x}\), each computed and simplified by Emmy when this page was built:

\(f(x) = \mathsf{exp}\left(\sin\left(x\right)\right)\)

\(f'(x) = \mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right)\)

\(f''(x) = \mathsf{exp}\left(\sin\left(x\right)\right)\,{\cos}^{2}\left(x\right) - \sin\left(x\right)\,\mathsf{exp}\left(\sin\left(x\right)\right)\)

\(f'''(x) = - {\sin}^{2}\left(x\right)\,\mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right) - 3\,\sin\left(x\right)\,\mathsf{exp}\left(\sin\left(x\right)\right)\,\cos\left(x\right)\)

f(x) = sin x, and Emmy's f'(x) = cos x as a slopef'(x) =0.96

The Taylor series, term by term

Emmy's taylor-series is a lazy stream of terms; the deck takes the first five:

\(e^{\sin x} = 1 + x + \frac{1}{2}\,{x}^{2} - \frac{1}{8}\,{x}^{4} + \cdots\)

(def f (fn [x] (e/exp (e/sin x))))

(mapv #(e/->TeX (e/simplify ((nth (iterate e/D f) %) 'x))) (range 4))
;; f, f', f'', f'''  as LaTeX, ready for KaTeX or for Manim's MathTex

(take 5 ((e/taylor-series f 0) 'x))
;; => (1 x (* 1/2 (expt x 2)) 0 (* -1/8 (expt x 4)))

Trajectories behind a port

TrajectorySolver is a protocol. Clojure RK4 ships; the :dynamics alias adds raster's Tsit5 and DP5, resampled onto the frame grid.

(require '[desargues.videos.physics :as phys]
         '[desargues.infrastructure.raster-adapter :as ra])

(def pendulum (phys/make-pendulum {:length 3.0 :damping 0.1}))

(phys/evolve pendulum {:dt 0.01 :duration 10.0})                          ; RK4 in Clojure
(phys/evolve pendulum {:dt 0.01 :duration 10.0 :solver (ra/raster-solver :tsit5)}) ; adaptive, resampled onto the dt grid
Damped pendulum: phys/evolve, RK4 at dt = 0.018.0s
3D double pendulum: Emmy derives the equations, RK4 integrates8.0s

Emmy derives the equations of motion

Write the bob positions as functions of the coordinates; Emmy differentiates the Lagrangian for you. The scene you just saw is this system, integrated.

(require '[desargues.videos.lagrangian :as lag])

;; Cartesian positions of the two bobs as functions of q = [th1 ph1 th2 ph2]
(def sys (lag/spherical-double-pendulum {:l1 1.4 :l2 1.2 :theta1 1.2 :theta2 2.2}))

;; L = T - V is built from those positions; Emmy's
;; Lagrangian->state-derivative turns it into q'' and the compiler makes
;; it fast. Then it is just another PhysicalSystem:
(phys/evolve sys {:dt 0.01 :duration 8.0})
(lag/positions-at sys (:state (last *1)))   ; => [[x1 y1 z1] [x2 y2 z2]]

Benchmarks

log-log chart: layout resolve, scene-graph expansion and recording render time versus size
Pure backend: linear in the number of elements and animations.

Solvers

bar chart: pendulum trajectory time per solver
A damped pendulum, 10 s at dt = 0.01, per solver.

Emmy conveyor

bar chart: differentiate and render to LaTeX, per function
Differentiate and render both to LaTeX, per function.

Measure it yourself

bb bench            # clojure -M:bench:dynamics
bb bench --manim    # adds a Manim render per quality (needs the env)

Writes bench/results/latest.edn and regenerates these charts under doc/bench/.

Layered, dependency arrow inward

desargues.api

High-level facade: expr, func, derivative, animate-derivative, render-layout!

desargues.scene

Backend-neutral animation DSL — the DIP seam (desargues.scene.protocols)

scene.manim / scene.data

Two backends: libpython-clj → Manim, or an EDN recorder

desargues.layout

elm-ui layout algebra with a pure extent estimator

desargues.domain

Pure Clojure + Emmy, Typed-Clojure annotated

desargues.config

Everything about the environment, resolved once at the boundary