Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
866173a831 | ||
|
|
7b59c81d02 |
@@ -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
|
||||
```
|
||||
|
||||
@@ -37,14 +38,14 @@ A minimal figure:
|
||||
\stdim{d}{4}
|
||||
|
||||
\begin{document}\begin{tikzpicture}
|
||||
\stface[role=act, bracket=true]{X}{(0,0)}{T}{d}
|
||||
\node[st op, right=6mm of X] (m) {$\times$};
|
||||
\stface[role=w]{W}{($(m)+(1.4,0)$)}{d}{d}
|
||||
\node[inner sep=0pt, fit=(X)(W)] (row) {};
|
||||
\stlane{row}
|
||||
\ststage{S1}{one band, placed by cursor}
|
||||
\strow{row}{T} % band height, declared once
|
||||
\stface[role=act, bracket=true]{X}{}{T}{d} % empty coord = at the cursor
|
||||
\stglyph{m}{$\times$}
|
||||
\stface[role=w]{W}{}{d}{d}
|
||||
\strowend
|
||||
\stcaption{X}{$\mathbf X$}{$T\times d$}
|
||||
\stcaption{W}{$\mathbf W$}{$d\times d$}
|
||||
\stnolane
|
||||
\end{tikzpicture}\end{document}
|
||||
```
|
||||
|
||||
@@ -52,13 +53,19 @@ Because `T` and `d` come from the ledger, the contracted axis is automatically o
|
||||
length in both operands, `d×d` is automatically square, and any other face of shape `T×d`
|
||||
in the figure is automatically identical to `X`.
|
||||
|
||||
Because the coordinates are empty, each object reserves its own width and the gap between
|
||||
them is `\stgutter`, declared once. Nothing here is a tuned offset, so growing a label can
|
||||
only push its neighbours apart — it can never land on top of one. And the band declares its
|
||||
height, so an object that does not fit is a failed build rather than something the reader
|
||||
discovers.
|
||||
|
||||
See `references/api.md` for the full macro list.
|
||||
|
||||
## Examples
|
||||
|
||||
| file | shows |
|
||||
|---|---|
|
||||
| `tp-ffn-allreduce.tex` | column-then-row sharding, exact tiling, one hue per TP rank, a collective as a real node |
|
||||
| `tp-ffn-allreduce.tex` | column-then-row sharding, exact tiling, one hue per TP rank, a collective as a real node, a `\stcol` split along the contracted axis |
|
||||
| `mha-causal.tex` | leading axes as stack depth, a physically swapped `Kᵀ`, a mask in a different grammar from the scores it gates |
|
||||
| `moe-topk-gather.tex` | scores → indices → Boolean support → gather, with all three cell grammars side by side |
|
||||
| `antipatterns.tex` | four figures that compile cleanly and still teach something false |
|
||||
@@ -77,8 +84,15 @@ Two LaTeX warnings produce a figure that is quietly wrong rather than visibly br
|
||||
greps for both and exits non-zero. A `Package supertensor Warning` — an undeclared role
|
||||
falling back to gray — is treated the same way.
|
||||
|
||||
A clean build still proves nothing about collisions, hue budget or whether the math is
|
||||
right. That is what `references/checklist.md` is for.
|
||||
The flow layout adds two of its own: an object that overflows its band, and a `\stcol`
|
||||
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 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
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -29,11 +29,13 @@ A figure built from raw TikZ has to re-earn every invariant by hand and usually
|
||||
- *geometry* — one `\stdim{axis}{cells}` per symbolic axis, one `\stsetrole{role}{color}`
|
||||
per tensor role. Declaring these makes the invariants automatic.
|
||||
See `references/geometry.md`.
|
||||
4. **Pick the smallest grammar** that exposes the mechanism (see below), then reserve
|
||||
stage lanes and draw. See `references/layout.md` and `references/api.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.
|
||||
4. **Pick the smallest grammar** that exposes the mechanism (see below), then draw with the
|
||||
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` 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
|
||||
@@ -47,7 +49,10 @@ shape or convention you inferred.
|
||||
| sharding, device ownership, channel groups | adjacent faces tiling a parent | two `\stface` calls, one role each |
|
||||
| leading axes (`B`, `h`) | depth | `\ststack{...}{sheets}` |
|
||||
| discrete choices (indices, token ids, expert ids) | symbols in cells, no ramp | `\stindexface` |
|
||||
| data movement, collectives, non-linear ops | arrows and nodes | `\starrow`, `\starrowlabel`, `st comm` |
|
||||
| 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.
|
||||
@@ -65,7 +70,8 @@ reference file with the full statement and the failure it prevents.
|
||||
from continuous score to discrete index to gathered value.
|
||||
- **Layout** (`references/layout.md`) — everything is a bounding box; tangency counts as
|
||||
collision. Reserved lanes for stage heading / tensors / symbols / shapes. Connectors on
|
||||
the background layer, text on the foreground layer.
|
||||
the background layer, text on the foreground layer. Place by cursor, not by hand-tuned
|
||||
coordinate: a magic-number offset is only valid for the content it was tuned against.
|
||||
- **Style** (`references/style.md`) — muted palette, one hue per role, ≤4 active hues per
|
||||
row plus gray, three separated lightness levels, non-periodic texture, no decoration.
|
||||
|
||||
@@ -82,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
|
||||
|
||||
|
||||
@@ -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."
|
||||
+473
-20
@@ -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 ---
|
||||
@@ -92,6 +114,254 @@
|
||||
line width=0.4pt},
|
||||
}
|
||||
|
||||
% --------------------------------------------------------- flow layout ------
|
||||
% Hand-placed absolute coordinates are the main source of layout bugs in these
|
||||
% figures. Every gap ends up 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. The cursor below removes both by construction:
|
||||
% gaps are declared once, positions are derived from the objects actually
|
||||
% drawn. Absolute placement still works -- pass a coordinate instead of `{}'.
|
||||
%
|
||||
% \ststage{S1}{stage heading} % anchored on the left rail
|
||||
% \strow{R1}{T} % band of height T units
|
||||
% \stface[role=q]{Q}{}{T}{d} % empty coord = place at the cursor
|
||||
% \stglyph{m}{$\times$}
|
||||
% \stface[role=w]{W}{}{d}{d}
|
||||
% \stcol{C}{2*d units} % a vertical pair in one band slot
|
||||
% \stface[role=r1]{Ca}{}{d}{d}
|
||||
% \stface[role=r2, gap=0pt]{Cb}{}{d}{d}
|
||||
% \stcolend
|
||||
% \strowend % fits the band, arms \stlane
|
||||
\newlength{\stgutter}\setlength{\stgutter}{6mm} % between objects in a band
|
||||
\newlength{\strowgap}\setlength{\strowgap}{3.5mm} % above a band
|
||||
\newlength{\stblockgap}\setlength{\stblockgap}{9mm} % above a stage heading
|
||||
\newlength{\stlinklen}\setlength{\stlinklen}{10mm} % bare connector reservation
|
||||
\newlength{\st@railx}\newlength{\st@cx}\newlength{\st@cy}
|
||||
\newlength{\st@ycur}\newlength{\st@bandh}\newlength{\st@tmpx}\newlength{\st@tmpy}
|
||||
% Column sub-flow state. Named st@v* rather than st@col*: \st@cols and \st@col
|
||||
% are already the column count and the resolved hue of a face.
|
||||
\newlength{\st@vx}\newlength{\st@vy}\newlength{\st@vw}\newlength{\st@vtop}
|
||||
\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@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}
|
||||
\newcommand{\stvgap}[1]{\global\advance\st@ycur by -\dimexpr#1\relax}
|
||||
|
||||
% The vertical cursor tracks the lowest ink so far, so the next band never has
|
||||
% to be positioned by eye.
|
||||
\newcommand{\st@lower}[1]{%
|
||||
\pgfextracty{\st@tmpy}{\pgfpointanchor{#1}{south}}%
|
||||
\ifdim\st@tmpy<\st@ycur \global\st@ycur=\st@tmpy \fi}
|
||||
\newcommand{\st@regall}[1]{\xdef\st@alllist{\st@alllist(#1)}}
|
||||
% 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}
|
||||
|
||||
\newcommand{\st@needrow}[1]{%
|
||||
\ifst@inrow\else
|
||||
\PackageError{supertensor}{\string#1\space needs an open \string\strow}%
|
||||
{Flow placement only works between \string\strow\space and
|
||||
\string\strowend. Pass an explicit coordinate instead.}%
|
||||
\fi}
|
||||
|
||||
% The gap belongs to the object that follows it, and the first object in a band
|
||||
% gets none -- that is what puts every band's left edge on the same rail.
|
||||
\newcommand{\st@leadgap}[1]{%
|
||||
\ifst@first
|
||||
\global\st@firstfalse
|
||||
\else
|
||||
\ifst@incol\global\advance\st@vy by -\dimexpr#1\relax
|
||||
\else\global\advance\st@cx by \dimexpr#1\relax\fi
|
||||
\fi}
|
||||
|
||||
% Reserve #1 of horizontal space, hand back the center coordinate for it.
|
||||
\newcommand{\st@flow}[1]{%
|
||||
\dimen0=\dimexpr#1\relax
|
||||
\dimen2=\st@cx \advance\dimen2 by 0.5\dimen0
|
||||
\edef\st@pos{(\the\dimen2,\the\st@cy)}%
|
||||
\global\advance\st@cx by \dimen0}
|
||||
|
||||
% Reserve #2 of vertical space inside a column, hand back the center for it.
|
||||
% Items are centered on the column's own axis, so a narrow shard sits under a
|
||||
% wide one without an eyeballed offset.
|
||||
\newcommand{\st@vflow}[2]{%
|
||||
\dimen0=\dimexpr#1\relax \dimen4=\dimexpr#2\relax
|
||||
\dimen2=\st@vx \advance\dimen2 by 0.5\dimen0
|
||||
\dimen6=\st@vy \advance\dimen6 by -0.5\dimen4
|
||||
\edef\st@pos{(\the\dimen2,\the\dimen6)}%
|
||||
\global\advance\st@vy by -\dimen4
|
||||
\ifdim\dimen0>\st@vw \global\st@vw=\dimen0 \fi}
|
||||
|
||||
% A band declares its height, so an object that does not fit is a build
|
||||
% failure rather than something the reader discovers.
|
||||
\newcommand{\st@checkh}[2]{%
|
||||
\dimen0=\dimexpr#2\relax \advance\dimen0 by -\st@bandh
|
||||
\ifdim\dimen0>4mm
|
||||
\PackageWarning{supertensor}{Object `#1' overflows its band by
|
||||
\the\dimen0. Raise the height declared in \string\strow\space or give it
|
||||
its own band}%
|
||||
\fi}
|
||||
|
||||
\newcommand{\ststage}[2]{%
|
||||
\dimen0=\st@ycur \advance\dimen0 by -\stblockgap
|
||||
\node[st stage, anchor=north west] (#1) at (\the\st@railx,\the\dimen0) {#2};
|
||||
\st@regall{#1}\st@lower{#1}}
|
||||
|
||||
% \strow{name}{height in axis units or a declared axis}
|
||||
\newcommand{\strow}[2]{%
|
||||
\gdef\st@rowlist{}\gdef\st@rowname{#1}\gdef\st@lastnode{}%
|
||||
\gdef\st@linklabel{}\gdef\st@linkprev{}%
|
||||
\global\st@inrowtrue\global\st@firsttrue
|
||||
% Resolve through the ledger first: pgfmath's parser cannot digest
|
||||
% \stresolve's \ifcsname on its own.
|
||||
\edef\st@bandu{\stresolve{#2}}%
|
||||
\pgfmathsetlengthmacro{\st@bh}{\st@bandu*\stunit}%
|
||||
\global\st@bandh=\st@bh
|
||||
\global\st@cx=\st@railx
|
||||
\dimen0=\st@ycur \advance\dimen0 by -\strowgap \advance\dimen0 by -0.5\st@bandh
|
||||
\global\st@cy=\dimen0}
|
||||
|
||||
\newcommand{\strowend}{%
|
||||
\ifdefempty{\st@rowlist}%
|
||||
{\PackageWarning{supertensor}{Empty \string\strow\space `\st@rowname'}}%
|
||||
{\edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\st@rowlist}] (\st@rowname) {};}\st@do
|
||||
\stlane{\st@rowname}\st@regall{\st@rowname}\st@lower{\st@rowname}}%
|
||||
\global\st@inrowfalse}
|
||||
|
||||
% \stcol{name}{height} ... \stcolend -- a vertical sub-flow occupying one slot
|
||||
% of the band. This is what a split along a shared axis looks like: two shards
|
||||
% stacked with gap=0pt tile their parent exactly, which no pair of hand-tuned
|
||||
% y offsets can guarantee.
|
||||
\newcommand{\stcol}[2]{%
|
||||
\st@needrow{\stcol}%
|
||||
\ifst@incol
|
||||
\PackageError{supertensor}{Nested \string\stcol}%
|
||||
{Close the open column with \string\stcolend\space first.}%
|
||||
\fi
|
||||
\st@leadgap{\stgutter}%
|
||||
\gdef\st@collist{}\gdef\st@colname{#1}%
|
||||
\global\st@vx=\st@cx \global\st@vw=0pt
|
||||
\edef\st@colu{\stresolve{#2}}%
|
||||
\pgfmathsetlengthmacro{\st@ch}{\st@colu*\stunit}%
|
||||
\global\st@vh=\st@ch
|
||||
\dimen0=\st@cy \advance\dimen0 by 0.5\st@vh
|
||||
\global\st@vtop=\dimen0 \global\st@vy=\dimen0
|
||||
\global\st@incoltrue\global\st@firsttrue}
|
||||
|
||||
\newcommand{\stcolend}{%
|
||||
\ifdefempty{\st@collist}%
|
||||
{\PackageWarning{supertensor}{Empty \string\stcol\space `\st@colname'}%
|
||||
\global\st@incolfalse}%
|
||||
{\dimen0=\st@vtop \advance\dimen0 by -\st@vy % height actually consumed
|
||||
\global\st@incolfalse
|
||||
\edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\st@collist}] (\st@colname) {};}\st@do
|
||||
\global\advance\st@cx by \st@vw
|
||||
\global\st@firstfalse
|
||||
% The column is centered on the band using its DECLARED height, so a
|
||||
% mismatch is not just an overflow -- it is a column drawn off-center.
|
||||
\dimen2=\dimen0 \advance\dimen2 by -\st@vh
|
||||
\ifdim\dimen2<0pt \dimen2=-\dimen2 \fi
|
||||
\ifdim\dimen2>2mm
|
||||
\PackageWarning{supertensor}{Column `\st@colname' consumes
|
||||
\the\dimen0\space but declares \the\st@vh; it is drawn off-center.
|
||||
Fix the height in \string\stcol}%
|
||||
\fi
|
||||
\st@checkh{\st@colname}{\the\dimen0}%
|
||||
\st@regrow{\st@colname}}}
|
||||
|
||||
% A node in the flow: an operator glyph, a collective, a note. It reserves its
|
||||
% own width, which is the whole point -- `right=6mm of X' reserves nothing, so
|
||||
% the next face is free to land on top of it.
|
||||
\newcommand{\stnode}[3][]{%
|
||||
\st@needrow{\stnode}\st@leadgap{\stgutter}%
|
||||
\ifst@incol
|
||||
\node[#1, anchor=north west] (#2) at (\the\st@vx,\the\st@vy) {#3};
|
||||
\pgfextractx{\st@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\advance\st@tmpx by -\st@vx
|
||||
\ifdim\st@tmpx>\st@vw \global\st@vw=\st@tmpx \fi
|
||||
\pgfextracty{\st@tmpy}{\pgfpointanchor{#2}{south}}%
|
||||
\global\st@vy=\st@tmpy
|
||||
\else
|
||||
\node[#1, anchor=west] (#2) at (\the\st@cx,\the\st@cy) {#3};
|
||||
\pgfextractx{\st@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\global\st@cx=\st@tmpx
|
||||
\fi
|
||||
\st@regrow{#2}}
|
||||
% Not \stop: that name is already taken by plain TeX.
|
||||
\newcommand{\stglyph}[2]{\stnode[st op]{#1}{#2}}
|
||||
\newcommand{\stcomm}[2]{\stnode[st comm]{#1}{#2}}
|
||||
\newcommand{\stgap}[1]{\global\advance\st@cx by \dimexpr#1\relax}
|
||||
|
||||
% \stlink{name}{label} -- a connector whose LABEL is a flow object. The label
|
||||
% reserves its own width and the arrows are drawn to the neighbours once the
|
||||
% right-hand one exists, so a label can never be wider than its connector.
|
||||
\newcommand{\stlink}[2]{%
|
||||
\st@needrow{\stlink}%
|
||||
\xdef\st@linkprev{\st@lastnode}%
|
||||
\ifblank{#2}%
|
||||
{\gdef\st@linklabel{}\stgap{\stlinklen}}%
|
||||
{\st@leadgap{\stgutter}%
|
||||
\node[st note, anchor=west, inner sep=1pt] (#1) at (\the\st@cx,\the\st@cy) {#2};
|
||||
\pgfextractx{\st@tmpx}{\pgfpointanchor{#1}{east}}%
|
||||
\global\st@cx=\st@tmpx
|
||||
\gdef\st@linklabel{#1}\st@regall{#1}}}
|
||||
\newcommand{\st@drawpendinglink}[1]{%
|
||||
\ifdefempty{\st@linkprev}{}{%
|
||||
\begin{pgfonlayer}{stbg}
|
||||
\ifdefempty{\st@linklabel}%
|
||||
{\draw[st arrow] (\st@linkprev.east) -- (#1.west);}%
|
||||
{\draw[st arrow] (\st@linkprev.east) -- (\st@linklabel.west);
|
||||
\draw[st arrow] (\st@linklabel.east) -- (#1.west);}%
|
||||
\end{pgfonlayer}
|
||||
\gdef\st@linkprev{}\gdef\st@linklabel{}}}
|
||||
|
||||
% Everything drawn through the flow, as one node -- for the meaning box anchor.
|
||||
\newcommand{\stbbox}[1]{%
|
||||
\edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\st@alllist}] (#1) {};}\st@do}
|
||||
|
||||
% Escape hatch: fold a hand-placed node into the bounding box and push the
|
||||
% vertical cursor below it, so the next band still knows where the ink ends.
|
||||
\newcommand{\sttrack}[1]{\st@regall{#1}\st@lower{#1}}
|
||||
|
||||
% \sttopformula{name}{math} -- the formula line, centered on everything drawn
|
||||
% so far. Call it AFTER the bands: a formula placed first can only be centered
|
||||
% on a figure whose width is not known yet, which is how the top line ends up
|
||||
% visibly off-center.
|
||||
\newcommand{\sttopformula}[2]{%
|
||||
\stbbox{st@bbt}%
|
||||
% For more than one line, wrap the math in amsmath's `gathered': TikZ's own
|
||||
% \\ handling does not survive the group \stformula puts around the content.
|
||||
\node[above=6mm of st@bbt, anchor=south] (#1) {\stformula{#2}};
|
||||
\st@regall{#1}}
|
||||
|
||||
% ------------------------------------------------------------- faces --------
|
||||
\newif\ifst@bracket
|
||||
\newif\ifst@border
|
||||
@@ -102,13 +372,65 @@
|
||||
pattern/.store in=\st@pattern,
|
||||
data/.store in=\st@data,
|
||||
level/.store in=\st@level,
|
||||
gap/.store in=\st@gap,
|
||||
bracket/.is if=st@bracket,
|
||||
border/.is if=st@border,
|
||||
tiles/.is if=st@tiles,
|
||||
role=neutral, pattern=dense, data={}, level=2,
|
||||
% gap= is the space BEFORE this object, replacing the standing gutter.
|
||||
% gap=0pt is how shards are made to tile their parent exactly.
|
||||
role=neutral, pattern=dense, data={}, level=2, gap=\stgutter,
|
||||
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
|
||||
@@ -143,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}%
|
||||
@@ -153,17 +478,36 @@
|
||||
% `role=\st@role' back through pgfkeys would define \st@role in terms of
|
||||
% itself and hang the run.
|
||||
\newcommand{\st@facecore}[2]{%
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#1) at #2 {};
|
||||
\st@basenode{#1}{#2}%
|
||||
\st@facebody{#1}}
|
||||
|
||||
% Shared entry for cursor placement: reserve the width, check the band.
|
||||
\newcommand{\st@flowbegin}[3]{%
|
||||
\st@needrow{\stface}\st@leadgap{\st@gap}%
|
||||
\ifst@incol \st@vflow{#2}{#3}\else \st@flow{#2}\st@checkh{#1}{#3}\fi}
|
||||
|
||||
% Neither the TikZ path parser nor calc expands a macro sitting where it
|
||||
% expects `('. Everything the cursor computes therefore goes through \edef and
|
||||
% reaches the parser as literal text.
|
||||
\newcommand{\st@coordat}[2]{\coordinate (#1) at #2;}
|
||||
\newcommand{\st@basenode}[2]{%
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#1) at #2 {};}
|
||||
|
||||
% \stface[keys]{name}{center coord}{rows}{cols}
|
||||
% rows/cols accept a declared axis name or a raw integer. Height <- rows,
|
||||
% width <- cols, always: a matrix face a x b never renders sideways.
|
||||
% An empty coord places the face at the row cursor. The bracket decoration is
|
||||
% drawn outside the node box, so its 3.4mm on each side is reserved too.
|
||||
\newcommand{\stface}[5][]{%
|
||||
\begingroup
|
||||
\st@setup{#1}{#4}{#5}%
|
||||
\st@facecore{#2}{#3}%
|
||||
\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}%
|
||||
\edef\st@do{\noexpand\st@facecore{#2}{\st@pos}}\st@do
|
||||
\st@regrow{#2}%
|
||||
}{\st@facecore{#2}{#3}}%
|
||||
\endgroup}
|
||||
|
||||
% \stindexface[keys]{name}{center coord}{rows}{cols}{entries}
|
||||
@@ -175,8 +519,18 @@
|
||||
\newcommand{\stindexface}[6][]{%
|
||||
\begingroup
|
||||
\st@setup{#1}{#4}{#5}%
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#2) at #3 {};
|
||||
\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}%
|
||||
\edef\st@do{\noexpand\st@basenode{#2}{\st@pos}}\st@do}%
|
||||
{\st@basenode{#2}{#3}}%
|
||||
\foreach \st@e [count=\st@z from 0] in {#6} {%
|
||||
\pgfmathtruncatemacro{\st@ii}{div(\st@z,\st@cols)+1}%
|
||||
\pgfmathtruncatemacro{\st@jj}{mod(\st@z,\st@cols)+1}%
|
||||
@@ -196,6 +550,7 @@
|
||||
\draw[draw=black!60, line width=0.5pt, rounded corners=1pt]
|
||||
(#2.south west) rectangle (#2.north east);
|
||||
\fi
|
||||
\ifblank{#3}{\st@regrow{#2}}{}%
|
||||
\endgroup}
|
||||
|
||||
\newcommand{\st@facebody}[1]{%
|
||||
@@ -205,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]%
|
||||
\IfInteger{\st@c}{%
|
||||
\ifnum\st@c>0
|
||||
\st@tile{#1}{\st@ii}{\st@jj}{\st@c}%
|
||||
\fi}}%
|
||||
\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} {%
|
||||
@@ -250,9 +606,17 @@
|
||||
\st@setup{#1}{#4}{#5}%
|
||||
\pgfmathtruncatemacro{\st@back}{#6-1}%
|
||||
\pgfmathsetlengthmacro{\st@dx}{1.3mm}%
|
||||
\coordinate (#2-c) at ($#3+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$);
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#2-front) at (#2-c) {};
|
||||
% The offset sheets are part of the object: reserve their extent too, or the
|
||||
% neighbour is spaced against the front sheet and lands on the back ones.
|
||||
\ifblank{#3}%
|
||||
{\pgfmathsetlengthmacro{\st@tw}{\st@w+\st@back*\st@dx}%
|
||||
\pgfmathsetlengthmacro{\st@th}{\st@h+\st@back*\st@dx}%
|
||||
\ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@tw+6.8mm}\fi
|
||||
\st@flowbegin{#2}{\st@tw}{\st@th}%
|
||||
\edef\st@do{\noexpand\st@coordat{#2-c}%
|
||||
{($\st@pos+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$)}}\st@do}%
|
||||
{\coordinate (#2-c) at ($#3+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$);}%
|
||||
\st@basenode{#2-front}{(#2-c)}%
|
||||
\ifnum\st@back>0
|
||||
% Ascending loop, descending index: `{\macro,...,1}' cannot infer its
|
||||
% direction from an unexpanded macro and runs away.
|
||||
@@ -269,8 +633,72 @@
|
||||
\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={(#2-front) ($(#2-front.north east)+(\st@back*\st@dx,\st@back*\st@dx)$)}]
|
||||
(#2) {};
|
||||
\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.
|
||||
@@ -286,7 +714,8 @@
|
||||
{\node[st sym, below=1.6mm of #1] (#1-sym) {#2};}%
|
||||
{\node[st sym, anchor=north]
|
||||
at ($(#1.center |- \st@lane.south)+(0,-1.6mm)$) (#1-sym) {#2};}%
|
||||
\node[st shape, below=0.6mm of #1-sym] (#1-shape) {#3};}
|
||||
\node[st shape, below=0.6mm of #1-sym] (#1-shape) {#3};%
|
||||
\st@regall{#1-shape}\st@lower{#1-shape}}
|
||||
\newcommand{\stcaptiontop}[2]{%
|
||||
\node[st sym, above=1.6mm of #1] (#1-top) {#2};}
|
||||
|
||||
@@ -301,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}
|
||||
@@ -330,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,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}
|
||||
|
||||
|
||||
+39
-51
@@ -2,6 +2,7 @@
|
||||
% Shows: leading axes as stack depth, a transpose that physically swaps the
|
||||
% face, equal edge length on the contracted axis, and a Boolean mask drawn in
|
||||
% a different grammar from the scores it gates.
|
||||
% Layout is entirely by cursor: no absolute coordinate appears below.
|
||||
% ../scripts/build.sh mha-causal.tex
|
||||
\documentclass[border=10pt]{standalone}
|
||||
\usepackage[cjk]{supertensor}
|
||||
@@ -19,79 +20,66 @@
|
||||
\begin{document}
|
||||
\begin{tikzpicture}
|
||||
|
||||
% ================================================================= formula ==
|
||||
\node (F) at (0,0) {\stformula{$\displaystyle
|
||||
\mathbf A^{(i)}=\mathrm{softmax}\!\left(
|
||||
\frac{\mathbf Q^{(i)}\mathbf K^{(i)\top}}{\sqrt{d_h}}+\mathbf M\right),\qquad
|
||||
\mathbf O^{(i)}=\mathbf A^{(i)}\mathbf V^{(i)}$}};
|
||||
|
||||
% ============================================================ stage A row ===
|
||||
\node[st stage, below=7mm of F] (SA) {每头打分:沿 $d_h$ 收缩};
|
||||
\coordinate (a) at ($(SA)+(-3.9,-1.9)$);
|
||||
|
||||
\ststack[role=q, bracket=true]{Q}{(a)}{T}{dh}{3}
|
||||
\node[st op, right=6mm of Q] (mA) {$\times$};
|
||||
\ststage{SA}{每头打分:沿 $d_h$ 收缩}
|
||||
\strow{rowA}{T}
|
||||
\ststack[role=q, bracket=true]{Q}{}{T}{dh}{3}
|
||||
\stglyph{mA}{$\times$}
|
||||
% K^T: the face is physically swapped, not relabelled. Its height equals Q's
|
||||
% width -- that is the contracted axis d_h, drawn at one edge length.
|
||||
\ststack[role=k, bracket=true]{KT}{($(mA)+(1.9,0)$)}{dh}{T}{3}
|
||||
\node[st op, right=6mm of KT] (eA) {$=$};
|
||||
\ststack[role=s]{S}{($(eA)+(2.0,0)$)}{T}{T}{3}
|
||||
|
||||
\node[inner sep=0pt, fit=(Q)(KT)(S)] (rowA) {};
|
||||
\stlane{rowA}
|
||||
\ststack[role=k, bracket=true]{KT}{}{dh}{T}{3}
|
||||
\stglyph{eA}{$=$}
|
||||
\ststack[role=s]{S}{}{T}{T}{3}
|
||||
\strowend
|
||||
\stcaption{Q}{$\mathbf Q^{(i)}$}{$h\times T\times d_h$}
|
||||
\stcaption{KT}{$\mathbf K^{(i)\top}$}{$h\times d_h\times T$}
|
||||
\stcaption{S}{$\mathbf S^{(i)}$}{$h\times T\times T$}
|
||||
\stnolane
|
||||
|
||||
% ============================================================ stage B row ===
|
||||
\node[st stage, below=9mm of Q-shape.south west, anchor=north west] (SB)
|
||||
{因果掩码与加权求和};
|
||||
\coordinate (b) at ($(SB)+(0.6,-2.0)$);
|
||||
|
||||
\ststage{SB}{因果掩码与加权求和}
|
||||
\strow{rowB}{T}
|
||||
% The mask is a Boolean support, not a magnitude: one flat level, exact
|
||||
% triangle, no stack -- it is shared by every head.
|
||||
\stface[role=w, pattern=data,
|
||||
data={300000,330000,333000,333300,333330,333333}]{M}{(b)}{T}{T}
|
||||
\ststack[role=s, pattern=causal]{A}{($(M.east)+(3.75,0)$)}{T}{T}{3}
|
||||
\starrowlabel{M.east}{A.west}{softmax}
|
||||
\node[st op, right=6mm of A] (mB) {$\times$};
|
||||
\ststack[role=v, bracket=true]{V}{($(mB)+(1.4,0)$)}{T}{dh}{3}
|
||||
\node[st op, right=6mm of V] (eB) {$=$};
|
||||
\ststack[role=v]{O}{($(eB)+(1.4,0)$)}{T}{dh}{3}
|
||||
|
||||
\node[inner sep=0pt, fit=(M)(A)(V)(O)] (rowB) {};
|
||||
\stlane{rowB}
|
||||
data={300000,330000,333000,333300,333330,333333}]{M}{}{T}{T}
|
||||
\stlink{lB}{softmax}
|
||||
\ststack[role=s, pattern=causal]{A}{}{T}{T}{3}
|
||||
\stglyph{mB}{$\times$}
|
||||
\ststack[role=v, bracket=true]{V}{}{T}{dh}{3}
|
||||
\stglyph{eB}{$=$}
|
||||
\ststack[role=v]{O}{}{T}{dh}{3}
|
||||
\strowend
|
||||
\stcaption{M}{$\mathbf M$}{$T\times T$}
|
||||
\stcaption{A}{$\mathbf A^{(i)}$}{$h\times T\times T$}
|
||||
\stcaption{V}{$\mathbf V^{(i)}$}{$h\times T\times d_h$}
|
||||
\stcaption{O}{$\mathbf O^{(i)}$}{$h\times T\times d_h$}
|
||||
\stnolane
|
||||
|
||||
% ============================================================ stage C row ===
|
||||
\node[st stage, below=9mm of M-shape.south west, anchor=north west] (SC)
|
||||
{沿 $d_h$ 拼接后投影};
|
||||
\coordinate (c) at ($(SC)+(1.2,-2.0)$);
|
||||
|
||||
% Concatenation reverses the split: three h-shards of width d_h tile a face of
|
||||
% width d exactly.
|
||||
\stface[role=v]{C1}{(c)}{T}{dh}
|
||||
\stface[role=v]{C2}{($(C1.east)+(1.5*\stunit,0)$)}{T}{dh}
|
||||
\stface[role=v]{C3}{($(C2.east)+(1.5*\stunit,0)$)}{T}{dh}
|
||||
\node[st op, right=6mm of C3] (mC) {$\times$};
|
||||
\stface[role=w]{WO}{($(mC)+(2.5,0)$)}{d}{d}
|
||||
\node[st op, right=6mm of WO] (eC) {$=$};
|
||||
\stface[role=v, bracket=true]{Y}{($(eC)+(2.5,0)$)}{T}{d}
|
||||
|
||||
\node[inner sep=0pt, fit=(C1)(WO)(Y)] (rowC) {};
|
||||
\stlane{rowC}
|
||||
\ststage{SC}{沿 $d_h$ 拼接后投影}
|
||||
\strow{rowC}{d} % the d x d projection is the tallest object here
|
||||
% Concatenation reverses the split: gap=0pt makes the three h-shards of width
|
||||
% d_h tile a face of width d exactly, with no eyeballed offset.
|
||||
\stface[role=v]{C1}{}{T}{dh}
|
||||
\stface[role=v, gap=0pt]{C2}{}{T}{dh}
|
||||
\stface[role=v, gap=0pt]{C3}{}{T}{dh}
|
||||
\stglyph{mC}{$\times$}
|
||||
\stface[role=w]{WO}{}{d}{d}
|
||||
\stglyph{eC}{$=$}
|
||||
\stface[role=v, bracket=true]{Y}{}{T}{d}
|
||||
\strowend
|
||||
\stcaption{C2}{$[\,\mathbf O^{(1)}\mid\mathbf O^{(2)}\mid\mathbf O^{(3)}\,]$}{$T\times d$}
|
||||
\stcaption{WO}{$\mathbf W_O$}{$d\times d$}
|
||||
\stcaption{Y}{$\mathbf Y$}{$T\times d$}
|
||||
\stnolane
|
||||
|
||||
% ================================================================= formula ==
|
||||
% Placed last so it is centered on the figure that was actually drawn.
|
||||
\sttopformula{F}{$\displaystyle
|
||||
\mathbf A^{(i)}=\mathrm{softmax}\!\left(
|
||||
\frac{\mathbf Q^{(i)}\mathbf K^{(i)\top}}{\sqrt{d_h}}+\mathbf M\right),\qquad
|
||||
\mathbf O^{(i)}=\mathbf A^{(i)}\mathbf V^{(i)}$}
|
||||
|
||||
% ============================================================== meaning box ==
|
||||
\node[inner sep=0pt, fit=(F)(rowA)(rowB)(rowC)(Y-shape)(C2-shape)] (all) {};
|
||||
\stbbox{all}
|
||||
\stmeaningbox{mb}{16.8cm}{all}
|
||||
{$T$ 序列长度,$d_h$ 单头宽度,$h$ 头数(图中 $h=3$,即堆叠的三张面),
|
||||
$d=h\,d_h$;批轴 $B$ 省略}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
% (graded lightness), an INDEX face (discrete symbols, no ramp), and a BOOLEAN
|
||||
% support face (one flat level) -- plus a gather whose output heights are data
|
||||
% dependent and must sum back to T*k.
|
||||
% Layout is entirely by cursor: no absolute coordinate appears below.
|
||||
% ../scripts/build.sh moe-topk-gather.tex
|
||||
\documentclass[border=10pt]{standalone}
|
||||
\usepackage[cjk]{supertensor}
|
||||
@@ -22,84 +23,69 @@
|
||||
\begin{document}
|
||||
\begin{tikzpicture}
|
||||
|
||||
% ================================================================= formula ==
|
||||
\node (F) at (0,0) {\stformula{$\displaystyle
|
||||
\mathbf G=\mathrm{softmax}(\mathbf{XW}_g),\quad
|
||||
\mathcal I_t=\operatorname*{top-}k_{e}\,\mathbf G_{t,e},\quad
|
||||
\mathbf D_{t,e}=\mathbf 1[e\in\mathcal I_t],\quad
|
||||
\mathbf X^{(e)}=\mathrm{gather}(\mathbf X,\mathbf D_{:,e})$}};
|
||||
|
||||
% ============================================================ stage A row ===
|
||||
\node[st stage, below=7mm of F] (SA) {打分:token 对专家};
|
||||
\coordinate (a) at ($(SA)+(-3.6,-1.8)$);
|
||||
|
||||
\stface[role=act, bracket=true]{X}{(a)}{T}{d}
|
||||
\node[st op, right=6mm of X] (mA) {$\times$};
|
||||
\stface[role=wg]{Wg}{($(mA)+(1.6,0)$)}{d}{E}
|
||||
\node[st op, right=6mm of Wg] (eA) {$=$};
|
||||
\stface[role=s]{G}{($(eA)+(1.6,0)$)}{T}{E}
|
||||
|
||||
\node[inner sep=0pt, fit=(X)(Wg)(G)] (rowA) {};
|
||||
\stlane{rowA}
|
||||
\ststage{SA}{打分:token 对专家}
|
||||
\strow{rowA}{T}
|
||||
\stface[role=act, bracket=true]{X}{}{T}{d}
|
||||
\stglyph{mA}{$\times$}
|
||||
\stface[role=wg]{Wg}{}{d}{E}
|
||||
\stglyph{eA}{$=$}
|
||||
\stface[role=s]{G}{}{T}{E}
|
||||
\strowend
|
||||
\stcaption{X}{$\mathbf X$}{$T\times d$}
|
||||
\stcaption{Wg}{$\mathbf W_g$}{$d\times E$}
|
||||
\stcaption{G}{$\mathbf G$}{$T\times E$}
|
||||
\stnolane
|
||||
|
||||
% ============================================================ stage B row ===
|
||||
\node[st stage, below=9mm of X-shape.south west, anchor=north west] (SB)
|
||||
{取前 $k$:连续分数 $\rightarrow$ 离散选择};
|
||||
\coordinate (b) at ($(SB.west)+(0.6,-1.7)$);
|
||||
|
||||
\ststage{SB}{取前 $k$:连续分数 $\rightarrow$ 离散选择}
|
||||
\strow{rowB}{T}
|
||||
% Indices are drawn as symbols, not as magnitudes: expert 3 is not "bigger"
|
||||
% than expert 0, so the index face gets no lightness ramp.
|
||||
\stindexface[role=idx]{I}{(b)}{T}{k}{0,1, 1,2, 2,3, 3,0, 0,2, 1,3}
|
||||
\stindexface[role=idx]{I}{}{T}{k}{0,1, 1,2, 2,3, 3,0, 0,2, 1,3}
|
||||
\stlink{lb}{one-hot}
|
||||
% The same routing decision as a boolean support: one flat level, exactly k
|
||||
% cells per row, and every unselected cell left unfilled.
|
||||
\stface[role=m, pattern=data, level=3,
|
||||
data={3300,0330,0033,3003,3030,0303}]{D}{($(I.east)+(3.1,0)$)}{T}{E}
|
||||
\starrowlabel{I.east}{D.west}{one-hot}
|
||||
|
||||
\node[st comm, right=9mm of D] (gz) {Gather};
|
||||
\starrow{D.east}{gz.west}
|
||||
|
||||
\node[inner sep=0pt, fit=(I)(D)(gz)] (rowB) {};
|
||||
\stlane{rowB}
|
||||
data={3300,0330,0033,3003,3030,0303}]{D}{}{T}{E}
|
||||
\stlink{lg}{}
|
||||
\stcomm{gz}{Gather}
|
||||
\strowend
|
||||
\stcaption{I}{$\mathcal I$}{$T\times k$}
|
||||
\stcaption{D}{$\mathbf D$}{$T\times E$}
|
||||
\stnolane
|
||||
|
||||
% ============================================================ stage C row ===
|
||||
% Every stage heading starts on the same left rail; only the vertical position
|
||||
% follows the previous row.
|
||||
\coordinate (cy) at ($(I-shape.south)+(0,-9mm)$);
|
||||
\node[st stage, anchor=north west] (SC) at (SB.west |- cy)
|
||||
{按专家聚合:每个缓冲区的高度是数据决定的};
|
||||
\coordinate (c) at ($(SC.west)+(0.5,-1.9)$);
|
||||
|
||||
% Each buffer keeps X's width d -- gather regroups rows, it never reshapes the
|
||||
% feature axis. The heights are n_e, and they must sum to T*k.
|
||||
\stindexface[role=idx, border=false]{t0}{(c)}{ne}{1}{1,4,5}
|
||||
\stface[role=act]{B0}{($(t0.east)+(2*\stunit,0)$)}{ne}{d}
|
||||
\stindexface[role=idx, border=false]{t1}{($(B0.east)+(1.1,0)$)}{ne}{1}{1,2,6}
|
||||
\stface[role=act]{B1}{($(t1.east)+(2*\stunit,0)$)}{ne}{d}
|
||||
\stindexface[role=idx, border=false]{t2}{($(B1.east)+(1.1,0)$)}{ne}{1}{2,3,5}
|
||||
\stface[role=act]{B2}{($(t2.east)+(2*\stunit,0)$)}{ne}{d}
|
||||
\stindexface[role=idx, border=false]{t3}{($(B2.east)+(1.1,0)$)}{ne}{1}{3,4,6}
|
||||
\stface[role=act]{B3}{($(t3.east)+(2*\stunit,0)$)}{ne}{d}
|
||||
|
||||
\ststage{SC}{按专家聚合:每个缓冲区的高度是数据决定的}
|
||||
\strow{rowC}{ne}
|
||||
% Each buffer keeps X's width d -- gather regroups rows, it never reshapes
|
||||
% the feature axis. The heights are n_e, and they must sum to T*k.
|
||||
% A token column and its buffer are one object: tight gap inside the pair,
|
||||
% the standing gutter (widened) between pairs.
|
||||
\stindexface[role=idx, border=false]{t0}{}{ne}{1}{1,4,5}
|
||||
\stface[role=act, gap=2.5mm]{B0}{}{ne}{d}
|
||||
\stindexface[role=idx, border=false, gap=12mm]{t1}{}{ne}{1}{1,2,6}
|
||||
\stface[role=act, gap=2.5mm]{B1}{}{ne}{d}
|
||||
\stindexface[role=idx, border=false, gap=12mm]{t2}{}{ne}{1}{2,3,5}
|
||||
\stface[role=act, gap=2.5mm]{B2}{}{ne}{d}
|
||||
\stindexface[role=idx, border=false, gap=12mm]{t3}{}{ne}{1}{3,4,6}
|
||||
\stface[role=act, gap=2.5mm]{B3}{}{ne}{d}
|
||||
\strowend
|
||||
\stcaptiontop{t0}{\stshapefont{token}}
|
||||
|
||||
\node[inner sep=0pt, fit=(t0)(B3)] (rowC) {};
|
||||
\stlane{rowC}
|
||||
\sttrack{t0-top}
|
||||
\stcaption{B0}{$\mathbf X^{(1)}$}{$n_1\times d$}
|
||||
\stcaption{B1}{$\mathbf X^{(2)}$}{$n_2\times d$}
|
||||
\stcaption{B2}{$\mathbf X^{(3)}$}{$n_3\times d$}
|
||||
\stcaption{B3}{$\mathbf X^{(4)}$}{$n_4\times d$}
|
||||
\stnolane
|
||||
|
||||
% ================================================================= formula ==
|
||||
% Placed last so it is centered on the figure that was actually drawn.
|
||||
\sttopformula{F}{$\displaystyle
|
||||
\mathbf G=\mathrm{softmax}(\mathbf{XW}_g),\quad
|
||||
\mathcal I_t=\operatorname*{top-}k_{e}\,\mathbf G_{t,e},\quad
|
||||
\mathbf D_{t,e}=\mathbf 1[e\in\mathcal I_t],\quad
|
||||
\mathbf X^{(e)}=\mathrm{gather}(\mathbf X,\mathbf D_{:,e})$}
|
||||
|
||||
% ============================================================== meaning box ==
|
||||
\node[inner sep=0pt, fit=(F)(rowA)(rowB)(rowC)(B3-shape)(t0-top)] (all) {};
|
||||
\stbbox{all}
|
||||
\stmeaningbox{mb}{16.6cm}{all}
|
||||
{$T$ token 数,$d$ 模型宽度,$E$ 专家数(图中 $E=4$),$k$ 每 token 选中的专家数
|
||||
(图中 $k=2$),$n_e$ 落到第 $e$ 个专家的 token 数}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
% Golden example 1 -- tensor parallel FFN, column-then-row sharding + AllReduce.
|
||||
% Shows: partition geometry (shards tile the parent exactly), one hue per TP
|
||||
% rank held across every stage, a collective node as a real operation.
|
||||
% rank held across every stage, a collective node as a real operation, and a
|
||||
% split along the contracted axis drawn as a column sub-flow.
|
||||
% Layout is entirely by cursor: no absolute coordinate appears below.
|
||||
% ../scripts/build.sh tp-ffn-allreduce.tex
|
||||
\documentclass[border=10pt]{standalone}
|
||||
\usepackage[cjk]{supertensor}
|
||||
@@ -14,74 +16,71 @@
|
||||
\stdim{bt}{6} % B*T rows
|
||||
\stdim{d}{4} % model width
|
||||
\stdim{dffl}{4} % d_ff / p (per-rank hidden width)
|
||||
\stdim{dff}{8} % d_ff = p * (d_ff/p); the height of the stacked W_2 column
|
||||
|
||||
\begin{document}
|
||||
\begin{tikzpicture}
|
||||
|
||||
% ================================================================= formula ==
|
||||
\node (F) at (0,0) {\stformula{$\mathbf{XW}_1=[\,\mathbf{XW}_1^{(1)}\mid
|
||||
\mathbf{XW}_1^{(2)}\,]=[\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,]$}};
|
||||
\node[below=1.2mm of F] (F2) {\stformula{$\displaystyle
|
||||
[\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,]
|
||||
\begin{bmatrix}\mathbf W_2^{(1)}\\[-1pt]\mathbf W_2^{(2)}\end{bmatrix}
|
||||
=\sum_{r}\mathbf H^{(r)}\mathbf W_2^{(r)}=\sum_r\mathbf P^{(r)}$}};
|
||||
|
||||
% ============================================================ stage A row ===
|
||||
\node[st stage, below=7mm of F2] (SA) {列切 $\mathbf W_1$:无通信};
|
||||
\coordinate (a) at ($(SA)+(-5.6,-1.8)$);
|
||||
|
||||
\stface[role=act, bracket=true]{X}{(a)}{bt}{d}
|
||||
\node[st op, right=5mm of X] (mA) {$\times$};
|
||||
% Two shards, tiled exactly: adjacent faces, no stretching, no gap.
|
||||
\stface[role=r1]{W1a}{($(mA)+(1.5,0)$)}{d}{dffl}
|
||||
\stface[role=r2]{W1b}{($(W1a.east)+(2*\stunit,0)$)}{d}{dffl}
|
||||
\node[st op, right=5mm of W1b] (eA) {$=$};
|
||||
\stface[role=r1]{Ha}{($(eA)+(1.5,0)$)}{bt}{dffl}
|
||||
\stface[role=r2]{Hb}{($(Ha.east)+(2*\stunit,0)$)}{bt}{dffl}
|
||||
|
||||
\node[inner sep=0pt, fit=(X)(W1a)(Ha)(Hb)] (rowA) {};
|
||||
\stlane{rowA}
|
||||
\ststage{SA}{列切 $\mathbf W_1$:无通信}
|
||||
\strow{rowA}{bt}
|
||||
\stface[role=act, bracket=true]{X}{}{bt}{d}
|
||||
\stglyph{mA}{$\times$}
|
||||
% Two shards, tiled exactly: gap=0pt makes them adjacent by construction,
|
||||
% so neither stretching nor an eyeballed offset can creep in.
|
||||
\stface[role=r1]{W1a}{}{d}{dffl}
|
||||
\stface[role=r2, gap=0pt]{W1b}{}{d}{dffl}
|
||||
\stglyph{eA}{$=$}
|
||||
\stface[role=r1]{Ha}{}{bt}{dffl}
|
||||
\stface[role=r2, gap=0pt]{Hb}{}{bt}{dffl}
|
||||
\strowend
|
||||
\stcaption{X}{$\mathbf X$}{$BT\times d$}
|
||||
\stcaption{W1a}{$\mathbf W_1^{(1)}$}{$d\times d_{\mathrm{ff}}/p$}
|
||||
\stcaption{W1b}{$\mathbf W_1^{(2)}$}{$d\times d_{\mathrm{ff}}/p$}
|
||||
\stcaption{Ha}{$\mathbf H^{(1)}$}{$BT\times d_{\mathrm{ff}}/p$}
|
||||
\stcaption{Hb}{$\mathbf H^{(2)}$}{$BT\times d_{\mathrm{ff}}/p$}
|
||||
\stnolane
|
||||
|
||||
% ============================================================ stage B row ===
|
||||
\node[st stage, below=9mm of X-shape.south west, anchor=north west] (SB)
|
||||
{行切 $\mathbf W_2$:一次 All-Reduce};
|
||||
\coordinate (b) at ($(SB)+(-0.4,-1.9)$);
|
||||
|
||||
\stface[role=r1]{Ga}{(b)}{bt}{dffl}
|
||||
\stface[role=r2]{Gb}{($(Ga.east)+(2*\stunit,0)$)}{bt}{dffl}
|
||||
\node[st op, right=5mm of Gb] (mB) {$\times$};
|
||||
% W_2 is split along the CONTRACTED axis: the two shards stack vertically and
|
||||
% together have exactly the height of H's width. Splitting reverses concat.
|
||||
\stface[role=r1]{W2a}{($(mB)+(1.35,0.46)$)}{dffl}{d}
|
||||
\stface[role=r2]{W2b}{($(W2a.south)+(0,-2*\stunit)$)}{dffl}{d}
|
||||
\node[st op, right=5mm of W2a.east |- W2a.south] (eB) {$=$};
|
||||
\stface[role=r1]{Pa}{($(eB)+(1.3,0)$)}{bt}{d}
|
||||
\node[st op, right=4mm of Pa] (plus) {$+$};
|
||||
\stface[role=r2]{Pb}{($(plus)+(1.3,0)$)}{bt}{d}
|
||||
|
||||
\node[st comm, right=9mm of Pb] (ar) {All-Reduce};
|
||||
\stface[role=act, bracket=true]{Y}{($(ar)+(1.9,0)$)}{bt}{d}
|
||||
\starrow{Pb.east}{ar.west}
|
||||
\starrow{ar.east}{Y.west}
|
||||
|
||||
\node[inner sep=0pt, fit=(Ga)(W2a)(W2b)(Pa)(Pb)(Y)] (rowB) {};
|
||||
\stlane{rowB}
|
||||
\ststage{SB}{行切 $\mathbf W_2$:一次 All-Reduce}
|
||||
\strow{rowB}{dff} % the stacked W_2 column is the tallest object here
|
||||
\stface[role=r1]{Ga}{}{bt}{dffl}
|
||||
\stface[role=r2, gap=0pt]{Gb}{}{bt}{dffl}
|
||||
\stglyph{mB}{$\times$}
|
||||
% W_2 is split along the CONTRACTED axis: the shards stack vertically and
|
||||
% together have exactly the height of H's width. Splitting reverses concat,
|
||||
% which is what gap=0pt inside the column states.
|
||||
\stcol{W2}{dff}
|
||||
\stface[role=r1]{W2a}{}{dffl}{d}
|
||||
\stface[role=r2, gap=0pt]{W2b}{}{dffl}{d}
|
||||
\stcolend
|
||||
\stglyph{eB}{$=$}
|
||||
\stface[role=r1]{Pa}{}{bt}{d}
|
||||
\stglyph{plus}{$+$}
|
||||
\stface[role=r2]{Pb}{}{bt}{d}
|
||||
\stlink{lc}{}
|
||||
\stcomm{ar}{All-Reduce}
|
||||
\stlink{ly}{}
|
||||
\stface[role=act, bracket=true]{Y}{}{bt}{d}
|
||||
\strowend
|
||||
\stcaption{Ga}{$\mathbf G^{(1)}$}{$BT\times d_{\mathrm{ff}}/p$}
|
||||
\stcaption{Gb}{$\mathbf G^{(2)}$}{$BT\times d_{\mathrm{ff}}/p$}
|
||||
\stcaption{W2b}{$\mathbf W_2^{(r)}$}{$d_{\mathrm{ff}}/p\times d$}
|
||||
\stcaption{W2}{$\mathbf W_2^{(r)}$}{$d_{\mathrm{ff}}/p\times d$}
|
||||
\stcaption{Pa}{$\mathbf P^{(1)}$}{$BT\times d$}
|
||||
\stcaption{Pb}{$\mathbf P^{(2)}$}{$BT\times d$}
|
||||
\stcaption{Y}{$\mathbf Y$}{$BT\times d$}
|
||||
\stnolane
|
||||
|
||||
% ================================================================= formula ==
|
||||
% Placed last so it is centered on the figure that was actually drawn.
|
||||
\sttopformula{F}{$\begin{gathered}
|
||||
\mathbf{XW}_1=[\,\mathbf{XW}_1^{(1)}\mid
|
||||
\mathbf{XW}_1^{(2)}\,]=[\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,]\\[1.2mm]
|
||||
[\,\mathbf H^{(1)}\mid\mathbf H^{(2)}\,]
|
||||
\begin{bmatrix}\mathbf W_2^{(1)}\\[-1pt]\mathbf W_2^{(2)}\end{bmatrix}
|
||||
=\sum_{r}\mathbf H^{(r)}\mathbf W_2^{(r)}=\sum_r\mathbf P^{(r)}
|
||||
\end{gathered}$}
|
||||
|
||||
% ============================================================== meaning box ==
|
||||
\node[inner sep=0pt, fit=(F)(rowA)(rowB)(Y-shape)(Ga-shape)] (all) {};
|
||||
\stbbox{all}
|
||||
\stmeaningbox{mb}{16.4cm}{all}
|
||||
{$BT$ 展平后的 token 数,$d$ 模型宽度,$d_{\mathrm{ff}}$ 前馈中间宽度,
|
||||
$p$ TP 并行度(图中 $p=2$)}
|
||||
|
||||
@@ -15,9 +15,9 @@ dimension. **Fix:** swap the arguments — `\ststack{KT}{...}{dh}{T}{3}`. See `g
|
||||
## 2. Shards that do not tile their parent
|
||||
|
||||
Two shards drawn with a gap, or stretched to fill a parent whose other shards were elided.
|
||||
Both assert a width that the tensor does not have. **Fix:** place each shard from the
|
||||
previous one's edge (`($(W1a.east)+(2*\stunit,0)$)`), and draw an ellipsis for anything
|
||||
omitted. See `geometry.md` §5–6.
|
||||
Both assert a width that the tensor does not have. **Fix:** draw the shards in one band
|
||||
and give every shard after the first `gap=0pt`, which *states* that they are adjacent
|
||||
instead of arranging for it; draw an ellipsis for anything omitted. See `geometry.md` §5–6.
|
||||
|
||||
## 3. An index drawn as a heatmap
|
||||
|
||||
@@ -41,14 +41,31 @@ See `style.md`.
|
||||
reads the resulting stripe as real structure. `pattern=dense` avoids it; if you write
|
||||
your own filler, check that rows 1, 2, 4, 5 of a tall face are not identical.
|
||||
- **A label wider than its connector.** The white underlay then covers the target tensor.
|
||||
Shorten the label or widen the gap — never let it sit on a face. See `layout.md`.
|
||||
`\stlink` makes this unrepresentable: the label reserves its own width in the band and
|
||||
the arrow is drawn to whatever lands beside it. See `layout.md`.
|
||||
- **A new hue for a regrouped view of the same data.** `X` and the per-expert buffers
|
||||
gathered out of `X` are the same object in a different order; a second hue claims they
|
||||
are different tensors.
|
||||
- **Captions hanging at different depths** because the faces in a row have different
|
||||
heights. Use `\stlane`.
|
||||
heights. Use `\strow`/`\strowend`, which arms `\stlane` for you.
|
||||
- **Hand-tuned offsets.** Each one is a magic number valid only for the content that was
|
||||
there when you tuned it; the figure that breaks is the *next* one, when a label grows two
|
||||
characters and lands on a face. Use the cursor. See `layout.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
|
||||
|
||||
+156
-12
@@ -8,32 +8,153 @@
|
||||
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).
|
||||
|
||||
## 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.
|
||||
|
||||
```tex
|
||||
\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=0pt` means *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 X` reserves 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:
|
||||
|
||||
```tex
|
||||
\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:
|
||||
|
||||
```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
|
||||
\stface[keys]{name}{(coord)}{rows}{cols}
|
||||
\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. `(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`.
|
||||
`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:
|
||||
|
||||
@@ -46,9 +167,12 @@ Keys:
|
||||
| `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.
|
||||
|
||||
```tex
|
||||
\stface[role=w, pattern=data, level=3,
|
||||
@@ -58,12 +182,21 @@ It deliberately has no lightness ramp — see `semantics.md`.
|
||||
|
||||
## Captions
|
||||
|
||||
`\strowend` calls `\stlane` for you, so in flow mode captions just follow the band:
|
||||
|
||||
```tex
|
||||
\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):
|
||||
|
||||
```tex
|
||||
\node[inner sep=0pt, fit=(A)(B)(C)] (rowA) {};
|
||||
\stlane{rowA}
|
||||
\stcaption{A}{$\mathbf A$}{$T\times d$} % symbol lane, shape lane
|
||||
\stcaption{A}{$\mathbf A$}{$T\times d$}
|
||||
\stnolane
|
||||
\stcaptiontop{A}{\stshapefont{token}} % occasional label above a face
|
||||
```
|
||||
|
||||
`\stcaption` defines `name-sym` and `name-shape` nodes; anchor the next stage heading
|
||||
@@ -71,8 +204,10 @@ against `name-shape.south`.
|
||||
|
||||
## Operators, connectors, nodes
|
||||
|
||||
Prefer `\stglyph` / `\stcomm` / `\stlink` (above). The raw forms are for absolute placement:
|
||||
|
||||
```tex
|
||||
\node[st op, right=6mm of A] (m) {$\times$};
|
||||
\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}
|
||||
@@ -85,13 +220,16 @@ Connectors route on the background layer automatically.
|
||||
## Bottom
|
||||
|
||||
```tex
|
||||
\node[inner sep=0pt, fit=(F)(rowA)(rowB)(Y-shape)] (all) {};
|
||||
\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 — include every caption and
|
||||
top label in that `fit` or the box will overlap them. An empty `{}` row is dropped.
|
||||
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
|
||||
|
||||
@@ -101,3 +239,9 @@ top label in that `fit` or the box will overlap them. An empty `{}` row is dropp
|
||||
- `\foreach {\macro,...,1}` cannot infer its direction from an unexpanded macro.
|
||||
- `\strole` is expandable on purpose (it is used inside `\edef`); the warning lives in
|
||||
`\stcheckrole`.
|
||||
- Neither the TikZ path parser nor the `calc` library 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 `\edef` before it is measured.
|
||||
- `\sttopformula` puts a group around its argument, which breaks TikZ's own `\\`. For more
|
||||
than one line, wrap the math in amsmath's `gathered`.
|
||||
|
||||
+13
-5
@@ -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)
|
||||
|
||||
@@ -26,17 +27,24 @@ Work through it against the rendered PNG. Any mandatory violation means redraw,
|
||||
|
||||
## 4. Full-size visual audit
|
||||
|
||||
Open the PNG at 100 %.
|
||||
Open the PNG at 100 %. Most of the first three items are automatic under the flow layout;
|
||||
they still need looking at, because the cursor only guarantees that objects do not *push*
|
||||
into each other, not that the figure reads correctly.
|
||||
|
||||
- [ ] 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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
+52
-21
@@ -4,11 +4,29 @@ Everything on the canvas is a bounding box: tensors, full offset stacks, bracket
|
||||
operators, arrow labels, annotations, symbols, shape labels, stage headings, the meaning
|
||||
box, the signature. **Tangency counts as collision.**
|
||||
|
||||
## Place by cursor, not by coordinate
|
||||
|
||||
The invariants below are *statements about the finished figure*; the flow layout in
|
||||
`api.md` is how you get them without checking each one by hand. Use it by default:
|
||||
`\ststage` / `\strow` … `\strowend` / `\stcol` … `\stcolend`, with an empty coordinate
|
||||
argument on every face.
|
||||
|
||||
The failure it removes is specific. With hand-written offsets, each gap is a magic number
|
||||
tuned against the *current* content, so the day a label grows by two characters it silently
|
||||
lands on the next tensor — the figure still compiles and still looks clean. With the
|
||||
cursor, every object reserves its own width, so growing one object can only push the rest
|
||||
apart. And because a band declares its height, an object that does not fit becomes a
|
||||
`Package supertensor Warning`, which `build.sh` turns into a failed build.
|
||||
|
||||
Hand placement remains available for the cases the cursor cannot express. When you use it,
|
||||
wrap the node in `\sttrack` so the bounding box and the vertical cursor still see it.
|
||||
|
||||
## Gutters
|
||||
|
||||
Define one base gutter `g ≥ 1 em`. Unrelated boxes stay at least `g` apart; stage bands at
|
||||
least `1.5g`. In practice: `right=5mm–9mm` between an operator and its operands, `7mm–9mm`
|
||||
between the last caption of one row and the next stage heading.
|
||||
Define one base gutter `g ≥ 1 em`; that is what `\stgutter` (6 mm) is. Unrelated boxes stay
|
||||
at least `g` apart; stage bands at least `1.5g` (`\strowgap`, `\stblockgap`). Set them once
|
||||
at the top of the figure rather than per call — a per-call `gap=` is for stating a
|
||||
*relationship* (`gap=0pt` = these shards tile), not for nudging.
|
||||
|
||||
Overlap is allowed only inside one declared composite:
|
||||
|
||||
@@ -16,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.
|
||||
@@ -33,30 +52,40 @@ shapes <- \stcaption arg 3
|
||||
```
|
||||
|
||||
Faces of different heights would otherwise hang their captions at different depths. Fix it
|
||||
with a shared baseline:
|
||||
with a shared baseline — `\strowend` arms one automatically:
|
||||
|
||||
```tex
|
||||
\node[inner sep=0pt, fit=(Q)(KT)(S)] (rowA) {};
|
||||
\stlane{rowA}
|
||||
\stcaption{Q}{$\mathbf Q^{(i)}$}{$h\times T\times d_h$}
|
||||
\strow{rowA}{T}
|
||||
...
|
||||
\stnolane
|
||||
\strowend
|
||||
\stcaption{Q}{$\mathbf Q^{(i)}$}{$h\times T\times d_h$}
|
||||
```
|
||||
|
||||
Every `\stcaption` between `\stlane` and `\stnolane` hangs from the bottom of `rowA`, so
|
||||
symbols and shapes form two flat lanes.
|
||||
Under absolute placement, arm it yourself with `\stlane{rowA}` … `\stnolane`. Either way
|
||||
every `\stcaption` inside hangs from the bottom of `rowA`, so symbols and shapes form two
|
||||
flat lanes.
|
||||
|
||||
Stage headings share one left rail. Anchor each heading below the previous row but at the
|
||||
previous *heading's* x, not at the previous row's content:
|
||||
Stage headings share one left rail. `\ststage` puts them there: the rail is a single stored
|
||||
`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.
|
||||
|
||||
```tex
|
||||
\coordinate (cy) at ($(I-shape.south)+(0,-9mm)$);
|
||||
\node[st stage, anchor=north west] (SC) at (SB.west |- cy) {...};
|
||||
```
|
||||
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`).
|
||||
|
||||
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`).
|
||||
`\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
|
||||
|
||||
@@ -67,8 +96,10 @@ never cover a face. Two consequences you still own:
|
||||
- A connector may not cross a box that is not one of its endpoints. Move the row, don't
|
||||
route over.
|
||||
- A label's white underlay may cover only its own connector — never a tensor, never
|
||||
another label. If the label is wider than the arrow, shorten the label or widen the gap.
|
||||
This is the single most common failure after a first draft.
|
||||
another label. This is the single most common failure after a first draft, and `\stlink`
|
||||
is the fix: it makes the label itself a flow object and draws the arrow to whatever
|
||||
lands on either side of it, so the label cannot be wider than its connector.
|
||||
`\starrowlabel` between two hand-placed nodes still has the old failure mode.
|
||||
|
||||
## Stacks
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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:" \
|
||||
|
||||
Executable
+175
@@ -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())
|
||||
@@ -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."
|
||||
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
@@ -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"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
% Regression test for the flow layout: stage headings on a shared left rail,
|
||||
% bands whose objects reserve their own width, connectors whose labels are flow
|
||||
% objects. No absolute coordinate appears anywhere in this figure.
|
||||
% ../scripts/build.sh flow.tex
|
||||
\documentclass[border=10pt]{standalone}
|
||||
\usepackage[cjk]{supertensor}
|
||||
|
||||
\stsetrole{act}{stTeal}
|
||||
\stsetrole{w}{stOrange}
|
||||
\stsetrole{o}{stViolet}
|
||||
|
||||
\stdim{T}{7}
|
||||
\stdim{d}{5}
|
||||
\stdim{r}{3}
|
||||
|
||||
\begin{document}
|
||||
\begin{tikzpicture}
|
||||
|
||||
\ststage{S1}{一、面、算子、带标签的连接线都由游标排布}
|
||||
\strow{R1}{T}
|
||||
\stface[role=act, bracket=true]{X}{}{T}{d}
|
||||
\stglyph{m1}{$\times$}
|
||||
\stface[role=w, bracket=true]{W}{}{d}{r}
|
||||
\stlink{l1}{一个很长的标签也不会压到下一个张量}
|
||||
\stface[role=o, bracket=true]{Y}{}{T}{r}
|
||||
\strowend
|
||||
\stcaption{X}{$\mathbf X$}{$T\times d$}
|
||||
\stcaption{W}{$\mathbf W$}{$d\times r$}
|
||||
\stcaption{Y}{$\mathbf Y$}{$T\times r$}
|
||||
|
||||
\ststage{S2}{二、堆叠的偏移页也计入宽度,行首自动回到同一条左轨}
|
||||
\strow{R2}{T}
|
||||
\ststack[role=act]{Q}{}{T}{r}{3}
|
||||
\stlink{l2}{}
|
||||
\stcomm{c1}{All-Reduce}
|
||||
\stlink{l3}{sum}
|
||||
\stface[role=o, pattern=causal]{A}{}{T}{T}
|
||||
\stgap{4mm}
|
||||
\stface[role=w, pattern=diag]{D}{}{r}{r}
|
||||
\strowend
|
||||
\stcaption{Q}{$\mathbf Q$}{$3\times T\times r$}
|
||||
\stcaption{A}{$\mathbf A$}{$T\times T$}
|
||||
\stcaption{D}{$\mathbf D$}{$r\times r$}
|
||||
|
||||
\ststage{S3}{三、列子流:两片沿收缩维叠放,恰好铺满一格}
|
||||
\strow{R3}{T}
|
||||
\stface[role=act]{H}{}{T}{d}
|
||||
\stglyph{m3}{$\times$}
|
||||
\stcol{Wc}{d}
|
||||
% d = 2 * (d/2)... here r=3 and d=5, so the two shards are 3 and 2 units.
|
||||
\stface[role=w]{Wa}{}{r}{T}
|
||||
\stface[role=o, gap=0pt]{Wb}{}{2}{T}
|
||||
\stcolend
|
||||
\stglyph{e3}{$=$}
|
||||
\stface[role=o, bracket=true]{Z}{}{T}{T}
|
||||
\strowend
|
||||
\stcaption{H}{$\mathbf H$}{$T\times d$}
|
||||
\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$ 低秩维}
|
||||
{}
|
||||
{三行的左端都落在同一条左轨上,行内间距由 \texttt{\string\stgutter} 声明一次,
|
||||
标签自己占位,所以任何一处变宽都只会把后面的东西推开,不会盖住它们;
|
||||
第三行的列子流用 \texttt{gap=0pt} 声明两片相邻,声明高度与实际不符会直接报警}
|
||||
\stsignature{流式排版自测}{mb}
|
||||
|
||||
\end{tikzpicture}
|
||||
\end{document}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user