Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Elastic Mesh Lab</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script> | |
| <style> | |
| body { background:#06090e; color:#cbd5e1; font-family:'Courier New',monospace; } | |
| .glass { background:rgba(10,18,35,0.98); border:1px solid #1e2d40; } | |
| #drawer { transition:transform .3s ease; z-index:200; } | |
| .drawer-closed { transform:translateY(100%); } | |
| .tog { transition:all .15s; } | |
| .tog.on { color:#fff; } | |
| .tog.off { background:#1e293b ; color:#64748b; } | |
| .lbtn { | |
| width:20px; height:20px; background:#1e293b; border:1px solid #334155; | |
| border-radius:3px; color:#94a3b8; font-weight:bold; font-size:12px; | |
| cursor:pointer; display:inline-flex; align-items:center; justify-content:center; | |
| } | |
| .lbtn:hover { background:#334155; color:#fff; } | |
| </style> | |
| </head> | |
| <body class="flex flex-col h-screen overflow-hidden"> | |
| <!-- ── HEADER ──────────────────────────────────────────────────────────────── --> | |
| <header class="glass flex-shrink-0 px-2 py-1.5 flex justify-between items-center"> | |
| <div class="flex flex-wrap gap-1 text-[8px] font-bold items-center"> | |
| <span id="b-mode" class="px-1.5 py-0.5 rounded bg-yellow-900/60 text-yellow-300 border border-yellow-800/60">TRAIN</span> | |
| <span id="b-arch" class="px-1.5 py-0.5 rounded bg-blue-900/60 text-blue-300 border border-blue-800/60">ADDIT</span> | |
| <span id="b-alpha" class="px-1.5 py-0.5 rounded bg-orange-900/60 text-orange-300 border border-orange-800/60">α:0.45</span> | |
| <!-- D · U · L controls --> | |
| <span class="px-1.5 py-0.5 rounded bg-green-900/60 text-green-300 border border-green-800/60 flex items-center gap-0.5"> | |
| <span class="text-yellow-300 mr-0.5">D</span> | |
| <button class="lbtn" onclick="quickL('inputs',-1)">−</button> | |
| <span id="b-inputs" class="w-3 text-center text-white">1</span> | |
| <button class="lbtn" onclick="quickL('inputs',+1)">+</button> | |
| <span class="text-orange-400 mx-1">U</span> | |
| <button class="lbtn" onclick="quickL('upper',-1)">−</button> | |
| <span id="b-upper" class="w-3 text-center text-white">3</span> | |
| <button class="lbtn" onclick="quickL('upper',+1)">+</button> | |
| <span class="text-cyan-400 mx-1">L</span> | |
| <button class="lbtn" onclick="quickL('lower',-1)">−</button> | |
| <span id="b-lower" class="w-3 text-center text-white">3</span> | |
| <button class="lbtn" onclick="quickL('lower',+1)">+</button> | |
| </span> | |
| <!-- CROSS CONNECT toggle --> | |
| <button id="b-cross" onclick="toggleCross()" | |
| class="px-1.5 py-0.5 rounded border font-bold text-[8px] transition-all bg-slate-900 text-slate-600 border-slate-700"> | |
| CROSS:OFF | |
| </button> | |
| <span id="b-data" class="px-1.5 py-0.5 rounded bg-pink-900/60 text-pink-300 border border-pink-800/60">HOUSNG</span> | |
| <!-- STIFFNESS INDICATOR --> | |
| <div class="flex items-center gap-1.5 px-2 border-l border-slate-700 ml-2"> | |
| <span class="text-[7px] text-slate-500 uppercase tracking-wider">Learning</span> | |
| <div class="w-16 bg-slate-900 rounded-sm h-2 overflow-hidden border border-slate-700"> | |
| <div id="stiff-bar" class="bg-amber-400 h-full transition-all duration-500 ease-out" style="width:0%"></div> | |
| </div> | |
| <span id="stiff-lbl" class="text-[8px] text-amber-500 w-5 text-right font-bold">0%</span> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2 ml-1"> | |
| <span id="bridge-lbl" class="text-[7px] text-amber-500 font-bold"></span> | |
| <span id="q-lbl" class="text-[8px] text-slate-600">Q:0</span> | |
| <div id="run-dot" class="w-2 h-2 rounded-full bg-slate-700"></div> | |
| <button onclick="openDrawer()" class="text-[10px] bg-blue-700 hover:bg-blue-600 px-2 py-1 rounded font-bold">⚙ DIALS</button> | |
| </div> | |
| </header> | |
| <!-- ── PLOTS ────────────────────────────────────────────────────────────────── --> | |
| <div class="flex-grow flex flex-col min-h-0 overflow-hidden"> | |
| <div id="mesh-plot" class="flex-grow min-h-0"></div> | |
| <div id="err-plot" style="height:58px" class="flex-shrink-0 border-t border-slate-900"></div> | |
| </div> | |
| <!-- ── BOTTOM PANEL ─────────────────────────────────────────────────────────── --> | |
| <div class="glass flex-shrink-0 border-t border-slate-800" style="height:174px"> | |
| <div class="flex border-b border-slate-800 text-[10px]"> | |
| <button onclick="tab('nodes')" id="tab-nodes" class="flex-1 py-1.5 bg-blue-900/40 text-blue-300 font-bold">NODES</button> | |
| <button onclick="tab('springs')" id="tab-springs" class="flex-1 py-1.5 text-slate-500">SPRINGS</button> | |
| <button onclick="tab('logs')" id="tab-logs" class="flex-1 py-1.5 text-slate-500">LOGS</button> | |
| </div> | |
| <div class="flex h-full overflow-hidden"> | |
| <div class="w-20 flex-shrink-0 border-r border-slate-800 flex flex-col items-center justify-center p-1 gap-0.5"> | |
| <div class="text-[7px] text-slate-600 uppercase tracking-widest">Tension</div> | |
| <div id="err-big" class="text-xl font-bold text-red-400 leading-none">0.00</div> | |
| <div id="pred-val" class="text-[8px] text-slate-500 w-full text-center truncate px-1">P:—</div> | |
| <div id="iter-lbl" class="text-[8px] text-slate-700">IT:0</div> | |
| </div> | |
| <div class="flex-grow overflow-y-auto p-1.5 text-[10px]"> | |
| <div id="pane-nodes"></div> | |
| <div id="pane-springs" class="hidden"></div> | |
| <div id="pane-logs" class="hidden text-[9px] text-slate-500"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ── DRAWER ───────────────────────────────────────────────────────────────── --> | |
| <aside id="drawer" class="drawer-closed fixed inset-x-0 bottom-0 glass border-t border-slate-700 p-4 flex flex-col gap-3 max-h-[93vh] overflow-y-auto"> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-orange-400 font-bold text-sm">ELASTIC MESH LABORATORY</span> | |
| <button onclick="closeDrawer()" class="text-slate-400 text-2xl leading-none">✕</button> | |
| </div> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <!-- MODE --> | |
| <div class="col-span-2 bg-slate-900 rounded p-3 border border-yellow-900/50"> | |
| <div class="text-yellow-400 text-[9px] font-bold mb-1">EXECUTION MODE</div> | |
| <div class="flex gap-2"> | |
| <button class="tog on flex-1 py-2 rounded text-xs font-bold bg-yellow-700" onclick="setMode('training',this,'bg-yellow-700')">TRAINING</button> | |
| <button class="tog off flex-1 py-2 rounded text-xs font-bold" onclick="setMode('inference',this,'bg-yellow-700')">INFERENCE</button> | |
| </div> | |
| </div> | |
| <!-- ARCH & ALPHA --> | |
| <div class="bg-slate-900 rounded p-3 border border-blue-900/50"> | |
| <div class="text-blue-400 text-[9px] font-bold mb-2">NODE ACTIVATION</div> | |
| <button class="tog on w-full mb-1 py-2 rounded text-[10px] font-bold bg-blue-700" onclick="pick('architecture','additive',this,'bg-blue-700')">ADDITIVE Σ</button> | |
| <button class="tog off w-full py-2 rounded text-[10px] font-bold" onclick="pick('architecture','multiplicative',this,'bg-blue-700')">MULTIPLICATIVE Π</button> | |
| </div> | |
| <div class="bg-slate-900 rounded p-3 border border-orange-900/50"> | |
| <div class="text-orange-400 text-[9px] font-bold mb-1">BACK-TENSION α</div> | |
| <input id="alpha-sl" type="range" min="0" max="100" value="45" step="5" class="w-full accent-orange-500 mt-1" oninput="document.getElementById('alpha-val').innerText=(this.value/100).toFixed(2)"> | |
| <div class="text-center text-orange-300 font-bold text-xl mt-0.5" id="alpha-val">0.45</div> | |
| </div> | |
| <!-- CROSS CONNECT --> | |
| <div class="col-span-2 bg-slate-900 rounded p-3 border border-amber-900/50"> | |
| <div class="text-amber-400 text-[9px] font-bold mb-1">CROSS-CONNECT</div> | |
| <div class="text-[9px] text-slate-500 mb-2"> | |
| OFF → N independent parallel hourglasses (default)<br> | |
| ON → Passive bridge vertices connect adjacent dimensions for physical cross-talk without gradient bleeding. | |
| </div> | |
| <button id="drawer-cross-btn" onclick="toggleCross()" class="w-full py-2 rounded text-xs font-bold border border-slate-700 bg-slate-800 text-slate-400"> | |
| CROSS-CONNECT: OFF | |
| </button> | |
| </div> | |
| <!-- TOPOLOGY --> | |
| <div class="col-span-2 bg-slate-900 rounded p-3 border border-green-900/50 grid grid-cols-3 gap-2"> | |
| <div class="bg-slate-800 rounded p-2 text-center border border-yellow-900/40"> | |
| <div class="text-yellow-400 text-[8px] mb-1">DIMENSIONS (D)</div> | |
| <div class="flex items-center justify-center gap-2"> | |
| <button class="lbtn text-sm" onclick="chL('inputs',-1)">−</button> | |
| <span id="dial-inputs" class="text-yellow-300 font-bold text-2xl w-8 text-center">1</span> | |
| <button class="lbtn text-sm" onclick="chL('inputs',+1)">+</button> | |
| </div> | |
| </div> | |
| <div class="bg-slate-800 rounded p-2 text-center border border-orange-900/40"> | |
| <div class="text-orange-400 text-[8px] mb-1">UPPER (U)</div> | |
| <div class="flex items-center justify-center gap-2"> | |
| <button class="lbtn text-sm" onclick="chL('upper',-1)">−</button> | |
| <span id="dial-upper" class="text-orange-300 font-bold text-2xl w-8 text-center">3</span> | |
| <button class="lbtn text-sm" onclick="chL('upper',+1)">+</button> | |
| </div> | |
| </div> | |
| <div class="bg-slate-800 rounded p-2 text-center border border-cyan-900/40"> | |
| <div class="text-cyan-400 text-[8px] mb-1">LOWER (L)</div> | |
| <div class="flex items-center justify-center gap-2"> | |
| <button class="lbtn text-sm" onclick="chL('lower',-1)">−</button> | |
| <span id="dial-lower" class="text-cyan-300 font-bold text-2xl w-8 text-center">3</span> | |
| <button class="lbtn text-sm" onclick="chL('lower',+1)">+</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <button onclick="applyConfig()" class="w-full bg-white text-black py-3 rounded font-bold text-sm hover:bg-slate-200"> | |
| APPLY & REBUILD TOPOLOGY | |
| </button> | |
| <div class="grid grid-cols-2 gap-2 mt-2"> | |
| <!-- DATASET --> | |
| <div class="bg-slate-900 rounded p-3 border border-pink-900/50"> | |
| <div class="text-pink-400 text-[9px] font-bold mb-2">DATASET</div> | |
| <select id="ds-sel" onchange="refreshDS()" class="w-full bg-black border border-slate-700 p-2 text-white text-xs rounded mb-2"> | |
| <option value="housing">Housing (A×2.5+B×1.2)</option> | |
| <option value="subtraction">Subtraction (A−B)</option> | |
| <option value="multiplication">Multiplication (A×B)</option> | |
| <option value="quadratic">Quadratic (A²+B)</option> | |
| </select> | |
| <div class="flex gap-2"> | |
| <input id="batch-n" type="number" value="30" class="w-16 bg-black border border-slate-700 p-2 text-white text-sm text-center rounded"> | |
| <button onclick="startBatch()" class="flex-1 bg-pink-800 hover:bg-pink-700 py-2 text-xs font-bold rounded">START BATCH</button> | |
| </div> | |
| </div> | |
| <!-- CUSTOM INPUT --> | |
| <div class="bg-slate-900 rounded p-3 border border-cyan-900/50 flex flex-col"> | |
| <div class="text-cyan-400 text-[9px] font-bold mb-1">CUSTOM INPUT PREVIEW</div> | |
| <div id="ds-examples" class="text-[9px] text-slate-500 mb-2">e.g. click me</div> | |
| <div class="grid grid-cols-3 gap-1 mb-1"> | |
| <input id="ca" type="text" value="5" oninput="updateExpected()" class="w-full bg-black border border-slate-700 p-1.5 text-white text-xs text-center rounded"> | |
| <input id="cb" type="text" value="3" oninput="updateExpected()" class="w-full bg-black border border-slate-700 p-1.5 text-white text-xs text-center rounded"> | |
| <input id="cc" type="text" placeholder="auto" class="w-full bg-black border border-slate-700 p-1.5 text-white text-xs text-center rounded"> | |
| </div> | |
| <div id="expected-lbl" class="text-[9px] text-yellow-400 font-bold mb-2 min-h-[14px] text-center flex-grow flex items-center justify-center">waiting...</div> | |
| <button onclick="runCustom()" class="w-full bg-cyan-800 hover:bg-cyan-700 py-2 text-xs font-bold rounded mt-auto">RUN CUSTOM</button> | |
| </div> | |
| </div> | |
| <button onclick="halt()" class="w-full border border-red-700 text-red-500 py-2 rounded text-xs font-bold mt-2">HALT ENGINE</button> | |
| </aside> | |
| <script> | |
| // ── STATE ────────────────────────────────────────────────────────────────────── | |
| const cfg = { mode: 'training', architecture: 'additive' }; | |
| const topo = { inputs: 1, upper: 3, lower: 3 }; | |
| let crossConnect = false; | |
| // ── CUSTOM INPUT VALIDATOR ─────────────────────────────────────────────────── | |
| const DS = { | |
| housing: { fn:(a,b)=>(a*2.5+b*1.2).toFixed(3), ex:[{a:4,b:2},{a:6,b:3}] }, | |
| subtraction: { fn:(a,b)=>(a-b).toFixed(3), ex:[{a:8,b:3},{a:5,b:5}] }, | |
| multiplication:{ fn:(a,b)=>(a*b).toFixed(3), ex:[{a:6,b:7},{a:3,b:9}] }, | |
| quadratic: { fn:(a,b)=>(a*a+b).toFixed(3), ex:[{a:3,b:2},{a:4,b:5}] }, | |
| }; | |
| async function refreshDS() { | |
| const ds = document.getElementById('ds-sel').value; | |
| const m = DS[ds]; | |
| document.getElementById('ds-examples').innerHTML = 'e.g. ' + | |
| m.ex.map(e => `<span class="cursor-pointer text-cyan-500 underline" onclick="fillC('${e.a}','${e.b}')">A=${e.a} B=${e.b}</span>`).join(' '); | |
| updateExpected(); | |
| await fetch('/config', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ dataset: ds }) | |
| }); | |
| } | |
| function fillC(a, b) { | |
| const n = topo.inputs; | |
| if (n > 1) { | |
| document.getElementById('ca').value = Array(n).fill(a).join(','); | |
| document.getElementById('cb').value = Array(n).fill(b).join(','); | |
| } else { | |
| document.getElementById('ca').value = a; | |
| document.getElementById('cb').value = b; | |
| } | |
| document.getElementById('cc').value = ''; | |
| updateExpected(); | |
| } | |
| function parseVals(str) { | |
| return String(str).split(',').map(s => parseFloat(s.trim())).filter(x => !isNaN(x)); | |
| } | |
| function updateExpected() { | |
| const a = document.getElementById('ca').value; | |
| const b = document.getElementById('cb').value; | |
| const ds = document.getElementById('ds-sel').value; | |
| const lbl = document.getElementById('expected-lbl'); | |
| if (!a || !b || !DS[ds]) { lbl.innerText = "Enter values"; return; } | |
| const fn = DS[ds].fn; | |
| const av = parseVals(a), bv = parseVals(b); | |
| if (!av.length || !bv.length) { lbl.innerText = "Invalid numbers"; return; } | |
| const n = topo.inputs; | |
| const results = Array.from({length:n}, (_,i) => fn(av[i%av.length], bv[i%bv.length])); | |
| lbl.innerText = n === 1 ? `Target: ${results[0]}` : `Targets: [${results.join(', ')}]`; | |
| } | |
| // ── CROSS CONNECT ───────────────────────────────────────────────────────────── | |
| async function toggleCross() { | |
| const res = await fetch('/toggle_cross', { method: 'POST' }); | |
| const data = await res.json(); | |
| crossConnect = data.cross_connect; | |
| updateCrossUI(); | |
| meshPlotted = false; | |
| } | |
| function updateCrossUI() { | |
| const btn1 = document.getElementById('b-cross'); | |
| const btn2 = document.getElementById('drawer-cross-btn'); | |
| const blbl = document.getElementById('bridge-lbl'); | |
| if (crossConnect) { | |
| btn1.className = 'px-1.5 py-0.5 rounded border font-bold text-[8px] transition-all bg-amber-800 text-amber-200 border-amber-600'; | |
| btn1.innerText = 'CROSS:ON'; | |
| btn2.className = 'w-full py-2 rounded text-xs font-bold border border-amber-600 bg-amber-900/60 text-amber-200'; | |
| btn2.innerText = 'CROSS-CONNECT: ON (Click to disable)'; | |
| blbl.innerText = topo.inputs >= 2 ? `⬡ ${ (topo.inputs - 1) * 2 } Passive Bridges` : ''; | |
| } else { | |
| btn1.className = 'px-1.5 py-0.5 rounded border font-bold text-[8px] transition-all bg-slate-900 text-slate-600 border-slate-700'; | |
| btn1.innerText = 'CROSS:OFF'; | |
| btn2.className = 'w-full py-2 rounded text-xs font-bold border border-slate-700 bg-slate-800 text-slate-400'; | |
| btn2.innerText = 'CROSS-CONNECT: OFF (Click to enable)'; | |
| blbl.innerText = ''; | |
| } | |
| } | |
| // ── UI TOGGLES ─────────────────────────────────────────────────────────────── | |
| let isDrawerOpen = false; | |
| function openDrawer() { isDrawerOpen = true; document.getElementById('drawer').classList.remove('drawer-closed'); refreshDS(); } | |
| function closeDrawer() { isDrawerOpen = false; document.getElementById('drawer').classList.add('drawer-closed'); } | |
| function chL(w, d) { | |
| const lim = { inputs:[1,16], upper:[1,16], lower:[1,16] }; | |
| topo[w] = Math.max(lim[w][0], Math.min(lim[w][1], topo[w]+d)); | |
| document.getElementById(`dial-${w}`).innerText = topo[w]; | |
| updateExpected(); | |
| } | |
| async function quickL(w, d) { | |
| const res = await fetch('/set_layer', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ layer: w, delta: d }) | |
| }); | |
| const data = await res.json(); | |
| forceSyncTopoUI(data); | |
| meshPlotted = false; | |
| } | |
| function syncTopoUI(d) { | |
| document.getElementById('b-inputs').innerText = d.n_inputs; | |
| document.getElementById('b-upper').innerText = d.n_upper; | |
| document.getElementById('b-lower').innerText = d.n_lower; | |
| document.getElementById('b-arch').innerText = d.architecture.slice(0,5).toUpperCase(); | |
| if (!isDrawerOpen) forceSyncTopoUI(d); | |
| } | |
| function forceSyncTopoUI(d) { | |
| topo.inputs = d.n_inputs; topo.upper = d.n_upper; topo.lower = d.n_lower; | |
| document.getElementById('dial-inputs').innerText = d.n_inputs; | |
| document.getElementById('dial-upper').innerText = d.n_upper; | |
| document.getElementById('dial-lower').innerText = d.n_lower; | |
| updateCrossUI(); | |
| updateExpected(); | |
| } | |
| async function setMode(m, btn, cls) { | |
| cfg.mode = m; | |
| btn.parentElement.querySelectorAll('.tog').forEach(b => { b.classList.add('off'); b.classList.remove('on', cls); }); | |
| btn.classList.remove('off'); btn.classList.add('on', cls); | |
| await fetch('/set_mode', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ mode: m }) }); | |
| document.getElementById('b-mode').innerText = m === 'training' ? 'TRAIN' : 'INFER'; | |
| } | |
| function pick(key, val, btn, cls) { | |
| cfg[key] = val; | |
| btn.parentElement.querySelectorAll('.tog').forEach(b => { b.classList.add('off'); b.classList.remove('on', cls); }); | |
| btn.classList.remove('off'); btn.classList.add('on', cls); | |
| } | |
| async function applyConfig() { | |
| const ds = document.getElementById('ds-sel').value; | |
| const alpha = parseFloat(document.getElementById('alpha-sl').value) / 100; | |
| await fetch('/config', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ | |
| ...cfg, dataset: ds, back_alpha: alpha, | |
| n_inputs: topo.inputs, n_upper: topo.upper, n_lower: topo.lower | |
| }) | |
| }); | |
| document.getElementById('b-arch').innerText = cfg.architecture.slice(0,5).toUpperCase(); | |
| document.getElementById('b-alpha').innerText = `α:${alpha.toFixed(2)}`; | |
| document.getElementById('b-data').innerText = ds.slice(0,6).toUpperCase(); | |
| meshPlotted = false; | |
| closeDrawer(); | |
| } | |
| async function startBatch() { | |
| await fetch('/generate', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ count: parseInt(document.getElementById('batch-n').value) }) }); | |
| closeDrawer(); | |
| } | |
| async function runCustom() { | |
| const a = document.getElementById('ca').value; | |
| const b = document.getElementById('cb').value; | |
| let c = document.getElementById('cc').value; | |
| if (!c) { | |
| const ds = document.getElementById('ds-sel').value; | |
| const fn = DS[ds].fn; | |
| const av = parseVals(a), bv = parseVals(b); | |
| if (av.length && bv.length) { | |
| c = Array.from({length:topo.inputs}, (_,i) => fn(av[i%av.length], bv[i%bv.length])).join(','); | |
| } | |
| } | |
| await fetch('/run_custom', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ a, b, c }) }); | |
| closeDrawer(); | |
| } | |
| async function halt() { await fetch('/halt', { method:'POST' }); closeDrawer(); } | |
| function tab(name) { | |
| ['nodes','springs','logs'].forEach(t => { | |
| document.getElementById(`pane-${t}`).classList.toggle('hidden', t !== name); | |
| document.getElementById(`tab-${t}`).className = t === name ? 'flex-1 py-1.5 bg-blue-900/40 text-blue-300 font-bold text-[10px]' : 'flex-1 py-1.5 text-slate-500 text-[10px]'; | |
| }); | |
| } | |
| // ── VISUALIZATION (Vertical Layout with Bridge Nodes) ───────────────────────── | |
| function buildPos(layers, n_inputs, n_upper, n_lower, bridgeNodeIds) { | |
| const pos = {}; | |
| const Y = [4.4, 2.1, 0.0, -2.1, -4.4]; | |
| const COL_W = n_inputs === 1 ? 0 : Math.min(9.0 / (n_inputs - 1), 4.2); | |
| const halfSp = COL_W * (n_inputs - 1) / 2; | |
| const bSprd = n_inputs === 1 ? 3.8 : Math.min(COL_W * 0.55, 1.8); | |
| layers.forEach((layer, li) => { | |
| const y = Y[li]; | |
| layer.forEach(nid => { | |
| const kind = nid[0]; | |
| let dim = 1, j = 1, total = 1; | |
| if ('ABC'.includes(kind)) { dim = parseInt(nid.slice(1)); } | |
| else { | |
| const parts = nid.slice(1).split('_'); | |
| dim = parseInt(parts[0]); j = parseInt(parts[1]); total = kind === 'U' ? n_upper : n_lower; | |
| } | |
| const cx = n_inputs === 1 ? 0 : -halfSp + (dim - 1) * COL_W; | |
| if ('ABC'.includes(kind)) { pos[nid] = [cx, y]; } | |
| else { | |
| const t = total === 1 ? 0 : (2*(j-1)/(total-1) - 1); | |
| pos[nid] = [cx + bSprd * t, y]; | |
| } | |
| }); | |
| }); | |
| bridgeNodeIds.forEach(nid => { | |
| const side = nid[1], d = parseInt(nid.slice(2)); | |
| const yVal = side === 'U' ? Y[1] : Y[3]; | |
| const xVal = -halfSp + (d - 0.5) * COL_W; | |
| pos[nid] = [xVal, yVal]; | |
| }); | |
| return pos; | |
| } | |
| function busShapes(pos, n_inputs) { | |
| const sh = []; | |
| const mg = n_inputs === 1 ? 1.4 : 0.8; | |
| const xs = k => Object.entries(pos).filter(([id]) => id[0] === k).map(([,v]) => v[0]); | |
| const rect = (mn, mx, yc, hh, fill, stroke) => sh.push({ | |
| type:'rect', xref:'x', yref:'y', x0:mn-mg, x1:mx+mg, y0:yc-hh, y1:yc+hh, fillcolor:fill, line:{color:stroke, width:2} | |
| }); | |
| const aXs=xs('A'), cXs=xs('C'), bXs=xs('B'); | |
| if(aXs.length) rect(Math.min(...aXs),Math.max(...aXs), 4.4, 0.35,'rgba(251,146,60,0.10)','rgba(251,146,60,0.65)'); | |
| if(cXs.length) rect(Math.min(...cXs),Math.max(...cXs), 0.0, 0.32,'rgba(56,189,248,0.10)','rgba(56,189,248,0.70)'); | |
| if(bXs.length) rect(Math.min(...bXs),Math.max(...bXs),-4.4, 0.35,'rgba(192,132,252,0.10)','rgba(192,132,252,0.65)'); | |
| return sh; | |
| } | |
| function springColor(k) { | |
| const t = Math.min(Math.abs(k) / 6, 1); | |
| if (k >= 0) return [`rgb(${Math.round(180+t*70)},${Math.round(100+t*80)},30)`, 1.0+t*3.5]; | |
| return [`rgb(30,${Math.round(80+t*100)},${Math.round(140+t*115)})`, 1.0+t*3.5]; | |
| } | |
| function buildTraces(nodes, springs, bridgeSprings, layers, n_inputs, n_upper, n_lower) { | |
| const bridgeNodeIds = Object.keys(nodes).filter(id => id[0] === 'X'); | |
| const pos = buildPos(layers, n_inputs, n_upper, n_lower, bridgeNodeIds); | |
| const traces = []; | |
| for (const [key, k] of Object.entries(springs)) { | |
| const [u, v] = key.split('→'); | |
| if (!pos[u] || !pos[v]) continue; | |
| const [col, wd] = springColor(k); | |
| traces.push({ type:'scatter', mode:'lines', x:[pos[u][0], pos[v][0]], y:[pos[u][1], pos[v][1]], line:{color:col, width:wd}, hoverinfo:'none' }); | |
| } | |
| for (const key of Object.keys(bridgeSprings)) { | |
| const [u, v] = key.split('→'); | |
| if (!pos[u] || !pos[v]) continue; | |
| traces.push({ type:'scatter', mode:'lines', x:[pos[u][0], pos[v][0]], y:[pos[u][1], pos[v][1]], line:{color:'#f59e0b', width:2.0, dash:'dot'}, hoverinfo:'none' }); | |
| } | |
| const allN = [...layers.flat(), ...bridgeNodeIds]; | |
| const NCOL = id => id[0]==='A'?'#fb923c': id[0]==='B'?'#c084fc': id[0]==='C'?'#38bdf8': id[0]==='X'?'#f59e0b': id[0]==='U'?'#4ade80': '#67e8f9'; | |
| const isIO = id => 'ABC'.includes(id[0]); | |
| const isBridge = id => id[0] === 'X'; | |
| traces.push({ | |
| type:'scatter', mode:'markers+text', | |
| x: allN.map(id => pos[id]?.[0] ?? 0), y: allN.map(id => pos[id]?.[1] ?? 0), | |
| text: allN.map(id => isIO(id) ? `${id}\n${Number(nodes[id]?.x ?? 0).toFixed(2)}` : ''), | |
| textposition: allN.map(id => id[0]==='B' ? 'bottom center' : 'top center'), | |
| textfont:{ size:9, color: allN.map(id => NCOL(id)) }, | |
| marker:{ | |
| size: allN.map(id => isIO(id) ? 18 : isBridge(id) ? 13 : 10 + Math.min(Math.abs(nodes[id]?.vel??0)*30, 8)), | |
| symbol: allN.map(id => isBridge(id) ? 'diamond' : 'circle'), | |
| color: allN.map(id => NCOL(id)), | |
| line:{ width: allN.map(id => isBridge(id)?3.0:2.5), color: allN.map(id => isBridge(id)?'#d97706':nodes[id]?.anchored?'#ef4444':'#22c55e') } | |
| }, | |
| hoverinfo:'none' | |
| }); | |
| const xMax = Math.max(5.5, n_inputs * 2.8); | |
| return { traces, layout: { margin:{l:8,r:8,t:8,b:8}, paper_bgcolor:'transparent', plot_bgcolor:'transparent', xaxis:{visible:false, range:[-xMax, xMax]}, yaxis:{visible:false, range:[-5.5, 5.5]}, showlegend:false, shapes: busShapes(pos, n_inputs) }}; | |
| } | |
| const ERR_LAYOUT = { | |
| margin:{l:30,r:6,t:3,b:12}, paper_bgcolor:'transparent', plot_bgcolor:'transparent', | |
| xaxis:{visible:false}, yaxis:{color:'#334155', gridcolor:'#0f172a', zeroline:true, zerolinecolor:'#22c55e', zerolinewidth:1}, showlegend:false, | |
| }; | |
| let meshPlotted = false, errPlotted = false, lastLayerKey = ''; | |
| refreshDS(); | |
| setInterval(async () => { | |
| try { | |
| const r = await fetch('/state'); | |
| const d = await r.json(); | |
| syncTopoUI(d); | |
| crossConnect = d.cross_connect; | |
| updateCrossUI(); | |
| document.getElementById('b-alpha').innerText = `α:${d.back_alpha.toFixed(2)}`; | |
| document.getElementById('b-data').innerText = (d.dataset_type||'').slice(0,6).toUpperCase(); | |
| const mBadge = document.getElementById('b-mode'); | |
| mBadge.innerText = d.mode === 'training' ? 'TRAIN' : 'INFER'; | |
| mBadge.className = d.mode === 'training' | |
| ? 'px-1.5 py-0.5 rounded bg-yellow-900/60 text-yellow-300 border border-yellow-800/60 text-[8px] font-bold' | |
| : 'px-1.5 py-0.5 rounded bg-green-900/60 text-green-300 border border-green-800/60 text-[8px] font-bold'; | |
| const stiffAct = d.stiffness_active || 0; | |
| const sBar = document.getElementById('stiff-bar'); | |
| sBar.style.width = `${stiffAct}%`; | |
| sBar.className = `h-full transition-all duration-500 ease-out ${stiffAct > 0.1 ? 'bg-amber-400' : 'bg-slate-700'}`; | |
| document.getElementById('stiff-lbl').innerText = `${stiffAct.toFixed(1)}%`; | |
| document.getElementById('run-dot').className = `w-2 h-2 rounded-full ${d.running ? 'bg-green-400' : 'bg-slate-700'}`; | |
| document.getElementById('q-lbl').innerText = `Q:${d.queue_size}`; | |
| document.getElementById('iter-lbl').innerText = `IT:${d.iter}`; | |
| const e = Math.abs(d.error); | |
| const col = e < 0.02 ? 'text-green-400' : e < 2 ? 'text-yellow-400' : 'text-red-400'; | |
| document.getElementById('err-big').className = `text-xl font-bold ${col} leading-none`; | |
| document.getElementById('err-big').innerText = d.error.toFixed(4); | |
| const preds = d.predictions || [d.prediction]; | |
| document.getElementById('pred-val').innerText = preds.length === 1 ? `P: ${preds[0].toFixed(3)}` : `P: [${preds.map(v=>v.toFixed(2)).join(',')}]`; | |
| // Nodes pane | |
| const order = d.layers.flat(); | |
| let nh = ''; | |
| order.forEach(id => { | |
| const n = d.nodes[id]; if (!n || !'ABC'.includes(id[0])) return; | |
| const icon = n.anchored ? '<span class="text-red-400">⊠</span>' : '<span class="text-green-500">◎</span>'; | |
| const COLS = {A:'#fb923c',B:'#c084fc',C:'#38bdf8'}; | |
| nh += `<div class="flex justify-between items-center py-0.5 border-b border-slate-900"> | |
| ${icon}<span class="ml-1 font-bold" style="color:${COLS[id[0]]}">${id}</span> | |
| <span class="text-white font-bold">${Number(n.x).toFixed(4)}</span> | |
| <span class="text-slate-700 text-[8px]">v:${(n.vel||0).toFixed(3)}</span> | |
| </div>`; | |
| }); | |
| const bridgeIds = Object.keys(d.nodes).filter(id => id[0] === 'X'); | |
| if (bridgeIds.length) { | |
| nh += `<div class="text-[8px] text-amber-600 py-0.5 border-b border-slate-900 mt-1">⬡ ${bridgeIds.length} passive bridges</div>`; | |
| bridgeIds.forEach(id => { | |
| const n = d.nodes[id]; | |
| nh += `<div class="flex justify-between items-center py-0.5 border-b border-slate-900/50"> | |
| <span class="text-amber-500 text-[9px]">◆ ${id}</span> | |
| <span class="text-amber-300 font-bold">${Number(n.x).toFixed(4)}</span> | |
| <span class="text-slate-700 text-[8px]">v:${(n.vel||0).toFixed(3)}</span> | |
| </div>`; | |
| }); | |
| } | |
| document.getElementById('pane-nodes').innerHTML = nh; | |
| // Springs pane | |
| let sh = ''; | |
| for (const [key, k] of Object.entries(d.springs)) { | |
| const kc = k < 0 ? 'text-blue-300' : k > 4 ? 'text-yellow-200' : 'text-purple-300'; | |
| sh += `<div class="flex justify-between py-0.5 border-b border-slate-900"> | |
| <span class="text-slate-500 text-[9px]">${key}</span> | |
| <span class="${kc} font-bold text-[10px]">${k.toFixed(4)}</span></div>`; | |
| } | |
| const bs = d.bridge_springs || {}; | |
| if (Object.keys(bs).length) { | |
| sh += `<div class="text-[8px] text-amber-600 py-0.5 border-b border-slate-800 mt-1">⬡ BRIDGE SPRINGS (Passive)</div>`; | |
| for (const [key, k] of Object.entries(bs)) { | |
| sh += `<div class="flex justify-between py-0.5 border-b border-slate-900/50"> | |
| <span class="text-amber-700 text-[9px]">${key}</span> | |
| <span class="text-amber-400 font-bold text-[10px]">${k.toFixed(4)}</span></div>`; | |
| } | |
| } | |
| document.getElementById('pane-springs').innerHTML = sh; | |
| document.getElementById('pane-logs').innerHTML = d.logs.map(l => `<div class="py-0.5 border-b border-slate-900/50">${l}</div>`).join(''); | |
| const layerKey = JSON.stringify(d.layers) + d.cross_connect; | |
| const { traces, layout } = buildTraces(d.nodes, d.springs, d.bridge_springs || {}, d.layers, d.n_inputs, d.n_upper, d.n_lower); | |
| if (!meshPlotted || layerKey !== lastLayerKey) { | |
| Plotly.newPlot('mesh-plot', traces, layout, {displayModeBar:false, responsive:true}); | |
| meshPlotted = true; lastLayerKey = layerKey; | |
| } else { | |
| Plotly.react('mesh-plot', traces, layout); | |
| } | |
| const hist = d.history; | |
| const eTrace = { type:'scatter', mode:'lines', x: hist.map((_,i)=>i), y: hist, line:{color:'#f97316',width:1.5}, fill:'tozeroy', fillcolor:'rgba(249,115,22,0.07)' }; | |
| if (!errPlotted) { Plotly.newPlot('err-plot',[eTrace],ERR_LAYOUT,{displayModeBar:false,responsive:true}); errPlotted = true; } | |
| else { Plotly.react('err-plot',[eTrace],ERR_LAYOUT); } | |
| } catch(e) { } | |
| }, 200); | |
| </script> | |
| </body> | |
| </html> |