Add source linter, negative test fixtures, and fallback guidance

- 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
This commit is contained in:
dela
2026-08-05 16:41:22 +08:00
parent 7b59c81d02
commit 866173a831
34 changed files with 786 additions and 54 deletions
+7 -3
View File
@@ -14,7 +14,8 @@ SKILL.md the skill entry point (lean; loads references on demand)
references/ geometry, semantics, layout, style, api, checklist, antipatterns
assets/supertensor.sty the macro package
scripts/preflight.sh is the TikZ + CJK path available?
scripts/build.sh compile, audit the log, export pdf/svg/png/thumb
scripts/lint.py reject source-level invariant escapes
scripts/build.sh lint, compile, audit the log, export pdf/svg/png/thumb
examples/ three golden examples + an anti-pattern gallery
```
@@ -87,8 +88,11 @@ The flow layout adds two of its own: an object that overflows its band, and a `\
whose contents do not add up to the height it declared (which means it is drawn
off-center). Both are things a reader would have to notice for you.
A clean build still proves nothing about collisions, hue budget or whether the math is
right. That is what `references/checklist.md` is for.
A clean build now also proves the source avoided untracked absolute objects, ledger
changes, raw rectangles, unclosed `\stgroup` blocks, side cards anchored to a single face
or doubled up on one band, and excess per-row hues. It still cannot prove that the math,
semantics or rendered relationships are right; that is what `references/checklist.md` is
for.
## Provenance
+9 -7
View File
@@ -18,8 +18,8 @@ A figure built from raw TikZ has to re-earn every invariant by hand and usually
## Workflow
1. **Preflight.** `./scripts/preflight.sh`. Exit 0 = TikZ+CJK path. Exit 1 = degraded
(say so in the delivery). Exit 2 = no LaTeX; fall back to SVG/matplotlib and say
explicitly that the figure is not TikZ.
(say so in the delivery). Exit 2 = no LaTeX; read `references/fallback.md` before
falling back and state explicitly which package guarantees are unavailable.
2. **Reduce** the input to one primary computation path. Drop equivalent objectives,
diagnostics, and secondary metrics unless asked for.
3. **Build two ledgers** before drawing anything:
@@ -33,9 +33,9 @@ A figure built from raw TikZ has to re-earn every invariant by hand and usually
flow layout — `\ststage` / `\strow` … `\strowend`, empty coordinate arguments, gaps
declared once. Reach for an absolute coordinate only when no band can express the
placement. See `references/api.md` and `references/layout.md`.
5. **Build and audit.** `./scripts/build.sh fig.tex`. A clean build only proves TeX was
happy; then run the visual audit in `references/checklist.md` against the PNG at full
size and at thumbnail size. Redraw on any mandatory-invariant violation.
5. **Build and audit.** `./scripts/build.sh fig.tex` runs the source linter, TeX checks and
exports. Then run the remaining visual/semantic audit in `references/checklist.md`
against the PNG at full size and thumbnail size. Redraw on any mandatory violation.
For code input, trace the concrete `matmul`, `einsum`, `reshape/view`, `transpose/permute`,
concat, broadcast, and collective calls. Keep code variable names where useful; state any
@@ -51,6 +51,8 @@ shape or convention you inferred.
| discrete choices (indices, token ids, expert ids) | symbols in cells, no ramp | `\stindexface` |
| data movement, collectives, non-linear ops | arrows and nodes | `\stlink`, `\stcomm` |
| a split along the contracted axis | a vertical pair inside one band slot | `\stcol` … `\stcolend` |
| adjacent objects that are one composite (heads of `q`, shards of `W`) | an outline in their own role hue | `\stgroup` … `\stgroupend` |
| an aside about a whole band | a side card off its right edge, one per band | `\stcallout` |
Combine grammars only when each one adds information. Known zeros stay unfilled; masks,
diagonals, sparsity and partitions must encode their exact structure.
@@ -86,8 +88,8 @@ and the `.tex` source plus vector artifact.
select an OS-specific CJK font unless the user asks and accepts the portability cost.
- **English figures:** `\usepackage[en]{supertensor}` — same geometry, English rail labels.
- Keep math in LaTeX, not raw Unicode.
- The identification line is `\stsignature{<subject>}{<box>}`; it renders
`<subject>@五道口纳什`. Change the handle with `\stsetauthor{...}` only when asked.
- Add `\stsignature{<subject>}{<box>}` only when the user or house template asks for an
identification line. It renders only the subject, with no author or handle.
## Iterating
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "Supertensor"
short_description: "Create shape-aware tensor and matrix figures"
default_prompt: "Use $supertensor to turn this tensor formula or code path into an editable, shape-correct figure."
+189 -13
View File
@@ -6,7 +6,7 @@
%% Options: cjk load ctex with the portable fandol fontset (XeLaTeX)
%% en English rail labels in the meaning box (default: zh)
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{supertensor}[2026/08/04 v0.1 shape-aware tensor figure toolkit]
\ProvidesPackage{supertensor}[2026/08/05 v0.2 shape-aware tensor figure toolkit]
\newif\ifst@cjk\st@cjkfalse
\newif\ifst@en\st@enfalse
@@ -46,8 +46,20 @@
% Role registry: draw macros take a ROLE, never a color, so one tensor role
% keeps one hue across every stage of the figure.
% \stsetrole{X}{stTeal} -> role "X" is teal everywhere
\newcommand{\stsetrole}[2]{\expandafter\gdef\csname st@role@#1\endcsname{#2}}
% \stsetrole{X}{stTeal} -> role "X" is teal everywhere. Repeating the same
% declaration is harmless; changing it would make one role change meaning
% halfway through the figure, so report it as a dirty-build warning.
\newcommand{\stsetrole}[2]{%
\edef\st@newrole{#2}%
\ifcsname st@role@#1\endcsname
\edef\st@oldrole{\csname st@role@#1\endcsname}%
\ifx\st@oldrole\st@newrole\else
\PackageWarning{supertensor}{Role `#1' was already mapped to
`\st@oldrole' and cannot be remapped to `\st@newrole'}%
\fi
\else
\expandafter\gdef\csname st@role@#1\endcsname{#2}%
\fi}
% Expandable on purpose: usable inside \edef. Undeclared roles fall back to
% neutral gray and are reported at the end of the run.
\newcommand{\strole}[1]{%
@@ -68,7 +80,17 @@
% then every face built from `d' has the same physical edge, everywhere.
\newlength{\stunit}\setlength{\stunit}{4.6mm}
\newlength{\sttilegap}\setlength{\sttilegap}{0.5mm}
\newcommand{\stdim}[2]{\expandafter\gdef\csname st@dim@#1\endcsname{#2}}
\newcommand{\stdim}[2]{%
\edef\st@newdim{#2}%
\ifcsname st@dim@#1\endcsname
\edef\st@olddim{\csname st@dim@#1\endcsname}%
\ifx\st@olddim\st@newdim\else
\PackageWarning{supertensor}{Axis `#1' was already declared as
`\st@olddim' cells and cannot be redeclared as `\st@newdim'}%
\fi
\else
\expandafter\gdef\csname st@dim@#1\endcsname{#2}%
\fi}
\newcommand{\stresolve}[1]{\ifcsname st@dim@#1\endcsname\csname st@dim@#1\endcsname\else#1\fi}
% --------------------------------------------------------- type hierarchy ---
@@ -122,14 +144,16 @@
\newlength{\st@vh}
\newif\ifst@inrow
\newif\ifst@incol
\newif\ifst@ingroup
\newif\ifst@first
\newcommand{\stlayoutreset}{%
\global\st@railx=0pt \global\st@ycur=0pt
\global\st@cx=0pt \global\st@cy=0pt \global\st@bandh=0pt
\global\st@inrowfalse \global\st@incolfalse
\global\st@inrowfalse \global\st@incolfalse \global\st@ingroupfalse
\gdef\st@rowlist{}\gdef\st@alllist{}\gdef\st@rowname{}%
\gdef\st@collist{}\gdef\st@colname{}%
\gdef\st@grouplist{}\gdef\st@gname{}%
\gdef\st@lastnode{}\gdef\st@linklabel{}\gdef\st@linkprev{}}
\stlayoutreset
\newcommand{\stleftrail}[1]{\global\st@railx=\dimexpr#1\relax}
@@ -144,14 +168,19 @@
% Inside a column the items belong to the column, not to the band: they must not
% terminate a pending connector, or the arrow would land on the first sheet of
% the stack instead of on the column as a whole.
% A group behaves the same way: the wrapper, not the wrapped face, is what a
% connector may attach to. Otherwise the arrow ends inside the outline and
% crosses a box that is not its endpoint.
\newcommand{\st@regrow}[1]{%
\ifst@incol
\xdef\st@collist{\st@collist(#1)}\st@regall{#1}%
\else\ifst@ingroup
\xdef\st@grouplist{\st@grouplist(#1)}\st@regall{#1}%
\else
\xdef\st@rowlist{\st@rowlist(#1)}\st@regall{#1}%
\st@drawpendinglink{#1}%
\gdef\st@lastnode{#1}%
\fi}
\fi\fi}
\newcommand{\st@needrow}[1]{%
\ifst@inrow\else
@@ -353,6 +382,55 @@
bracket=false, border=true, tiles=true,
}
% Validate face keys before drawing. Unknown patterns used to fall through to
% dense, which produced a plausible but semantically false picture.
\newif\ifst@validpattern
\newcommand{\st@checkpattern}{%
\st@validpatternfalse
\foreach \st@known in {solid,dense,diag,band,lower,upper,causal,empty,data}{%
\IfStrEq{\st@pattern}{\st@known}{\global\st@validpatterntrue}{}}%
\ifst@validpattern\else
\PackageWarning{supertensor}{Unknown face pattern `\st@pattern'.
Use solid, dense, diag, band, lower, upper, causal, empty, or data}%
\fi}
\newcommand{\st@checklevel}{%
\IfInteger{\st@level}{%
\ifnum\st@level<0
\PackageWarning{supertensor}{Face level `\st@level' is outside 0--3}%
\def\st@level{2}%
\else\ifnum\st@level>3
\PackageWarning{supertensor}{Face level `\st@level' is outside 0--3}%
\def\st@level{2}%
\fi\fi
}{\PackageWarning{supertensor}{Face level `\st@level' is not an integer in 0--3}%
\def\st@level{2}}}
% pattern=data is a rectangular rows-by-cols array of digits 0--3. Validate
% all three facts before \StrChar reaches a missing or malformed cell.
\newcommand{\st@checkdata}{%
\IfStrEq{\st@pattern}{data}{%
\def\st@datacount{0}%
\foreach \st@drow [count=\st@di] in \st@data {\xdef\st@datacount{\st@di}}%
\ifnum\st@datacount=\st@rows\else
\PackageWarning{supertensor}{pattern=data has \st@datacount\space rows;
expected \st@rows}%
\fi
\foreach \st@drow in \st@data {%
\StrLen{\st@drow}[\st@dlen]%
\ifnum\st@dlen=\st@cols\else
\PackageWarning{supertensor}{Data row `\st@drow' has \st@dlen\space cells;
expected \st@cols}%
\fi
\edef\st@badchars{\st@drow}%
\StrSubstitute{\st@badchars}{0}{}[\st@badchars]%
\StrSubstitute{\st@badchars}{1}{}[\st@badchars]%
\StrSubstitute{\st@badchars}{2}{}[\st@badchars]%
\StrSubstitute{\st@badchars}{3}{}[\st@badchars]%
\ifdefempty{\st@badchars}{}{%
\PackageWarning{supertensor}{Data row `\st@drow' contains values outside 0--3}}%
}%
}{}}
% \st@hash{i}{j}{n} -> \st@hv in 0..n-1.
% Nested mods on purpose. Any polynomial in (i,j) reduced mod 3 is periodic
% with period 3 in BOTH directions, so a polynomial hash makes rows 1,2,4,5 of
@@ -387,6 +465,9 @@
\pgfkeys{/st/face/.cd,#1}%
\edef\st@rows{\stresolve{#2}}%
\edef\st@cols{\stresolve{#3}}%
\st@checkpattern
\st@checklevel
\st@checkdata
\stcheckrole{\st@role}%
\edef\st@col{\strole{\st@role}}%
\pgfmathsetlengthmacro{\st@w}{\st@cols*\stunit}%
@@ -438,6 +519,13 @@
\newcommand{\stindexface}[6][]{%
\begingroup
\st@setup{#1}{#4}{#5}%
\def\st@indexcount{0}%
\foreach \st@e [count=\st@zi] in {#6} {\xdef\st@indexcount{\st@zi}}%
\pgfmathtruncatemacro{\st@indexexpected}{\st@rows*\st@cols}%
\ifnum\st@indexcount=\st@indexexpected\else
\PackageWarning{supertensor}{Index face `#2' has \st@indexcount\space entries;
expected \st@indexexpected\space for \st@rows\space x \st@cols}%
\fi
\ifblank{#3}%
{\ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@w+6.8mm}\else\let\st@tw\st@w\fi
\st@flowbegin{#2}{\st@tw}{\st@h}%
@@ -472,9 +560,10 @@
\foreach \st@row [count=\st@ii] in \st@data {%
\foreach \st@jj in {1,...,\st@cols} {%
\StrChar{\st@row}{\st@jj}[\st@c]%
\ifnum\st@c>0
\st@tile{#1}{\st@ii}{\st@jj}{\st@c}%
\fi}}%
\IfInteger{\st@c}{%
\ifnum\st@c>0
\ifnum\st@c<4 \st@tile{#1}{\st@ii}{\st@jj}{\st@c}\fi
\fi}{} }}%
}{%
\foreach \st@ii in {1,...,\st@rows} {%
\foreach \st@jj in {1,...,\st@cols} {%
@@ -547,6 +636,69 @@
\ifblank{#3}{\st@regrow{#2}}{}%
\endgroup}
% ------------------------------------------------------------- grouping ----
% \stgroup[keys]{name} ... \stgroupend -- a thin rounded outline naming the
% objects drawn between them as one composite ("these three sheets are q";
% "these two shards are W"). A sub-flow, like \stcol, and for the same reason:
%
% - the members are drawn INSIDE the block, so a group can only ever wrap
% adjacent objects -- one that reached across the band would swallow
% whatever sat in between;
% - the group, not the last member, terminates a pending \stlink and 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 the outline.
%
% This is the one place a tensor-colored outline is allowed, because here the
% outline IS the object being drawn -- see style.md.
\newlength{\stgrouppad}\setlength{\stgrouppad}{1.6mm}
\pgfkeys{
/st/group/.cd,
role/.store in=\st@grole,
pad/.store in=\st@gpad,
role=neutral, pad=\stgrouppad,
}
\newcommand{\stgroup}[2][]{%
\st@needrow{\stgroup}%
\ifst@ingroup
\PackageError{supertensor}{Nested \string\stgroup}%
{Close the open group with \string\stgroupend\space first.}%
\fi
\ifst@incol
\PackageError{supertensor}{\string\stgroup\space inside \string\stcol}%
{A group reserves its padding along the band, not down the column. Wrap
the whole \string\stcol\space in the group instead.}%
\fi
\pgfkeys{/st/group/.cd,#1}%
\stcheckrole{\st@grole}%
% Snapshot everything the closing macro needs: the keys are re-read by the
% next face and the role ledger lookup must not be deferred.
\xdef\st@gname{#2}%
\xdef\st@gcolname{\strole{\st@grole}}%
\xdef\st@gpadval{\st@gpad}%
\st@leadgap{\stgutter}%
\global\advance\st@cx by \dimexpr\st@gpad\relax
\gdef\st@grouplist{}%
\global\st@ingrouptrue\global\st@firsttrue}
\newcommand{\stgroupend}{%
\global\st@ingroupfalse
\ifdefempty{\st@grouplist}%
{\PackageWarning{supertensor}{Empty \string\stgroup\space `\st@gname'}}%
{\global\advance\st@cx by \dimexpr\st@gpadval\relax
\begin{pgfonlayer}{stbg}
\edef\st@do{\noexpand\node[inner sep=\st@gpadval, outer sep=0pt,
draw=\st@gcolname!65, line width=0.5pt, rounded corners=2pt,
fit={\st@grouplist}] (\st@gname) {};}\st@do
\end{pgfonlayer}
\global\st@firstfalse
% No \st@checkh: the overhang is 2*pad by construction, not driven by
% content, and the members were already checked against the band. Warning
% about it would only teach authors to inflate the declared band height.
% \strowend fits the group, so the caption lane still clears it.
\edef\st@do{\noexpand\st@regrow{\st@gname}}\st@do}}
% ------------------------------------------------- symbol / shape captions ---
% Symbol immediately under the block, shape on the next line. Both are reserved
% lanes: nothing else may be placed between a face and its caption.
@@ -578,6 +730,32 @@
\draw[st arrow,#1] (#2) -- node[st note, above, fill=white, inner sep=1pt] {#4} (#3);
\end{pgfonlayer}}
% --------------------------------------------------------------- callout ---
% \stcallout{name}{text width}{anchor}{title}{body} -- a side note card hanging
% off the RIGHT edge of a finished band, top-aligned with it.
%
% It is deliberately hard to misuse. Inside an open band it is an error: a
% commentary card between two operands reads as a step in the computation,
% which is the antipattern layout.md names. Outside one it still pushes the
% vertical cursor below its own bottom edge, so a card taller than its band
% opens visible space rather than colliding with the next stage -- which is the
% signal that the text belongs in \stmeaningbox instead.
\newcommand{\stcallout}[5]{%
\ifst@inrow
\PackageError{supertensor}{\string\stcallout\space inside an open
\string\strow}%
{A callout is an aside, not an operand. Close the band with
\string\strowend\space first. If the text explains a step rather than
the band, it belongs in the stage heading or \string\stmeaningbox.}%
\fi
\node[anchor=north west, draw=black!18, fill=black!3, rounded corners=1.5pt,
inner xsep=7pt, inner ysep=6pt, text width=#2] (#1)
at ([xshift=\stgutter]#3.north east) {%
\scriptsize\raggedright
\ifblank{#4}{}{\textbf{#4}\par\vspace{1.5pt}}%
#5\par};
\sttrack{#1}}
% ------------------------------------------------------------ meaning box ---
\ifst@en
\def\st@lblaxes{Axes}\def\st@lblobj{Objects}\def\st@lblmech{Mechanism}
@@ -607,10 +785,8 @@
\newcommand{\stsetrail}[1]{\setlength{\st@raillen}{#1}}
% ------------------------------------------------------------- signature ----
% One centered identification line, outside the meaning box, low contrast.
\def\st@author{五道口纳什}
\newcommand{\stsetauthor}[1]{\def\st@author{#1}}
% Optional centered subject line, outside the meaning box, low contrast.
\newcommand{\stsignature}[2]{%
\node[below=2.2mm of #2, font=\scriptsize, text=black!45] (st-signature) {#1@\st@author};}
\node[below=2.2mm of #2, font=\scriptsize, text=black!45] (st-signature) {#1};}
\endinput
+1
View File
@@ -1,6 +1,7 @@
% Anti-pattern gallery -- four ways to draw a figure that compiles cleanly and
% still teaches the reader something false. Left of each pair is wrong.
% ../scripts/build.sh antipatterns.tex
% supertensor-lint: allow-absolute, allow-missing-formula
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{supertensor}
+12 -1
View File
@@ -54,7 +54,18 @@ See `style.md`.
- **A formula line placed first.** It can only be centered on a figure whose width is not
known yet, so it ends up visibly off-center. Call `\sttopformula` after the bands.
- **A floating commentary card between two operands.** If it is not a real operation, it
belongs in the stage subtitle or the bottom box.
belongs in the stage subtitle, the bottom box, or a `\stcallout` beside the whole band.
A card anchored to a single face reads as a step in the computation, and two cards on one
band turn the figure into a dashboard; both are lint errors.
- **A callout that should have been the meaning box.** If the card is taller than the band
it hangs off, it is not an aside — it is the **Mechanism** row, and leaving it as a card
only opens white space, since the callout pushes the vertical cursor below itself.
- **A group border used as decoration.** `\stgroup` names its members as one composite
object; drawn around whatever happened to be adjacent, it invents a grouping the
computation does not have. If you cannot caption the outline, do not draw it.
- **A group whose hue invents a new object.** The outline around the three `q` sheets is
still `q`. A fresh hue there claims a fourth tensor exists; use the members' role, or
`neutral` when the members really are of mixed roles. See `semantics.md`.
- **A meaning box that repeats the shapes.** The shapes are already under every block. The
box is for what the axes *mean* and what the operation *does*.
- **Solving crowding by shrinking type.** The type hierarchy is a hard floor; move the
+59 -2
View File
@@ -8,18 +8,26 @@
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
```tex
\stsetrole{q}{stTeal} % role -> color. Macros take a ROLE, never a color.
\stdim{T}{6} % symbolic axis -> physical edge length in cells
\stsetauthor{...} % default 五道口纳什
\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).
@@ -48,6 +56,7 @@ the next tensor, and two stages started from two different `x` share no rail.
| `\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 |
@@ -90,6 +99,50 @@ 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:
```tex
\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 `\stlink` and 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`:
```tex
\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
```tex
@@ -118,6 +171,8 @@ Keys:
`\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.
```tex
\stface[role=w, pattern=data, level=3,
@@ -167,13 +222,15 @@ Connectors route on the background layer automatically.
```tex
\stbbox{all} % flow: everything drawn so far
\stmeaningbox{mb}{16.6cm}{all}{axes text}{objects text}{mechanism text}
\stsignature{因果多头注意力(掩码 + 拼接投影)}{mb}
\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
- `\ststack` never re-enters `\stface`; if you extend the package, do not pass
+10 -7
View File
@@ -1,7 +1,8 @@
# Pre-delivery checklist
A clean `build.sh` proves only that TeX was happy. Nothing below is checked by the compiler.
Work through it against the rendered PNG. Any mandatory violation means redraw, not patch.
`build.sh` already checks the source rules, package invariants, missing glyphs and text-box
overflow. The math, semantics and rendered relationships below still require inspection.
Any mandatory violation means redraw, not patch.
## 1. Math (before looking at the picture)
@@ -30,18 +31,20 @@ Open the PNG at 100 %. Most of the first three items are automatic under the flo
they still need looking at, because the cursor only guarantees that objects do not *push*
into each other, not that the figure reads correctly.
- [ ] No absolute coordinate that a `\strow`/`\stcol` could have expressed. Every remaining
hand-placed node is wrapped in `\sttrack`.
- [ ] `\sttopformula` called after the bands, so the formula is centered on the real figure.
- [ ] Every lint exemption in the source is genuinely required and explained; deliverables
normally have none.
- [ ] No forbidden intersection, tangency, clipping or occlusion — including stack offset
sheets, brackets, arrow labels and the meaning box.
- [ ] Every connector's white label underlay covers only its own connector.
- [ ] No connector crosses a box that is not its endpoint.
- [ ] Symbols and shapes sit on two flat lanes per row; stage headings share a left rail.
- [ ] Every `\stgroup` outline names a composite the computation actually has, is captioned,
and carries its members' role hue (`neutral` only for genuinely mixed members).
- [ ] Every `\stcallout` is an aside about its whole band, not a step: one per band, no
taller than the band, and nothing in it that belongs in `\stmeaningbox`.
- [ ] Top zone compact (≤2 formula lines, no shape underbraces).
- [ ] Bottom box: one column, ≤3 rows, no overflow, no shrunken type.
- [ ] Signature outside the box, one line, names what the figure actually shows, not clipped
and not visually dominant.
- [ ] If requested, signature is outside the box, one line, accurate, unclipped and subdued.
- [ ] Structural support exact: known zeros unfilled, masks and diagonals exactly right.
## 5. Thumbnail audit
+18
View File
@@ -0,0 +1,18 @@
# Fallback without LaTeX
Use this path only when `scripts/preflight.sh` exits 2. A fallback is not equivalent to the
TikZ package: say explicitly that cursor placement, ledger locking, face-data validation,
caption lanes and package warnings are unavailable.
## Preserve manually
1. Recompute the shape/semantics ledger before drawing.
2. Use one scale function from symbolic axis names to physical lengths; never size two
occurrences of the same axis independently.
3. Keep scores, indices and masks in distinct grammars; preserve exact zeros and support.
4. Derive every placement from previous bounding boxes and one gutter constant.
5. Export SVG plus PNG and inspect both full-size and at 360 px using `checklist.md`.
Prefer SVG for editability. Use matplotlib only when it can emit SVG and the tensor cells
remain individually inspectable. Do not imitate package compliance in the delivery: name
the fallback renderer and list any inferred shape, convention or reduced guarantee.
+5 -1
View File
@@ -17,8 +17,12 @@ to exactly one physical edge length across the whole figure**. Equal shapes ther
an equivalence class automatically: `Q` and `V` at `T×d_h` come out identical without you
lining anything up by hand.
Declaring the same axis twice with the same value is allowed. Redeclaring it with a
different value emits a package warning, preserves the first value, and fails `build.sh`.
Raw integers are accepted (`\stface{A}{(0,0)}{4}{4}`) but they opt out of the guarantee.
Use them only for a face whose axis appears nowhere else.
Use them only for a face whose axis appears nowhere else. The source linter rejects a
repeated raw dimension greater than one; give repeated dimensions a symbolic name.
## The rules
+18 -3
View File
@@ -34,6 +34,7 @@ Overlap is allowed only inside one declared composite:
- shards tiling a parent,
- outline sheets in one `\ststack`,
- a bracket around its own tensor,
- a `\stgroup` outline around its own members,
- a connector endpoint touching its source/target border.
Every other intersection or occlusion is forbidden.
@@ -68,9 +69,23 @@ Stage headings share one left rail. `\ststage` puts them there: the rail is a si
`x` (`\stleftrail` to move it), and the `y` is derived from the lowest ink drawn so far, so
a heading can neither drift right nor collide with the row above it.
Explanatory prose belongs in the stage subtitle, the bottom box, or above its own
connector. Never drop a floating commentary card between two operands unless it is a real
operation node (`st comm`).
Explanatory prose belongs in the stage subtitle, the bottom box, above its own connector,
or on a `\stcallout` card hanging off the right edge of a band. Never drop a floating
commentary card between two operands unless it is a real operation node (`st comm`).
`\stcallout` is that rule made structural: it refuses to open inside a band, the linter
rejects it unless it is anchored to a `\strow` name, and a second card on one band is an
error — side-by-side cards are a dashboard, not a figure. A card that ends up much taller
than its band is telling you the same thing the overflow warning does: that text is not an
aside, it is the **Mechanism** row of `\stmeaningbox`.
## Composites
`\stcol` and `\stgroup` are the two sub-flows, and both exist so that a *relationship* can
be stated rather than arranged for. A group wraps its members from the inside, which is
what makes an arrow attach to the outline rather than end inside it — a connector whose
endpoint is a member but which crosses the group border is the ordinary version of "a
connector may not cross a box that is not its endpoint".
## Layers
+6
View File
@@ -65,3 +65,9 @@ One tensor role keeps one hue for the whole figure — that is what `\stsetrole`
gathered, resharded or regrouped view of the same data keeps the *same* role color; a new
hue means a new object. Derived tensors may reuse their parent's family rather than
spending a hue (`V → O → Y` in the MHA example are all violet).
A `\stgroup` outline follows the same rule, because a group *is* a regrouped view: give it
the role of the objects it wraps — the three-sheet `q` stack and the outline that names it
as one composite are the same object, and a new hue there would claim a new tensor exists.
Only when the members genuinely differ in role does the group take `role=neutral`; that is
also the honest signal that the box is naming an arrangement rather than an object.
+18 -6
View File
@@ -21,8 +21,9 @@ stages that preserve the primary path. No unrelated branches, no dashboard panel
| operator | `\Large` | `st op` |
| symbol | `\small` | `\stcaption` arg 2 |
| shape | `\scriptsize`, muted | `\stcaption` arg 3 |
| side card | `\scriptsize\bfseries` title, `\scriptsize` body | `\stcallout`, same tier as `st note` |
| bottom prose | `\small` | `\stmeaningbox` |
| signature | `\scriptsize`, low contrast | `\stsignature` |
| optional signature | `\scriptsize`, low contrast | `\stsignature` |
Never shrink below this to make something fit — see `layout.md`.
@@ -32,6 +33,10 @@ Separate tiles with a small white gutter and 0.5–1 pt corner rounding (`\st@ti
this). Thin neutral brackets, `black!55`–`black!70` outer borders. No saturated
tensor-colored outlines, no continuous spreadsheet grid.
The one exception is `\stgroup`, whose outline is drawn at `role!65`: there the outline
*is* the object being named, so the hue is doing semantic work rather than decorating a
face that already has its own fill.
**Encode support before magnitude.** Every known zero stays white/unfilled; every shown
nonzero gets color. A diagonal matrix must read instantly as colored diagonal cells on a
white field. `pattern=diag/band/lower/upper/causal/data` fill exactly the structural
@@ -69,16 +74,23 @@ Narrow bold label rail, left-aligned ragged-right `\small` content, 8–10 pt in
Keep each row compact: prefer symbol semantics over numeric configuration. When it is too
long, **remove content** — never add cards, columns or smaller type. Pass `{}` to omit a row.
## Signature
## Side cards
One centered line below the box, outside it, low-contrast gray, `\scriptsize` or smaller:
`\stsignature{<subject>}{<fit node>}` renders `<subject>@五道口纳什`. The subject must name
what this figure actually visualizes. Keep it on one line, with a small but visible gap.
`\stcallout` is the only sanctioned floating text card, and it is deliberately narrow in
scope: one per band, hung off the right edge of a *finished* band, never between two
operands. When the text outgrows the height of its band, it is not an aside — move it into
the **Mechanism** row of `\stmeaningbox` instead of widening or shrinking the card.
## Optional signature
Add a centered line only when the user or house template requests it. Keep it below the
box, outside it, low-contrast gray and on one line. `\stsignature{<subject>}{<fit node>}`
renders only the subject. Do not append an author, handle or brand identity.
## Never
Charts or metric insets not present in the primary formula. Decorative pills, banners,
shadows, repeated separators, explanatory cards.
shadows, repeated separators, explanatory cards other than one `\stcallout` per band.
## Reference image
+6 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Compile a supertensor figure and export every delivery artifact.
# Lint, compile, and export every delivery artifact for a supertensor figure.
#
# ./scripts/build.sh figure.tex [outdir]
#
@@ -23,6 +23,11 @@ OUT="${2:-$SRCDIR/build}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
mkdir -p "$OUT"
if [[ "${ST_SKIP_LINT:-0}" != "1" ]]; then
echo "==> lint $BASE"
python3 "$ROOT/scripts/lint.py" "$SRC"
fi
echo "==> xelatex $BASE"
# supertensor.sty lives in assets/; keep it off the user's texmf tree.
TEXINPUTS="$ROOT/assets:$SRCDIR:" \
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""Static source checks for supertensor figures.
The TeX package owns geometry at render time; this linter catches source-level
escapes that TeX cannot see: mutable ledgers, hand placement, formula order,
raw rectangles, repeated anonymous dimensions, and per-row hue budget.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
DIM_RE = re.compile(r"\\stdim\{([^{}]+)\}\{([^{}]+)\}")
ROLE_RE = re.compile(r"\\stsetrole\{([^{}]+)\}\{([^{}]+)\}")
FACE_RE = re.compile(
r"\\(stface|ststack|stindexface)(?:\[[^\]]*\])?"
r"\{([^{}]+)\}\{([^{}]*)\}\{([^{}]+)\}\{([^{}]+)\}",
re.DOTALL,
)
ROW_RE = re.compile(r"\\strow\{[^{}]+\}\{[^{}]+\}(.*?)\\strowend", re.DOTALL)
ROW_NAME_RE = re.compile(r"\\strow\{([^{}]+)\}\{[^{}]+\}")
ROLE_KEY_RE = re.compile(r"role\s*=\s*([A-Za-z0-9_-]+)")
CALLOUT_RE = re.compile(r"\\stcallout\{([^{}]+)\}\{[^{}]+\}\{([^{}]+)\}")
def strip_comments(source: str) -> str:
return re.sub(r"(?<!\\)%.*$", "", source, flags=re.MULTILINE)
def line_of(source: str, offset: int) -> int:
return source.count("\n", 0, offset) + 1
def directives(source: str) -> set[str]:
found: set[str] = set()
for match in re.finditer(r"^\s*%\s*supertensor-lint:\s*(.+)$", source, re.MULTILINE):
found.update(item.strip() for item in match.group(1).split(","))
return found
def check_unique(
source: str, pattern: re.Pattern[str], kind: str, errors: list[str]
) -> dict[str, str]:
values: dict[str, str] = {}
for match in pattern.finditer(source):
name, value = (part.strip() for part in match.groups())
previous = values.get(name)
if previous is not None and previous != value:
errors.append(
f"line {line_of(source, match.start())}: {kind} {name!r} changes "
f"from {previous!r} to {value!r}"
)
else:
values[name] = value
return values
def lint(path: Path) -> list[str]:
raw = path.read_text(encoding="utf-8")
allowed = directives(raw)
source = strip_comments(raw)
errors: list[str] = []
dimensions = check_unique(source, DIM_RE, "axis", errors)
roles = check_unique(source, ROLE_RE, "role", errors)
formula_positions = [m.start() for m in re.finditer(r"\\sttopformula\b", source)]
row_ends = [m.start() for m in re.finditer(r"\\strowend\b", source)]
if not formula_positions and "allow-missing-formula" not in allowed:
errors.append("missing \\sttopformula (add an explicit lint exemption for galleries/tests)")
elif formula_positions and row_ends and formula_positions[-1] < row_ends[-1]:
errors.append(
f"line {line_of(source, formula_positions[-1])}: \\sttopformula must follow the last \\strowend"
)
tracked = set(re.findall(r"\\sttrack\{([^{}]+)\}", source))
raw_dimensions: dict[str, list[int]] = {}
for match in FACE_RE.finditer(source):
_macro, name, coordinate, rows, cols = match.groups()
if coordinate.strip() and name not in tracked and "allow-absolute" not in allowed:
errors.append(
f"line {line_of(source, match.start())}: absolute object {name!r} has no \\sttrack{{{name}}}"
)
for value in (rows.strip(), cols.strip()):
if value.isdigit() and value != "1":
raw_dimensions.setdefault(value, []).append(line_of(source, match.start()))
if "allow-repeated-raw-dim" not in allowed:
for value, lines in sorted(raw_dimensions.items()):
if len(lines) > 1:
errors.append(
f"lines {', '.join(map(str, lines))}: raw dimension {value!r} is reused; declare a symbolic axis with \\stdim"
)
if "allow-raw-tikz" not in allowed:
for match in re.finditer(
r"\\(?:draw|fill|path)\b[^;]*\brectangle\b", source, re.DOTALL
):
errors.append(
f"line {line_of(source, match.start())}: hand-drawn rectangle bypasses \\stface"
)
# \stgroup is a block. An unclosed one silently swallows the rest of the
# band into the group's fit list, which TeX only reports as an empty row.
opened = len(re.findall(r"\\stgroup(?![A-Za-z])", source))
closed = len(re.findall(r"\\stgroupend(?![A-Za-z])", source))
if opened != closed:
errors.append(
f"{opened} \\stgroup vs {closed} \\stgroupend: every group must be closed"
)
# A callout hangs off a BAND. Anchored to a face it becomes the floating
# commentary card between two operands that layout.md forbids, and two of
# them on one band is a dashboard, not a figure.
band_names = set(ROW_NAME_RE.findall(source))
callout_anchors: dict[str, str] = {}
for match in CALLOUT_RE.finditer(source):
name, anchor = (part.strip() for part in match.groups())
line = line_of(source, match.start())
if anchor not in band_names:
errors.append(
f"line {line}: callout {name!r} is anchored to {anchor!r}, which is not a "
f"\\strow band; a card hanging off a single object reads as a step"
)
elif anchor in callout_anchors:
errors.append(
f"line {line}: callout {name!r} is the second card on band {anchor!r} "
f"(after {callout_anchors[anchor]!r}); one aside per band"
)
callout_anchors.setdefault(anchor, name)
for row_number, row in enumerate(ROW_RE.finditer(source), start=1):
row_roles = set(ROLE_KEY_RE.findall(row.group(1)))
colors = {roles.get(role, "<undeclared>") for role in row_roles}
active = colors - {"stGray"}
if len(active) > 4:
errors.append(
f"line {line_of(source, row.start())}: row {row_number} uses {len(active)} active hue families "
f"({', '.join(sorted(active))}); maximum is 4 plus gray"
)
# Declared dimensions are intentionally allowed to be illustrative. This
# lookup merely keeps the declaration parse exercised and future-proof.
del dimensions
return errors
def main() -> int:
parser = argparse.ArgumentParser(description="lint a supertensor .tex source")
parser.add_argument("sources", nargs="+", type=Path)
args = parser.parse_args()
failed = False
for path in args.sources:
if not path.is_file():
print(f"supertensor-lint: no such file: {path}", file=sys.stderr)
failed = True
continue
errors = lint(path)
if errors:
failed = True
print(f"!! {path}", file=sys.stderr)
for error in errors:
print(f" {error}", file=sys.stderr)
else:
print(f" lint {path}")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
+11 -7
View File
@@ -12,7 +12,7 @@ QUIET=0
[[ "${1:-}" == "--quiet" ]] && QUIET=1
say() { [[ $QUIET -eq 1 ]] || echo -e "$*"; }
ok=0; warn=0; fail=0
ok=0; warn=0; fail=0; cjk_warn=0; export_warn=0
check() { # name, command
local name="$1"; shift
if "$@" >/dev/null 2>&1; then say " ok $name"; ok=$((ok+1)); return 0
@@ -27,11 +27,11 @@ check "tikz.sty" kpsewhich tikz.sty || fail=$((fail+1))
check "xstring.sty" kpsewhich xstring.sty || fail=$((fail+1))
say "--- chinese figures ---"
check "ctex.sty" kpsewhich ctex.sty || warn=$((warn+1))
check "fandol font" kpsewhich FandolSong-Regular.otf || warn=$((warn+1))
check "ctex.sty" kpsewhich ctex.sty || { warn=$((warn+1)); cjk_warn=1; }
check "fandol font" kpsewhich FandolSong-Regular.otf || { warn=$((warn+1)); cjk_warn=1; }
say "--- raster / vector export ---"
check "pdftocairo" command -v pdftocairo || warn=$((warn+1))
check "pdftocairo" command -v pdftocairo || { warn=$((warn+1)); export_warn=1; }
check "latexmk (optional)" command -v latexmk || true
if [[ $fail -gt 0 ]]; then
@@ -44,9 +44,13 @@ fi
if [[ $warn -gt 0 ]]; then
say ""
say "RESULT: degraded."
say " - missing ctex/fandol -> English-label figures only; do not substitute"
say " an OS-specific CJK font without telling the user it costs portability."
say " - missing pdftocairo -> deliver PDF only, and say PNG/SVG were skipped."
if [[ $cjk_warn -eq 1 ]]; then
say " - missing ctex/fandol -> English-label figures only; do not substitute"
say " an OS-specific CJK font without telling the user it costs portability."
fi
if [[ $export_warn -eq 1 ]]; then
say " - missing pdftocairo -> deliver PDF only, and say PNG/SVG were skipped."
fi
exit 1
fi
say ""
+25 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build every example and the smoke test. Any dirty build fails the run.
# Build every valid example, then prove invalid TeX and lint fixtures fail.
#
# ./scripts/test.sh
#
@@ -21,8 +21,30 @@ for f in "$ROOT"/tests/*.tex "$ROOT"/examples/*.tex; do
fi
done
for f in "$ROOT"/tests/invalid/*.tex; do
[[ -e "$f" ]] || continue
name="$(basename "$f")"
if ST_SKIP_LINT=1 "$ROOT/scripts/build.sh" "$f" >/dev/null 2>&1; then
echo " FAIL $name (invalid fixture built cleanly)"
fail=$((fail+1))
else
echo " ok $name (rejected by package/build)"
fi
done
for f in "$ROOT"/tests/lint-invalid/*.tex; do
[[ -e "$f" ]] || continue
name="$(basename "$f")"
if python3 "$ROOT/scripts/lint.py" "$f" >/dev/null 2>&1; then
echo " FAIL $name (invalid fixture passed lint)"
fail=$((fail+1))
else
echo " ok $name (rejected by lint)"
fi
done
if [[ $fail -gt 0 ]]; then
echo "$fail failing figure(s); rerun scripts/build.sh on one to see why" >&2
echo "$fail failing check(s); rerun the reported build or lint command to inspect" >&2
exit 1
fi
echo "all figures build clean"
echo "all positive and negative checks passed"
+3
View File
@@ -58,6 +58,9 @@
\stcaption{Wc}{$\mathbf W^{(r)}$}{$d\times T$}
\stcaption{Z}{$\mathbf Z$}{$T\times T$}
\sttopformula{F}{$\displaystyle
\operatorname{flow}(X,W)\;:\;\text{对象按自身边界框依次占位}$}
\stbbox{all}
\stmeaningbox{mb}{15cm}{all}
{$T$ 序列长,$d$ 模型维,$r$ 低秩维}
+78
View File
@@ -0,0 +1,78 @@
% Regression test for \stgroup and \stcallout.
% - a group over one stack (the three sheets are one composite object),
% - a group over two adjacent shards (the pair is one partitioned parent),
% - a side card hanging off the right edge of each band.
% No absolute coordinate appears anywhere in this figure.
% ../scripts/build.sh group-callout.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{supertensor}
\stsetrole{act}{stTeal}
\stsetrole{w}{stOrange}
\stsetrole{q}{stTeal}
\stsetrole{k}{stViolet}
\stsetrole{v}{stCoral}
\stdim{T}{7}
\stdim{d}{5}
\stdim{dh}{3}
\begin{document}
\begin{tikzpicture}
\ststage{S1}{一、分组框:把已经排好的面命名为一个复合对象}
\strow{R1}{T}
\stface[role=act, bracket=true]{X}{}{T}{d}
\stlink{l1}{三路投影}
\stgroup[role=q]{QG}
\ststack[role=q]{Q}{}{T}{dh}{3}
\stgroupend
\stgroup[role=k]{KG}
\ststack[role=k]{K}{}{T}{dh}{3}
\stgroupend
\stgroup[role=v]{VG}
\ststack[role=v]{V}{}{T}{dh}{3}
\stgroupend
\strowend
\stcaption{X}{$\mathbf X$}{$T\times d$}
\stcaption{QG}{$\mathbf q$}{$h\times T\times d_h$}
\stcaption{KG}{$\mathbf k$}{$h\times T\times d_h$}
\stcaption{VG}{$\mathbf v$}{$h\times T\times d_h$}
\stcallout{N1}{4.2cm}{R1}{分组框在说什么}%
{外框与里面的面同色,因为它圈的是同一个对象的多头视图,不是一个新张量。
框的内边距计入行的包围盒,所以下面的符号轨仍然从框底起算。}
\ststage{S2}{二、分组框圈住两片相邻的分片,标注它们合起来是谁}
\strow{R2}{T}
\stface[role=act]{H}{}{T}{d}
\stglyph{m2}{$\times$}
\stgroup[role=w, pad=2.2mm]{WG}
\stcol{Wc}{d}
\stface[role=w]{Wa}{}{dh}{T}
\stface[role=w, gap=0pt]{Wb}{}{2}{T}
\stcolend
\stgroupend
\stlink{l2}{}
\stface[role=q, bracket=true]{Z}{}{T}{T}
\strowend
\stcaption{H}{$\mathbf H$}{$T\times d$}
\stcaption{WG}{$\mathbf W$}{$d\times T$}
\stcaption{Z}{$\mathbf Z$}{$T\times T$}
\stcallout{N2}{4.2cm}{R2}{一行一张卡}%
{同一个 band 上挂两张卡会被 lint 拒绝:并排的说明卡是仪表盘,不是图。}
\sttopformula{F}{$\displaystyle \mathbf Z=\mathbf H\,\mathbf W,\qquad
\mathbf W=\begin{bmatrix}\mathbf W_a\\ \mathbf W_b\end{bmatrix}$}
\stbbox{all}
\stmeaningbox{mb}{17cm}{all}
{$T$ 序列长,$d$ 模型维,$d_h$ 头宽,$h$ 头数}
{}
{分组框是横向的子流:成员写在块里,所以它只能圈住相邻的对象;
两侧的内边距各占一份宽度,箭头也接在外框上而不是成员上——
否则连接线会穿过一个并非自己端点的框。
说明卡挂在 band 右侧,并把纵向游标压到自己底边以下,所以卡片过高只会撑开空白,
不会压住下一段。}
\end{tikzpicture}
\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stdim{d}{4}
\stdim{d}{7}
\begin{document}\begin{tikzpicture}
\stface{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{idx}{stOrange}
\stdim{m}{2}
\stdim{n}{3}
\begin{document}\begin{tikzpicture}
\stindexface[role=idx]{I}{(0,0)}{m}{n}{0,1,2,3,4}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{m}{2}
\stdim{n}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=data,data={303}]{A}{(0,0)}{m}{n}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{m}{2}
\stdim{n}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=data,data={303,34}]{A}{(0,0)}{m}{n}
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=solid,level=9]{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=checkerboard]{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stsetrole{x}{stCoral}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x]{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface{A}{(0,0)}{d}{d}
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+12
View File
@@ -0,0 +1,12 @@
% A callout anchored to a face rather than to a band: the card lands beside a
% single operand and reads as a step in the computation.
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{T}{4}
\begin{document}\begin{tikzpicture}
\strow{R1}{T}
\stface{A}{}{T}{T}
\strowend
\stcallout{N1}{3cm}{A}{aside}{anchored to a face, not to the band}
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+13
View File
@@ -0,0 +1,13 @@
% Two asides on one band: a dashboard, not a figure. The second card belongs
% in \stmeaningbox.
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{T}{4}
\begin{document}\begin{tikzpicture}
\strow{R1}{T}
\stface{A}{}{T}{T}
\strowend
\stcallout{N1}{3cm}{R1}{first}{one aside per band}
\stcallout{N2}{3cm}{R1}{second}{this one has nowhere to go}
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+9
View File
@@ -0,0 +1,9 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\sttopformula{F}{$A$}
\strow{row}{d}
\stface{A}{}{d}{d}
\strowend
\end{tikzpicture}\end{document}
+12
View File
@@ -0,0 +1,12 @@
% An unclosed \stgroup swallows the rest of the band into its fit list; TeX
% only reports the resulting empty row, which does not point at the cause.
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{T}{4}
\begin{document}\begin{tikzpicture}
\strow{R1}{T}
\stgroup{G}
\stface{A}{}{T}{T}
\strowend
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+18
View File
@@ -0,0 +1,18 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\stsetrole{a}{stTeal}
\stsetrole{b}{stOrange}
\stsetrole{c}{stCoral}
\stsetrole{d}{stViolet}
\stsetrole{e}{stInk}
\stdim{x}{2}
\begin{document}\begin{tikzpicture}
\strow{row}{x}
\stface[role=a]{A}{}{x}{x}
\stface[role=b]{B}{}{x}{x}
\stface[role=c]{C}{}{x}{x}
\stface[role=d]{D}{}{x}{x}
\stface[role=e]{E}{}{x}{x}
\strowend
\sttopformula{F}{$A+B+C+D+E$}
\end{tikzpicture}\end{document}
+6
View File
@@ -0,0 +1,6 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\begin{document}\begin{tikzpicture}
\fill (0,0) rectangle (2,2);
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+2
View File
@@ -1,3 +1,5 @@
% supertensor-lint: allow-absolute, allow-missing-formula
% Legacy absolute-placement smoke test; flow layout is covered by flow.tex.
\documentclass[border=8pt]{standalone}
\usepackage[cjk]{supertensor}