Skip to content

Commit ee39dae

Browse files
thomasahleclaude
andcommitted
book_layout: Zero support, wide-diagram scaling, public API
Completing the engine: - Zero renders as a labeled 0 node (a 0 scalar for order-0); previously raised NotImplementedError. - to_book_tikz gains scale= and max_width=: a diagram wider than max_width is uniformly scaled (transform shape) to fit, so wide gradients/products stay on the page instead of overflowing. - Public API: exported as tensorgrad.to_book_tikz and added a Tensor.to_book_tikz(**kwargs) convenience method (lazy import). All three call paths agree. - Fixed a SyntaxWarning (module docstring is now a raw string; it contains \dloop). 31 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018PzG3QbNtaFmABBHBG39wp
1 parent 5eda897 commit ee39dae

4 files changed

Lines changed: 73 additions & 7 deletions

File tree

tensorgrad/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
from .typing import typed # noqa: F401
1414
from .compiler.runtime import Output, compile, grad # noqa: F401
1515
from .extras.expectation import Expectation # noqa: F401
16+
from .extras.book_layout import to_book_tikz # noqa: F401
1617
from .functions import frobenius2, kronecker, diag, sum, log, pow, trace # noqa: F401
1718
from sympy import symbols # noqa: F401

tensorgrad/extras/book_layout.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Book-grammar layout for tensor diagrams.
1+
r"""Book-grammar layout for tensor diagrams.
22
33
Turns a tensorgrad ``Tensor`` into a diagram laid out the way the Tensor
44
Cookbook draws them by hand. This is deliberately NOT a general graph-drawing
@@ -51,7 +51,9 @@
5151
from numbers import Number
5252
from typing import Optional
5353

54-
from tensorgrad.tensor import Delta, Derivative, Function, Product, Rename, Sum, Tensor, Variable
54+
from tensorgrad.tensor import (
55+
Delta, Derivative, Function, Product, Rename, Sum, Tensor, Variable, Zero,
56+
)
5557

5658
try:
5759
from tensorgrad.extras.expectation import Expectation
@@ -165,6 +167,15 @@ def walk(t: Tensor) -> dict[str, str]:
165167
label = t.name
166168
g.atoms.append(AtomSpec(aid, "var", label, toks))
167169
return dict(zip(list(t.edges), toks))
170+
if isinstance(t, Zero):
171+
edges = list(t.edges)
172+
aid = len(g.atoms)
173+
if not edges:
174+
g.atoms.append(AtomSpec(aid, "scalar", "0", []))
175+
return {}
176+
toks = [fresh("w") for _ in edges]
177+
g.atoms.append(AtomSpec(aid, "var", "0", toks))
178+
return dict(zip(edges, toks))
168179
if isinstance(t, Delta):
169180
edges = list(t.edges)
170181
if len(edges) == 2:
@@ -1211,11 +1222,28 @@ def to_book_tikz(
12111222
left: Optional[str] = None,
12121223
right: Optional[str] = None,
12131224
baseline: str = "-.25em",
1225+
scale: Optional[float] = None,
1226+
max_width: Optional[float] = None,
12141227
) -> str:
1215-
"""Render a tensorgrad Tensor as book-style TikZ (uses tikz-styles.tex)."""
1216-
lines: list[str] = [
1217-
rf"\begin{{tikzpicture}}[baseline={baseline}, inner sep=1pt]"
1218-
]
1219-
_emit_layout(layout_any(tensor, left, right), lines, prefix="", dx=0.0)
1228+
"""Render a tensorgrad Tensor as book-style TikZ (uses tikz-styles.tex).
1229+
1230+
Args:
1231+
left/right: force the named free edge to exit that side (covariance).
1232+
baseline: TikZ baseline anchor for inline use.
1233+
scale: explicit TikZ scale factor for the whole picture.
1234+
max_width: if the laid-out diagram is wider than this (in cm), scale
1235+
it down to fit -- wide gradients/products then stay on the page
1236+
instead of overflowing. Ignored if `scale` is given.
1237+
"""
1238+
layout = layout_any(tensor, left, right)
1239+
if scale is None and max_width is not None and layout.xmax > max_width > 0:
1240+
scale = max_width / layout.xmax
1241+
opts = f"baseline={baseline}, inner sep=1pt"
1242+
if scale is not None and abs(scale - 1.0) > 1e-6:
1243+
# `transform shape` scales node glyphs too, so the whole diagram
1244+
# shrinks uniformly instead of nodes overlapping at moved coordinates
1245+
opts += f", scale={scale:.3f}, transform shape"
1246+
lines: list[str] = [rf"\begin{{tikzpicture}}[{opts}]"]
1247+
_emit_layout(layout, lines, prefix="", dx=0.0)
12201248
lines.append(r"\end{tikzpicture}")
12211249
return "\n".join(lines)

tensorgrad/tensor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ def edges(self) -> KeysView[str]:
131131
"""Returns an _ordered_ set of edge names"""
132132
return self.shape.keys()
133133

134+
def to_book_tikz(self, **kwargs) -> str:
135+
"""Render this tensor as a book-style tensor diagram (TikZ).
136+
137+
See :func:`tensorgrad.extras.book_layout.to_book_tikz` for options
138+
(``left``/``right`` to fix free-edge sides, ``max_width`` to fit wide
139+
diagrams). Call ``.simplify()`` first if the expression still contains
140+
derivatives you want evaluated.
141+
"""
142+
from tensorgrad.extras.book_layout import to_book_tikz
143+
144+
return to_book_tikz(self, **kwargs)
145+
134146
@property
135147
def shape(self) -> dict[str, Symbol]:
136148
if not hasattr(self, "_shape"):

tests/extras/test_book_layout.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,28 @@ def test_expectation_nested_in_sum():
324324
assert len(lay.boxes) == 1
325325
ids = set(nd.id for nd in lay.nodes)
326326
assert all(a in ids for a in lay.boxes[0][0])
327+
328+
329+
def test_zero_tensor():
330+
from tensorgrad.tensor import Zero
331+
332+
assert "0" in to_book_tikz(Zero(i=n)) # zero vector -> labeled 0 node
333+
assert to_book_tikz(Zero(i=n, j=n)).count(r"\node") >= 1
334+
assert "0" in to_book_tikz(Zero()) # scalar zero
335+
336+
337+
def test_max_width_scales_wide_diagrams():
338+
import tensorgrad.functions as F
339+
340+
z = Variable("z", i=n)
341+
y = Variable("y", i=n)
342+
ce = F.cross_entropy(z, y, dim="i").grad(z, {"i": "j"}).simplify()
343+
wide = to_book_tikz(ce)
344+
narrowed = to_book_tikz(ce, max_width=8)
345+
assert "transform shape" in narrowed and "scale=" in narrowed
346+
assert "transform shape" not in wide # unconstrained stays full size
347+
348+
349+
def test_scale_explicit():
350+
tex = to_book_tikz(Product([_A(), Variable("B", j=n, k=n)]), scale=0.5)
351+
assert "scale=0.5" in tex and "transform shape" in tex

0 commit comments

Comments
 (0)