import { useMemo } from "react";
// The weights, drawn as the mechanism rather than as a heatmap of everything.
// A linear map is a bipartite graph of units; a convolution is a set of taps
// over a receptive field; an SSM is a bank of states with a decay each, an
// input map and an output map. Those are three different pictures because they
// are three different mechanisms, and rendering all of them as coloured
// rectangles would throw away the only thing the diagram is for.
const POS = "#ffc061"; // a positive weight
const NEG = "#6d9eff"; // a negative one
const DIM = "#4a5878";
const S = {
card: {
background: "#101725",
border: "1px solid #1f2a3d",
borderRadius: 8,
padding: "9px 11px",
marginBottom: 9,
},
head: {
display: "flex",
alignItems: "baseline",
gap: 7,
flexWrap: "wrap",
marginBottom: 6,
},
name: { fontSize: 12, color: "#c3cdde", fontWeight: 600 },
role: {
fontSize: 10,
padding: "1px 6px",
borderRadius: 99,
background: "#1c2740",
color: "#9fb3d9",
letterSpacing: 0.4,
},
shape: { fontSize: 11, color: "#6b7a96", fontVariantNumeric: "tabular-nums" },
note: { fontSize: 10.5, color: "#5d6b84", marginTop: 5, lineHeight: 1.45 },
finding: (level) => ({
display: "flex",
gap: 7,
alignItems: "baseline",
fontSize: 11.5,
lineHeight: 1.45,
padding: "5px 9px",
borderRadius: 6,
marginBottom: 4,
background: level === "warn" ? "#26202c" : "#141d22",
border: `1px solid ${level === "warn" ? "#5c4426" : "#22402f"}`,
color: level === "warn" ? "#f0c88a" : "#9dcaa8",
}),
hist: { display: "block", marginTop: 6 },
empty: {
color: "#5d6b84",
fontSize: 12.5,
padding: "18px 4px",
lineHeight: 1.5,
},
};
const colour = (v, absmax) => {
const t = absmax ? Math.min(1, Math.abs(v) / absmax) : 0;
return {
stroke: v >= 0 ? POS : NEG,
opacity: 0.08 + 0.9 * t ** 0.7,
width: 0.4 + 1.6 * t,
};
};
// --- a matrix, drawn as a matrix --------------------------------------------
// A dense map's structure is "every entry is free", and the way to show that is
// every entry. Drawing it as a bipartite graph means hundreds of crossing
// lines: a hairball that reads as one grey smudge, which is neither pretty nor
// informative. Lines are kept only for maps small enough that the wires are
// individually visible.
function Matrix({ values, absmax, width = 250, maxCell = 12, gamma = 0.6 }) {
const rows = values.length;
const cols = values[0]?.length ?? 0;
const cell = Math.max(
2,
Math.min(maxCell, Math.floor(width / Math.max(cols, 1))),
);
const w = cols * cell;
const h = rows * cell;
return (
);
}
// Small enough that individual wires are legible — the picture "everything is
// connected to everything" only lands when you can count the wires.
function WireDiagram({ tensor }) {
const rows = tensor.values.length; // output units
const cols = tensor.values[0]?.length ?? 0; // input units
const absmax = Math.max(1e-9, tensor.stats.absmax);
const h = Math.max(120, Math.max(rows, cols) * 9);
const w = 250;
const x0 = 34;
const x1 = w - 34;
const yOf = (i, n) => (n <= 1 ? h / 2 : 12 + (i * (h - 24)) / (n - 1));
const edges = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const v = tensor.values[r][c];
const { stroke, opacity, width } = colour(v, absmax);
if (opacity < 0.12) continue; // below this it is a smudge, not a weight
edges.push(
,
);
}
}
return (
);
}
// --- a convolution: the same taps applied everywhere -------------------------
function ConvDiagram({ tensor }) {
const taps = tensor.values[0]?.length ?? 0;
const absmax = Math.max(1e-9, tensor.stats.absmax);
const row = tensor.values[0] ?? [];
const w = 250;
const h = 108;
const y0 = 22;
const y1 = h - 30;
const xOf = (i) => (taps <= 1 ? w / 2 : 26 + (i * (w - 52)) / (taps - 1));
return (
);
}
// --- an SSM: states that decay, with an input and an output map --------------
// Assembled from whichever of the roles the layer actually has, so an S4D
// (decay + C + D) and a Mamba (decay + D + projections) each draw the parts
// they really own instead of a common denominator that flatters both.
function SSMDiagram({ decay, input, output, skip }) {
const src = decay ?? input ?? output;
if (!src) return null;
const flat = src.values.flat();
const n = Math.min(12, flat.length);
const w = 250;
const h = 132;
const cx = w / 2;
const yOf = (i) => (n <= 1 ? h / 2 : 26 + (i * (h - 62)) / (n - 1));
// A decay parameter is stored as a log; what matters visually is how long a
// state remembers, so it is mapped to a 0..1 retention rather than drawn raw.
const decays = decay
? decay.values
.flat()
.slice(0, n)
.map((v) => Math.exp(-Math.exp(Math.min(6, v))))
: new Array(n).fill(0.5);
const inVals = input ? input.values.flat() : null;
const outVals = output ? output.values.flat() : null;
const inMax = input ? Math.max(1e-9, input.stats.absmax) : 1;
const outMax = output ? Math.max(1e-9, output.stats.absmax) : 1;
return (
);
}
// --- anything with no shape worth drawing ------------------------------------
function StripDiagram({ tensor }) {
const flat = tensor.values.flat();
const absmax = Math.max(1e-9, tensor.stats.absmax);
const n = Math.min(flat.length, 48);
const w = 250;
const h = 26;
const bw = w / n;
return (
);
}
function LinearDiagram({ tensor }) {
const rows = tensor.values.length;
const cols = tensor.values[0]?.length ?? 0;
if (Math.max(rows, cols) <= 12) return ;
return (
);
}
// --- the one picture every family shares ------------------------------------
// Each family is a map over positions along the swept axis; what differs is the
// structure that map is *forced* to have. Side by side on the same axes, the
// difference is the picture: a convolution is a narrow band repeated down every
// diagonal, an SSM is lower-triangular and fading, a dense map fills the
// square. Nothing here is inferred from the class name — it is measured off the
// mixer's own impulse response.
function OperatorPanel({ operator, mixer }) {
if (!operator) return null;
const { causal, bandwidth, tied, size } = operator;
const notes = [];
if (causal >= 0.99) {
notes.push("causal — nothing reaches backwards in the sweep");
} else if (causal <= 0.6) {
notes.push(
`bidirectional — ${Math.round(100 * (1 - causal))}% of the influence runs backwards`,
);
} else {
notes.push(`mostly causal (${Math.round(100 * causal)}% of the influence)`);
}
if (bandwidth === 0) {
notes.push(
/attention/i.test(mixer ?? "")
? "no fixed off-diagonal structure: attention's mixing is computed from the data, so it is not in the parameters to draw"
: "diagonal — this layer maps each position to itself",
);
} else if (bandwidth < size - 1) {
notes.push(`banded ±${bandwidth} — a position only reaches that far`);
} else {
notes.push("dense — every position reaches every other");
}
if (tied >= 0.9 && bandwidth > 0) {
notes.push(
"weights tied along each diagonal: the same kernel, repeated at every position",
);
} else if (tied < 0.6) {
notes.push("untied — every position pair has its own weight");
}
return (
position → positionimpulse response
{size} × {size}
{/* A steep curve on purpose: an SSM's causal tail is a couple of percent
of its local term, so a linear ramp renders the one structure worth
seeing as black. Stated below rather than left as a flattering
default. */}
Row i is what an impulse at position j does to position{" "}
i. {notes.join(" · ")}.
{operator.reach != null && (
<>
{" "}
Influence away from the diagonal is{" "}
{(100 * operator.reach).toFixed(1)}%
{" "}
of the local term. Shading is |value|0.35, so a small
tail stays visible.
>
)}
);
}
// The distribution, which says things min/max/std cannot: the same three
// numbers describe a healthy spread and a spike at zero with two outliers.
function Histogram({ histogram }) {
if (!histogram?.bins?.length) return null;
const bins = histogram.bins;
const peak = Math.max(...bins, 1);
const w = 250;
const h = 26;
const bw = w / bins.length;
return (
);
}
// Findings, not a score. Each one carries the number that produced it, because
// "this layer looks unhealthy" is not something anyone can act on.
function Health({ notes }) {
if (!notes?.length) return null;
return (
No weights to draw. The viewer was opened on a spec — a saved
architecture, not a model — so there are no parameters to read. Open it
with td.viz.show(model) to see
them.
);
}
if (!layer) return
this layer has no parameters
;
// The SSM parts are one mechanism, so they are drawn as one figure; the rest
// of the tensors keep their own cards.
const by = (role) => layer.tensors.find((t) => t.role === role);
const ssm = {
decay: by("ssm_decay"),
input: by("ssm_in"),
output: by("ssm_out"),
skip: by("skip"),
};
const hasSSM = !!(ssm.decay || ssm.input || ssm.output);
const drawn = new Set(
hasSSM
? [ssm.decay, ssm.input, ssm.output, ssm.skip]
.filter(Boolean)
.map((t) => t.name)
: [],
);
return (
{hasSSM && (
state-space recurrencessm
Each circle is a state with its own decay: the loop shows how much
of it survives one step. Edges in are B, edges out are C.