// Main DGAC Playground app. const { useState, useEffect, useMemo, useRef } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "alpha": 0.9, "beta": 0.5, "topLayers": 5, "attrLayers": 5, "cpropLayers": 1, "dataset": "hetero", "showAttrGraph": true, "animateIter": true }/*EDITMODE-END*/; // Smooth transition ease curve reused for nodes + edges when step changes. const SMOOTH_T = "cubic-bezier(0.4, 0, 0.2, 1)"; const NODE_XITION = { transition: `transform 0.6s ${SMOOTH_T}` }; const LINE_XITION = { transition: `x1 0.6s ${SMOOTH_T}, y1 0.6s ${SMOOTH_T}, x2 0.6s ${SMOOTH_T}, y2 0.6s ${SMOOTH_T}, stroke 0.3s, stroke-width 0.3s, opacity 0.3s`, }; // ============================================================== // MixedMath — renders text with $...$ tex segments interleaved. // Chinese text stays in normal font; math segments are KaTeX-rendered. // Uses renderToString + dangerouslySetInnerHTML so React fully owns // the DOM (no ref/innerHTML conflict that can trigger update loops). // ============================================================== function MixedMath({ text }) { const parts = useMemo(() => { const out = []; const re = /\$([^$]+)\$/g; let last = 0, m; while ((m = re.exec(text)) !== null) { if (m.index > last) out.push({ t: "txt", v: text.slice(last, m.index) }); out.push({ t: "tex", v: m[1] }); last = m.index + m[0].length; } if (last < text.length) out.push({ t: "txt", v: text.slice(last) }); return out; }, [text]); return <>{parts.map((p, i) => p.t === "tex" ? : {p.v})}; } function KatexInline({ tex }) { const html = useMemo(() => { if (!window.katex) return null; try { return window.katex.renderToString(tex, { throwOnError: false, displayMode: false, strict: "ignore", output: "html", }); } catch (e) { console.warn("[KaTeX]", tex, e); return null; } }, [tex]); if (html == null) return {tex}; return ; } // ============================================================== // CDETheatre — the playground's signature animation. // // Two side-by-side SVG panels showing the same graph evolving under: // LEFT · GRAND only ∂x/∂t = div(D∇x) // RIGHT · CDE ∂x/∂t = div(D∇x) − div(v·x) // // Time scrubber lets the user drag t ∈ [0, T]. Node colour = nearest- // centroid prediction, mis-classified nodes get dark stroke. Right panel // overlays per-node velocity arrows showing the convection direction. // // Mechanism-only — no ACC display in the theatre. The toy ODE setup // (fixed centroids + no add_source term) makes toy ACC drop too steeply, // which is not paper behaviour. Real performance numbers live in the // Table 3 / Table 2 panel below (see extras.jsx). // ============================================================== function CDETheatre({ cde, tIdx, setTIdx, autoplay, setAutoplay, tweaks, stepId }) { const G = window.DEMO_GRAPHS[tweaks.dataset]; const PW = 380, PH = 300; // each panel const numSteps = cde.steps; const t = Math.max(0, Math.min(numSteps, tIdx)); // Auto-play React.useEffect(() => { if (!autoplay) return; const id = setInterval(() => { setTIdx(prev => { if (prev >= numSteps) { setAutoplay(false); return numSteps; } return prev + 1; }); }, 280); return () => clearInterval(id); }, [autoplay, numSteps]); // Project current X(t) for both panels — same projection rule. const projGrand = window.CDE_MATH.project2DAnchored(cde.trajGrand[t], cde.centers); const projCDE = window.CDE_MATH.project2DAnchored(cde.trajCDE[t], cde.centers); const velsCDE = cde.velFields[t]; // Predicted classes by nearest centroid const predGrand = window.CDE_MATH.classifyNearest(cde.trajGrand[t], cde.centers); const predCDE = window.CDE_MATH.classifyNearest(cde.trajCDE[t], cde.centers); // ACC display intentionally removed — see header comment. const clusterColors = G.clusters.map(c => c.color); // Pos of node n in panel using projection proj. Anchor is its layout (n.tx, n.ty); // we add a small displacement from the 2D projection of X(t). const posOf = (n, proj) => { const ax = n.tx, ay = n.ty; const [dx, dy] = proj[n.id]; const k = 0.16; return [(ax + dx*k) * PW, (ay + dy*k) * PH]; }; const renderPanel = (pred, proj, panelKind) => { const showVel = panelKind === "cde" && tweaks.showAttrGraph !== false; return ( {/* edges */} {G.edges.map(([a,b], i) => { const [x1,y1] = posOf(G.nodes[a], proj); const [x2,y2] = posOf(G.nodes[b], proj); const sameClu = G.nodes[a].cluster === G.nodes[b].cluster; return ; })} {/* nodes — drawn before arrows so the arrows sit on top */} {G.nodes.map((n, i) => { const [cx, cy] = posOf(n, proj); const c = clusterColors[pred[i]]; const wrong = pred[i] !== n.cluster; return ( {wrong && } ); })} {/* velocity arrows on CDE panel — drawn LAST so they layer on top of nodes. Projected to first 2 dims; arrows start outside the node radius. */} {showVel && ( <> {G.nodes.map((n, i) => { const [cx, cy] = posOf(n, proj); const v = velsCDE[i]; const rawLen = Math.sqrt(v[0]*v[0] + v[1]*v[1]); if (rawLen < 0.005) return null; const mag = Math.min(0.2, rawLen); const drawLen = 10 + (mag/0.2) * 12; const dx = v[0]/rawLen, dy = v[1]/rawLen; const margin = 9; const sx = cx + dx*margin, sy = cy + dy*margin; const ex = sx + dx*drawLen, ey = sy + dy*drawLen; return ( ); })} )} ); }; const tReal = (t * cde.tau).toFixed(2); return (
{/* time controller */}
{ setTIdx(+e.target.value); setAutoplay(false); }} style={{flex:1, accentColor:"#1b1a18"}}/> t = {tReal} / {cde.T.toFixed(1)}
{/* dual panel */}
{/* LEFT — GRAND only */}
GRAND only · 纯热扩散
div(D∇x)
{renderPanel(predGrand, projGrand, "grand")}
{/* RIGHT — CDE */}
CDE · 扩散 + 对流 ★
div(D∇x) − div(v·x)
{renderPanel(predCDE, projCDE, "cde")}
{/* legend */}
● 节点颜色 = 当前分类预测(按最近 centroid) ⬤ 误分节点 → velocity 箭头(每节点 V_ij 平均, Eq.10)
剧场仅演示 ODE 演化机制(节点位置 = X(t) 投影;箭头 = V_ij 方向)· 真实分类性能见下方 Paper Table 3 panel。
); } // === Step scrubber === function Scrubber({ step, steps, idx, setIdx, playing, setPlaying }) { return (
{idx+1} / {steps.length}
{/* dots */}
{steps.map((s,i)=>(
step {String(idx+1).padStart(2,"0")} {step.title}

); } // === Header === function Header(){ return (
IJCAI 2023 · INTERACTIVE WALK-THROUGH

CDE · Graph Neural Convection-Diffusion with Heterophily

对流-扩散 PDE · 异质图友好 · learnable velocity
); } // === Summary strip === function BottomStrip() { const items = [ { k:"+20%", v:"Roman-empire ACC vs GRAND" }, { k:"3/9", v:"hetero datasets where CDE is best (line 845-846)" }, { k:"~1%", v:"推理时间增量 vs GRAND (line 1208-1211)" }, { tex:"V_{ij}=\\sigma\\bigl(W(x_j-x_i)\\bigr)", v:"learnable per-edge velocity (Eq.10)" }, ]; const Kx = window.InlineKatex; return (
{items.map((x,i)=>(
{x.tex ? (
{Kx ? : {x.tex}}
) : (
{x.k}
)}
{x.v}
))}
); } // === App === function App() { const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS); const [idx, setIdx] = useState(()=>{ const saved = localStorage.getItem("dgac_step"); return saved ? Math.min(parseInt(saved), window.STEPS.length-1) : 0; }); const [playing, setPlaying] = useState(false); const [tweaksVisible, setTweaksVisible] = useState(false); const [iter, setIter] = useState(0); // Time scrubber for the dual-panel CDE theatre const [tIdx, setTIdx] = useState(0); const [autoplay, setAutoplay] = useState(false); const steps = window.STEPS; const step = steps[idx]; const activeSet = useMemo(()=>new Set(step.active), [step]); // CDE result — runs both GRAND-only and CDE forward passes, returns full // trajectories of X(t) for the time-scrubbed dual-panel theatre. // alpha slider repurposed as integration time T (∈ [0.5, 5]); // beta slider repurposed as convection strength (β=0 → CDE = GRAND). const cde = useMemo(()=>{ const G = window.DEMO_GRAPHS[tweaks.dataset]; return window.CDE_MATH.runCDE(G, { rDim: 8, K: 4, T: 0.5 + tweaks.alpha * 4.5, // alpha ∈ [0.1, 1] → T ∈ [0.95, 5] tau: 0.25, kappa: 0.5, // gentler heat diffusion w0: tweaks.beta * 0.15, // β=0 → no convection (CDE=GRAND); β=1 → strong seed: 33, }); }, [tweaks.dataset, tweaks.alpha, tweaks.beta]); // Clamp tIdx when steps change (e.g. T changes → fewer steps) useEffect(()=>{ if (tIdx > cde.steps) setTIdx(cde.steps); }, [cde.steps, tIdx]); useEffect(()=>{ localStorage.setItem("dgac_step", idx); }, [idx]); // autoplay removed — steps only advance via user action. // iteration ticker for halo animation useEffect(()=>{ const t = setInterval(()=>setIter(i=>i+1), 600); return ()=>clearInterval(t); }, []); // edit-mode wiring useEffect(()=>{ const handler = (e)=>{ if (e.data?.type === "__activate_edit_mode") setTweaksVisible(true); if (e.data?.type === "__deactivate_edit_mode") setTweaksVisible(false); }; window.addEventListener("message", handler); window.parent.postMessage({type:"__edit_mode_available"}, "*"); return ()=>window.removeEventListener("message", handler); }, []); const setTweak = (k, v)=>{ setTweaks(prev=>{ const next = {...prev, [k]: v}; window.parent.postMessage({type:"__edit_mode_set_keys", edits:{[k]:v}}, "*"); return next; }); }; return (
{/* Full-width architecture diagram */}
架构流程图 · ARCHITECTURE
{ const i = steps.findIndex(s=>s.id===id); if (i>=0) setIdx(i); }}/>
{/* Inline Tweaks bar — lives right under the architecture, above the grid */} {/* Main grid */}
{/* Left column: graph view + loss */}
双 panel 时间剧场 · DUAL-PANEL TIME THEATRE ★
step {tIdx} / {cde.steps}
{/* Right column: formulas */}
公式 · FORMULAS
{/* Scrubber full width */}
CDE · IJCAI 2023 CDE Playground · interactive walkthrough
); } ReactDOM.createRoot(document.getElementById("root")).render();