// Extra visual panels: loss breakdown with animation, β ablation mini-chart const { useState: useStateX, useEffect: useEffectX, useMemo: useMemoX } = React; // Inline KaTeX for HTML context (LossBreakdown). Simpler than pipeline's SubKatex // (no foreignObject needed since we're already in HTML). Renders into a span; // falls back to the raw TeX string if KaTeX isn't loaded or parse fails. function InlineKatex({ tex, style, displayMode=false }) { const ref = React.useRef(null); React.useEffect(() => { if (!ref.current) return; if (!window.katex) { ref.current.textContent = tex; return; } try { window.katex.render(tex, ref.current, { throwOnError: false, displayMode, strict: "ignore", }); } catch (e) { if (ref.current) ref.current.textContent = tex; } }, [tex, displayMode]); return ; } function LossBarCard({ nameTex, formulaTex, value, color, active, pulsing }) { const pct = Math.max(4, Math.min(100, value*100)); return (
{active && pulsing && (
)}
{value.toFixed(3)}
); } function LossBreakdown({ active, tick }) { // simulate decreasing losses as tick increases (looping every 40) const t = (tick % 40) / 40; const decay = (lvl, base) => base * (0.4 + 0.6*Math.exp(-lvl*3*t)) * (0.9 + 0.1*Math.sin(tick*0.7+lvl)); const C_L = "oklch(0.50 0.05 260)"; // Paper-faithful LaTeX. L_cont = Eq.18, L_cluster = Eq.19, L_recons = Eq.20, // L_dec = Sec. 5.3 (sits inside L_cont with weight w_dec). const items = [ { k: "L_cont", nameTex: "\\mathcal{L}_{\\mathrm{cont}}", fTex: "w_{\\mathrm{dec}}\\,\\mathcal{L}_{\\mathrm{dec}} + w_{\\mathrm{cont}}\\,(\\mathcal{L}_{\\mathrm{nod}} + \\mathcal{L}_{\\mathrm{nei}} + \\mathcal{L}_{\\mathrm{clu}})", v: decay(1.2, 0.55), color: "oklch(0.58 0.13 35)", }, { k: "L_cluster", nameTex: "\\mathcal{L}_{\\mathrm{cluster}}", fTex: "-\\tfrac{1}{n}\\sum_{i}\\log\\dfrac{\\exp(\\cos(H_i,\\bar{H}_{k})/\\tau)}{\\sum_{j}\\exp(\\cos(H_i,\\bar{H}_{j})/\\tau)}", v: decay(0.8, 0.60), color: "oklch(0.55 0.13 150)", }, { k: "L_recons", nameTex: "\\mathcal{L}_{\\mathrm{recons}}", fTex: "\\tfrac{1}{n}\\sum_{i}\\bigl(1-\\cos(H_i,\\hat{X}_i)\\bigr)^{\\varepsilon}", v: decay(1.0, 0.72), color: "oklch(0.55 0.13 300)", }, { k: "L_dec", nameTex: "\\mathcal{L}_{\\mathrm{dec}}\\;\\,{\\scriptstyle(\\in\\mathcal{L}_{\\mathrm{cont}})}", fTex: "\\bigl\\|{Z^{(t)}}^{\\!\\top}Z^{(t)}-I\\bigr\\|^{2} + \\bigl\\|{Z^{(a)}}^{\\!\\top}Z^{(a)}-I\\bigr\\|^{2}", v: decay(0.6, 0.40), color: "oklch(0.55 0.13 250)", }, ]; return (
损失分解 · LOSS TERMS {active ? `epoch ${tick}` : "—"}
{items.map(it => ( ))}
(论文 Eq.17; 嵌在 里以 加权)— 全部自监督,不需要任何节点标签 是分层对比(节点 / 邻居 / 簇三粒度 + 去相关), 对齐 (Eq.20)。
); } // ============================================================ // HomophilyDial — drag a slider that rewires the graph in real // time; see three baselines' accuracy react. Core message: // pure GCN (topology-only) crashes on heterophilic graphs, // DGAC stays stable because the attribute branch compensates. // ============================================================ function HomophilyDial({ tweaks }){ const [h, setH] = useStateX(0.3); // default to heterophilic so the story pops const N = 20; const K = 4; // Build a graph where fraction `h` of each node's edges go to same-cluster // neighbors, the rest to other clusters. Deterministic (seeded). const G = useMemoX(()=>{ const rng = (()=>{ let s = 0xC0FFEE; return ()=>{ s=(s*1664525+1013904223)>>>0; return s/4294967296; }; })(); const nodes = []; for (let i=0;i { if (a===b) return; const k = an.cluster===nodes[i].cluster && n.id!==i).map(n=>n.id); else pool = nodes.filter(n=>n.cluster!==nodes[i].cluster).map(n=>n.id); const j = pool[Math.floor(rng()*pool.length)]; addE(i, j); } } return { nodes, edges }; }, [h]); // Run three "models" on G and compute accuracy: // - k-means on raw features (no graph) — baseline // - GCN-like: diffuse on topology only, then k-means // - DGAC: full pipeline const { accKmeans, accGcn, accDgac } = useMemoX(()=>{ const M = window.DGAC_MATH; const truth = G.nodes.map(n=>n.cluster); // Weak features so topology actually has to contribute. Feature separation 0.35 // with noise 0.45 → raw k-means only gets ~50-70%. This makes the GCN/DGAC lift // (and crash at low h) visible in the three-model comparison. const D = 6; const rng = (()=>{ let s=42; return ()=>{ s=(s*1664525+1013904223)>>>0; return s/4294967296; }; })(); const centers = []; for (let c=0;c Ht[i].map((v,k) => tweaks.beta*v + (1-tweaks.beta)*Ha[i][k])); const km3 = M.kmeans(Hf, K); const refined = M.cprop(Ahat, km3.assign, K, tweaks.alpha, tweaks.cpropLayers); const acc3 = M.matchAccuracy(refined.assign, truth, K); return { accKmeans: acc1, accGcn: acc2, accDgac: acc3 }; }, [h, tweaks.alpha, tweaks.beta, tweaks.topLayers, tweaks.attrLayers, tweaks.cpropLayers]); // Precompute per-h curves for the background lines (heavy — memoized on tweaks only) const curves = useMemoX(()=>{ const M = window.DGAC_MATH; const D = 6; const xs = Array.from({length:11}, (_,i)=>i/10); const km = [], gcn = [], dgac = []; for (const hx of xs){ const rng = (()=>{ let s=0xC0FFEE; return ()=>{ s=(s*1664525+1013904223)>>>0; return s/4294967296; }; })(); const nodes = []; for (let i=0;i{ if(a===b) return; const k=an.cluster===nodes[i].cluster && id!==i ? id : -1).filter(x=>x>=0) : nodes.map((n,id)=>n.cluster!==nodes[i].cluster ? id : -1).filter(x=>x>=0); addE(i, pool[Math.floor(rng()*pool.length)]); } } const truth = nodes.map(n=>n.cluster); const r2 = (()=>{ let s=42; return ()=>{ s=(s*1664525+1013904223)>>>0; return s/4294967296; }; })(); const centers = []; for (let c=0;cHt[i].map((v,k)=>tweaks.beta*v+(1-tweaks.beta)*Ha[i][k])); const km3 = M.kmeans(Hf, K); const ref = M.cprop(Ahat, km3.assign, K, tweaks.alpha, tweaks.cpropLayers); dgac.push(M.matchAccuracy(ref.assign, truth, K)); } return { xs, km, gcn, dgac }; }, [tweaks.alpha, tweaks.beta, tweaks.topLayers, tweaks.attrLayers, tweaks.cpropLayers]); // Layout const W = 560, H = 220, pad = 30; const xToPx = x => pad + x*(W-pad-10); const yToPx = y => H - pad - y*(H-pad-14); const line = (arr, color, dash) => ( `${i===0?"M":"L"}${xToPx(curves.xs[i])},${yToPx(y)}`).join(" ")} fill="none" stroke={color} strokeWidth={2.2} strokeDasharray={dash||""}/> ); const CLR = { km: "#a8a194", gcn: "oklch(0.58 0.13 35)", dgac: "oklch(0.50 0.05 260)", }; // node positions in SVG graph const Gw = 220, Gh = 220; const nx = x => 20 + x*(Gw-40); const ny = y => 20 + y*(Gh-40); return (
同质性消融 · 实时重连 h = {h.toFixed(2)}
拖动 同质率 h:邻居中同簇边的比例。纯 GCN(只用拓扑)在异质端崩盘,DGAC 靠属性分支稳住 — 这是论文核心论点。
setH(+e.target.value)} style={{width:"100%", accentColor:"#1b1a18", marginBottom:10}}/>
{/* Left: live graph */} {G.edges.map(([a,b],i)=>{ const na = G.nodes[a], nb = G.nodes[b]; const cross = na.cluster !== nb.cluster; return ; })} {G.nodes.map(n=>{ const colors = ["oklch(0.55 0.13 250)","oklch(0.60 0.13 140)","oklch(0.60 0.15 60)","oklch(0.55 0.15 340)"]; return ; })} 跨簇边 = 橙 · 簇内边 = 灰 {/* Right: ACC curves */} {[0.25,0.5,0.75,1.0].map(y=>( {y.toFixed(2)} ))} {[0,0.25,0.5,0.75,1].map(x=>( {x} ))} ACC h (同质率) {line(curves.km, CLR.km, "3 3")} {line(curves.gcn, CLR.gcn)} {line(curves.dgac, CLR.dgac)} {/* current h marker */}
{/* Live ACC readout */}
{[ {k:"k-means", v:accKmeans, c:CLR.km, note:"无图"}, {k:"纯 GCN", v:accGcn, c:CLR.gcn, note:"只拓扑"}, {k:"DGAC", v:accDgac, c:CLR.dgac, note:"双分支"}, ].map(m=>(
{m.k} {m.note}
{(m.v*100).toFixed(1)}%
))}
); } // ============================================================ // ConfidenceBars — shows the 20 nodes' C-prop soft probabilities // as 4-slice stacked bars. As Lc grows, watch confused nodes // either lock in or collapse (over-smoothing). // ============================================================ function ConfidenceBars({ tweaks, dgac }){ if (!dgac) return null; const N = dgac.N; const K = dgac.K; // Use soft C matrix (post-cprop) if Lc>0, else one-hot from k-means const C = tweaks.cpropLayers > 0 ? dgac.refined.C : (()=>{ const arr = Array.from({length:N}, ()=>new Array(K).fill(0)); for (let i=0;i{ const s = row.reduce((a,b)=>a+b,0) || 1; return row.map(x=>x/s); }); // entropy per node — high = confused const entropies = norm.map(r => { let h = 0; for (const p of r) if (p>1e-9) h -= p*Math.log2(p); return h; }); const maxE = Math.log2(K); // sort nodes by entropy (most confused first) for visual emphasis const order = Array.from({length:N}, (_,i)=>i).sort((a,b)=>entropies[b]-entropies[a]); return (
C-prop 置信度 · 每节点软概率 Lc = {tweaks.cpropLayers}
每根条是一个节点在 4 个簇上的概率分布。Lc 越大信息在图上传得越远: 混乱节点应该变坚定,但拉得过大会让所有节点都塌到同一簇 — 过度平滑
{order.map(i => { const r = norm[i]; return (
{r.map((p,k)=>(
))}
n{i.toString().padStart(2,"0")}
); })}
{clusterColors.map((c,k)=>( 簇 {k} ))} 平均熵 = {(entropies.reduce((a,b)=>a+b,0)/N).toFixed(3)} / {maxE.toFixed(2)}
); } function _UNUSED_BetaAblation({ tweaks }){ const { beta, dataset } = tweaks; // Sweep β ∈ [0,1], run real DGAC pipeline for each β on BOTH graphs, plot ACC. const N = 21; const xs = Array.from({length:N}, (_,i)=>i/(N-1)); const { accHetero, accHomo } = useMemoX(()=>{ const Ghe = window.DEMO_GRAPHS.hetero; const Gho = window.DEMO_GRAPHS.homo; const run = (G, b) => { const r = window.DGAC_MATH.runDGAC(G, {...tweaks, beta: b}); return r.accFinal; }; return { accHetero: xs.map(b=>run(Ghe, b)), accHomo: xs.map(b=>run(Gho, b)), }; }, [tweaks.alpha, tweaks.topLayers, tweaks.attrLayers, tweaks.cpropLayers]); const W = 280, H = 120, pad = 22; const ymin = Math.min(0.4, Math.min(...accHetero, ...accHomo) - 0.05); const ymax = 1.0; const xToPx = x => pad + x*(W-pad-10); const yToPx = y => H - pad - (y-ymin)/(ymax-ymin)*(H-pad-10); const line = (arr, color, dim) => ( `${i===0?"M":"L"}${xToPx(xs[i])},${yToPx(y)}`).join(" ")} fill="none" stroke={color} strokeWidth={dim?1.2:2.2} opacity={dim?0.35:1}/> ); const cur = dataset==="hetero" ? accHetero : accHomo; const curI = Math.round(beta*(N-1)); return (
β 消融 · 实时计算 当前 β = {beta.toFixed(2)} · ACC = {(cur[curI]*100).toFixed(1)}%
{/* y gridlines */} {[0.5,0.6,0.7,0.8,0.9,1.0].filter(y=>y>=ymin).map(y=>( {y.toFixed(1)} ))} {/* x axis */} {[0,0.25,0.5,0.75,1].map(x=>( {x} ))} {/* curves */} {line(accHetero, "oklch(0.58 0.13 35)", dataset!=="hetero")} {line(accHomo, "oklch(0.55 0.13 250)", dataset!=="homo")} {/* current beta marker */} {/* labels */} 异质 (Texas-like) 同质 (Cora-like) ACC vs β
曲线现场由引擎生成:每个 β 跑一次完整 DGAC 并计算准确率。改 α / L / Lc 曲线整体会变。
); } // ---------- InputConstruction: X / A / Â heatmaps ---------- // Honest live view of what gets fed into the model. function Heat({ M, size, colorFn, title, sub, cellGap=0 }) { if (!M || !M.length) return null; const rows = M.length, cols = M[0].length; const cellW = (size - (cols-1)*cellGap) / cols; const cellH = (size - (rows-1)*cellGap) / rows; return (
{title} {sub}
{M.map((row, i) => row.map((v, j) => { const c = colorFn(v, i, j); if (!c) return null; return ; }))}
); } function InputConstruction({ activeSet, tweaks, dgac }) { if (!dgac || !dgac.H0) return null; const { H0, Ahat, Shat, truth, N } = dgac; const on = k => activeSet.has(k); const activeInput = on("input-x") || on("input-a") || on("a-enc") || on("s-enc"); // Build raw A (binary) from  — any positive off-diagonal const A = React.useMemo(() => { const out = Array.from({length:N}, ()=>new Array(N).fill(0)); for (let i=0;i 1e-9) out[i][j] = 1; } return out; }, [Ahat, N]); // X range for colormap const {xmin, xmax} = React.useMemo(()=>{ let lo=Infinity, hi=-Infinity; for (const r of H0) for (const v of r){ if (vhi) hi=v; } return {xmin:lo, xmax:hi}; }, [H0]); const clusterColors = [ "oklch(0.62 0.15 40)", "oklch(0.62 0.15 150)", "oklch(0.62 0.15 250)", "oklch(0.62 0.15 320)", ]; const xColor = (v)=>{ const t = (v - xmin) / (xmax - xmin + 1e-9); // diverging: cool (low) → warm (high), around mid const r = Math.round(80 + t*(230-80)); const g = Math.round(150 - Math.abs(t-0.5)*120); const b = Math.round(230 - t*(230-60)); return `rgb(${r},${g},${b})`; }; const aColor = (v, i, j)=>{ if (v < 0.5) return "#fdfaf2"; if (i===j) return "oklch(0.55 0.13 250 / 0.45)"; // self-loop return "oklch(0.45 0.16 250)"; }; const ahatColor = (v)=>{ if (v < 1e-6) return "#fdfaf2"; const t = Math.min(1, v/0.5); return `oklch(${0.95 - t*0.5} 0.13 250 / ${0.25 + t*0.75})`; }; const shatColor = (v)=>{ if (v < 1e-6) return "#fdfaf2"; const t = Math.min(1, v/0.5); return `oklch(${0.95 - t*0.5} 0.13 35 / ${0.25 + t*0.75})`; }; return (
输入构造 · INPUT CONSTRUCTION
模型实际吃进去的三件套
{/* Row 1: feature matrix X with cluster bar */}
X · 节点特征矩阵 {N}×{H0[0].length}
{/* truth cluster strip (left of matrix) */} {truth.map((c,i)=>( ))} {H0.map((row, i) => row.map((v, j) => ( )))}
真值簇色带 │ 行 = 节点 {xmin.toFixed(2)} … {xmax.toFixed(2)}
{/* Row 2: three adjacency matrices in a flex row */}
v>0.5).length - N)/2}条边`}/>
{/* flow note */}
 喂拓扑分支 · Ŝ 喂属性分支 · 两条都用 α·Â·H + H₀ 做多步扩散 —— Ŝ 不是论文里来的,是从 X 的 kNN 临时造的第二张图
); } window.LossBreakdown = LossBreakdown; window.HomophilyDial = HomophilyDial; window.ConfidenceBars = ConfidenceBars; window.InputConstruction = InputConstruction;