%% supertensor.sty -- shape-aware tensor figure toolkit %% Geometry invariants are enforced by construction: every face is sized from a %% declared axis length, so equal shapes get equal edges, a x a is a square, and %% a transpose physically swaps width and height. %% %% 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] \newif\ifst@cjk\st@cjkfalse \newif\ifst@en\st@enfalse \DeclareOption{cjk}{\st@cjktrue} \DeclareOption{en}{\st@entrue} \DeclareOption{zh}{\st@enfalse} \ProcessOptions\relax \RequirePackage{amsmath} \RequirePackage{amssymb} \RequirePackage{xcolor} \RequirePackage{array} \RequirePackage{etoolbox} \RequirePackage{xstring} \RequirePackage{tikz} \usetikzlibrary{calc,positioning,arrows.meta,backgrounds,fit,shapes.geometric,% decorations.pathreplacing,decorations.markings} \ifst@cjk \RequirePackage[UTF8,fontset=fandol]{ctex} \fi % ---------------------------------------------------------------- layers ---- % Connectors on the background, tensors on main, all text on the foreground. \pgfdeclarelayer{stbg} \pgfdeclarelayer{stfg} \pgfsetlayers{stbg,main,stfg} % ---------------------------------------------------------------- palette --- % Muted, mid-chroma paper colors. Do not add saturated primaries. \definecolor{stTeal}{HTML}{4F8FA5} \definecolor{stOrange}{HTML}{EE995B} \definecolor{stCoral}{HTML}{C95B5B} \definecolor{stViolet}{HTML}{8A74B5} \definecolor{stGray}{HTML}{85898F} \definecolor{stInk}{HTML}{1A1A1A} % 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}} % 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]{% \ifcsname st@role@#1\endcsname\csname st@role@#1\endcsname\else stGray\fi} \newcommand{\stcheckrole}[1]{% \ifcsname st@role@#1\endcsname\else \PackageWarning{supertensor}{Undeclared role `#1' -- drawn in neutral gray. Declare it with \string\stsetrole\space so the hue budget stays visible}% \fi} \stsetrole{neutral}{stGray} % Three separated lightness levels. Never render data cells below level 1. \newcommand{\stlevelpct}[1]{\ifcase#1 0\or30\or55\or80\else55\fi} % ------------------------------------------------------------ geometry ------ % The geometry ledger, made executable. Declare each symbolic axis once: % \stdim{d}{8} \stdim{dh}{4} % 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{\stresolve}[1]{\ifcsname st@dim@#1\endcsname\csname st@dim@#1\endcsname\else#1\fi} % --------------------------------------------------------- type hierarchy --- \newcommand{\stformula}[1]{{\large #1}} \newcommand{\ststagelabel}[1]{{\small\bfseries\color{black!55}#1}} \newcommand{\stoperator}[1]{{\Large #1}} \newcommand{\stsymfont}[1]{{\small #1}} \newcommand{\stshapefont}[1]{{\scriptsize\color{black!55}#1}} \newcommand{\stprose}[1]{{\small #1}} \tikzset{ st sym/.style = {font=\small, text=stInk, inner sep=1pt}, st shape/.style = {font=\scriptsize, text=black!55, inner sep=1pt}, st stage/.style = {font=\small\bfseries, text=black!55, inner sep=2pt}, st op/.style = {font=\Large, text=stInk, inner sep=2pt}, st note/.style = {font=\scriptsize, text=black!55, inner sep=2pt}, st arrow/.style = {-{Stealth[length=2.2mm,width=1.6mm]}, draw=black!45, line width=0.5pt}, st comm/.style = {draw=black!45, fill=black!4, rounded corners=1.2pt, font=\scriptsize, inner sep=3pt}, st brace/.style = {decorate, decoration={brace,amplitude=3pt}, draw=black!45, line width=0.4pt}, } % ------------------------------------------------------------- faces -------- \newif\ifst@bracket \newif\ifst@border \newif\ifst@tiles \pgfkeys{ /st/face/.cd, role/.store in=\st@role, pattern/.store in=\st@pattern, data/.store in=\st@data, level/.store in=\st@level, bracket/.is if=st@bracket, border/.is if=st@border, tiles/.is if=st@tiles, role=neutral, pattern=dense, data={}, level=2, bracket=false, border=true, tiles=true, } % \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 % a face literally identical -- the eye reads that stripe as structure in the % data. Composing coprime moduli (17, 19) pushes the period past any face we % would ever draw. \newcommand{\st@hash}[3]{% \pgfmathtruncatemacro{\st@hv}% {mod(mod(11*#1+7*#2,17)*mod(5*#1+13*#2,19)+3*#1+#2,#3)}} % \st@cellfill{i}{j} -> \st@lv in 0..3 (0 = known zero, left unfilled) \newcommand{\st@cellfill}[2]{% \edef\st@p{\st@pattern}% \IfStrEq{\st@p}{solid}{\def\st@lv{\st@level}}{% \IfStrEq{\st@p}{diag}{\pgfmathtruncatemacro{\st@lv}{ifthenelse(#1==#2,3,0)}}{% \IfStrEq{\st@p}{band}{\pgfmathtruncatemacro{\st@lv}{ifthenelse(abs(#1-#2)<=1,3,0)}}{% \IfStrEq{\st@p}{empty}{\def\st@lv{0}}{% \IfStrEq{\st@p}{lower}{% \ifnum#1<#2 \def\st@lv{0}\else\st@hash{#1}{#2}{2}% \pgfmathtruncatemacro{\st@lv}{\st@hv+2}\fi}{% \IfStrEq{\st@p}{upper}{% \ifnum#1>#2 \def\st@lv{0}\else\st@hash{#1}{#2}{2}% \pgfmathtruncatemacro{\st@lv}{\st@hv+2}\fi}{% \IfStrEq{\st@p}{causal}{% \ifnum#1<#2 \def\st@lv{0}\else\st@hash{#1}{#2}{3}% \pgfmathtruncatemacro{\st@lv}{\st@hv+1}\fi}{% % default, including `dense' \st@hash{#1}{#2}{3}\pgfmathtruncatemacro{\st@lv}{\st@hv+1}}}}}}}}} % Shared setup: read keys, resolve the axis names through the geometry ledger. \newcommand{\st@setup}[3]{% \pgfkeys{/st/face/.cd,#1}% \edef\st@rows{\stresolve{#2}}% \edef\st@cols{\stresolve{#3}}% \stcheckrole{\st@role}% \edef\st@col{\strole{\st@role}}% \pgfmathsetlengthmacro{\st@w}{\st@cols*\stunit}% \pgfmathsetlengthmacro{\st@h}{\st@rows*\stunit}} % \st@facecore{name}{coord} -- draws one face from the already-resolved state. % \ststack calls this directly rather than re-entering \stface: passing % `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@facebody{#1}} % \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. \newcommand{\stface}[5][]{% \begingroup \st@setup{#1}{#4}{#5}% \st@facecore{#2}{#3}% \endgroup} % \stindexface[keys]{name}{center coord}{rows}{cols}{entries} % An INDEX face: the cells carry discrete symbols, not magnitudes. Entries are % comma-separated in row-major order (rows*cols of them); `.' leaves a cell % blank. Deliberately a different grammar from \stface -- outlined cells, no % lightness ramp -- because an index that is drawn like a score invites the % reader to compare 7 > 2 as if the numbers meant size. \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 {}; \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}% \edef\st@ee{\st@e}% \IfStrEq{\st@ee}{.}{}{% \draw[draw=\st@col!70, line width=0.4pt, rounded corners=0.7pt, fill=\st@col!12] ($(#2.north west)+(\st@jj*\stunit-\stunit+0.5\sttilegap,% -\st@ii*\stunit+\stunit-0.5\sttilegap)$) rectangle ($(#2.north west)+(\st@jj*\stunit-0.5\sttilegap,% -\st@ii*\stunit+0.5\sttilegap)$); \node[font=\tiny, text=\st@col!85!black, inner sep=0pt] at ($(#2.north west)+(\st@jj*\stunit-0.5\stunit,% -\st@ii*\stunit+0.5\stunit)$) {\st@ee};}}% \ifst@border \draw[draw=black!60, line width=0.5pt, rounded corners=1pt] (#2.south west) rectangle (#2.north east); \fi \endgroup} \newcommand{\st@facebody}[1]{% \def\st@n{#1}% \ifst@tiles \IfStrEq{\st@pattern}{data}{% \foreach \st@row [count=\st@ii] in \st@data {% \foreach \st@jj in {1,...,\st@cols} {% \StrChar{\st@row}{\st@jj}[\st@c]% \ifnum\st@c>0 \st@tile{#1}{\st@ii}{\st@jj}{\st@c}% \fi}}% }{% \foreach \st@ii in {1,...,\st@rows} {% \foreach \st@jj in {1,...,\st@cols} {% \st@cellfill{\st@ii}{\st@jj}% \ifnum\st@lv>0 \st@tile{#1}{\st@ii}{\st@jj}{\st@lv}% \fi}}% }% \else \fill[\st@col!\stlevelpct{\st@level}, rounded corners=1pt] (#1.south west) rectangle (#1.north east); \fi \ifst@border \draw[draw=black!60, line width=0.5pt, rounded corners=1pt] (#1.south west) rectangle (#1.north east); \fi \ifst@bracket \draw[black!55, line width=0.5pt] ($(#1.north west)+(-1.1mm,0.6mm)$) -- ++(-1.1mm,0) -- ($(#1.south west)+(-2.2mm,-0.6mm)$) -- ++(1.1mm,0); \draw[black!55, line width=0.5pt] ($(#1.north east)+(1.1mm,0.6mm)$) -- ++(1.1mm,0) -- ($(#1.south east)+(2.2mm,-0.6mm)$) -- ++(-1.1mm,0); \fi} % \st@tile{face}{row}{col}{level} -- one rounded tile with a white gutter. \newcommand{\st@tile}[4]{% \fill[\st@col!\stlevelpct{#4}, rounded corners=0.7pt] ($(#1.north west)+(#3*\stunit-\stunit+0.5\sttilegap,% -#2*\stunit+\stunit-0.5\sttilegap)$) rectangle ($(#1.north west)+(#3*\stunit-0.5\sttilegap,% -#2*\stunit+0.5\sttilegap)$);} % \ststack[keys]{name}{center}{rows}{cols}{sheets} % Leading axes become depth. Back sheets are outline-only and are included in % the bounding box, so neighbours can be spaced against the real extent. \newcommand{\ststack}[6][]{% \begingroup \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) {}; \ifnum\st@back>0 % Ascending loop, descending index: `{\macro,...,1}' cannot infer its % direction from an unexpanded macro and runs away. \foreach \st@kk in {1,...,\st@back} {% \pgfmathtruncatemacro{\st@k}{\st@back+1-\st@kk}% \draw[draw=black!35, line width=0.4pt, rounded corners=1pt, fill=white] ($(#2-front.south west)+(\st@k*\st@dx,\st@k*\st@dx)$) rectangle ($(#2-front.north east)+(\st@k*\st@dx,\st@k*\st@dx)$);} \fi \st@facebody{#2-front}% % Braces are load-bearing: a bare coordinate expression contains a comma and % would be split into two pgfkeys keys. \node[inner sep=0pt, outer sep=0pt, fit={(#2-front) ($(#2-front.north east)+(\st@back*\st@dx,\st@back*\st@dx)$)}] (#2) {}; \endgroup} % ------------------------------------------------- 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. % \stlane{node} declares a shared caption baseline: every later \stcaption % hangs from the bottom of that node instead of from its own block, so symbols % and shapes occupy two flat lanes even when the faces have different heights. % Typical use: draw the row, \node[fit=(A)(B)(C)] (row) {};, \stlane{row}. \def\st@lane{} \newcommand{\stlane}[1]{\def\st@lane{#1}} \newcommand{\stnolane}{\def\st@lane{}} \newcommand{\stcaption}[3]{% \ifdefempty{\st@lane}% {\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};} \newcommand{\stcaptiontop}[2]{% \node[st sym, above=1.6mm of #1] (#1-top) {#2};} % ------------------------------------------------------------ connectors ---- % Routed on the background layer so a connector can never cover a face. \newcommand{\starrow}[3][]{% \begin{pgfonlayer}{stbg} \draw[st arrow,#1] (#2) -- (#3); \end{pgfonlayer}} \newcommand{\starrowlabel}[4][]{% \begin{pgfonlayer}{stbg} \draw[st arrow,#1] (#2) -- node[st note, above, fill=white, inner sep=1pt] {#4} (#3); \end{pgfonlayer}} % ------------------------------------------------------------ meaning box --- \ifst@en \def\st@lblaxes{Axes}\def\st@lblobj{Objects}\def\st@lblmech{Mechanism} \else \def\st@lblaxes{轴}\def\st@lblobj{对象}\def\st@lblmech{机制} \fi \newcommand{\stsetlabels}[3]{\def\st@lblaxes{#1}\def\st@lblobj{#2}\def\st@lblmech{#3}} % \stmeaningbox{node name}{total width}{below-of anchor}{axes}{objects}{mechanism} % One full-width, low-contrast box, one reading column, at most three rows. % Leave a row empty to drop it. \newcommand{\stmeaningbox}[6]{% \node[below=4mm of #3, anchor=north, draw=black!18, fill=black!3, rounded corners=1.5pt, inner xsep=9pt, inner ysep=8pt, text width=#2] (#1) {% \small \setlength{\tabcolsep}{0pt}% \renewcommand{\arraystretch}{1.25}% \begin{tabular}{@{}p{\st@raillen}@{\hspace{7pt}}p{\dimexpr#2-\st@raillen-7pt\relax}@{}} % \ifblank, not \IfStrEq: the row content is math and CJK, and must not % be expanded just to test for emptiness. \ifblank{#4}{}{\textbf{\st@lblaxes} & \raggedright\arraybackslash #4 \\} \ifblank{#5}{}{\textbf{\st@lblobj} & \raggedright\arraybackslash #5 \\} \ifblank{#6}{}{\textbf{\st@lblmech} & \raggedright\arraybackslash #6 \\} \end{tabular}};} \newlength{\st@raillen}\setlength{\st@raillen}{3.2em} \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}} \newcommand{\stsignature}[2]{% \node[below=2.2mm of #2, font=\scriptsize, text=black!45] (st-signature) {#1@\st@author};} \endinput