feat: add superfig paper-figure toolkit

Standalone LaTeX/TikZ skill for non-tensor paper figures: node/edge
macros, lint-on-warning build, golden examples, and negative fixtures.
This commit is contained in:
dela
2026-08-17 09:40:12 +08:00
commit db5598fbf7
39 changed files with 1732 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
% Anti-pattern gallery -- four figures that compile cleanly and still teach
% the reader something false. Each pair is wrong / right.
% ../scripts/build.sh antipatterns.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{a}{sfTeal}
\sfsetrole{b}{sfOrange}
\sfsetrole{c}{sfCoral}
\sfsetrole{d}{sfViolet}
\newcommand{\bad}[1]{{\color{sfCoral}$\times$}\;#1}
\newcommand{\good}[1]{{\color{sfTeal}$\checkmark$}\;#1}
\begin{document}
\begin{tikzpicture}
% (1) A decorative arrow vs a data-flow edge.
\sfnode[role=a, at={(0,0)}]{A1}{编码器}{16mm}{10mm}
\sfnode[role=b, at={(32mm,0)}]{A2}{指标卡}{16mm}{10mm}
\sfarrow{A1}{A2}
\sfnode[role=a, at={(64mm,0)}]{A3}{编码器}{16mm}{10mm}
\sfnode[role=b, at={(96mm,0)}]{A4}{解码器}{16mm}{10mm}
\sfarrowlabel{A3.east}{A4.west}{隐状态}
\node[sf stage, anchor=south west] at ($(A1.north west)+(0,6mm)$)
{(1) 装饰箭头};
\node[sf shape, anchor=north] (A1c) at ($(A1.south)!0.5!(A2.south)+(0,-3mm)$)
{\bad{箭头不承载关系}};
\node[sf shape, anchor=north] (A3c) at ($(A3.south)!0.5!(A4.south)+(0,-3mm)$)
{\good{隐状态从编码流向解码}};
% (2) A group that is only a visual border vs a real composite.
\sfnode[role=a, at={(0,-32mm)}]{B1}{输入}{14mm}{10mm}
\sfnode[role=d, at={(24mm,-32mm)}]{B2}{输出}{14mm}{10mm}
\sfgroup[role=a]{Bg}{(B1)(B2)}{}
\sfnode[role=b, at={(56mm,-32mm)}]{B3}{注意力}{16mm}{10mm}
\sfnode[role=b, at={(82mm,-32mm)}]{B4}{前馈}{14mm}{10mm}
\sfgroup[role=b]{Bb}{(B3)(B4)}{}
\sfarrow{B3.east}{B4.west}
\node[sf stage, anchor=south west] at ($(B1.north west)+(0,8mm)$)
{(2) 组框只是装饰边};
\node[sf shape, anchor=north] (B1c) at ($(B1.south)!0.5!(B2.south)+(0,-7mm)$)
{\bad{首尾框在一起,不是一个模块}};
\node[sf shape, anchor=north] (B3c) at ($(B3.south)!0.5!(B4.south)+(0,-7mm)$)
{\good{主路模块是真实复合对象}};
% (3) One node doing two jobs vs a split.
\sfnode[role=b, at={(0,-68mm)}]{C1}{注意力 + LN + 残差}{38mm}{12mm}
\sfnode[role=b, at={(56mm,-68mm)}]{C2}{注意力}{16mm}{10mm}
\sfnode[role=c, at={(84mm,-68mm)}]{C3}{残差加}{16mm}{10mm}
\sfarrow{C2.east}{C3.west}
\node[sf stage, anchor=south west] at ($(C1.north west)+(0,6mm)$)
{(3) 一个节点做两件事};
\node[sf shape, anchor=north] (C1c) at ($(C1.south)+(0,-3mm)$)
{\bad{机制被一口吞掉}};
\node[sf shape, anchor=north] (C2c) at ($(C2.south)!0.5!(C3.south)+(0,-3mm)$)
{\good{一步一个对象}};
% (4) A new hue for the same object vs one role, two levels.
\sfnode[role=a, level=2, at={(0,-100mm)}]{D1}{$x$}{14mm}{10mm}
\sfnode[role=c, level=2, at={(24mm,-100mm)}]{D2}{$x$ 归一化}{20mm}{10mm}
\sfnode[role=a, level=1, at={(64mm,-100mm)}]{D3}{$x$}{14mm}{10mm}
\sfnode[role=a, level=3, at={(90mm,-100mm)}]{D4}{$x$ 归一化}{20mm}{10mm}
\node[sf stage, anchor=south west] at ($(D1.north west)+(0,6mm)$)
{(4) 同一对象换了色相};
\node[sf shape, anchor=north] (D1c) at ($(D1.south)!0.5!(D2.south)+(0,-3mm)$)
{\bad{换色相像换了对象}};
\node[sf shape, anchor=north] (D3c) at ($(D3.south)!0.5!(D4.south)+(0,-3mm)$)
{\good{同一角色,深浅分层}};
\node[inner sep=0pt, fit=(A1)(A4)(D1)(D4)(A1c)(D3c)(Bg)(Bb)] (all) {};
\sfmeaningbox{mb}{118mm}{all}
{}
{四张错图都能干净编译。编译器不检查图讲的事情对不对}
{交付前按 \texttt{references/checklist.md} 做一次人眼审图}
\end{tikzpicture}
\end{document}
+48
View File
@@ -0,0 +1,48 @@
% superfig golden example 2 -- a small branchy architecture, not a tensor
% shape diagram. Main path uses the cursor; the residual is a skip edge;
% the group is captioned on the shared lane, not as an overlay.
% ../scripts/build.sh branch-architecture.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{enc}{sfTeal}
\sfsetrole{attn}{sfOrange}
\sfsetrole{ffn}{sfViolet}
\sfsetrole{out}{sfCoral}
\begin{document}
\begin{tikzpicture}
\sfstage{S}{双分支结构:主路 + 残差}
\sfvgap{7mm}
\sfrow{R1}{12mm}
\sfnode[role=enc]{in}{输入}{16mm}{10mm}
\sfconn{e1}{}
\sfnode[role=attn]{attn}{注意力}{18mm}{10mm}
\sfconn{e2}{特征}
\sfnode[role=ffn]{ffn}{前馈}{18mm}{10mm}
\sfgap{\sflinklen}
\sfnode[role=enc]{add}{融合}{14mm}{10mm}
\sfconn{e4}{}
\sfnode[role=out]{out}{输出}{14mm}{10mm}
\sfrowend
% No overlay caption: it would sit on the residual. Caption the group below.
\sfgroup[role=attn]{block}{(attn)(ffn)}{}
\sfarrow{block.east}{add.west}
\sfarrowlabel[bend left=18]{in.north}{add.north}{残差}
\sflane{R1}
\sfcaption{in}{输入}{源数据}
\sfcaption{block}{主路模块}{注意力 + 前馈}
\sfcaption{add}{融合}{残差相加}
\sfcaption{out}{输出}{目标表示}
\sfbbox{all}
\sfmeaningbox{mb}{108mm}{all}
{主路逐层处理,残差边跳过主路}
{输入、主路模块、融合节点、输出}
{残差把输入直接送入融合,减轻深层优化负担}
\end{tikzpicture}
\end{document}
+56
View File
@@ -0,0 +1,56 @@
% superfig golden example 4 -- a dual-encoder dependency, not a linear pipeline.
% Two encoder rows share a rail; fusion is placed from their east anchors.
% ../scripts/build.sh dependency-graph.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{text}{sfTeal}
\sfsetrole{img}{sfOrange}
\sfsetrole{fuse}{sfViolet}
\sfsetrole{out}{sfCoral}
\begin{document}
\begin{tikzpicture}
\sfstage{S1}{双塔编码}
\sfrow{R1}{12mm}
\sfnode[role=text]{txt}{文本}{14mm}{10mm}
\sfconn{e1}{}
\sfnode[role=text]{te}{文本编码}{18mm}{10mm}
\sfrowend
\sflane{R1}
\sfcaption{txt}{text}{token 序列}
\sfcaption{te}{$E_t$}{文本向量}
\sfrow{R2}{12mm}
\sfnode[role=img]{img}{图像}{14mm}{10mm}
\sfconn{e2}{}
\sfnode[role=img]{ve}{视觉编码}{18mm}{10mm}
\sfrowend
\sflane{R2}
\sfcaption{img}{image}{视觉输入}
\sfcaption{ve}{$E_v$}{视觉向量}
% Fusion sits to the right of the two encoders, on their shared east line.
\sfnode[role=fuse, at={($(te.east)!0.5!(ve.east)+(20mm,0)$)}]{fu}{融合}{16mm}{10mm}
\sfnode[role=out, at={($(fu.east)+(16mm,0)$)}]{dec}{解码}{16mm}{10mm}
\sfnode[role=out, at={($(dec.east)+(16mm,0)$)}]{out}{输出}{14mm}{10mm}
\sfarrow{te.east}{fu.west}
\sfarrow{ve.east}{fu.west}
\sfarrow{fu.east}{dec.west}
\sfarrow{dec.east}{out.west}
\sfnolane
\sfcaption{fu}{fuse}{晚融合}
\sfcaption{dec}{decode}{条件生成}
\sfcaption{out}{out}{目标文本}
\sfbbox{all}
\sftopformula{F}{%
$y = \mathrm{Dec}\bigl(\mathrm{Fuse}(E_t(x),\, E_v(I))\bigr)$}
\sfmeaningbox{mb}{112mm}{all}
{两个编码器互不共享权重,只在融合节点会合}
{文本塔、视觉塔、融合、解码}
{各塔独立编码;融合后才进入解码,所以任一侧的依赖都经过融合节点}
\end{tikzpicture}
\end{document}
+47
View File
@@ -0,0 +1,47 @@
% superfig golden example 1 -- one horizontal paper-figure pipeline.
% Main path is input -> model -> prediction. Loss is a side object, not a
% station on the forward path.
% ../scripts/build.sh pipeline.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{input}{sfTeal}
\sfsetrole{model}{sfOrange}
\sfsetrole{loss}{sfCoral}
\sfsetrole{output}{sfViolet}
\begin{document}
\begin{tikzpicture}
\sfstage{SA}{推理流程:一次前向}
\sfrow{R1}{16mm}
\sfnode[role=input]{x}{输入 $x$}{16mm}{12mm}
\sfconn{e1}{预处理}
\sfnode[role=model]{f}{模型 $f_\theta$}{18mm}{12mm}
\sfconn{e2}{logits}
\sfnode[role=output]{y}{预测 $\hat y$}{16mm}{12mm}
\sfrowend
% Loss compares the prediction with the target; it is not on the main path.
% Hang it below ŷ with enough shaft that no caption sits on the arrow.
\sfnode[role=loss, at={($(y.south)+(0,-22mm)$)}]{s}{损失 $L$}{14mm}{12mm}
\sfarrowlabel{y.south}{s.north}{$L(\hat y,y)$}
\sflane{R1}
\sfcaption{x}{$x$}{原始输入}
\sfcaption{f}{$f_\theta$}{可学习参数}
\sfnolane
\sfcaption{s}{$L$}{与真值比较}
\sfbbox{all}
\sftopformula{F}{%
$x \;\xrightarrow{\;f_\theta\;}\; \hat y,\qquad
\min_\theta\; L\bigl(f_\theta(x),\,y\bigr)$}
\sfmeaningbox{mb}{96mm}{all}
{一次从输入到预测的前向;损失在预测之后单独计算}
{数据、模型参数、预测、损失}
{预处理后送入模型;模型产生 logits 得到预测;损失比较预测与真值,梯度再回到参数}
\sfsignature{推理流程示意}{mb}
\end{tikzpicture}
\end{document}
+46
View File
@@ -0,0 +1,46 @@
% superfig golden example 3 -- a state/time flow.
% One decode step: previous KV plus a new token produce the next state
% and the emitted token. The callout is an aside on the finished state band.
% ../scripts/build.sh state-flow.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{state}{sfTeal}
\sfsetrole{step}{sfOrange}
\sfsetrole{tok}{sfViolet}
\begin{document}
\begin{tikzpicture}
\sfstage{S}{解码一步:旧状态 + 新 token}
\sfvgap{26mm}
\sfrow{R1}{14mm}
\sfnode[role=state]{kv}{KV$_t$}{16mm}{11mm}
\sfconn{c1}{读}
\sfnode[role=step]{dec}{解码}{16mm}{11mm}
\sfconn{c2}{追加}
\sfnode[role=state]{kv2}{KV$_{t+1}$}{18mm}{11mm}
\sfrowend
\sflane{R1}
\sfcaption{kv}{KV$_t$}{已缓存键值}
\sfcaption{dec}{一步}{读 KV,写新列}
\sfcaption{kv2}{KV$_{t+1}$}{状态推进}
\sfcallout{N1}{38mm}{R1}{为何保留 KV}{%
下一步只读已有列、只追加 $x_t$ 这一列。重算前缀是另一条路径,这张图不画。}
% Tokens enter from above so the caption lane below the state row stays clear.
\sfnode[role=tok, at={($(dec.north west)+(-4mm,14mm)$)}]{xt}{$x_t$}{14mm}{10mm}
\sfnode[role=tok, at={($(dec.north east)+(4mm,14mm)$)}]{yt}{$y_t$}{14mm}{10mm}
\sfarrow{xt.south}{dec.north}
\sfarrow{dec.north}{yt.south}
\sfbbox{all}
\sftopformula{F}{%
$(\mathrm{KV}_{t+1},\, y_t) = \mathrm{Decode}(\mathrm{KV}_t,\, x_t)$}
\sfmeaningbox{mb}{108mm}{all}
{自回归一步:状态沿时间推进}
{缓存 KV、解码算子、本步 token}
{解码读 $\mathrm{KV}_t$ 与 $x_t$,写出 $y_t$,并把新列追加为 $\mathrm{KV}_{t+1}$}
\end{tikzpicture}
\end{document}