Penrose (declarative diagramming)
The survey's purest declarative, constraint-optimization system: you write what a diagram means in three small languages and a numerical optimizer places every shape — the inverse of Manim's imperative Mobject / scene-graph model, and the one design here a Sparkles layout layer could borrow wholesale.
| Field | Value |
|---|---|
| Language | TypeScript (@penrose/core, a pure JS/npm package; runs in the browser and Node); original prototype in Haskell |
| License | MIT |
| Repository | penrose/penrose (~8,000 stars) |
| Documentation | penrose.cs.cmu.edu/docs |
| Category | Declarative constraint-optimization diagramming system (static vector diagrams, not a video/animation engine) |
| First release | SIGGRAPH 2020 paper (ACM TOG 39(4), Article 144, Jul 2020); Haskell prototype on Hackage 0.1.1.1 (2019) |
| Latest release | v3.3.0 (Sep 28, 2025) |
| Paradigm | A trio — Domain + Substance + Style — compiled to constrained numerical optimization |
| Layout engine | L-BFGS energy minimization over reverse-mode autodiff (energy + gradient compiled to JS at runtime) |
| Output format | SVG (editor also exports PNG, PDF, and SVG-for-LaTeX) |
NOTE
This page is grounded in the SIGGRAPH 2020 paper (PDF, ACM DOI), the official docs at penrose.cs.cmu.edu, and the penrose/penrose source + wiki. Penrose renders in TypeScript to an SVG DOM, so no ci-compiled D probe reproduces its output; the catalog's dependency-free probes (bezier-eval.d, affine-transform.d) reimplement the shared vector math (piecewise-Bézier paths, affine composition) that Penrose's SVG output also rests on. Penrose is the survey's outlier: every other subject is an imperative or reactive animation engine, and Penrose is a static-diagram layout system — several axes below are therefore findings of absence.
Overview
What it solves
Penrose attacks a problem upstream of animation entirely: placing the shapes of a technical diagram so they faithfully encode a mathematical statement. The tagline is "Create beautiful diagrams just by typing notation in plain text" (README). Where Manim's author writes Circle().shift(LEFT) and owns every coordinate, a Penrose author writes only that a Set is a subset of another and lets the system decide where the two circles go — the abstract content and its visual realization are kept in separate files. Figure 1 of the paper states the separation directly (paper):
"Penrose is a framework for specifying how mathematical statements should be interpreted as visual diagrams. A clean separation between abstract mathematical objects and their visual representation provides new capabilities beyond existing code- or GUI-based tools."
Because the mapping from meaning to picture is user-defined rather than hard-coded, "the same set of statements … is given three different visual interpretations … via Euclidean, spherical, and hyperbolic geometry" (paper, Fig. 1) — one Substance program, many diagrams.
Design philosophy
The abstract is the clearest statement of the design bet, and doubles as this page's cited primary quote (paper, ACM TOG 39(4):144):
"We introduce a system called Penrose for creating mathematical diagrams. Its basic functionality is to translate abstract statements written in familiar math-like notation into one or more possible visual representations. Rather than rely on a fixed library of visualization tools, the visual representation is user-defined in a constraint-based specification language; diagrams are then generated automatically via constrained numerical optimization. The system is user-extensible to many domains of mathematics, and is fast enough for iterative design exploration. In contrast to tools that specify diagrams via direct manipulation or low-level graphics programming, Penrose enables rapid creation and exploration of diagrams that faithfully preserve the underlying mathematical meaning."
Three commitments fall out of that paragraph. First, declarative over imperative: you state relations and requirements, never coordinates — the constraint-and-optimization-based layout paradigm the survey names, with Penrose as its exemplar. Second, user-extensible domains: the visualization vocabulary is not a fixed toolbox but a .domain/.style pair the author can write for any field. Third, a diagram is a family, not a point: "one or more possible visual representations" means the optimizer's non-unique solutions are a feature — resampling explores the family (see determinism). The ACM subject classification files it accordingly under "Software and its engineering → Domain specific languages" (paper, CCS Concepts).
How it works
A Penrose diagram is a trio of three plain-text programs plus the engine that combines them (docs):
- A Domain (
.domain) program — "describes for a given domain the types of objects, predicates, and functions that comprise diagrams in this domain." It is the schema: what kinds of things exist and what can be said about them. - A Substance (
.substance) program — "defines the objects and relationships in the diagram." It is the content: which things exist and how they relate, written in math-like notation with no visual detail. - A Style (
.style) program — "tells Penrose how to display the objects and relationships." It maps Substance declarations to shapes and states the layout requirements.
The canonical Venn-diagram example (README) shows all three. Domain declares the vocabulary — a type and three relational predicates:
-- setTheory.domain
type Set
predicate Disjoint(Set s1, Set s2)
predicate Intersecting(Set s1, Set s2)
predicate Subset(Set s1, Set s2)Substance declares the objects and asserts relations over them — no geometry:
-- tree.substance
Set A, B, C, D, E, F, G
Subset(B, A)
Subset(C, A)
Subset(D, B)
Disjoint(E, D)
Disjoint(B, C)
AutoLabel AllStyle is where geometry and the optimization problem appear. Each forall rule matches a pattern of Substance objects and, for each match, creates shapes and states requirements with ensure (a hard constraint) and encourage (a soft objective):
-- venn.style
canvas {
width = 800
height = 700
}
forall Set x {
shape x.icon = Circle { }
shape x.text = Equation {
string : x.label
fontSize : "32px"
}
ensure contains(x.icon, x.text)
encourage norm(x.text.center - x.icon.center) == 0
layer x.text above x.icon
}
forall Set x; Set y
where Subset(x, y) {
ensure disjoint(y.text, x.icon, 10)
ensure contains(y.icon, x.icon, 5)
layer x.icon above y.icon
}
forall Set x; Set y
where Disjoint(x, y) {
ensure disjoint(x.icon, y.icon)
}Read the last two rules as the whole idea: "for every Subset(x, y), keep x's circle inside y's" and "for every Disjoint(x, y), keep the two circles apart" — the picture is never drawn, only constrained. The Style shape library supplies the primitives a rule can instantiate: Circle, Ellipse, Rectangle, Line, Path, Polygon, Polyline, Text, Equation, Image, and Group. Undefined shape parameters (here the circle's radius and center) "have default values which may or may not be adjusted upon optimization" — every unpinned number becomes a varying degree of freedom the optimizer is free to move.
The numerical optimizer
The Style compiler turns the ensure/encourage statements into one scalar energy and minimizes it. Its final phase "Find[s] all the objective and constraint functions in the translation [then] Generate[s] the objective function"; when that function runs on the current values, "each of the resulting energies is weighted … and summed to yield the overall energy" — constraints enter as large-penalty terms, objectives as ordinary terms. The gradient comes from reverse-mode autodiff over a computational graph: "given a computational graph of the energy function, it returns a computational graph of the gradient," and the compiler notes "autodiff is automatically taken with respect to the intermediate expressions/computations." The descent step is quasi-Newton — the guide "currently appl[ies] L-BFGS to get the actual descent direction." Both the energy and its gradient are compiled to JavaScript at runtime and JIT-optimized before the loop runs, so each iteration is cheap. After each step "the varying state is updated … the translation is evaluated and the list of shapes is updated for the front-end to render." The pipeline, end to end:
Domain + Substance + Style ──parse──▶ matched selectors ──▶ varying state x
energy E(x) = Σ wᵢ · (ensure|encourage)ᵢ(x) ──autodiff──▶ ∇E(x)
minimize E via L-BFGS ──▶ converged x* ──toSVG──▶ SVG diagramObject & scene model
Penrose has no retained mutable scene graph in the Mobject sense — nothing you build once and then mutate frame by frame. Its object model is a bipartite one: Substance holds the abstract objects (a flat set of typed declarations and relations, e.g. Set A and Subset(B, A)), and Style produces the visual objects (shape instances such as x.icon = Circle { }) by pattern-matching Substance with forall selectors. The shapes are not a tree the author navigates; they are the output of matching, keyed by the Substance object they belong to (x.icon, x.text). Grouping exists — the Group shape composes children — but there is no parent-pointer/dirty-flag machinery because there is no per-frame mutation to invalidate: the "scene" is recomputed from the trio each time the optimizer runs. This is the survey's declarative counterpart to the imperative Mobject: instead of a node you translate and rotate, you have a relation (Subset) whose geometric meaning (contains(...)) the optimizer enforces. In the survey's relational-layout terms, the diagram is its relations; coordinates are derived, never authored.
Animation & timing model
Absent by design — Penrose is a static-diagram system. There is no timeline, no frame index, no easing/rate function, no play-loop, and no execution model in the sense every other subject in this survey has one. A Penrose program specifies a picture, not a motion. The only notions of "progression" are internal and produce still images:
- The optimizer's iteration count. The API exposes
step/stepTimesto advance the L-BFGS loop N iterations at a time, andisOptimizedto test convergence — so the solve has a trajectory, but it is a means to a single static layout, not an authored animation curve. (One could film the descent, but nothing in the language expresses time.) - Staged layout. Layout stages let the author "divide the layout optimization problem into multiple stages" (e.g.
layout = [shape, label, overall], positioning shapes before labels). This orders the solve, not the display — it exists because "if constraints and objectives differ on what they consider 'good' states, then they will effectively compete", and sequencing them improves reliability and solve time. - Resampling. Producing another member of the diagram family (below) is a new static layout, not a tween between two.
Any actual animation of a Penrose diagram is a downstream concern (interpolating between two converged states, or embedding the SVG in a video tool) and is out of scope for the engine itself.
Rendering backend & rasterization
Penrose's core renderer emits vector SVG, and does not rasterize. The toSVG function "renders a PenroseState as an SVGSVGElement" — a DOM tree of <circle>, <path>, <text>, etc. — and rasterization to pixels is delegated entirely to whatever consumes that SVG: the browser's own SVG renderer in the editor, or a PNG/PDF exporter. There is no CPU-vs-GPU rasterizer question inside Penrose the way there is for Cairo vs OpenGL in the Manim forks, because Penrose stops at the resolution-independent vector description. Two consequences follow. First, output is crisp at any scale and diff-friendly as text (the SVG is the artifact). Second, anti-aliasing, color compositing, and gamma are the renderer's job, not Penrose's — an SVG fill is handed to the viewer untouched. The shape geometry the SVG carries is exactly the survey's vector primitives: circles, polygons, and piecewise-Bézier Paths (the bezier-eval.d math), positioned by the affine placement the optimizer solves for (the affine-transform.d math).
Typesetting & text
Math labels are Penrose's most animation-relevant text feature, and it reaches them through a self-contained JS math engine, not a system TeX install. The Equation shape typesets its string as math-mode TeX rendered by MathJax to SVG; the Labels wiki states the mechanism plainly:
"all labels are converted to SVG strings by MathJax the first time the scene is rendered."
The Text shape covers plain (non-math) strings. Because MathJax emits SVG <path> geometry, an equation's glyphs arrive as real vector outlines — the survey's glyph-outline extraction reached, like Motion Canvas's Latex, through an in-process compiler rather than Manim's system LaTeX + dvisvgm pipeline. Two notes on where this sits versus the survey's text-shaping axis: MathJax owns both shaping and outline production for math, so Penrose never touches HarfBuzz/Pango directly; and the label's rendered box feeds back into the optimizer — a label is a shape with a measured size that ensure contains(x.icon, x.text) can constrain, which is why staged layout often places shapes first and labels second.
Output & encoding
No video codec, muxing, or frame encoding — another finding of absence. The survey's codec/muxing/pixel-format axis simply does not apply: a Penrose run produces a single diagram, so there is no frame stream to encode. The core output is one SVG element (toSVG); the editor adds "four export formats: PNG, SVG, SVG for LaTeX, and PDF". The LaTeX-oriented export "exports Equation as raw texts, and you can customize the styling in LaTeX by importing the SVG using the svg TeX package", and — a neat round-trip — "SVGs exported by the editor contain necessary metadata (e.g. source trio programs) for the editor to re-load them into the workspace." For batch use, the roger CLI "can process these trios to generate SVGs, accepting them either as individual files or consolidated in a .trio.json configuration file" (using). Where Manim's terminal artifact is an .mp4, Penrose's is a .svg (or a directory of them, one per variation).
Interactivity, preview & authoring
This is a genuine strength. Penrose ships a browser-based IDE with "separate tabs for editing the three core components: .substance, .style, and .domain" (using); you edit the trio and compile to see the diagram panel update. The signature interactive act is resampling to explore the diagram family: "To view another layout of the diagram, click 'resample.'" Each result is reproducible via a variation string (see determinism), and the editor's Diagram Variations panel shows several alternate layouts side by side for comparison. The authoring loop is fast on purpose — the paper's own criterion is being "fast enough for iterative design exploration" (paper) — because the energy and gradient are compiled to JIT-friendly JavaScript and the optimizer converges in a fraction of a second for typical trios.
Extensibility & API surface
Extensibility is layered from the DSL up to the JS API:
User-defined domains. The headline extensibility axis is the language itself: a new
.domain+.stylepair teaches Penrose an entirely new field of mathematics or engineering — the "user-extensible to many domains" property of the abstract. Nothing about set theory, geometry, or graph drawing is baked into the engine; it is all trio content.Constraint / objective library.
ensure/encouragedraw from a library of differentiable functions (contains,disjoint,overlapping,norm, …); because everything is autodiff-traced, an author composes them freely and the gradient follows automatically.@penrose/coreJS API. The engine is a plain npm package with a small, composable surface (API docs) —compile(trio →PenroseState),optimize/step/stepTimes(run the solver),toSVG(render), plus adiagramconvenience that chains all three. The documented pattern:javascriptimport { compile, optimize, toSVG, showError } from '@penrose/core'; const compiled = await compile(trio); if (compiled.isErr()) throw new Error(showError(compiled.error)); const converged = optimize(compiled.value); const rendered = await toSVG(converged.value, async () => undefined); document.getElementById('diagram').appendChild(rendered);Embeddable + scriptable. Because it is pure JS/TS returning an
SVGSVGElement, Penrose embeds in any web page or Node script, androgerdrives it headlessly from the command line.
Determinism, caching & performance
Penrose's reproducibility primitive is the variation string. The docs (using):
"each diagram layout is uniquely identified by a single variation string. Changing this string generates another layout alternative (similar to clicking 'resample')."
The variation string seeds the optimizer's randomness (initial sample of the varying state), so a fixed variation deterministically reproduces the same diagram — the static-diagram analogue of the survey's deterministic frame sampling, and what makes a Penrose diagram shareable and regression-testable. Note the subtlety versus a video engine: the optimizer is a numerical process, so bit-identical SVG across machines depends on floating-point and L-BFGS stability, not on a fixed frame formula.
On caching, Penrose has no Manim-style content-hash / partial-file layer (there are no frames to cache); its performance story is instead (a) compiling the energy and gradient to JIT-optimized JavaScript so each L-BFGS iteration is fast, (b) staged layout to cut solve time and avoid constraint/objective thrash, and (c) the small problem sizes typical of diagrams. The design target — "fast enough for iterative design exploration" (paper) — is met by making the solve cheap rather than by caching its result.
Strengths
- Fully declarative layout. You state relations and requirements; the optimizer finds coordinates. This is the cleanest separation of meaning from appearance in the survey and the whole point of borrowing the model.
- User-extensible to any domain. A
.domain+.stylepair, not a fixed toolbox, defines the visual vocabulary — the same engine draws Venn diagrams, Euclidean constructions, or graph layouts. - A diagram is a family. Non-unique optima are a feature: resampling + variation strings explore alternative layouts of the same statement, each reproducible.
- Resolution-independent SVG output that is crisp at any scale and diffable as text; math labels are real vector outlines via in-process MathJax (no system TeX).
- Genuinely good authoring loop — a browser IDE with live compile, resample, and a Diagram Variations panel, fast enough for iteration.
- Small, embeddable JS API (
compile/optimize/toSVG) plus arogerCLI; MIT-licensed.
Weaknesses
- Not an animation engine. No timeline, frames, easing, or play-loop — the survey's timing and encoding axes are outright absent. Any motion is a downstream bolt-on.
- Layout is a numerical optimization, so it can fail to converge, land in a poor local minimum, or need staged layout and hand-tuning of constraint weights to behave — less predictable than placing coordinates directly.
- Reproducibility is per-variation-string, not bit-identical across machines: the optimizer's floating-point path can differ, so SVGs are not guaranteed byte-equal on different hardware.
- Learning curve of a three-language, constraint-based model — thinking in
ensure/encourageandforallselectors is a paradigm shift from imperative drawing. - Rasterization/anti-aliasing/gamma are out of Penrose's hands — it emits SVG and defers pixels to the viewer, so pixel-exact output depends on the external renderer.
- No cross-run render caching (though there are no frames to cache, iteration relies on a cheap solve rather than a cache).
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Trio of DSLs (Domain / Substance / Style) | Cleanly separates abstract meaning from visual mapping; each reusable independently | Three languages to learn; indirection between "what exists" and "what it looks like" |
Constraint-based layout via ensure/encourage | Author states requirements, not coordinates; layout composes from relations | Must express intent as differentiable penalties; competing terms can fight |
| Constrained numerical optimization (L-BFGS + reverse-mode AD) | One general engine lays out any domain; gradients are automatic | Can miss/local-minimum; needs staged layout and weight tuning; not always predictable |
| Energy + gradient compiled to JIT'd JavaScript | Fast enough for iterative design exploration | Adds a runtime codegen step; performance tied to the JS engine |
| A diagram is a family (resample + variation string) | "One or more possible visual representations"; explore alternatives, reproduce any one | No single canonical output; determinism is per-string, not cross-machine bit-exact |
| SVG (vector) output; rasterization deferred | Resolution-independent, diffable, embeddable; no rasterizer to maintain | Anti-aliasing/gamma/pixels controlled by the external viewer, not Penrose |
In-process MathJax for Equation labels | Math typesetting with no system TeX/dvisvgm; labels are real vector outlines | Ships a large JS math engine; MathJax coverage, not full LaTeX |
| Static diagrams, no timing model | Focus the system on the hard part — faithful placement — not motion | Not a video engine; animation is entirely out of scope |
Haskell prototype → TypeScript rewrite (@penrose/core) | A pure JS/npm engine embeds in browsers and Node, powering the live editor | A full reimplementation; SIGGRAPH-'20 examples predate the current languages |
Sources
Primary sources — the SIGGRAPH 2020 paper, the official docs, and the penrose/penrose source + wiki:
- Penrose: From Mathematical Notation to Beautiful Diagrams — Ye, Ni, Krieger, Ma'ayan, Wise, Aldrich, Sunshine, Crane; ACM TOG 39(4), Article 144, Jul 2020. The abstract (constraint-based specification → constrained numerical optimization), Fig. 1 (meaning/representation separation, three geometries), and the design criteria quoted above.
- Documentation reference — the Domain/Substance/Style definitions quoted verbatim; the Style shape library (shape list, defaults,
ensureOnCanvas); using Penrose (editor, resample, variation string, export formats,roger);@penrose/coreAPI (compile/optimize/step/toSVG, the render pattern). - How the Style compiler works (wiki) — objective-function generation, weighted-and-summed energies, autodiff over
varyingState. - Autodiff guide (wiki) — reverse-mode symbolic autodiff (
gradAllSymbolic), the energy/gradient computational graph, L-BFGS descent, runtime JS compilation. - Diagram Layout in Stages (blog) — staged layout, the constraint-vs-objective competition it resolves.
- Labels in Penrose (wiki) — MathJax converting labels/
Equationstrings to SVG; math-mode TeX. - README / homepage — the tagline and the Venn-diagram trio example (Domain/Substance/Style) quoted above.
- Hackage
penrose0.1.1.1 — the original Haskell reference implementation (2019), synopsis "Create beautiful diagrams just by typing mathematical notation in plain text." LICENSE— MIT.v3.3.0release — current version.