everydaytok commited on
Commit
1feca89
Β·
verified Β·
1 Parent(s): 2baf456

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +154 -146
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Elastic Mesh Lab</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
9
  <style>
@@ -17,27 +17,31 @@
17
  cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
18
  }
19
  .lbtn:hover { background:#334155; color:#fff; }
20
- .ptype { display:inline-block; padding:1px 5px; border-radius:3px; font-size:9px; font-weight:bold; }
21
- .pt-box_proj { background:#1e3a2f; color:#4ade80; }
22
- .pt-halfspace { background:#1e2d4a; color:#60a5fa; }
23
- .pt-sphere { background:#3a1e3a; color:#e879f9; }
24
- .pt-simplex { background:#3a2a1e; color:#fb923c; }
25
- .pt-elastic { background:#3a1e1e; color:#f87171; }
26
- .pt-unknown { background:#1e1e1e; color:#64748b; }
 
 
 
 
 
 
 
27
  </style>
28
  </head>
29
  <body class="flex flex-col h-screen overflow-hidden">
30
 
31
  <!-- ── HEADER ──────────────────────────────────────────────────────────────── -->
32
  <header class="glass flex-shrink-0 px-2 py-1.5 flex justify-between items-center gap-1">
33
-
34
- <!-- Mode + topology badges -->
35
  <div class="flex flex-wrap gap-1 items-center text-[8px] font-bold">
36
- <span id="b-mode" class="px-1.5 py-0.5 rounded bg-yellow-900/60 text-yellow-300 border border-yellow-800/60">IDLE</span>
37
- <span id="b-type" class="px-1.5 py-0.5 rounded bg-slate-800 text-slate-400 border border-slate-700">β€”</span>
38
- <span id="b-dim" class="px-1.5 py-0.5 rounded bg-slate-900 text-slate-600 border border-slate-800">D:32</span>
39
-
40
- <!-- Inline topology U / L -->
41
  <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">
42
  <span class="text-orange-400 mr-0.5">U</span>
43
  <button class="lbtn" onclick="quickL('upper',-1)">βˆ’</button>
@@ -49,14 +53,12 @@
49
  <button class="lbtn" onclick="quickL('lower',+1)">+</button>
50
  </span>
51
  </div>
52
-
53
- <!-- Controls -->
54
  <div class="flex items-center gap-1 flex-shrink-0">
55
- <span id="q-lbl" class="text-[8px] text-slate-600">Q:0</span>
56
  <div id="run-dot" class="w-2 h-2 rounded-full bg-slate-700"></div>
57
- <button onclick="doTrainOffline()" class="text-[9px] bg-purple-800 hover:bg-purple-700 px-2 py-1 rounded font-bold">⚑ TRAIN</button>
58
- <button onclick="doTrainVisual()" class="text-[9px] bg-blue-800 hover:bg-blue-700 px-2 py-1 rounded font-bold">πŸ‘ VIZ</button>
59
- <button onclick="doInfer()" class="text-[9px] bg-green-800 hover:bg-green-700 px-2 py-1 rounded font-bold">β–Ά INFER</button>
60
  <button onclick="doHalt()" class="text-[9px] bg-red-900 hover:bg-red-800 px-2 py-1 rounded font-bold">β– </button>
61
  <button onclick="openDrawer()" class="text-[9px] bg-slate-700 hover:bg-slate-600 px-2 py-1 rounded font-bold">βš™</button>
62
  </div>
@@ -77,7 +79,6 @@
77
  <button onclick="tab('logs')" id="tab-logs" class="flex-1 py-1 text-slate-500">LOGS</button>
78
  </div>
79
  <div class="flex h-full overflow-hidden">
80
- <!-- Tension meter -->
81
  <div class="w-20 flex-shrink-0 border-r border-slate-800 flex flex-col items-center justify-center p-1 gap-0.5">
82
  <div class="text-[7px] text-slate-600 uppercase tracking-widest">β€–errorβ€–</div>
83
  <div id="err-big" class="text-xl font-bold text-red-400 leading-none">0.00</div>
@@ -85,7 +86,6 @@
85
  <div id="iter-lbl" class="text-[8px] text-slate-700">IT:0</div>
86
  <div id="step-lbl" class="text-[7px] text-slate-800">S:0</div>
87
  </div>
88
- <!-- Panes -->
89
  <div class="flex-grow overflow-y-auto p-1.5 text-[10px]">
90
  <div id="pane-nodes"></div>
91
  <div id="pane-springs" class="hidden"></div>
@@ -98,24 +98,34 @@
98
  <!-- ── DRAWER ───────────────────────────────────────────────────────────────── -->
99
  <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-[90vh] overflow-y-auto">
100
  <div class="flex justify-between items-center">
101
- <span class="text-orange-400 font-bold">ELASTIC MESH LABORATORY</span>
102
  <button onclick="closeDrawer()" class="text-slate-400 text-2xl leading-none">βœ•</button>
103
  </div>
104
 
105
- <!-- Architecture info (read-only for now) -->
106
- <div class="bg-slate-900 rounded p-3 border border-blue-900/50 text-[9px]">
107
- <div class="text-blue-400 font-bold mb-2">ARCHITECTURE</div>
108
- <div class="text-slate-400 space-y-1">
109
- <div>β€’ Spring type: <span class="text-white">Matrix K ∈ ℝ^(DΓ—D)</span> per edge</div>
110
- <div>β€’ Update rule: <span class="text-white">Matrix LMS β€” joint optimal step</span></div>
111
- <div>β€’ Inference: <span class="text-white">EWC-regularised online adaptation</span></div>
112
- <div>β€’ Convergence: <span class="text-white">β€–errorβ€– &lt; 0.08 or 400 steps</span></div>
 
 
 
 
 
 
 
 
 
 
113
  </div>
114
  </div>
115
 
116
- <!-- Topology controls (detailed) -->
117
  <div class="bg-slate-900 rounded p-3 border border-green-900/50">
118
- <div class="text-green-400 text-[9px] font-bold mb-2">TOPOLOGY A β†’ [U] β†’ C ← [L] ← B</div>
119
  <div class="grid grid-cols-2 gap-2">
120
  <div class="bg-slate-800 rounded p-2 text-center border border-orange-900/30">
121
  <div class="text-orange-400 text-[8px] mb-1">UPPER (A-side)</div>
@@ -137,34 +147,28 @@
137
  <div id="spring-info" class="text-[8px] text-slate-600 mt-2 text-center"></div>
138
  </div>
139
 
140
- <!-- Training controls -->
141
  <div class="bg-slate-900 rounded p-3 border border-purple-900/50">
142
- <div class="text-purple-400 text-[9px] font-bold mb-2">OFFLINE TRAINING (fast, no display)</div>
143
- <div class="flex gap-2 items-center mb-2">
144
  <label class="text-[9px] text-slate-500 w-16">Epochs</label>
145
  <input id="ep-n" type="number" value="5" min="1" max="50"
146
  class="w-16 bg-black border border-slate-700 p-1 text-white text-sm text-center rounded">
147
  <button onclick="doTrainOffline()"
148
  class="flex-1 bg-purple-800 hover:bg-purple-700 py-2 text-xs font-bold rounded">⚑ START</button>
149
  </div>
150
- <div class="text-[8px] text-slate-600">
151
- Runs full LMS epochs at CPU speed. Fisher info accumulates for EWC.
152
- </div>
153
  </div>
154
 
155
- <!-- Inference controls -->
156
  <div class="bg-slate-900 rounded p-3 border border-green-900/50">
157
  <div class="text-green-400 text-[9px] font-bold mb-2">INFERENCE TEST</div>
158
- <div class="flex gap-2 items-center mb-2">
159
  <label class="text-[9px] text-slate-500 w-16">N samples</label>
160
- <input id="inf-n" type="number" value="200" min="10" max="556"
161
  class="w-16 bg-black border border-slate-700 p-1 text-white text-sm text-center rounded">
162
  <button onclick="doInfer()"
163
  class="flex-1 bg-green-800 hover:bg-green-700 py-2 text-xs font-bold rounded">β–Ά RUN</button>
164
  </div>
165
- <div class="text-[8px] text-slate-600">
166
- Mesh sees A+B only. C_pred vs C_target β†’ accuracy per problem type.
167
- </div>
168
  </div>
169
 
170
  <div class="flex gap-2">
@@ -176,7 +180,7 @@
176
  </aside>
177
 
178
  <script>
179
- // ── LOCAL STATE ───────────────────────────────────────────────────────────────
180
  const topo = { upper: 3, lower: 3 };
181
 
182
  function syncTopoUI(nu, nl) {
@@ -185,10 +189,9 @@ function syncTopoUI(nu, nl) {
185
  document.getElementById(`b-${k}`).innerText = topo[k];
186
  document.getElementById(`dial-${k}`).innerText = topo[k];
187
  });
188
- const d = 32; // from server dim
189
- const n = nu + nl;
190
  document.getElementById('spring-info').innerText =
191
- `${2*n} spring matrices Γ— DΒ² = ${2*n}Γ—${d*d} = ${2*n*d*d} parameters`;
192
  }
193
 
194
  async function quickL(w, d) {
@@ -204,7 +207,6 @@ async function quickL(w, d) {
204
  function openDrawer() { document.getElementById('drawer').classList.remove('drawer-closed'); }
205
  function closeDrawer() { document.getElementById('drawer').classList.add('drawer-closed'); }
206
 
207
- // ── ACTIONS ───────────────────────────────────────────────────────────────────
208
  async function doTrainOffline() {
209
  const ep = parseInt(document.getElementById('ep-n').value);
210
  await fetch('/train_offline', {
@@ -237,22 +239,17 @@ function tab(name) {
237
  }
238
 
239
  // ── VISUALIZATION ─────────────────────────────────────────────────────────────
240
- // Vertical bilateral hourglass:
241
- // A (top bus) β†’ U1..Un (upper bulge) β†’ C (center waist)
242
- // B (bot bus) β†’ L1..Ln (lower bulge) β†’ C
243
  const Y = { A:4.3, U:2.0, C:0.0, L:-2.0, B:-4.3 };
244
- const SPREAD = { A:0, U:3.6, C:0, L:3.6, B:0 };
245
  const NCOL = id =>
246
  id==='A' ? '#fb923c' : id==='B' ? '#c084fc' : id==='C' ? '#38bdf8' :
247
  id.startsWith('U') ? '#4ade80' : '#67e8f9';
248
 
249
  function nodePos(layers) {
250
- const pos = {};
251
- const ymap = [Y.A, Y.U, Y.C, Y.L, Y.B];
252
- const smap = [SPREAD.A, SPREAD.U, SPREAD.C, SPREAD.L, SPREAD.B];
253
  layers.forEach((layer, li) => {
254
- const n = layer.length;
255
- const s = smap[li] || 0;
256
  layer.forEach((id, i) => {
257
  pos[id] = [n===1 ? 0 : s*(2*i/(n-1)-1), ymap[li]];
258
  });
@@ -260,71 +257,64 @@ function nodePos(layers) {
260
  return pos;
261
  }
262
 
263
- function busShapes(pos) {
264
- const sh = [];
265
- const rect = (xc,yc,hw,hh,fill,stroke) => sh.push({
266
  type:'rect', xref:'x', yref:'y',
267
  x0:xc-hw, x1:xc+hw, y0:yc-hh, y1:yc+hh,
268
  fillcolor:fill, line:{color:stroke, width:2}
269
  });
270
- rect(0, Y.A, 2.0, 0.32, 'rgba(251,146,60,0.09)', 'rgba(251,146,60,0.60)');
271
- rect(0, Y.C, 1.6, 0.30, 'rgba(56,189,248,0.09)', 'rgba(56,189,248,0.65)');
272
- rect(0, Y.B, 2.0, 0.32, 'rgba(192,132,252,0.09)','rgba(192,132,252,0.60)');
273
- return sh;
 
274
  }
275
 
276
  function springColor(frob, mean) {
277
- const t = Math.min(frob / 4, 1);
278
- const r = mean >= 0
279
- ? [Math.round(160+t*90), Math.round(90+t*80), 30]
280
  : [30, Math.round(80+t*100), Math.round(120+t*135)];
281
- return [`rgb(${r[0]},${r[1]},${r[2]})`, 0.8 + t*3.2];
282
  }
283
 
284
  function buildTraces(nodes, springs, layers) {
285
  const pos = nodePos(layers);
286
  const traces = [];
287
 
288
- // Edge traces
289
  for (const [key, s] of Object.entries(springs)) {
290
  const [u, v] = key.split('β†’');
291
  if (!pos[u] || !pos[v]) continue;
292
  const [col, wd] = springColor(s.frob, s.mean);
293
  traces.push({
294
  type:'scatter', mode:'lines',
295
- x:[pos[u][0], pos[v][0]], y:[pos[u][1], pos[v][1]],
296
  line:{color:col, width:wd}, hoverinfo:'none', showlegend:false
297
  });
298
  }
299
 
300
- // Node trace
301
  const allN = layers.flat();
302
  traces.push({
303
  type:'scatter', mode:'markers+text',
304
  x: allN.map(id => pos[id][0]),
305
  y: allN.map(id => pos[id][1]),
306
- text: allN.map(id => {
307
- const n = nodes[id];
308
- return `${id}\nβ€–${n?.norm?.toFixed(2) ?? '?'}β€–`;
309
- }),
310
  textposition: allN.map(id => id==='B' ? 'bottom center' : 'top center'),
311
  textfont:{ size:9, color: allN.map(id => NCOL(id)) },
312
  marker:{
313
  size: allN.map(id => {
314
- const vn = nodes[id]?.vel_norm ?? 0;
315
  const base = ['A','B','C'].includes(id) ? 20 : 13;
316
- return base + Math.min(vn*20, 8);
317
  }),
318
  color: allN.map(id => NCOL(id)),
319
  opacity: allN.map(id => 0.75 + Math.min((nodes[id]?.vel_norm??0)*1.5, 0.25)),
320
  line:{
321
- width:2.5,
322
  color: allN.map(id => nodes[id]?.anchored ? '#ef4444' : '#22c55e')
323
  }
324
  },
325
  hoverinfo:'none', showlegend:false
326
  });
327
-
328
  return traces;
329
  }
330
 
@@ -332,10 +322,10 @@ function meshLayout(layers) {
332
  return {
333
  margin:{l:6,r:6,t:6,b:6},
334
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
335
- xaxis:{visible:false, range:[-5.5, 5.5]},
336
- yaxis:{visible:false, range:[-5.5, 5.5]},
337
  showlegend:false,
338
- shapes: busShapes(nodePos(layers)),
339
  };
340
  }
341
 
@@ -343,22 +333,11 @@ const ERR_LAYOUT = {
343
  margin:{l:28,r:5,t:3,b:10},
344
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
345
  xaxis:{visible:false},
346
- yaxis:{
347
- color:'#334155', gridcolor:'#0f172a',
348
- zeroline:true, zerolinecolor:'#22c55e', zerolinewidth:1
349
- },
350
  showlegend:false,
351
  };
352
 
353
- // Problem type color mapping
354
- const TYPE_COL = {
355
- box_proj: '#4ade80',
356
- halfspace: '#60a5fa',
357
- sphere: '#e879f9',
358
- simplex: '#fb923c',
359
- elastic: '#f87171',
360
- };
361
-
362
  let meshPlotted = false, errPlotted = false, lastLayerKey = '';
363
 
364
  // ── POLL ──────────────────────────────────────────────────────────────────────
@@ -367,126 +346,155 @@ setInterval(async () => {
367
  const r = await fetch('/state');
368
  const d = await r.json();
369
 
370
- // Sync topo
371
  syncTopoUI(d.n_upper, d.n_lower);
 
 
372
 
373
- // Header badges
374
- const modeColor = {
375
  training: 'bg-yellow-700 text-yellow-100',
376
  inference: 'bg-green-800 text-green-100',
377
  idle: 'bg-slate-800 text-slate-400',
378
  }[d.mode] || 'bg-slate-800 text-slate-400';
379
- document.getElementById('b-mode').className = `px-1.5 py-0.5 rounded text-[8px] font-bold ${modeColor}`;
380
- document.getElementById('b-mode').innerText = d.mode.toUpperCase();
381
- document.getElementById('b-type').innerText = d.current_type || 'β€”';
382
- document.getElementById('b-type').className =
383
- `ptype pt-${d.current_type || 'unknown'}`;
384
- document.getElementById('b-dim').innerText = `D:${d.dim}`;
385
  document.getElementById('run-dot').className =
386
  `w-2 h-2 rounded-full ${d.running ? 'bg-green-400' : 'bg-slate-700'}`;
387
  document.getElementById('q-lbl').innerText = `Q:${d.queue_size}`;
 
 
388
 
389
- // Tension meter
390
  const e = d.error;
391
- const col = e < 0.05 ? 'text-green-400' : e < 0.5 ? 'text-yellow-400' : 'text-red-400';
392
  document.getElementById('err-big').className = `text-xl font-bold ${col} leading-none`;
393
  document.getElementById('err-big').innerText = e.toFixed(4);
394
  document.getElementById('pred-val').innerText = `β€–Cβ€–:${d.pred_norm.toFixed(2)}`;
395
- document.getElementById('iter-lbl').innerText = `IT:${d.iter}`;
396
- document.getElementById('step-lbl').innerText = `S:${d.step_count}`;
397
 
398
- // ── Nodes pane ───────────────────���─────────────────────────────────────
399
  let nh = '';
400
- (d.layers || []).flat().forEach(id => {
401
  const n = d.nodes[id]; if (!n) return;
402
  const icon = n.anchored ? '<span class="text-red-400">⊠</span>' : '<span class="text-green-500">β—Ž</span>';
403
- const vcol = n.anchored ? 'text-orange-300' : 'text-sky-300';
404
- const head = (n.x_head || []).map(v => v.toFixed(2)).join(' ');
405
  nh += `<div class="py-0.5 border-b border-slate-900/60">
406
  <div class="flex justify-between">
407
- ${icon}<span class="${vcol} ml-1 font-bold">${id}</span>
 
 
408
  <span class="text-white">β€–xβ€–=${n.norm}</span>
409
  <span class="text-slate-600 text-[8px]">v=${n.vel_norm.toFixed(3)}</span>
410
  </div>
411
- <div class="text-[8px] text-slate-700 mt-0.5 font-mono">[${head}…]</div>
412
  </div>`;
413
  });
414
  document.getElementById('pane-nodes').innerHTML = nh;
415
 
416
- // ── Springs pane ───────────────────────────────────────────────────────
417
  let sh = '';
418
- for (const [key, s] of Object.entries(d.springs || {})) {
419
  const fc = s.frob > 3 ? 'text-yellow-200' : s.mean < 0 ? 'text-blue-300' : 'text-purple-300';
420
  const fi = s.fish > 0.01 ? 'text-orange-400' : 'text-slate-700';
421
  sh += `<div class="flex justify-between py-0.5 border-b border-slate-900/60 items-center">
422
  <span class="text-slate-500 text-[9px]">${key}</span>
423
- <span class="${fc} font-bold text-[10px]">β€–Kβ€–=${s.frob}</span>
424
- <span class="text-slate-600 text-[8px]">ΞΌ=${s.mean}</span>
425
  <span class="${fi} text-[8px]">F=${s.fish}</span>
426
  </div>`;
427
  }
428
  document.getElementById('pane-springs').innerHTML = sh;
429
 
430
- // ── Accuracy pane ──────────────────────────────────────────────────────
431
  const acc = d.type_acc || {};
432
  const done = d.n_test_done || 0;
433
- const total= d.test_size || 0;
 
434
  let ah = `<div class="text-[9px] text-slate-500 mb-2">
435
- Test progress: <span class="text-white font-bold">${done}</span> / ${total}
436
  </div>`;
437
 
438
  if (Object.keys(acc).length > 0) {
439
- ah += `<table class="w-full text-[9px] border-collapse">
440
- <tr class="text-slate-600 border-b border-slate-800">
441
- <th class="text-left py-0.5">Type</th>
442
- <th class="text-right py-0.5">N</th>
443
- <th class="text-right py-0.5">Acc%</th>
444
- <th class="text-right py-0.5">Avgβ€–eβ€–</th>
445
- </tr>`;
446
- for (const [t, v] of Object.entries(acc)) {
447
  const aCol = v.acc >= 80 ? 'text-green-400' : v.acc >= 50 ? 'text-yellow-400' : 'text-red-400';
448
- ah += `<tr class="border-b border-slate-900/60">
449
- <td class="py-0.5"><span class="ptype pt-${t}">${t}</span></td>
 
 
 
450
  <td class="text-right text-slate-400">${v.n}</td>
451
  <td class="text-right ${aCol} font-bold">${v.acc}%</td>
452
  <td class="text-right text-slate-400">${v.avg_err}</td>
 
453
  </tr>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  }
455
- ah += '</table>';
456
  } else {
457
- ah += '<div class="text-slate-700 text-[9px]">Run inference to see per-type accuracy.</div>';
458
  }
459
  document.getElementById('pane-accuracy').innerHTML = ah;
460
 
461
- // ── Logs pane ──────────────────────────────────────────────────────────
462
  document.getElementById('pane-logs').innerHTML =
463
  d.logs.map(l => `<div class="py-0.5 border-b border-slate-900/40 font-mono">${l}</div>`).join('');
464
 
465
- // ── Mesh plot ──────────────────────────────────────────────────────────
466
  const layerKey = JSON.stringify(d.layers);
467
  const traces = buildTraces(d.nodes, d.springs, d.layers);
468
  const layout = meshLayout(d.layers);
469
  if (!meshPlotted || layerKey !== lastLayerKey) {
470
  Plotly.newPlot('mesh-plot', traces, layout, {displayModeBar:false, responsive:true});
471
- meshPlotted = true;
472
- lastLayerKey = layerKey;
473
  } else {
474
  Plotly.react('mesh-plot', traces, layout);
475
  }
476
 
477
- // ── Error history chart ────────────────────────────────────────────────
478
  const hist = d.history || [];
479
- const eTrace = {
480
  type:'scatter', mode:'lines',
481
- x: hist.map((_,i) => i), y: hist,
482
- line:{color:'#f97316', width:1.5},
483
  fill:'tozeroy', fillcolor:'rgba(249,115,22,0.07)'
484
  };
485
  if (!errPlotted) {
486
- Plotly.newPlot('err-plot',[eTrace], ERR_LAYOUT, {displayModeBar:false, responsive:true});
487
  errPlotted = true;
488
  } else {
489
- Plotly.react('err-plot', [eTrace], ERR_LAYOUT);
490
  }
491
 
492
  } catch(e) { /* silent */ }
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Elastic Mesh Lab v3</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
9
  <style>
 
17
  cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
18
  }
19
  .lbtn:hover { background:#334155; color:#fff; }
20
+ .ptype { display:inline-block; padding:1px 5px; border-radius:3px;
21
+ font-size:9px; font-weight:bold; }
22
+ .pt-box_proj { background:#1e3a2f; color:#4ade80; }
23
+ .pt-halfspace { background:#1e2d4a; color:#60a5fa; }
24
+ .pt-sphere { background:#3a1e3a; color:#e879f9; }
25
+ .pt-simplex { background:#3a2a1e; color:#fb923c; }
26
+ .pt-elastic { background:#3a1e1e; color:#f87171; }
27
+ .pt-unknown { background:#1e1e1e; color:#64748b; }
28
+ .ood-badge { display:inline-block; padding:0 4px; border-radius:2px;
29
+ font-size:7px; font-weight:bold; background:#7c2d12;
30
+ color:#fca5a5; margin-left:3px; }
31
+ .seen-badge { display:inline-block; padding:0 4px; border-radius:2px;
32
+ font-size:7px; font-weight:bold; background:#14532d;
33
+ color:#86efac; margin-left:3px; }
34
  </style>
35
  </head>
36
  <body class="flex flex-col h-screen overflow-hidden">
37
 
38
  <!-- ── HEADER ──────────────────────────────────────────────────────────────── -->
39
  <header class="glass flex-shrink-0 px-2 py-1.5 flex justify-between items-center gap-1">
 
 
40
  <div class="flex flex-wrap gap-1 items-center text-[8px] font-bold">
41
+ <span id="b-mode" class="px-1.5 py-0.5 rounded bg-slate-800 text-slate-400 border border-slate-700">IDLE</span>
42
+ <span id="b-type" class="ptype pt-unknown">β€”</span>
43
+ <span id="b-dim" class="px-1.5 py-0.5 rounded bg-slate-900 text-slate-600 border border-slate-800">D:64</span>
44
+ <span id="b-thr" class="px-1.5 py-0.5 rounded bg-slate-900 text-slate-600 border border-slate-800">Ο„:0.02</span>
 
45
  <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">
46
  <span class="text-orange-400 mr-0.5">U</span>
47
  <button class="lbtn" onclick="quickL('upper',-1)">βˆ’</button>
 
53
  <button class="lbtn" onclick="quickL('lower',+1)">+</button>
54
  </span>
55
  </div>
 
 
56
  <div class="flex items-center gap-1 flex-shrink-0">
57
+ <span id="q-lbl" class="text-[8px] text-slate-600">Q:0</span>
58
  <div id="run-dot" class="w-2 h-2 rounded-full bg-slate-700"></div>
59
+ <button onclick="doTrainOffline()" class="text-[9px] bg-purple-800 hover:bg-purple-700 px-2 py-1 rounded font-bold">⚑TRAIN</button>
60
+ <button onclick="doTrainVisual()" class="text-[9px] bg-blue-800 hover:bg-blue-700 px-2 py-1 rounded font-bold">πŸ‘VIZ</button>
61
+ <button onclick="doInfer()" class="text-[9px] bg-green-800 hover:bg-green-700 px-2 py-1 rounded font-bold">β–ΆINFER</button>
62
  <button onclick="doHalt()" class="text-[9px] bg-red-900 hover:bg-red-800 px-2 py-1 rounded font-bold">β– </button>
63
  <button onclick="openDrawer()" class="text-[9px] bg-slate-700 hover:bg-slate-600 px-2 py-1 rounded font-bold">βš™</button>
64
  </div>
 
79
  <button onclick="tab('logs')" id="tab-logs" class="flex-1 py-1 text-slate-500">LOGS</button>
80
  </div>
81
  <div class="flex h-full overflow-hidden">
 
82
  <div class="w-20 flex-shrink-0 border-r border-slate-800 flex flex-col items-center justify-center p-1 gap-0.5">
83
  <div class="text-[7px] text-slate-600 uppercase tracking-widest">β€–errorβ€–</div>
84
  <div id="err-big" class="text-xl font-bold text-red-400 leading-none">0.00</div>
 
86
  <div id="iter-lbl" class="text-[8px] text-slate-700">IT:0</div>
87
  <div id="step-lbl" class="text-[7px] text-slate-800">S:0</div>
88
  </div>
 
89
  <div class="flex-grow overflow-y-auto p-1.5 text-[10px]">
90
  <div id="pane-nodes"></div>
91
  <div id="pane-springs" class="hidden"></div>
 
98
  <!-- ── DRAWER ───────────────────────────────────────────────────────────────── -->
99
  <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-[90vh] overflow-y-auto">
100
  <div class="flex justify-between items-center">
101
+ <span class="text-orange-400 font-bold">ELASTIC MESH LAB v3</span>
102
  <button onclick="closeDrawer()" class="text-slate-400 text-2xl leading-none">βœ•</button>
103
  </div>
104
 
105
+ <!-- Architecture info -->
106
+ <div class="bg-slate-900 rounded p-3 border border-blue-900/50 text-[9px] space-y-1">
107
+ <div class="text-blue-400 font-bold mb-1">v3 CHANGES</div>
108
+ <div>β‘  <span class="text-white">Layer norm</span> after every spring transform β€” no weight explosion</div>
109
+ <div>β‘‘ <span class="text-white">Threshold = 0.02</span> β€” genuine precision (was 0.08)</div>
110
+ <div>β‘’ <span class="text-white">DIM = 64</span> β€” double the embedding space</div>
111
+ <div>β‘£ <span class="text-white">OOD test</span> β€” sphere+simplex unseen during training</div>
112
+ </div>
113
+
114
+ <!-- OOD legend -->
115
+ <div class="bg-slate-900 rounded p-3 border border-slate-700 text-[9px]">
116
+ <div class="text-slate-400 font-bold mb-2">TEST SPLIT</div>
117
+ <div class="flex gap-3 flex-wrap">
118
+ <div><span class="seen-badge">SEEN</span> <span class="text-slate-400">box_proj Β· halfspace Β· elastic</span></div>
119
+ <div><span class="ood-badge">OOD</span> <span class="text-slate-400">sphere Β· simplex</span></div>
120
+ </div>
121
+ <div class="text-slate-600 mt-1">
122
+ OOD types never appear in training data. High OOD accuracy β†’ mesh learned geometry, not patterns.
123
  </div>
124
  </div>
125
 
126
+ <!-- Topology -->
127
  <div class="bg-slate-900 rounded p-3 border border-green-900/50">
128
+ <div class="text-green-400 text-[9px] font-bold mb-2">TOPOLOGY Aβ†’[U]β†’C←[L]←B</div>
129
  <div class="grid grid-cols-2 gap-2">
130
  <div class="bg-slate-800 rounded p-2 text-center border border-orange-900/30">
131
  <div class="text-orange-400 text-[8px] mb-1">UPPER (A-side)</div>
 
147
  <div id="spring-info" class="text-[8px] text-slate-600 mt-2 text-center"></div>
148
  </div>
149
 
150
+ <!-- Training -->
151
  <div class="bg-slate-900 rounded p-3 border border-purple-900/50">
152
+ <div class="text-purple-400 text-[9px] font-bold mb-2">OFFLINE TRAINING</div>
153
+ <div class="flex gap-2 items-center">
154
  <label class="text-[9px] text-slate-500 w-16">Epochs</label>
155
  <input id="ep-n" type="number" value="5" min="1" max="50"
156
  class="w-16 bg-black border border-slate-700 p-1 text-white text-sm text-center rounded">
157
  <button onclick="doTrainOffline()"
158
  class="flex-1 bg-purple-800 hover:bg-purple-700 py-2 text-xs font-bold rounded">⚑ START</button>
159
  </div>
 
 
 
160
  </div>
161
 
162
+ <!-- Inference -->
163
  <div class="bg-slate-900 rounded p-3 border border-green-900/50">
164
  <div class="text-green-400 text-[9px] font-bold mb-2">INFERENCE TEST</div>
165
+ <div class="flex gap-2 items-center">
166
  <label class="text-[9px] text-slate-500 w-16">N samples</label>
167
+ <input id="inf-n" type="number" value="500" min="10" max="2000"
168
  class="w-16 bg-black border border-slate-700 p-1 text-white text-sm text-center rounded">
169
  <button onclick="doInfer()"
170
  class="flex-1 bg-green-800 hover:bg-green-700 py-2 text-xs font-bold rounded">β–Ά RUN</button>
171
  </div>
 
 
 
172
  </div>
173
 
174
  <div class="flex gap-2">
 
180
  </aside>
181
 
182
  <script>
183
+ // ── STATE ──────────────────────────────────────────────────────────────────────
184
  const topo = { upper: 3, lower: 3 };
185
 
186
  function syncTopoUI(nu, nl) {
 
189
  document.getElementById(`b-${k}`).innerText = topo[k];
190
  document.getElementById(`dial-${k}`).innerText = topo[k];
191
  });
192
+ const d = 64, n = nu + nl;
 
193
  document.getElementById('spring-info').innerText =
194
+ `${2*n} K matrices Γ— ${d}Β² = ${2*n*d*d} parameters | layer-normed`;
195
  }
196
 
197
  async function quickL(w, d) {
 
207
  function openDrawer() { document.getElementById('drawer').classList.remove('drawer-closed'); }
208
  function closeDrawer() { document.getElementById('drawer').classList.add('drawer-closed'); }
209
 
 
210
  async function doTrainOffline() {
211
  const ep = parseInt(document.getElementById('ep-n').value);
212
  await fetch('/train_offline', {
 
239
  }
240
 
241
  // ── VISUALIZATION ─────────────────────────────────────────────────────────────
 
 
 
242
  const Y = { A:4.3, U:2.0, C:0.0, L:-2.0, B:-4.3 };
 
243
  const NCOL = id =>
244
  id==='A' ? '#fb923c' : id==='B' ? '#c084fc' : id==='C' ? '#38bdf8' :
245
  id.startsWith('U') ? '#4ade80' : '#67e8f9';
246
 
247
  function nodePos(layers) {
248
+ const pos = {};
249
+ const ymap = [Y.A, Y.U, Y.C, Y.L, Y.B];
250
+ const smap = [0, 3.6, 0, 3.6, 0];
251
  layers.forEach((layer, li) => {
252
+ const n = layer.length, s = smap[li] || 0;
 
253
  layer.forEach((id, i) => {
254
  pos[id] = [n===1 ? 0 : s*(2*i/(n-1)-1), ymap[li]];
255
  });
 
257
  return pos;
258
  }
259
 
260
+ function busShapes() {
261
+ const rect = (xc,yc,hw,hh,fill,stroke) => ({
 
262
  type:'rect', xref:'x', yref:'y',
263
  x0:xc-hw, x1:xc+hw, y0:yc-hh, y1:yc+hh,
264
  fillcolor:fill, line:{color:stroke, width:2}
265
  });
266
+ return [
267
+ rect(0, Y.A, 2.0, 0.32, 'rgba(251,146,60,0.09)', 'rgba(251,146,60,0.60)'),
268
+ rect(0, Y.C, 1.6, 0.30, 'rgba(56,189,248,0.09)', 'rgba(56,189,248,0.65)'),
269
+ rect(0, Y.B, 2.0, 0.32, 'rgba(192,132,252,0.09)','rgba(192,132,252,0.60)'),
270
+ ];
271
  }
272
 
273
  function springColor(frob, mean) {
274
+ const t = Math.min(frob / 3, 1);
275
+ const rgb = mean >= 0
276
+ ? [Math.round(140+t*110), Math.round(80+t*80), 30]
277
  : [30, Math.round(80+t*100), Math.round(120+t*135)];
278
+ return [`rgb(${rgb[0]},${rgb[1]},${rgb[2]})`, 0.7 + t*3.0];
279
  }
280
 
281
  function buildTraces(nodes, springs, layers) {
282
  const pos = nodePos(layers);
283
  const traces = [];
284
 
 
285
  for (const [key, s] of Object.entries(springs)) {
286
  const [u, v] = key.split('β†’');
287
  if (!pos[u] || !pos[v]) continue;
288
  const [col, wd] = springColor(s.frob, s.mean);
289
  traces.push({
290
  type:'scatter', mode:'lines',
291
+ x:[pos[u][0],pos[v][0]], y:[pos[u][1],pos[v][1]],
292
  line:{color:col, width:wd}, hoverinfo:'none', showlegend:false
293
  });
294
  }
295
 
 
296
  const allN = layers.flat();
297
  traces.push({
298
  type:'scatter', mode:'markers+text',
299
  x: allN.map(id => pos[id][0]),
300
  y: allN.map(id => pos[id][1]),
301
+ text: allN.map(id => `${id}\nβ€–${nodes[id]?.norm?.toFixed(2)??'?'}β€–`),
 
 
 
302
  textposition: allN.map(id => id==='B' ? 'bottom center' : 'top center'),
303
  textfont:{ size:9, color: allN.map(id => NCOL(id)) },
304
  marker:{
305
  size: allN.map(id => {
 
306
  const base = ['A','B','C'].includes(id) ? 20 : 13;
307
+ return base + Math.min((nodes[id]?.vel_norm??0)*20, 8);
308
  }),
309
  color: allN.map(id => NCOL(id)),
310
  opacity: allN.map(id => 0.75 + Math.min((nodes[id]?.vel_norm??0)*1.5, 0.25)),
311
  line:{
312
+ width: 2.5,
313
  color: allN.map(id => nodes[id]?.anchored ? '#ef4444' : '#22c55e')
314
  }
315
  },
316
  hoverinfo:'none', showlegend:false
317
  });
 
318
  return traces;
319
  }
320
 
 
322
  return {
323
  margin:{l:6,r:6,t:6,b:6},
324
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
325
+ xaxis:{visible:false, range:[-5.5,5.5]},
326
+ yaxis:{visible:false, range:[-5.5,5.5]},
327
  showlegend:false,
328
+ shapes: busShapes(),
329
  };
330
  }
331
 
 
333
  margin:{l:28,r:5,t:3,b:10},
334
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
335
  xaxis:{visible:false},
336
+ yaxis:{color:'#334155', gridcolor:'#0f172a',
337
+ zeroline:true, zerolinecolor:'#22c55e', zerolinewidth:1},
 
 
338
  showlegend:false,
339
  };
340
 
 
 
 
 
 
 
 
 
 
341
  let meshPlotted = false, errPlotted = false, lastLayerKey = '';
342
 
343
  // ── POLL ──────────────────────────────────────────────────────────────────────
 
346
  const r = await fetch('/state');
347
  const d = await r.json();
348
 
 
349
  syncTopoUI(d.n_upper, d.n_lower);
350
+ document.getElementById('b-dim').innerText = `D:${d.dim}`;
351
+ document.getElementById('b-thr').innerText = `Ο„:${d.conv_thresh}`;
352
 
353
+ const modeCol = {
 
354
  training: 'bg-yellow-700 text-yellow-100',
355
  inference: 'bg-green-800 text-green-100',
356
  idle: 'bg-slate-800 text-slate-400',
357
  }[d.mode] || 'bg-slate-800 text-slate-400';
358
+ document.getElementById('b-mode').className = `px-1.5 py-0.5 rounded text-[8px] font-bold ${modeCol}`;
359
+ document.getElementById('b-mode').innerText = d.mode.toUpperCase();
360
+ document.getElementById('b-type').className = `ptype pt-${d.current_type||'unknown'}`;
361
+ document.getElementById('b-type').innerText = d.current_type || 'β€”';
 
 
362
  document.getElementById('run-dot').className =
363
  `w-2 h-2 rounded-full ${d.running ? 'bg-green-400' : 'bg-slate-700'}`;
364
  document.getElementById('q-lbl').innerText = `Q:${d.queue_size}`;
365
+ document.getElementById('iter-lbl').innerText = `IT:${d.iter}`;
366
+ document.getElementById('step-lbl').innerText = `S:${d.step_count}`;
367
 
 
368
  const e = d.error;
369
+ const col = e < 0.02 ? 'text-green-400' : e < 0.2 ? 'text-yellow-400' : 'text-red-400';
370
  document.getElementById('err-big').className = `text-xl font-bold ${col} leading-none`;
371
  document.getElementById('err-big').innerText = e.toFixed(4);
372
  document.getElementById('pred-val').innerText = `β€–Cβ€–:${d.pred_norm.toFixed(2)}`;
 
 
373
 
374
+ // Nodes pane
375
  let nh = '';
376
+ (d.layers||[]).flat().forEach(id => {
377
  const n = d.nodes[id]; if (!n) return;
378
  const icon = n.anchored ? '<span class="text-red-400">⊠</span>' : '<span class="text-green-500">β—Ž</span>';
379
+ const head = (n.x_head||[]).map(v=>v.toFixed(2)).join(' ');
 
380
  nh += `<div class="py-0.5 border-b border-slate-900/60">
381
  <div class="flex justify-between">
382
+ ${icon}<span class="ml-1 font-bold" style="color:${
383
+ id==='A'?'#fb923c':id==='B'?'#c084fc':id==='C'?'#38bdf8':
384
+ id.startsWith('U')?'#4ade80':'#67e8f9'}">${id}</span>
385
  <span class="text-white">β€–xβ€–=${n.norm}</span>
386
  <span class="text-slate-600 text-[8px]">v=${n.vel_norm.toFixed(3)}</span>
387
  </div>
388
+ <div class="text-[8px] text-slate-700 font-mono">[${head}…]</div>
389
  </div>`;
390
  });
391
  document.getElementById('pane-nodes').innerHTML = nh;
392
 
393
+ // Springs pane β€” layer-normed so frob should be ~1-3 now
394
  let sh = '';
395
+ for (const [key, s] of Object.entries(d.springs||{})) {
396
  const fc = s.frob > 3 ? 'text-yellow-200' : s.mean < 0 ? 'text-blue-300' : 'text-purple-300';
397
  const fi = s.fish > 0.01 ? 'text-orange-400' : 'text-slate-700';
398
  sh += `<div class="flex justify-between py-0.5 border-b border-slate-900/60 items-center">
399
  <span class="text-slate-500 text-[9px]">${key}</span>
400
+ <span class="${fc} font-bold text-[9px]">β€–Kβ€–=${s.frob}</span>
401
+ <span class="text-slate-600 text-[8px]">Οƒ=${s.std}</span>
402
  <span class="${fi} text-[8px]">F=${s.fish}</span>
403
  </div>`;
404
  }
405
  document.getElementById('pane-springs').innerHTML = sh;
406
 
407
+ // Accuracy pane β€” SEEN vs OOD clearly separated
408
  const acc = d.type_acc || {};
409
  const done = d.n_test_done || 0;
410
+ const total= d.test_size || 0;
411
+
412
  let ah = `<div class="text-[9px] text-slate-500 mb-2">
413
+ Progress: <span class="text-white font-bold">${done}</span> / ${total}
414
  </div>`;
415
 
416
  if (Object.keys(acc).length > 0) {
417
+ // Split into SEEN and OOD for display
418
+ const oodTypes = Object.entries(acc).filter(([,v]) => v.ood);
419
+ const seenTypes = Object.entries(acc).filter(([,v]) => !v.ood);
420
+
421
+ const mkRow = ([t, v]) => {
 
 
 
422
  const aCol = v.acc >= 80 ? 'text-green-400' : v.acc >= 50 ? 'text-yellow-400' : 'text-red-400';
423
+ const badge = v.ood
424
+ ? '<span class="ood-badge">OOD</span>'
425
+ : '<span class="seen-badge">SEEN</span>';
426
+ return `<tr class="border-b border-slate-900/60">
427
+ <td class="py-0.5"><span class="ptype pt-${t}">${t}</span>${badge}</td>
428
  <td class="text-right text-slate-400">${v.n}</td>
429
  <td class="text-right ${aCol} font-bold">${v.acc}%</td>
430
  <td class="text-right text-slate-400">${v.avg_err}</td>
431
+ <td class="text-right text-slate-600">${v.avg_steps}s</td>
432
  </tr>`;
433
+ };
434
+
435
+ ah += `<table class="w-full text-[9px] border-collapse">
436
+ <tr class="text-slate-600 border-b border-slate-800">
437
+ <th class="text-left py-0.5">Type</th>
438
+ <th class="text-right">N</th>
439
+ <th class="text-right">Acc%</th>
440
+ <th class="text-right">Avgβ€–eβ€–</th>
441
+ <th class="text-right">Steps</th>
442
+ </tr>
443
+ ${seenTypes.map(mkRow).join('')}
444
+ ${oodTypes.length ? '<tr><td colspan="5" class="pt-1 pb-0.5 text-[8px] text-slate-600">── OOD (not in training) ──</td></tr>' : ''}
445
+ ${oodTypes.map(mkRow).join('')}
446
+ </table>`;
447
+
448
+ // Summary verdict
449
+ if (done > 20) {
450
+ const oodAcc = oodTypes.reduce((s,[,v])=>s+v.acc*v.n,0) / Math.max(oodTypes.reduce((s,[,v])=>s+v.n,0),1);
451
+ const seenAcc = seenTypes.reduce((s,[,v])=>s+v.acc*v.n,0) / Math.max(seenTypes.reduce((s,[,v])=>s+v.n,0),1);
452
+ const verdict = oodAcc >= 80
453
+ ? 'βœ“ Geometric generalisation confirmed'
454
+ : oodAcc >= 50
455
+ ? '~ Partial generalisation'
456
+ : 'βœ— Pattern memorisation only';
457
+ const vc = oodAcc >= 80 ? 'text-green-400' : oodAcc >= 50 ? 'text-yellow-400' : 'text-red-400';
458
+ ah += `<div class="mt-2 p-2 bg-slate-900 rounded text-[9px]">
459
+ <div class="${vc} font-bold">${verdict}</div>
460
+ <div class="text-slate-500 mt-0.5">
461
+ Seen avg: ${seenAcc.toFixed(1)}% | OOD avg: ${oodAcc.toFixed(1)}%
462
+ </div>
463
+ </div>`;
464
  }
 
465
  } else {
466
+ ah += '<div class="text-slate-700 text-[9px]">Run inference to see accuracy split.</div>';
467
  }
468
  document.getElementById('pane-accuracy').innerHTML = ah;
469
 
470
+ // Logs pane
471
  document.getElementById('pane-logs').innerHTML =
472
  d.logs.map(l => `<div class="py-0.5 border-b border-slate-900/40 font-mono">${l}</div>`).join('');
473
 
474
+ // Mesh plot
475
  const layerKey = JSON.stringify(d.layers);
476
  const traces = buildTraces(d.nodes, d.springs, d.layers);
477
  const layout = meshLayout(d.layers);
478
  if (!meshPlotted || layerKey !== lastLayerKey) {
479
  Plotly.newPlot('mesh-plot', traces, layout, {displayModeBar:false, responsive:true});
480
+ meshPlotted = true; lastLayerKey = layerKey;
 
481
  } else {
482
  Plotly.react('mesh-plot', traces, layout);
483
  }
484
 
485
+ // Error chart
486
  const hist = d.history || [];
487
+ const et = {
488
  type:'scatter', mode:'lines',
489
+ x:hist.map((_,i)=>i), y:hist,
490
+ line:{color:'#f97316',width:1.5},
491
  fill:'tozeroy', fillcolor:'rgba(249,115,22,0.07)'
492
  };
493
  if (!errPlotted) {
494
+ Plotly.newPlot('err-plot',[et],ERR_LAYOUT,{displayModeBar:false,responsive:true});
495
  errPlotted = true;
496
  } else {
497
+ Plotly.react('err-plot',[et],ERR_LAYOUT);
498
  }
499
 
500
  } catch(e) { /* silent */ }