A constraint-based diagram system designed for AI agents to build, inspect, and verify technical diagrams programmatically.
When an AI agent generates a diagram, it typically produces an image or markup it cannot inspect. It cannot ask "do these two boxes overlap?" or "is the label centered?" — it writes code, renders, and hopes. If the layout is wrong, it has no geometric feedback to guide corrections.
Pelican gives agents a queryable model of diagram geometry. Every element has a bounding box the agent can read. Every spatial relationship — containment, overlap, alignment, distance — can be checked programmatically. The agent builds a diagram, solves the layout, inspects the results, and fixes problems in a tight loop.
Pelican separates diagrams into marks (leaf elements with intrinsic size) and relations (layout constraints that position marks relative to each other). A single-pass local propagation solver computes all positions deterministically in linear time.
from pelican import Diagram, Rect, Circle, Text, Stack, Align
d = Diagram()
d.add(Rect("header", width=200, height=40, fill="#4A90D9", rx=6))
d.add(Text("title", "System Overview", font_size=14))
d.add(Circle("node", r=20, fill="#50C878"))
d.add(Stack("layout", ["header", "node"], direction="vertical", spacing=20))
d.add(Align("center_title", ["header", "title"], alignment="center"))
d.solve()After solving, the agent can inspect every element's geometry:
d.bbox("header")
# {'left': 0.0, 'top': 0.0, 'width': 200.0, 'height': 40.0, ...}
d.bbox("node")
# {'left': 80.0, 'top': 60.0, 'width': 40.0, 'height': 40.0, ...}And verify spatial properties:
d.disjoint("header", "node") # True — no overlap
d.distance("header", "node") # 20.0 — the spacing we requested
d.aligned("header", "node", "center_x") # True — vertically centeredRendering produces SVG:
d.to_svg("diagram.svg") # file
d.to_png("diagram.png") # file
d.render() # inline in JupyterEach element carries an axis-aligned bounding box with eight properties: left, top, right, bottom, center_x, center_y, width, height. These form two independent 2-variable linear systems (one per axis) — setting any two properties on an axis determines the other two.
Dimension ownership prevents conflicting constraints. When a relation sets a dimension on an element, it claims ownership. If a second relation tries to set the same dimension, Pelican raises an OwnershipError with a diagnostic message naming both relations. The agent reads the error and restructures.
Relation order is the control surface. Relations execute in the order they are added. Position-setting relations (Stack, Grid) should be added before overlay relations (Align). This gives agents explicit, predictable control over layout evaluation.
| Mark | What it defines |
|---|---|
Rect(id, width, height) |
Rectangle with optional position, corner radius, fill/stroke |
Circle(id, r) |
Circle with optional center position |
Text(id, content) |
Text with measured width/height (Pillow or heuristic) |
Line(id, x1, y1, x2, y2) |
Line segment between two points |
Path(id, d) |
SVG path data with explicit bbox |
LaTeX(id, expression, use_external=False) |
Safe fallback text by default; trusted input can opt into latex + dvisvgm rendering |
| Relation | What it constrains |
|---|---|
Stack(id, children, direction, spacing) |
Sequential positioning along an axis |
Align(id, children, alignment) |
Shared alignment on left, center_x, right, top, center_y, bottom, or center |
Grid(id, children, cols, row_spacing, col_spacing) |
Row-major grid layout |
Distribute(id, children, axis, spacing) |
Even distribution along an axis |
Contain(id, children, padding) |
Background rectangle enclosing children with padding |
Arrow(id, source, target) |
Directed connector with arrowhead, clipped to bbox edges |
Pelican includes data visualization primitives that share the same constraint model:
from pelican import Axes, FunctionPlot, ScatterPlot, LinePlot
import math
d = Diagram()
d.add(Axes("ax", x_range=(0, 6.28), y_range=(-1.2, 1.2),
width=280, height=200, x_label="x", y_label="sin(x)"))
d.add(FunctionPlot("curve", math.sin,
x_range=(0, 6.28), y_range=(-1.2, 1.2),
width=280, height=200, stroke="#E91E63"))
d.add(Align("overlay", ["ax", "curve"], alignment="center"))
d.solve()ScatterPlot and LinePlot take x_data/y_data arrays. All plot marks participate in the same layout and verification system as other elements.
Every spatial predicate returns a boolean. verify() runs batch checks and returns a structured report:
report = d.verify([
{"predicate": "contains", "args": ["outer", "inner"]},
{"predicate": "disjoint", "args": ["a", "b"]},
{"predicate": "aligned", "args": ["a", "b"], "axis": "center_x"},
])
report.all_passed # True/False
report.summary() # "Verification: 3/3 checks passed"
report.failures() # list of CheckResult with diagnostic detailsIndividual checks: d.contains(a, b), d.overlaps(a, b), d.disjoint(a, b), d.distance(a, b), d.aligned(a, b, axis).
Verification uses Shapely for geometric predicates (DE-9IM model). The solver never calls Shapely — it stays fast with arithmetic only.
d.state() # all element bboxes as dicts
d.bbox("elem") # single element bbox
d.ownership("elem") # which relation owns each dimension
d.snapshot() # full scenegraph state including hierarchy
d.save_state("diagram-state.json")
clone = d.clone()
reloaded = Diagram.load_state("diagram-state.json")
rebuilt = Diagram.from_json(d.state().to_json())Diagram.from_state(...), from_json(...), and load_state(...) rebuild a diagram from serialized specs and resolve it again. FunctionPlot roundtrips when its callable is importable (for example math.sin); anonymous lambdas and local closures remain cloneable in-memory but are not reconstructable from serialized state.
Pelican can expose the provenance of solved geometry and suggest parameter edits that would produce a desired geometric change:
d.solve(trace=True)
expr = d.trace("node", "center_x", resolve=True)
solutions = d.suggest_updates("node", "center_x", 180.0)
chosen = d.apply_suggestion("node", "center_x", 180.0)
d.set_param("layout", "spacing", 24).solve()
d.nudge("node", dx=10, dy=-5)The Textual TUI also supports inverse nudging with the arrow keys after selecting an element.
Requires Python 3.12+. Managed with uv.
uv add pelicanOptional extras:
uv add "pelican[text]" # Pillow for accurate text measurement
uv add "pelican[latex]" # latextools for LaTeX rendering
uv add "pelican[png]" # cairosvg for PNG exportFor the installable CLI/TUI workflow:
uv tool install pelican
pelican --helpFrom a local checkout, uv tool install . exposes the same pelican command.
Pelican now treats the saved state file as the shared artifact between agents, scripts, and the TUI. The file persists:
- element specs and build order
- solved geometry
- symbolic traces for inverse editing
- UI metadata like the current TUI selection
Every mutating CLI command re-solves with trace=True before saving, so the state file stays ready for both agent inspection and interactive curation.
pelican init scene.json
pelican add scene.json Rect box --param width=200 --param height=80 --param x=20 --param y=20
pelican add scene.json Text title --param content="System Overview" --param font_size=14
pelican add scene.json Align center_title --child box --child title --param alignment=center
pelican summary scene.json
pelican doctor
pelican bbox scene.json box --json
pelican explain scene.json title center_x 180 --json
pelican suggest scene.json title center_x 180 --json
pelican ops scene.json '[{"op":"set","element_id":"box","param":"x","value":40}]' --diff --dry-run
pelican tui scene.jsonCommon commands:
pelican typeslists supported marks, relations, and plots.pelican doctorreports optional capabilities like TUI, PNG export, and external LaTeX support.pelican state <file>prints the full persisted JSON document.pelican add/remove/setmutates the scene file one command at a time.pelican ops <file> ...applies a JSON list of edits in one batch for agent workflows.- mutating commands support
--dry-runand--diffso agents can preview exact state changes before saving. pelican trace/explain/suggest/apply/nudgedrives inverse editing from the CLI.pelican render <file> --svg-out out.svg --png-out out.pngexports renders.pelican tui <file>opens the Textual editor on the same file, restores selection state, autosaves by default, supports undo/redo withCtrl+Z/Ctrl+Y, inverse nudging with the arrow keys, and direct source-param editing withp/P,h/l,H/L, ande.
Required: drawsvg (SVG generation), kiwisolver (linear constraint solving), shapely (geometric verification).
Optional: Pillow (text measurement), cairosvg (PNG export). Trusted LaTeX rendering requires latex and dvisvgm on the system PATH and must be enabled with use_external=True.
Pelican draws on a wide body of work in constraint-based graphics, program synthesis, and diagram systems.
- Bluefish (Pollock et al., UIST 2024) — The most direct architectural influence. Pelican's dimension ownership model, mark/relation separation, and single-pass local propagation solver are adapted from Bluefish's compound scenegraph design. Bluefish showed that local propagation with ownership tracking produces deterministic, debuggable layouts that scale linearly.
- Penrose (Ye et al., SIGGRAPH 2020) — Demonstrated that mathematical diagrams can be generated from declarative specifications with automatic layout via optimization. Penrose's Bloom API influenced our inspectable geometry model. We chose local propagation over Penrose's L-BFGS optimization for determinism and debuggability, but Penrose's rich constraint library (contains, disjoint, perpendicular, collinear) informed our verification predicates.
- SketchPad (Sutherland, 1963) and ThingLab (Borning, 1981) — The original constraint-based graphics systems. Established that graphical objects should be defined by constraints between their properties, not by explicit coordinates.
- DeltaBlue (Freeman-Benson & Maloney, 1990) — The foundational incremental local propagation algorithm. Introduced walkabout strength annotations and the separation of constraint planning from execution.
- SkyBlue (Sannella, 1994) — Extended DeltaBlue with multi-way constraints and cycle solvers.
- Cassowary (Badros, Borning & Stuckey, 2001) — Incremental simplex-based linear constraint solver. Pelican uses kiwisolver (a C++ Cassowary implementation) as an optional backend for linear systems.
- Babelsberg (Felgentreff et al., 2014) — Showed how constraint solving can be integrated into object-oriented programming with cooperating solver backends.
- HotDrink (Freeman et al.) — Multi-way dataflow constraint systems for reactive GUI programming.
- Sketch-n-Sketch (Chugh et al., PLDI 2016) — The direct inspiration for Pelican's trace system and inverse solver. Sketch-n-Sketch instruments program evaluation to produce run-time traces that record how each output value was computed from source-code literals. When the user directly manipulates the output, trace equations are inverted to infer program updates. Pelican implements this approach: every solved dimension carries a symbolic trace expression, and the inverse solver finds parameter changes by walking the expression tree.
- Lillicon (Bernstein & Li, TOG 2015) — Synthesized different program representations for the same graphic design based on intended edits, eliminating the need for secondary direct manipulation edits.
- AIDL (Jones et al., Computer Graphics Forum 2025) — Solver-aided hierarchical DSL for LLM-driven CAD. Demonstrated the key architectural insight that Pelican builds on: separate high-level structural reasoning (what LLMs are good at) from low-level geometric computation (what constraint solvers are good at). In few-shot settings, AIDL outperforms even languages the LLM was trained on.
- MagicGeo (arXiv 2025) — Training-free geometric diagram generation via LLM autoformalization into constraints with formal solver verification.
- GeoLoom (arXiv 2025) — Text-to-diagram via formal constraint language and Monte Carlo coordinate optimization.
- LayoutGPT (Feng et al., NeurIPS 2023) — LLM as visual planner via CSS-style prompts for layout generation.
- SVGenius (ACM MM 2025) — Benchmark for LLM SVG understanding, editing, and generation.
- Vega / Vega-Lite (UW IDL) — Declarative visualization grammar with automatic layout from high-level specifications.
- Charticulator (Ren et al., Microsoft Research, 2018) — Transforms chart specifications into mathematical layout constraints solved via sparse linear programming.
- D3-force — Velocity Verlet integration with pluggable forces, treating links as weak geometric constraints.
- Manim (3Blue1Brown) — Programmatic math animation engine. Influenced our approach to function plots and mathematical notation.
- Graphviz — Text-based graph layout (DOT language), demonstrating that agent-friendly diagram tools should accept text input.
- kiwisolver — Fast C++ Cassowary implementation with Python bindings, used by matplotlib internally. Pelican's optional linear constraint backend.
- Shapely — Computational geometry library (DE-9IM spatial predicates). Powers Pelican's verification layer.
- drawsvg — SVG generation with Jupyter display support. Pelican's rendering backend.
- Adaptagrams (Wybrow et al.) — C++ constraint-based layout and connector routing library with Python SWIG bindings. Informed our approach to separation constraints.
- Gaphas — Python diagramming widget with built-in constraint solver. Demonstrated that constraint-based diagramming is viable in pure Python.