- scripts/lint.py: reject raw rectangles, absolute coordinates, hue-budget and callout/group/formula-order violations at the source level - tests/invalid/ + tests/lint-invalid/: negative fixtures proving the package and linter reject bad input; test.sh now runs both directions - references/fallback.md: degraded path when no LaTeX is available - tests/group-callout.tex: exercise \stgroup and \stcallout - agents/openai.yaml: agent config - Docs and .sty updated to match
11 KiB
supertensor.sty API
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{supertensor} % cjk: ctex + fandol (XeLaTeX). en: English rail labels.
Build with ./scripts/build.sh fig.tex — it puts assets/ on TEXINPUTS, so the package
does not need to be installed into your texmf tree.
The build first runs scripts/lint.py. It rejects ledger changes, untracked absolute
objects, repeated anonymous dimensions, raw TikZ rectangles, a formula placed before the
last row, an unclosed \stgroup, a callout anchored to anything other than a band or a
second callout on one band, and more than four active hue families per row. Intentional galleries/tests may
put % supertensor-lint: allow-absolute, allow-missing-formula near the top; do not add an
exemption to a deliverable merely to make it pass.
Ledgers
\stsetrole{q}{stTeal} % role -> color. Macros take a ROLE, never a color.
\stdim{T}{6} % symbolic axis -> physical edge length in cells
\stsetlabels{A}{O}{M} % override the three meaning-box rail labels
\stsetrail{3.2em} % width of the bold label rail
Colors: stTeal stOrange stCoral stViolet stGray stInk. An unknown role falls back to gray
and emits a package warning, which build.sh turns into a failed build.
Redeclaring an axis or role with the same value is harmless; changing its value emits a
warning and keeps the original mapping.
Lengths: \stunit (one cell, 4.6 mm) and \sttilegap (white gutter, 0.5 mm).
Flow layout
This is the default. Leave the coordinate argument empty and the object is placed by a
cursor. Hand-written offsets are the main source of layout bugs in these figures: every
gap becomes a tuned magic number, so a label that grows by two characters silently lands on
the next tensor, and two stages started from two different x share no rail.
\ststage{SA}{stage heading} % on the left rail, below the previous band
\strow{rowA}{T} % open a band, declared height T
\stface[role=q]{Q}{}{T}{d} % empty coord = place at the cursor
\stglyph{mA}{$\times$} % operator glyph; reserves its own width
\stface[role=w]{W}{}{d}{d}
\stlink{lA}{softmax} % connector whose LABEL is a flow object
\stface[role=s]{S}{}{T}{d}
\strowend % fit the band, arm the caption lanes
\stcaption{Q}{$\mathbf Q$}{$T\times d$}
| macro | does |
|---|---|
\ststage{name}{text} |
stage heading on the left rail, below all ink so far |
\strow{name}{height} |
open a band; height is an axis name or an integer |
\strowend |
fit the band into name, then \stlane it |
\stcol{name}{height} … \stcolend |
vertical sub-flow filling one slot of the band |
\stgroup[role=]{name} … \stgroupend |
outline naming the objects inside it as one composite |
\stglyph{name}{$\times$} |
operator glyph (not \stop — plain TeX owns that name) |
\stcomm{name}{All-Reduce} |
collective node |
\stnode[style]{name}{text} |
any node, placed and measured by the cursor |
\stlink{name}{label} |
connector; empty label reserves \stlinklen of bare arrow |
\stgap{4mm} / \stvgap{4mm} |
one-off extra space, horizontal / vertical |
\stbbox{all} |
everything drawn so far, as one node, for \stmeaningbox |
\sttopformula{F}{math} |
the formula line, centered on what was actually drawn |
\sttrack{node} |
fold a hand-placed node into the bbox and the vertical cursor |
\stleftrail{x} / \stlayoutreset |
move the rail / start over |
Gaps are declared once: \stgutter (6 mm, between objects in a band), \strowgap (3.5 mm,
above a band), \stblockgap (9 mm, above a stage heading), \stlinklen (10 mm).
Four properties follow by construction, and each of them is a bug class removed:
- The gap belongs to the object that follows it, and the first object in a band gets
none — so every band starts flush on the same rail, and
gap=0ptmeans exactly adjacent, which is how shards are made to tile their parent. - Every object reserves its own width, including a stack's offset sheets and a bracket's
overhang.
right=6mm of Xreserves nothing, so the next face is free to land on top. - A connector's label is a flow object, so a label can never be wider than its arrow.
- A band declares its height, so an object that does not fit is a build failure
(
Package supertensor Warning), not something the reader discovers.
Call \sttopformula after the bands. A formula placed first can only be centered on a
figure whose width is not yet known — that is how the top line ends up visibly off-center.
\stcol is what a split along the contracted axis looks like:
\stcol{W2}{dff} % declared total height
\stface[role=r1]{W2a}{}{dffl}{d}
\stface[role=r2, gap=0pt]{W2b}{}{dffl}{d} % tiles W2a exactly
\stcolend
A column that consumes a height other than the one it declares is drawn off-center, so that is a warning too.
Absolute placement still works everywhere — pass a coordinate instead of {}. Mix freely,
but wrap hand-placed nodes in \sttrack so the cursor knows about them.
Groups
\stgroup … \stgroupend is the second sub-flow. It draws a thin rounded outline in the
role hue around whatever is placed between them, naming those objects as one composite:
\stgroup[role=q]{qg} % keys: role= (hue), pad= (default \stgrouppad)
\ststack[role=q]{qh}{}{T}{dh}{3} % "these three sheets are q"
\stgroupend
\stcaption{qg}{$\mathbf q$}{$B\times T\times h\times d_h$}
The members go inside the block, and three properties follow from that:
- a group can only wrap adjacent objects — one that reached across the band would swallow whatever sat between its members;
- the group, not its last member, terminates a pending
\stlinkand sources the next one, so an arrow lands on the outline instead of ending inside it and crossing a border that is not its endpoint; - the padding is reserved on both sides, so the neighbour cannot land tangent to it.
\strowend fits the group, so \stcaption{qg}{...} hangs from the caption lane below the
outline, not below the member. A group may contain a \stcol; it may not sit inside one,
and it may not nest. An unclosed group is a lint error.
Callouts
\stcallout{name}{text width}{band}{title}{body} hangs a side note card off the right edge
of a finished band, top-aligned with it. Call it after \strowend:
\strowend
\stcaption{g}{$\mathbf g$}{$B\times T\times h\times d_k$}
\stcallout{n1}{5.2cm}{rowB}{下界化换来了什么}{Kimi Linear 的 $g$ 无下界……}
Arg 3 must be a \strow band name — one aside per band, and never anchored to a single
face. Both rules are lint errors, because a card beside one operand reads as a step in the
computation (layout.md). Inside an open band it is a package error.
A callout pushes the vertical cursor below its own bottom edge. A card taller than its band
therefore opens visible white space rather than colliding with the next stage — which is
the signal that its text belongs in \stmeaningbox instead.
Faces
\stface[keys]{name}{}{rows}{cols} % flow
\stface[keys]{name}{(coord)}{rows}{cols} % absolute
\ststack[keys]{name}{(coord)}{rows}{cols}{sheets}
\stindexface[keys]{name}{(coord)}{rows}{cols}{entries}
rows/cols accept a declared axis name or a raw integer. A non-empty (coord) must
include its own parentheses — {(0,0)}, {($(A.east)+(1.5,0)$)}. name becomes a TikZ
node you can anchor against; \ststack also defines name-front.
Keys:
| key | default | meaning |
|---|---|---|
role= |
neutral |
hue, via \stsetrole |
pattern= |
dense |
solid dense diag band lower upper causal empty data |
data= |
— | with pattern=data: comma-separated rows, one digit per cell, 0–3 = level |
level= |
2 |
level for pattern=solid and the flat level of a mask |
bracket= |
false |
thin neutral matrix brackets |
border= |
true |
outer black!60 border |
tiles= |
true |
false = one flat filled rectangle |
gap= |
\stgutter |
flow only: space before this object. gap=0pt = exactly adjacent |
\stindexface entries are row-major, rows*cols of them; . leaves a cell blank.
It deliberately has no lightness ramp — see semantics.md.
The package validates the entry count, pattern name, level range, and every pattern=data
row's count, width and 0–3 domain; any mismatch makes build.sh fail.
\stface[role=w, pattern=data, level=3,
data={3300,0330,0033,3003,3030,0303}]{D}{(0,0)}{T}{E}
\stindexface[role=idx]{I}{(b)}{T}{k}{0,1, 1,2, 2,3, 3,0, 0,2, 1,3}
Captions
\strowend calls \stlane for you, so in flow mode captions just follow the band:
\strowend
\stcaption{A}{$\mathbf A$}{$T\times d$} % symbol lane, shape lane
\stcaptiontop{A}{\stshapefont{token}} % occasional label above a face
Arming the lane by hand (absolute placement):
\node[inner sep=0pt, fit=(A)(B)(C)] (rowA) {};
\stlane{rowA}
\stcaption{A}{$\mathbf A$}{$T\times d$}
\stnolane
\stcaption defines name-sym and name-shape nodes; anchor the next stage heading
against name-shape.south.
Operators, connectors, nodes
Prefer \stglyph / \stcomm / \stlink (above). The raw forms are for absolute placement:
\node[st op, right=6mm of A] (m) {$\times$}; % reserves nothing -- see layout.md
\node[st comm, right=9mm of P] (ar) {All-Reduce};
\starrow{P.east}{ar.west}
\starrowlabel{M.east}{A.west}{softmax}
Styles: st sym st shape st stage st op st note st arrow st comm st brace.
Text helpers: \stformula \ststagelabel \stoperator \stsymfont \stshapefont \stprose.
Connectors route on the background layer automatically.
Bottom
\stbbox{all} % flow: everything drawn so far
\stmeaningbox{mb}{16.6cm}{all}{axes text}{objects text}{mechanism text}
\stsignature{因果多头注意力(掩码 + 拼接投影)}{mb} % optional
Arg 2 is the total box width; arg 3 is the node it hangs below. \stbbox already contains
every caption and top label; if you build the fit by hand, include them yourself or the
box will overlap them. An empty {} row is dropped.
\stsignature renders only its subject; it has no author or handle mechanism.
Gotchas
\ststacknever re-enters\stface; if you extend the package, do not passrole=\st@roleback through pgfkeys — it defines the macro in terms of itself and hangs.- A coordinate expression inside
fit=needs braces:fit={(a) ($(b)+(1,0)$)}. \foreach {\macro,...,1}cannot infer its direction from an unexpanded macro.\stroleis expandable on purpose (it is used inside\edef); the warning lives in\stcheckrole.- Neither the TikZ path parser nor the
calclibrary expands a macro sitting where it expects(. Every cursor-computed coordinate therefore reaches the parser as literal text, via\edef ... \noexpand. Same class of trap: pgfmath cannot digest\stresolve's\ifcsname, so a ledger lookup must be pre-resolved with\edefbefore it is measured. \sttopformulaputs a group around its argument, which breaks TikZ's own\\. For more than one line, wrap the math in amsmath'sgathered.