|
| 1 | +--- |
| 2 | +name: tensor-diagram-videos |
| 3 | +description: How to make publication-quality manim videos of tensor-diagram |
| 4 | + derivations for the Tensor Cookbook — the animation grammar, the book's |
| 5 | + notation rules, the visual style, the review workflow, and the manim |
| 6 | + pitfalls, all learned across three videos (trace_delete, kronecker_trace, |
| 7 | + softmax_jacobian) and ~40 review rounds. |
| 8 | +--- |
| 9 | + |
| 10 | +# Making tensor-diagram derivation videos |
| 11 | + |
| 12 | +The product: a 15–40s silent 1080p60 video in which a Matrix-Cookbook-style |
| 13 | +identity is *derived*, not asserted. The standard was set by three videos in |
| 14 | +this folder: `trace_delete.mp4`, `kronecker_trace.mp4`, `softmax_jacobian.mp4`, |
| 15 | +all rendered from scenes in `advanced_rules.py`. |
| 16 | + |
| 17 | +The one-sentence philosophy: **the animation is only a proof if every frame |
| 18 | +is a well-formed diagram and every mobject has an ancestry.** Nothing may |
| 19 | +appear, vanish, flip, or morph without a semantic reason. |
| 20 | + |
| 21 | +## The book's notation (non-negotiable) |
| 22 | + |
| 23 | +Check `paper/chapters/functions.tex` before inventing anything. Known rules: |
| 24 | + |
| 25 | +- **Function application**: the function is a plain text node (no circle); |
| 26 | + the *input* edge carries an arrow pointing INTO the function. Solid arrow |
| 27 | + for vector→vector functions (softmax), **densely dotted** for elementwise |
| 28 | + ones (exp, pow_k) — and for elementwise functions the data wire stays on |
| 29 | + the argument (`exp ←··· z ——`, the free wire belongs to z). |
| 30 | +- **Sum** = wire terminated by a copy-dot. **diag(v)** = 3-edged copy-dot |
| 31 | + with v hanging off. **1/x** = `pow₋₁` applied by arrow — not a fraction — |
| 32 | + though converting pow↔fraction *on screen* is a good beat (go full circle: |
| 33 | + open with the fraction, work in pow form, return to fractions at the end). |
| 34 | +- No brackets when arrows already disambiguate. Scalar-argument functions |
| 35 | + (pow on a sum) still get their application arrow. |
| 36 | +- A matrix is a node with two edges; vectors have one (a and b in aᵀXb are |
| 37 | + closed by the trace — count edges before drawing!). |
| 38 | +- **Covariance**: a named edge exits the same side in every term and every |
| 39 | + frame. Two free edges never cross incidentally — after the twist |
| 40 | + convention, a crossing MEANS a swap. Rotating a node 180° IS transposition |
| 41 | + (so a turned glyph legitimately reads as Aᵀ — relabel by spinning it |
| 42 | + upright while the ᵀ fades in). |
| 43 | +- Claim only what is derived (the softmax video says "Jacobian of softmax", |
| 44 | + not "Hessian of cross-entropy", because the CE connection is never shown). |
| 45 | + |
| 46 | +## Visual style |
| 47 | + |
| 48 | +- White background, `config.frame_height = 5.6` (set in `__init__` before |
| 49 | + `super().__init__`), same pixels → everything ~40% bigger. Title top |
| 50 | + (`to_edge(UP, buff≈0.4)`), gray caption bottom (`Tex … scale(0.62)`, |
| 51 | + `to_edge(DOWN, buff=0.18)`), one clause per beat — the captions ARE the |
| 52 | + voiceover script. |
| 53 | +- Semantic color, constant per object across formula AND diagram: |
| 54 | + variables `#C03B2B` (red), function names `#1F6FB2` (blue), recognized |
| 55 | + results `#188A54` (green), derivative apparatus `#B07000` (amber), |
| 56 | + structural wires/dots/signs black. The amber thread is pedagogy: whisker → |
| 57 | + dangling stub → final edge are visibly the same object. |
| 58 | +- **Uniform glyph sizes.** No shrunken denominators or mini-copies; retune |
| 59 | + layout instead. |
| 60 | +- Glyph halos (`set_background_stroke(color=WHITE, width=6)`), never |
| 61 | + `BackgroundRectangle` masks — rectangles clip *other* wires passing behind |
| 62 | + (visible the moment the scene moves). A curve's clearance around its own |
| 63 | + riding label must be a real parameter gap in the curve, sized from the |
| 64 | + local radius. |
| 65 | +- Every node gets clearance — edges stop short of glyph ink, including |
| 66 | + operator glyphs like flatten triangles (stubs may start *under* a |
| 67 | + white-filled node so the fill covers the join). |
| 68 | +- Arrows must clear both endpoints: tips short of subscripts (`pow₋₂`), |
| 69 | + tails outside the source glyph. When in doubt, widen the gap between the |
| 70 | + nodes. |
| 71 | +- Finish: `Indicate` when a result lands, `Circumscribe` the final formula, |
| 72 | + settle the finished diagram up under its equation, hold ≥2.5s. |
| 73 | + |
| 74 | +## The animation grammar |
| 75 | + |
| 76 | +1. **Every frame is a valid diagram.** Right edge count per tensor, no |
| 77 | + transient crossings, invariants hold mid-animation, not just at |
| 78 | + endpoints (e.g. delayed growth so dangling edges never cross). |
| 79 | +2. **Glyphs keep identity.** A symbol persists (and may glide, with |
| 80 | + `path_arc` to hop over others), appears, or disappears — it never smears |
| 81 | + into a different symbol. The one sanctioned morph is a semantic renaming |
| 82 | + (exp absorbing its normalizer to *become* softmax). Titles: build the |
| 83 | + formula in sync with the diagram (X → AXB → Tr(AXB) → ∂·/∂z); keep it |
| 84 | + centered early and slide left as `= …` grows; hand-assemble fractions so |
| 85 | + the ∂ exists as its own animatable glyph; early title stages sit at the |
| 86 | + equation midline, not the numerator slot. |
| 87 | +3. **Count-match every transform.** One wire → one wire. If topology |
| 88 | + changes, split the target at the join (de Casteljau at a bezier apex; |
| 89 | + closed loops split into quadrants) so correspondence is 1:1. A mismatch |
| 90 | + makes manim silently duplicate → ghost arcs. |
| 91 | +4. **Direction-match: anchored end first.** Transforms map start→start. A |
| 92 | + target curve parameterized from its free end makes the source pivot/flip |
| 93 | + about the wrong end (this bug shipped four times: trace closure, A–X |
| 94 | + edge, j1f, jw2). Build every replacement wire starting from the end that |
| 95 | + stays attached. |
| 96 | +5. **`Transform` keeps the SOURCE mobject alive** (wearing the target's |
| 97 | + shape); `ReplacementTransform` swaps the target in. Corollaries: never |
| 98 | + FadeOut/group/animate the *target* of a plain Transform (ghost loops, |
| 99 | + stray minus signs, unmoved edges — this bug shipped five times); after a |
| 100 | + Transform, later code must reference the source name. |
| 101 | +6. **`Transform` preserves z-order; `ReplacementTransform` re-layers to |
| 102 | + top.** Use Transform when a morphing wire must stay under labels. |
| 103 | +7. **Copies spawn from their parent.** Product rule, chain rule: `.copy()` |
| 104 | + superimposed on the original, then transform the copies out to the new |
| 105 | + rows/groups. Factor swaps arc over each other (`animate(path_arc=…)`). |
| 106 | +8. **The whisker is the promised edge.** One whisker per derivative loop |
| 107 | + (per new edge); it survives distribution (loop→loop, dot→dot, |
| 108 | + whisker→whisker at hand-offs) and finally *becomes* the new edge. New |
| 109 | + edges are born already pointing in their final direction and never |
| 110 | + reverse (mirror sub-layouts if needed to keep all j's on one side). |
| 111 | +9. **Amber circle = pending derivative; evaluation spends it.** Circling a |
| 112 | + function is applying ∂; when you evaluate (exp′=exp, pow₋₁′=−pow₋₂) the |
| 113 | + circle fades — and the relabel is the semantic event (the minus is BORN |
| 114 | + at pow₋₁′=−pow₋₂: keep ONE sign object centered between terms and flip |
| 115 | + it + → − at that moment). A chain-rule link through a scalar carries no |
| 116 | + d-edge at all. |
| 117 | +10. **Dots terminate their edges.** A sliding sum-dot drags its wire |
| 118 | + (shorten in step). Dot-absorption is articulated: slide → merge → |
| 119 | + collapse, with the index-level receipt (Σₖδⱼₖ…) as the caption. |
| 120 | +11. **Static↔tracker hand-offs must be seamless both ways**: animate the |
| 121 | + outgoing static to exactly the incoming `always_redraw`'s initial |
| 122 | + state (labels glide with relaxing wires; freeze redraws to statics |
| 123 | + before a group shift). |
| 124 | +12. **Bundles are offset curves of ONE center path** (foot-curl → dome → |
| 125 | + foot-curl), strands at ±offset along numeric normals — guaranteed |
| 126 | + parallel, and the top strand takes the *inside* of every turn. Draw the |
| 127 | + pair at zero gap until the bundling operator is resolved; the gap |
| 128 | + opening IS the resolution. Deforming closed curves = one parametric |
| 129 | + curve with keyframed shape parameters (never piecewise arcs), labels |
| 130 | + riding a fixed parameter point through a real gap. |
| 131 | +13. **More steps beat magic.** Rewrite big jumps as chains of small ones, |
| 132 | + each with a caption: quotient → product rule with explicit factors; |
| 133 | + pow↔fraction conversions; collect terms onto one line (swapping factors |
| 134 | + so covariance holds) *before* the final recognition; every `=` in the |
| 135 | + classical derivation gets a diagram move, and the object mapping in |
| 136 | + each move is reasoned (z's persist, arrows persist, machinery is |
| 137 | + absorbed into the name it justifies, split wires turn rather than |
| 138 | + respawn). |
| 139 | + |
| 140 | +## Workflow |
| 141 | + |
| 142 | +1. Read the relevant chapter first; the book may already contain the exact |
| 143 | + decomposition/figure (softmax) or pose the identity as an exercise. |
| 144 | +2. Storyboard the beats; the user may supply frame-by-frame sketches — |
| 145 | + follow them panel by panel. |
| 146 | +3. Build the scene in `advanced_rules.py` (helpers there: `glyph` halo |
| 147 | + text, `farrow` dotted/solid application arrows, `wire`, `cdot`, |
| 148 | + `dcircle`, `dloop` (whisker options), `node`, caption closure, |
| 149 | + `smgroup`-style builders). Fixed coordinate slots per beat; builders |
| 150 | + return dicts of named parts so transforms can be per-piece. |
| 151 | +4. Iterate at `-ql`, extracting frames at beat boundaries: |
| 152 | + `ffmpeg -ss T -i video.mp4 -frames:v 1 out.png` (get duration via |
| 153 | + `ffprobe`), and Read them. Check *mid-transition* frames, not just |
| 154 | + settled ones. |
| 155 | +5. **Final review at 1080p (`-qh`) — delivery resolution.** 480p |
| 156 | + anti-aliasing hides exactly the shippable-defect class: 1–2px gaps, |
| 157 | + wires grazing glyphs, mask edges. Use PIL crops for pixel-level checks |
| 158 | + of junctions. |
| 159 | +6. Ship: copy .py + .mp4 to `paper/animations/`, commit with explicit |
| 160 | + pathspec, push. One commit per review round. |
| 161 | +7. Expect ~5–15 user review rounds per video. Each note usually |
| 162 | + generalizes: fix the instance, then apply the principle everywhere |
| 163 | + (e.g. "circle is spent" came from one pow note; uniform font sizes from |
| 164 | + one screenshot). |
| 165 | + |
| 166 | +## Tooling pitfalls |
| 167 | + |
| 168 | +- Patch scripts: `assert old in s` for EVERY replacement; a failed assert |
| 169 | + before `write` means the file is untouched and the subsequent render |
| 170 | + shows the OLD video — check which. Region-scope all `s.index` anchors to |
| 171 | + the class (`base = s.index("class Foo"); s.index(anchor, base)`) — a |
| 172 | + global `index` can match an earlier scene and splice duplicated chunks |
| 173 | + into the file. |
| 174 | +- Multiple scenes share `advanced_rules.py`; anchor text ("# ---- title |
| 175 | + ----", "self.cap = None") repeats across scenes. |
| 176 | +- The scratchpad can be wiped between sessions; the repo copy is the |
| 177 | + source of truth — restore, then re-apply. |
| 178 | +- manim: `MathTex(a, b, c)` part-splitting enables per-glyph color/animate; |
| 179 | + `set_background_stroke` for halos; `FadeOut(m, target_position=…)` fades |
| 180 | + while translating (absorption); `FadeIn(m, target_position=…)` arrives |
| 181 | + from there; `Rotate(m, about_point=…)` for in-place glyph spins; |
| 182 | + `DashedLine(...).add_tip(...)` for dotted arrows; `always_redraw` |
| 183 | + closures over `ValueTracker`s for deforming geometry (keyframe param |
| 184 | + arrays + `np.interp`); `Date`-like nondeterminism absent but LaTeX |
| 185 | + compile cache makes re-renders fast. |
| 186 | +- Render one scene: `python -m manim render -qh --disable_caching |
| 187 | + advanced_rules.py SceneName` (venv: `.venv/bin/python`; latexmk not |
| 188 | + needed — manim compiles its own tex). |
0 commit comments