Spaces:
Runtime error
Runtime error
Update index.html
Browse files- index.html +120 -107
index.html
CHANGED
|
@@ -57,8 +57,8 @@
|
|
| 57 |
<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>
|
| 58 |
</div>
|
| 59 |
<div class="flex items-center gap-2 ml-1">
|
| 60 |
-
<!--
|
| 61 |
-
<span id="
|
| 62 |
<span id="q-lbl" class="text-[8px] text-slate-600">Q:0</span>
|
| 63 |
<div id="run-dot" class="w-2 h-2 rounded-full bg-slate-700"></div>
|
| 64 |
<button onclick="openDrawer()" class="text-[10px] bg-blue-700 hover:bg-blue-600 px-2 py-1 rounded font-bold">β DIALS</button>
|
|
@@ -135,15 +135,16 @@
|
|
| 135 |
</div>
|
| 136 |
|
| 137 |
<!-- CROSS CONNECT -->
|
| 138 |
-
<div class="col-span-2 bg-slate-900 rounded p-3 border border-
|
| 139 |
-
<div class="text-
|
| 140 |
CROSS-CONNECT
|
| 141 |
-
<span class="text-slate-600 font-normal ml-1">
|
| 142 |
</div>
|
| 143 |
<div class="text-[9px] text-slate-500 mb-2">
|
| 144 |
-
OFF β
|
| 145 |
-
ON β
|
| 146 |
-
|
|
|
|
| 147 |
</div>
|
| 148 |
<button id="drawer-cross-btn" onclick="toggleCross()"
|
| 149 |
class="w-full py-2 rounded text-xs font-bold border border-slate-700 bg-slate-800 text-slate-400">
|
|
@@ -256,62 +257,59 @@
|
|
| 256 |
const cfg = { mode: 'training', architecture: 'additive' };
|
| 257 |
const topo = { inputs: 1, upper: 3, lower: 3 };
|
| 258 |
let crossConnect = false;
|
| 259 |
-
let
|
|
|
|
| 260 |
|
| 261 |
// ββ CROSS CONNECT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 262 |
async function toggleCross() {
|
| 263 |
const res = await fetch('/toggle_cross', { method: 'POST' });
|
| 264 |
const data = await res.json();
|
| 265 |
crossConnect = data.cross_connect;
|
| 266 |
-
|
|
|
|
| 267 |
updateCrossUI();
|
| 268 |
meshPlotted = false;
|
| 269 |
}
|
| 270 |
|
| 271 |
function updateCrossUI() {
|
| 272 |
-
const btn1
|
| 273 |
-
const btn2
|
| 274 |
-
const info
|
| 275 |
-
const
|
| 276 |
-
const n
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
const
|
| 280 |
-
? (n-1) * ((u >= 2 ? 1 : 0) + (l >= 2 ? 1 : 0))
|
| 281 |
-
: 0);
|
| 282 |
|
| 283 |
if (crossConnect) {
|
| 284 |
-
btn1.className = 'px-1.5 py-0.5 rounded border font-bold text-[8px] transition-all bg-
|
| 285 |
btn1.innerText = 'CROSS:ON';
|
| 286 |
-
btn2.className = 'w-full py-2 rounded text-xs font-bold border border-
|
| 287 |
btn2.innerText = 'CROSS-CONNECT: ON (click to disable)';
|
| 288 |
-
info.innerText
|
| 289 |
-
? `${
|
| 290 |
-
: 'No
|
| 291 |
-
|
| 292 |
-
|
| 293 |
} else {
|
| 294 |
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';
|
| 295 |
btn1.innerText = 'CROSS:OFF';
|
| 296 |
btn2.className = 'w-full py-2 rounded text-xs font-bold border border-slate-700 bg-slate-800 text-slate-400';
|
| 297 |
btn2.innerText = 'CROSS-CONNECT: OFF (click to enable)';
|
| 298 |
-
info.innerText
|
| 299 |
-
|
| 300 |
}
|
| 301 |
}
|
| 302 |
|
| 303 |
// ββ TOPOLOGY ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 304 |
function updateSpringCount() {
|
| 305 |
const { inputs: n, upper: u, lower: l } = topo;
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
const
|
| 309 |
-
|
| 310 |
-
: 0;
|
| 311 |
-
const totalSprings = n * (2*u + 2*l); // unchanged by merge
|
| 312 |
-
const totalNodes = 3*n + n*u + n*l - ns;
|
| 313 |
document.getElementById('spring-count').innerText =
|
| 314 |
-
`${
|
| 315 |
document.getElementById('custom-dim-hint').innerText =
|
| 316 |
n === 1 ? '(single value)' : `(${n} values, comma-separated)`;
|
| 317 |
updateCrossUI();
|
|
@@ -467,22 +465,22 @@ function tab(name) {
|
|
| 467 |
|
| 468 |
// ββ VISUALIZATION βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 469 |
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
const pos = {};
|
| 472 |
const Y = [4.4, 2.1, 0.0, -2.1, -4.4];
|
| 473 |
const COL_W = n_inputs === 1 ? 0 : Math.min(9.0 / (n_inputs - 1), 4.2);
|
| 474 |
const halfSp = COL_W * (n_inputs - 1) / 2;
|
| 475 |
const bSprd = n_inputs === 1 ? 3.8 : Math.min(COL_W * 0.55, 1.8);
|
| 476 |
|
| 477 |
-
// Build a flat lookup: nid β layer index (to get Y)
|
| 478 |
-
const layerOf = {};
|
| 479 |
-
layers.forEach((layer, li) => layer.forEach(nid => { layerOf[nid] = li; }));
|
| 480 |
-
|
| 481 |
layers.forEach((layer, li) => {
|
| 482 |
const y = Y[li];
|
| 483 |
layer.forEach(nid => {
|
| 484 |
const kind = nid[0];
|
| 485 |
-
// Parse dim and j from node ID
|
| 486 |
let dim = 1, j = 1, total = 1;
|
| 487 |
if ('ABC'.includes(kind)) {
|
| 488 |
dim = parseInt(nid.slice(1));
|
|
@@ -492,7 +490,7 @@ function buildPos(layers, n_inputs, n_upper, n_lower) {
|
|
| 492 |
j = parseInt(parts[1]);
|
| 493 |
total = kind === 'U' ? n_upper : n_lower;
|
| 494 |
}
|
| 495 |
-
const cx = n_inputs === 1 ? 0 : -halfSp + (dim-1)*COL_W;
|
| 496 |
if ('ABC'.includes(kind)) {
|
| 497 |
pos[nid] = [cx, y];
|
| 498 |
} else {
|
|
@@ -501,14 +499,25 @@ function buildPos(layers, n_inputs, n_upper, n_lower) {
|
|
| 501 |
}
|
| 502 |
});
|
| 503 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
return pos;
|
| 505 |
}
|
| 506 |
|
| 507 |
function busShapes(pos, n_inputs) {
|
| 508 |
const sh = [];
|
| 509 |
const mg = n_inputs === 1 ? 1.4 : 0.8;
|
| 510 |
-
const xs = k => Object.entries(pos).filter(([id])=>id[0]===k).map(([,v])=>v[0]);
|
| 511 |
-
const rect = (mn,mx,yc,hh,fill,stroke) => sh.push({
|
| 512 |
type:'rect', xref:'x', yref:'y',
|
| 513 |
x0:mn-mg, x1:mx+mg, y0:yc-hh, y1:yc+hh,
|
| 514 |
fillcolor:fill, line:{color:stroke, width:2}
|
|
@@ -526,35 +535,21 @@ function springColor(k) {
|
|
| 526 |
return [`rgb(30,${Math.round(80+t*100)},${Math.round(140+t*115)})`, 1.0+t*3.5];
|
| 527 |
}
|
| 528 |
|
| 529 |
-
/
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
};
|
| 539 |
-
try {
|
| 540 |
-
const du = getDim(u), dv = getDim(v);
|
| 541 |
-
return !isNaN(du) && !isNaN(dv) && du !== dv;
|
| 542 |
-
} catch { return false; }
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
function buildTraces(nodes, springs, layers, n_inputs, n_upper, n_lower) {
|
| 546 |
-
const pos = buildPos(layers, n_inputs, n_upper, n_lower);
|
| 547 |
const traces = [];
|
| 548 |
|
| 549 |
-
//
|
| 550 |
-
const normalEdges = [], sharedEdges = [];
|
| 551 |
for (const [key, k] of Object.entries(springs)) {
|
| 552 |
const [u, v] = key.split('β');
|
| 553 |
if (!pos[u] || !pos[v]) continue;
|
| 554 |
-
(isSharedSpring(key) ? sharedEdges : normalEdges).push([key, k, u, v]);
|
| 555 |
-
}
|
| 556 |
-
|
| 557 |
-
for (const [key, k, u, v] of normalEdges) {
|
| 558 |
const [col, wd] = springColor(k);
|
| 559 |
traces.push({
|
| 560 |
type:'scatter', mode:'lines',
|
|
@@ -564,36 +559,29 @@ function buildTraces(nodes, springs, layers, n_inputs, n_upper, n_lower) {
|
|
| 564 |
});
|
| 565 |
}
|
| 566 |
|
| 567 |
-
//
|
| 568 |
-
for (const
|
| 569 |
-
const
|
| 570 |
-
|
| 571 |
traces.push({
|
| 572 |
type:'scatter', mode:'lines',
|
| 573 |
x:[pos[u][0], pos[v][0]], y:[pos[u][1], pos[v][1]],
|
| 574 |
-
line:{color:
|
| 575 |
hoverinfo:'none', showlegend:false
|
| 576 |
});
|
| 577 |
}
|
| 578 |
|
| 579 |
-
//
|
| 580 |
-
const
|
|
|
|
|
|
|
| 581 |
const NCOL = id => {
|
| 582 |
const k = id[0];
|
| 583 |
return k==='A'?'#fb923c': k==='B'?'#c084fc': k==='C'?'#38bdf8':
|
| 584 |
-
k==='U'?'#4ade80': '#67e8f9';
|
| 585 |
};
|
| 586 |
-
const isIO
|
| 587 |
-
|
| 588 |
-
// Mark shared vertices (appear in springs from multiple dims)
|
| 589 |
-
const sharedNodes = new Set();
|
| 590 |
-
for (const key of Object.keys(springs)) {
|
| 591 |
-
if (isSharedSpring(key)) {
|
| 592 |
-
const [u, v] = key.split('β');
|
| 593 |
-
if ('UL'.includes((u||'')[0])) sharedNodes.add(u);
|
| 594 |
-
if ('UL'.includes((v||'')[0])) sharedNodes.add(v);
|
| 595 |
-
}
|
| 596 |
-
}
|
| 597 |
|
| 598 |
traces.push({
|
| 599 |
type:'scatter', mode:'markers+text',
|
|
@@ -607,17 +595,19 @@ function buildTraces(nodes, springs, layers, n_inputs, n_upper, n_lower) {
|
|
| 607 |
textfont:{ size:9, color: allN.map(id => NCOL(id)) },
|
| 608 |
marker:{
|
| 609 |
size: allN.map(id => {
|
| 610 |
-
const v
|
| 611 |
-
const base = isIO(id) ? 18 : (
|
| 612 |
return base + Math.min(v*30, 8);
|
| 613 |
}),
|
| 614 |
-
|
|
|
|
| 615 |
opacity: allN.map(id => 0.75 + Math.min(Math.abs(nodes[id]?.vel??0)*1.8, 0.25)),
|
| 616 |
line:{
|
| 617 |
-
width: allN.map(id =>
|
| 618 |
color: allN.map(id =>
|
| 619 |
-
|
| 620 |
-
: nodes[id]?.anchored ? '#ef4444'
|
|
|
|
| 621 |
)
|
| 622 |
}
|
| 623 |
},
|
|
@@ -627,8 +617,9 @@ function buildTraces(nodes, springs, layers, n_inputs, n_upper, n_lower) {
|
|
| 627 |
return traces;
|
| 628 |
}
|
| 629 |
|
| 630 |
-
function meshLayout(layers, n_inputs, n_upper, n_lower) {
|
| 631 |
-
const
|
|
|
|
| 632 |
const xMax = Math.max(5.5, n_inputs * 2.8);
|
| 633 |
return {
|
| 634 |
margin:{l:8,r:8,t:8,b:8},
|
|
@@ -658,7 +649,8 @@ setInterval(async () => {
|
|
| 658 |
|
| 659 |
syncTopoUI(d.n_inputs, d.n_upper, d.n_lower);
|
| 660 |
crossConnect = d.cross_connect;
|
| 661 |
-
|
|
|
|
| 662 |
updateCrossUI();
|
| 663 |
document.getElementById('b-alpha').innerText = `Ξ±:${d.back_alpha.toFixed(2)}`;
|
| 664 |
document.getElementById('b-data').innerText = (d.dataset_type||'').slice(0,6).toUpperCase();
|
|
@@ -697,6 +689,20 @@ setInterval(async () => {
|
|
| 697 |
<span class="text-slate-700 text-[8px]">v:${(n.vel||0).toFixed(3)}</span>
|
| 698 |
</div>`;
|
| 699 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
const hids = order.filter(id => 'UL'.includes(id[0]));
|
| 701 |
if (hids.length) {
|
| 702 |
const avgV = hids.reduce((s,id) => s + Math.abs(d.nodes[id]?.vel||0), 0) / hids.length;
|
|
@@ -704,17 +710,23 @@ setInterval(async () => {
|
|
| 704 |
}
|
| 705 |
document.getElementById('pane-nodes').innerHTML = nh;
|
| 706 |
|
| 707 |
-
// Springs pane β
|
| 708 |
let sh = '';
|
| 709 |
for (const [key, k] of Object.entries(d.springs)) {
|
| 710 |
-
const
|
| 711 |
-
const kc = shared ? 'text-violet-300'
|
| 712 |
-
: (k < 0 ? 'text-blue-300' : k > 4 ? 'text-yellow-200' : 'text-purple-300');
|
| 713 |
-
const pfx = shared ? '<span class="text-violet-500 mr-1">β</span>' : '';
|
| 714 |
sh += `<div class="flex justify-between py-0.5 border-b border-slate-900">
|
| 715 |
-
<span
|
| 716 |
<span class="${kc} font-bold text-[10px]">${k.toFixed(4)}</span></div>`;
|
| 717 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
document.getElementById('pane-springs').innerHTML = sh;
|
| 719 |
|
| 720 |
// Logs
|
|
@@ -722,9 +734,10 @@ setInterval(async () => {
|
|
| 722 |
d.logs.map(l => `<div class="py-0.5 border-b border-slate-900/50">${l}</div>`).join('');
|
| 723 |
|
| 724 |
// Mesh plot
|
| 725 |
-
const layerKey = JSON.stringify(d.layers) + d.cross_connect;
|
| 726 |
-
const
|
| 727 |
-
const
|
|
|
|
| 728 |
if (!meshPlotted || layerKey !== lastLayerKey) {
|
| 729 |
Plotly.newPlot('mesh-plot', traces, layout, {displayModeBar:false, responsive:true});
|
| 730 |
meshPlotted = true; lastLayerKey = layerKey;
|
|
|
|
| 57 |
<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>
|
| 58 |
</div>
|
| 59 |
<div class="flex items-center gap-2 ml-1">
|
| 60 |
+
<!-- bridge counter, hidden when 0 -->
|
| 61 |
+
<span id="bridge-lbl" class="text-[7px] text-amber-500"></span>
|
| 62 |
<span id="q-lbl" class="text-[8px] text-slate-600">Q:0</span>
|
| 63 |
<div id="run-dot" class="w-2 h-2 rounded-full bg-slate-700"></div>
|
| 64 |
<button onclick="openDrawer()" class="text-[10px] bg-blue-700 hover:bg-blue-600 px-2 py-1 rounded font-bold">β DIALS</button>
|
|
|
|
| 135 |
</div>
|
| 136 |
|
| 137 |
<!-- CROSS CONNECT -->
|
| 138 |
+
<div class="col-span-2 bg-slate-900 rounded p-3 border border-amber-900/50">
|
| 139 |
+
<div class="text-amber-400 text-[9px] font-bold mb-1">
|
| 140 |
CROSS-CONNECT
|
| 141 |
+
<span class="text-slate-600 font-normal ml-1">passive bridge vertices</span>
|
| 142 |
</div>
|
| 143 |
<div class="text-[9px] text-slate-500 mb-2">
|
| 144 |
+
OFF β N independent parallel hourglasses (default)<br>
|
| 145 |
+
ON β a passive bridge vertex is inserted between each adjacent<br>
|
| 146 |
+
dimension pair (upper & lower). Bridge springs are fixed<br>
|
| 147 |
+
and never learned β cross-talk happens through physics only.
|
| 148 |
</div>
|
| 149 |
<button id="drawer-cross-btn" onclick="toggleCross()"
|
| 150 |
class="w-full py-2 rounded text-xs font-bold border border-slate-700 bg-slate-800 text-slate-400">
|
|
|
|
| 257 |
const cfg = { mode: 'training', architecture: 'additive' };
|
| 258 |
const topo = { inputs: 1, upper: 3, lower: 3 };
|
| 259 |
let crossConnect = false;
|
| 260 |
+
let nBridges = 0;
|
| 261 |
+
let bridgeK = 0.20;
|
| 262 |
|
| 263 |
// ββ CROSS CONNECT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 264 |
async function toggleCross() {
|
| 265 |
const res = await fetch('/toggle_cross', { method: 'POST' });
|
| 266 |
const data = await res.json();
|
| 267 |
crossConnect = data.cross_connect;
|
| 268 |
+
nBridges = data.n_bridges || 0;
|
| 269 |
+
bridgeK = data.bridge_k || 0.20;
|
| 270 |
updateCrossUI();
|
| 271 |
meshPlotted = false;
|
| 272 |
}
|
| 273 |
|
| 274 |
function updateCrossUI() {
|
| 275 |
+
const btn1 = document.getElementById('b-cross');
|
| 276 |
+
const btn2 = document.getElementById('drawer-cross-btn');
|
| 277 |
+
const info = document.getElementById('cross-info');
|
| 278 |
+
const blbl = document.getElementById('bridge-lbl');
|
| 279 |
+
const n = topo.inputs;
|
| 280 |
+
|
| 281 |
+
// Estimated bridge count when server hasn't confirmed yet
|
| 282 |
+
const nb = nBridges || (crossConnect && n >= 2 ? (n - 1) * 2 : 0);
|
|
|
|
|
|
|
| 283 |
|
| 284 |
if (crossConnect) {
|
| 285 |
+
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';
|
| 286 |
btn1.innerText = 'CROSS:ON';
|
| 287 |
+
btn2.className = 'w-full py-2 rounded text-xs font-bold border border-amber-600 bg-amber-900/60 text-amber-200';
|
| 288 |
btn2.innerText = 'CROSS-CONNECT: ON (click to disable)';
|
| 289 |
+
info.innerText = nb
|
| 290 |
+
? `${nb} bridge ${nb !== 1 ? 'vertices' : 'vertex'} β passive k=${bridgeK}, not learned`
|
| 291 |
+
: 'No bridges (need Dβ₯2)';
|
| 292 |
+
blbl.innerText = nb ? `⬑${nb}` : '';
|
| 293 |
+
blbl.className = 'text-[7px] text-amber-500';
|
| 294 |
} else {
|
| 295 |
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';
|
| 296 |
btn1.innerText = 'CROSS:OFF';
|
| 297 |
btn2.className = 'w-full py-2 rounded text-xs font-bold border border-slate-700 bg-slate-800 text-slate-400';
|
| 298 |
btn2.innerText = 'CROSS-CONNECT: OFF (click to enable)';
|
| 299 |
+
info.innerText = `${n} independent parallel hourglass${n !== 1 ? 'es' : ''}`;
|
| 300 |
+
blbl.innerText = '';
|
| 301 |
}
|
| 302 |
}
|
| 303 |
|
| 304 |
// ββ TOPOLOGY ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 305 |
function updateSpringCount() {
|
| 306 |
const { inputs: n, upper: u, lower: l } = topo;
|
| 307 |
+
const nb = crossConnect && n >= 2 ? (n - 1) * 2 : 0; // XU + XL per pair
|
| 308 |
+
const learnSprings = n * (2*u + 2*l);
|
| 309 |
+
const bridgeSprings = nb * 2; // each bridge has 2 springs
|
| 310 |
+
const totalNodes = 3*n + n*u + n*l + nb;
|
|
|
|
|
|
|
|
|
|
| 311 |
document.getElementById('spring-count').innerText =
|
| 312 |
+
`${learnSprings} learned + ${bridgeSprings} bridge springs | ${totalNodes} nodes`;
|
| 313 |
document.getElementById('custom-dim-hint').innerText =
|
| 314 |
n === 1 ? '(single value)' : `(${n} values, comma-separated)`;
|
| 315 |
updateCrossUI();
|
|
|
|
| 465 |
|
| 466 |
// ββ VISUALIZATION βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 467 |
|
| 468 |
+
/**
|
| 469 |
+
* Build x,y positions for every node.
|
| 470 |
+
* Bridge nodes (XU{d}, XL{d}) sit at the midpoint between their two
|
| 471 |
+
* adjacent boundary hidden nodes, on the same Y row.
|
| 472 |
+
*/
|
| 473 |
+
function buildPos(layers, n_inputs, n_upper, n_lower, bridgeNodeIds = []) {
|
| 474 |
const pos = {};
|
| 475 |
const Y = [4.4, 2.1, 0.0, -2.1, -4.4];
|
| 476 |
const COL_W = n_inputs === 1 ? 0 : Math.min(9.0 / (n_inputs - 1), 4.2);
|
| 477 |
const halfSp = COL_W * (n_inputs - 1) / 2;
|
| 478 |
const bSprd = n_inputs === 1 ? 3.8 : Math.min(COL_W * 0.55, 1.8);
|
| 479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
layers.forEach((layer, li) => {
|
| 481 |
const y = Y[li];
|
| 482 |
layer.forEach(nid => {
|
| 483 |
const kind = nid[0];
|
|
|
|
| 484 |
let dim = 1, j = 1, total = 1;
|
| 485 |
if ('ABC'.includes(kind)) {
|
| 486 |
dim = parseInt(nid.slice(1));
|
|
|
|
| 490 |
j = parseInt(parts[1]);
|
| 491 |
total = kind === 'U' ? n_upper : n_lower;
|
| 492 |
}
|
| 493 |
+
const cx = n_inputs === 1 ? 0 : -halfSp + (dim - 1) * COL_W;
|
| 494 |
if ('ABC'.includes(kind)) {
|
| 495 |
pos[nid] = [cx, y];
|
| 496 |
} else {
|
|
|
|
| 499 |
}
|
| 500 |
});
|
| 501 |
});
|
| 502 |
+
|
| 503 |
+
// Bridge nodes: midpoint between right boundary of dim d and left of dim d+1
|
| 504 |
+
// x = -halfSp + (d - 0.5) * COL_W (simplifies to the column midpoint)
|
| 505 |
+
bridgeNodeIds.forEach(nid => {
|
| 506 |
+
const side = nid[1]; // 'U' or 'L'
|
| 507 |
+
const d = parseInt(nid.slice(2));
|
| 508 |
+
const yVal = side === 'U' ? Y[1] : Y[3]; // same row as upper/lower hidden
|
| 509 |
+
const xVal = -halfSp + (d - 0.5) * COL_W;
|
| 510 |
+
pos[nid] = [xVal, yVal];
|
| 511 |
+
});
|
| 512 |
+
|
| 513 |
return pos;
|
| 514 |
}
|
| 515 |
|
| 516 |
function busShapes(pos, n_inputs) {
|
| 517 |
const sh = [];
|
| 518 |
const mg = n_inputs === 1 ? 1.4 : 0.8;
|
| 519 |
+
const xs = k => Object.entries(pos).filter(([id]) => id[0] === k).map(([,v]) => v[0]);
|
| 520 |
+
const rect = (mn, mx, yc, hh, fill, stroke) => sh.push({
|
| 521 |
type:'rect', xref:'x', yref:'y',
|
| 522 |
x0:mn-mg, x1:mx+mg, y0:yc-hh, y1:yc+hh,
|
| 523 |
fillcolor:fill, line:{color:stroke, width:2}
|
|
|
|
| 535 |
return [`rgb(30,${Math.round(80+t*100)},${Math.round(140+t*115)})`, 1.0+t*3.5];
|
| 536 |
}
|
| 537 |
|
| 538 |
+
/**
|
| 539 |
+
* Build all Plotly traces.
|
| 540 |
+
* Learnable springs: solid colour based on spring constant.
|
| 541 |
+
* Bridge springs: dashed amber lines.
|
| 542 |
+
* Bridge nodes: amber diamonds, distinct from regular hidden nodes.
|
| 543 |
+
*/
|
| 544 |
+
function buildTraces(nodes, springs, bridgeSprings, layers, n_inputs, n_upper, n_lower) {
|
| 545 |
+
const bridgeNodeIds = Object.keys(nodes).filter(id => id[0] === 'X');
|
| 546 |
+
const pos = buildPos(layers, n_inputs, n_upper, n_lower, bridgeNodeIds);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
const traces = [];
|
| 548 |
|
| 549 |
+
// 1. Learnable spring edges
|
|
|
|
| 550 |
for (const [key, k] of Object.entries(springs)) {
|
| 551 |
const [u, v] = key.split('β');
|
| 552 |
if (!pos[u] || !pos[v]) continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
const [col, wd] = springColor(k);
|
| 554 |
traces.push({
|
| 555 |
type:'scatter', mode:'lines',
|
|
|
|
| 559 |
});
|
| 560 |
}
|
| 561 |
|
| 562 |
+
// 2. Bridge spring edges β amber dashed, drawn on top
|
| 563 |
+
for (const key of Object.keys(bridgeSprings)) {
|
| 564 |
+
const [u, v] = key.split('β');
|
| 565 |
+
if (!pos[u] || !pos[v]) continue;
|
| 566 |
traces.push({
|
| 567 |
type:'scatter', mode:'lines',
|
| 568 |
x:[pos[u][0], pos[v][0]], y:[pos[u][1], pos[v][1]],
|
| 569 |
+
line:{color:'#f59e0b', width:2.0, dash:'dot'},
|
| 570 |
hoverinfo:'none', showlegend:false
|
| 571 |
});
|
| 572 |
}
|
| 573 |
|
| 574 |
+
// 3. All nodes (regular + bridge)
|
| 575 |
+
const regularNodes = layers.flat();
|
| 576 |
+
const allN = [...regularNodes, ...bridgeNodeIds];
|
| 577 |
+
|
| 578 |
const NCOL = id => {
|
| 579 |
const k = id[0];
|
| 580 |
return k==='A'?'#fb923c': k==='B'?'#c084fc': k==='C'?'#38bdf8':
|
| 581 |
+
k==='X'?'#f59e0b': k==='U'?'#4ade80': '#67e8f9';
|
| 582 |
};
|
| 583 |
+
const isIO = id => 'ABC'.includes(id[0]);
|
| 584 |
+
const isBridge = id => id[0] === 'X';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
|
| 586 |
traces.push({
|
| 587 |
type:'scatter', mode:'markers+text',
|
|
|
|
| 595 |
textfont:{ size:9, color: allN.map(id => NCOL(id)) },
|
| 596 |
marker:{
|
| 597 |
size: allN.map(id => {
|
| 598 |
+
const v = Math.abs(nodes[id]?.vel ?? 0);
|
| 599 |
+
const base = isIO(id) ? 18 : isBridge(id) ? 13 : 10;
|
| 600 |
return base + Math.min(v*30, 8);
|
| 601 |
}),
|
| 602 |
+
symbol: allN.map(id => isBridge(id) ? 'diamond' : 'circle'),
|
| 603 |
+
color: allN.map(id => NCOL(id)),
|
| 604 |
opacity: allN.map(id => 0.75 + Math.min(Math.abs(nodes[id]?.vel??0)*1.8, 0.25)),
|
| 605 |
line:{
|
| 606 |
+
width: allN.map(id => isBridge(id) ? 3.0 : 2.5),
|
| 607 |
color: allN.map(id =>
|
| 608 |
+
isBridge(id) ? '#d97706'
|
| 609 |
+
: nodes[id]?.anchored ? '#ef4444'
|
| 610 |
+
: '#22c55e'
|
| 611 |
)
|
| 612 |
}
|
| 613 |
},
|
|
|
|
| 617 |
return traces;
|
| 618 |
}
|
| 619 |
|
| 620 |
+
function meshLayout(nodes, layers, n_inputs, n_upper, n_lower) {
|
| 621 |
+
const bridgeNodeIds = Object.keys(nodes).filter(id => id[0] === 'X');
|
| 622 |
+
const pos = buildPos(layers, n_inputs, n_upper, n_lower, bridgeNodeIds);
|
| 623 |
const xMax = Math.max(5.5, n_inputs * 2.8);
|
| 624 |
return {
|
| 625 |
margin:{l:8,r:8,t:8,b:8},
|
|
|
|
| 649 |
|
| 650 |
syncTopoUI(d.n_inputs, d.n_upper, d.n_lower);
|
| 651 |
crossConnect = d.cross_connect;
|
| 652 |
+
nBridges = d.n_bridges || 0;
|
| 653 |
+
bridgeK = d.bridge_k || 0.20;
|
| 654 |
updateCrossUI();
|
| 655 |
document.getElementById('b-alpha').innerText = `Ξ±:${d.back_alpha.toFixed(2)}`;
|
| 656 |
document.getElementById('b-data').innerText = (d.dataset_type||'').slice(0,6).toUpperCase();
|
|
|
|
| 689 |
<span class="text-slate-700 text-[8px]">v:${(n.vel||0).toFixed(3)}</span>
|
| 690 |
</div>`;
|
| 691 |
});
|
| 692 |
+
// Bridge nodes summary
|
| 693 |
+
const bridgeIds = Object.keys(d.nodes).filter(id => id[0] === 'X');
|
| 694 |
+
if (bridgeIds.length) {
|
| 695 |
+
nh += `<div class="text-[8px] text-amber-600 py-0.5 border-b border-slate-900">
|
| 696 |
+
⬑ ${bridgeIds.length} bridge nodes β passive k=${bridgeK}</div>`;
|
| 697 |
+
bridgeIds.forEach(id => {
|
| 698 |
+
const n = d.nodes[id];
|
| 699 |
+
nh += `<div class="flex justify-between items-center py-0.5 border-b border-slate-900/50">
|
| 700 |
+
<span class="text-amber-500 text-[9px]">β ${id}</span>
|
| 701 |
+
<span class="text-amber-300 font-bold">${Number(n.x).toFixed(4)}</span>
|
| 702 |
+
<span class="text-slate-700 text-[8px]">v:${(n.vel||0).toFixed(3)}</span>
|
| 703 |
+
</div>`;
|
| 704 |
+
});
|
| 705 |
+
}
|
| 706 |
const hids = order.filter(id => 'UL'.includes(id[0]));
|
| 707 |
if (hids.length) {
|
| 708 |
const avgV = hids.reduce((s,id) => s + Math.abs(d.nodes[id]?.vel||0), 0) / hids.length;
|
|
|
|
| 710 |
}
|
| 711 |
document.getElementById('pane-nodes').innerHTML = nh;
|
| 712 |
|
| 713 |
+
// Springs pane β learnable first, then bridge springs in amber
|
| 714 |
let sh = '';
|
| 715 |
for (const [key, k] of Object.entries(d.springs)) {
|
| 716 |
+
const kc = k < 0 ? 'text-blue-300' : k > 4 ? 'text-yellow-200' : 'text-purple-300';
|
|
|
|
|
|
|
|
|
|
| 717 |
sh += `<div class="flex justify-between py-0.5 border-b border-slate-900">
|
| 718 |
+
<span class="text-slate-500 text-[9px]">${key}</span>
|
| 719 |
<span class="${kc} font-bold text-[10px]">${k.toFixed(4)}</span></div>`;
|
| 720 |
}
|
| 721 |
+
const bs = d.bridge_springs || {};
|
| 722 |
+
if (Object.keys(bs).length) {
|
| 723 |
+
sh += `<div class="text-[8px] text-amber-600 py-0.5 border-b border-slate-800 mt-1">⬑ BRIDGE (passive, fixed)</div>`;
|
| 724 |
+
for (const [key, k] of Object.entries(bs)) {
|
| 725 |
+
sh += `<div class="flex justify-between py-0.5 border-b border-slate-900/50">
|
| 726 |
+
<span class="text-amber-700 text-[9px]">${key}</span>
|
| 727 |
+
<span class="text-amber-400 font-bold text-[10px]">${k.toFixed(4)}</span></div>`;
|
| 728 |
+
}
|
| 729 |
+
}
|
| 730 |
document.getElementById('pane-springs').innerHTML = sh;
|
| 731 |
|
| 732 |
// Logs
|
|
|
|
| 734 |
d.logs.map(l => `<div class="py-0.5 border-b border-slate-900/50">${l}</div>`).join('');
|
| 735 |
|
| 736 |
// Mesh plot
|
| 737 |
+
const layerKey = JSON.stringify(d.layers) + d.cross_connect + d.n_bridges;
|
| 738 |
+
const bridgeSprings = d.bridge_springs || {};
|
| 739 |
+
const traces = buildTraces(d.nodes, d.springs, bridgeSprings, d.layers, d.n_inputs, d.n_upper, d.n_lower);
|
| 740 |
+
const layout = meshLayout(d.nodes, d.layers, d.n_inputs, d.n_upper, d.n_lower);
|
| 741 |
if (!meshPlotted || layerKey !== lastLayerKey) {
|
| 742 |
Plotly.newPlot('mesh-plot', traces, layout, {displayModeBar:false, responsive:true});
|
| 743 |
meshPlotted = true; lastLayerKey = layerKey;
|