| """ |
| visualize.py -- live visualizer for Mycel-LM: watch the fungal colony grow in 3D as |
| the model generates, token by token. |
| |
| Captures per generated token (via instrument.Recorder, which MycelBlock writes to): |
| * tip POSITIONS (all pos_dim axes) for one layer's colony -- the 3D scatter |
| * per-tip growth TRAIL over the growth steps -- the hyphae extending this token |
| * per-layer colony density + trait-station sites + attention + token stream |
| |
| Renders one self-contained HTML dashboard (data embedded -> opens via file://). The |
| colony is a Three.js network you can drag-rotate / scroll-zoom: hyphal tips linked to |
| their nearest neighbours as filaments (so it reads as a mycelial web, not loose dots), |
| tips coloured by local density, with trait STATIONS as orange wire-spheres and faint |
| grey growth trails. Three.js loads from a CDN (needs network the first time). |
| |
| Usage: |
| python visualize.py --prompt "the mycelium spreads" --tokens 50 |
| """ |
| import argparse, json, os, sys, webbrowser |
| import torch |
|
|
| from model import QuazimotoLM, QuazimotoConfig |
| import instrument |
|
|
| PKG_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
|
|
|
| def find_ckpt(path): |
| if path and os.path.isfile(path): |
| return path |
| folder = (os.path.dirname(path) if path else os.path.join(PKG_DIR, "chkpt")) or "." |
| if not os.path.isdir(folder): |
| return None |
| pts = [os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(".pt")] |
| return max(pts, key=os.path.getmtime) if pts else None |
|
|
|
|
| def load_tokenizer(tok_dir): |
| sys.path.insert(0, tok_dir) |
| from spike_tokenizer import SpikeTokenizer |
| return SpikeTokenizer(vocab_file=os.path.join(tok_dir, "tokenizer.json")) |
|
|
|
|
| @torch.no_grad() |
| def run_capture(model, cfg, tok, ids, n_tokens, temperature, top_k, device): |
| rec = instrument.Recorder(phase_layer=cfg.n_layer // 2, attn_layer=cfg.n_layer // 2) |
| instrument.set_rec(rec) |
| idx = torch.tensor([ids], device=device) |
| try: |
| for _ in range(n_tokens): |
| cond = idx[:, -cfg.block_size:] |
| rec.begin() |
| logits, _, _ = model(cond) |
| lg = torch.nan_to_num(logits[:, -1, :].float(), nan=0.0, |
| posinf=1e4, neginf=-1e4) / max(temperature, 1e-6) |
| if top_k: |
| v = torch.topk(lg, min(top_k, lg.size(-1))).values |
| lg = lg.masked_fill(lg < v[:, [-1]], float("-inf")) |
| probs = torch.softmax(lg, dim=-1) |
| nxt = (torch.argmax(lg, -1, keepdim=True) if temperature <= 1e-3 |
| else torch.multinomial(probs, 1)) |
| top = torch.topk(torch.softmax(logits[:, -1].float(), -1), 5) |
| rec.end(token=int(nxt), |
| char=tok.decode([int(nxt)], skip_special_tokens=False), |
| top=[[int(t), round(float(p), 3)] for t, p in zip(top.indices[0], top.values[0])]) |
| idx = torch.cat([idx, nxt], dim=1) |
| finally: |
| instrument.set_rec(None) |
| return rec.frames, idx[0].tolist() |
|
|
|
|
| def main(): |
| p = argparse.ArgumentParser(description="Mycel-LM 3D colony visualizer") |
| p.add_argument("--ckpt", default="") |
| p.add_argument("--tok_dir", default=PKG_DIR) |
| p.add_argument("--prompt", default="the mycelium spreads") |
| p.add_argument("--tokens", type=int, default=50) |
| p.add_argument("--temperature", type=float, default=0.0) |
| p.add_argument("--top_k", type=int, default=40) |
| p.add_argument("--device", default="cpu") |
| p.add_argument("--out", default=os.path.join(PKG_DIR, "viz.html")) |
| p.add_argument("--no_open", action="store_true") |
| args = p.parse_args() |
|
|
| path = find_ckpt(args.ckpt) |
| if path is None: |
| print("No checkpoint found; pass --ckpt or train one."); return |
| ckpt = torch.load(path, map_location=args.device, weights_only=False) |
| cfg = QuazimotoConfig(**ckpt["family_config"]) |
| model = QuazimotoLM(cfg); model.load_state_dict(ckpt["model"], strict=False) |
| model.to(args.device).eval() |
| tok = load_tokenizer(args.tok_dir) |
| print(f"loaded {path} (step {ckpt.get('step')}) | capturing {args.tokens} tokens ...") |
|
|
| ids = tok.encode(args.prompt, add_special_tokens=False) |
| frames, _ = run_capture(model, cfg, tok, ids, args.tokens, |
| args.temperature, args.top_k or None, args.device) |
|
|
| pl = cfg.n_layer // 2 |
| blk = model.layers[pl].quaz |
| stations = blk.stations.anchors.detach().cpu().tolist() if getattr(blk, "use_stations", False) else [] |
|
|
| data = { |
| "meta": {"ckpt": os.path.basename(path), "step": ckpt.get("step"), |
| "n_layer": cfg.n_layer, "n_tips": cfg.n_tips, "pos_dim": cfg.mycel_pos_dim, |
| "bound": cfg.osc_bound, "phase_layer": pl, "prompt": args.prompt, |
| "stations": stations, |
| "tip_rings": bool(getattr(cfg, "use_tip_rings", False)), |
| "tip_ring_size": getattr(cfg, "tip_ring_size", 0)}, |
| "prompt_chars": [tok.decode([t], skip_special_tokens=False) for t in ids], |
| "frames": frames, |
| } |
| html = HTML_TEMPLATE.replace("/*__DATA__*/", json.dumps(data)) |
| with open(args.out, "w", encoding="utf-8") as f: |
| f.write(html) |
| print(f"wrote {args.out} ({os.path.getsize(args.out)//1024} KB)") |
| if not args.no_open: |
| webbrowser.open("file://" + os.path.abspath(args.out)) |
|
|
|
|
| HTML_TEMPLATE = r"""<!doctype html> |
| <html><head><meta charset="utf-8"><title>Mycel-LM live</title> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> |
| <style> |
| :root{--bg:#0d1117;--panel:#161b22;--ink:#e6edf3;--mut:#8b949e;--ac:#58a6ff;--hot:#f0883e;--spore:#3fb950} |
| *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.4 ui-monospace,Menlo,Consolas,monospace} |
| header{padding:10px 16px;background:var(--panel);border-bottom:1px solid #30363d;display:flex;gap:16px;align-items:center;flex-wrap:wrap} |
| h1{font-size:15px;margin:0;color:var(--spore)} .mut{color:var(--mut)} |
| #wrap{display:grid;grid-template-columns:320px 1fr 300px;gap:12px;padding:12px} |
| .panel{background:var(--panel);border:1px solid #30363d;border-radius:8px;padding:10px} |
| .panel h2{font-size:12px;margin:0 0 8px;color:var(--mut);text-transform:uppercase;letter-spacing:.5px} |
| #stream{line-height:1.9;max-height:120px;overflow:auto} |
| .tk{padding:1px 2px;border-radius:3px;cursor:pointer;white-space:pre} |
| .tk.prompt{color:var(--mut)} .tk.cur{background:var(--spore);color:#000} .tk:hover{background:#30363d} |
| .ctrl{display:flex;gap:10px;align-items:center} input[type=range]{width:260px} |
| button{background:#21262d;color:var(--ink);border:1px solid #30363d;border-radius:6px;padding:4px 10px;cursor:pointer} |
| button:hover{border-color:var(--spore)} svg{display:block;width:100%;height:auto} |
| #colony3d{width:100%;height:440px;border-radius:6px;overflow:hidden} |
| .bar{height:10px;background:#21262d;border-radius:5px;overflow:hidden;margin:2px 0}.bar>div{height:100%} |
| table{border-collapse:collapse;width:100%}td{padding:1px 4px}.lbl{color:var(--mut);width:52px} |
| .grid{display:grid;gap:3px}.cell{height:15px;border-radius:2px}.small{font-size:11px;color:var(--mut)} |
| </style></head><body> |
| <header><h1>Mycel-LM · colony 3D</h1><span class="mut" id="meta"></span> |
| <span style="flex:1"></span> |
| <div class="ctrl"><button id="play">▶ play</button><input type="range" id="seek" min="0" value="0"><span id="pos" class="mut"></span></div> |
| </header> |
| <div id="wrap"> |
| <div style="display:flex;flex-direction:column;gap:12px"> |
| <div class="panel"><h2>Token stream</h2><div id="stream"></div></div> |
| <div class="panel"><h2>Trait activity (this token)</h2><div id="traits"></div></div> |
| <div class="panel"><h2>Top predictions</h2><div id="top"></div></div> |
| </div> |
| <div style="display:flex;flex-direction:column;gap:12px"> |
| <div class="panel"><h2>Colony — layer <span id="pl"></span> (drag rotate, scroll zoom; wire-spheres = trait stations)</h2> |
| <div id="colony3d"></div></div> |
| <div class="panel"><h2>Colony density per layer (bright = dense/clustered)</h2><div id="heat"></div></div> |
| </div> |
| <div style="display:flex;flex-direction:column;gap:12px"> |
| <div class="panel"><h2>Attention — layer <span id="al"></span> (last token → context)</h2><div id="attn"></div></div> |
| <div class="panel"><h2>Notes</h2><div class="small" id="notes"></div></div> |
| </div> |
| </div> |
| <script> |
| const D=/*__DATA__*/; const M=D.meta,F=D.frames,NL=M.n_layer,NT=M.n_tips,PD=M.pos_dim,BND=M.bound; |
| let i=0,playing=false,timer=null; const $=id=>document.getElementById(id); |
| $("meta").textContent=`${M.ckpt} · step ${M.step} · ${NT} tips · ${PD}D colony · ${NL} layers · "${M.prompt}"`; |
| $("pl").textContent=M.phase_layer; $("al").textContent=M.phase_layer; |
| const seek=$("seek"); seek.max=F.length-1; |
| const hue2=v=>`hsl(${(1-Math.max(0,Math.min(1,v)))*140},70%,${30+45*Math.max(0,Math.min(1,v))}%)`; |
| |
| // hsl(green->orange) -> [r,g,b] in 0..1 for Three.js vertex colours (density mode) |
| function colRGB(v){v=Math.max(0,Math.min(1,v));const h=(1-v)*140/360,s=0.7,l=0.32+0.42*v; |
| const a=s*Math.min(l,1-l),f=n=>{const k=(n+h*12)%12;return l-a*Math.max(-1,Math.min(k-3,9-k,1));};return[f(0),f(8),f(4)];} |
| // ring PHASE -> [r,g,b]: hue = phase around the wheel, saturation/brightness = coherence r. |
| // Synchronized tips share a hue (colony turns one colour); desynced tips are a rainbow. |
| function phaseRGB(psi,r){const h=(psi/(2*Math.PI))+0.5,s=0.25+0.65*Math.max(0,Math.min(1,r)),l=0.5; |
| const a=s*Math.min(l,1-l),f=n=>{const k=(n+h*12)%12;return l-a*Math.max(-1,Math.min(k-3,9-k,1));};return[f(0),f(8),f(4)];} |
| |
| // ---- Three.js colony ---- |
| const KNN=3; // links per tip -> the mycelial web |
| const MAXE=NT*KNN; // max filament segments |
| let scene,camera,renderer,controls,geom,points,webGeom,web,trailGeom,trail; |
| function init3d(){ |
| const el=$("colony3d"),W=el.clientWidth||600,H=440; |
| scene=new THREE.Scene(); scene.background=new THREE.Color(0x0d1117); |
| camera=new THREE.PerspectiveCamera(55,W/H,0.1,1000); camera.position.set(BND*2.4,BND*1.7,BND*2.4); |
| renderer=new THREE.WebGLRenderer({antialias:true}); renderer.setSize(W,H); el.appendChild(renderer.domElement); |
| controls=new THREE.OrbitControls(camera,renderer.domElement); controls.enableDamping=true; controls.target.set(0,0,0); |
| scene.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(2*BND,2*BND,2*BND)), |
| new THREE.LineBasicMaterial({color:0x30363d}))); |
| // filament web: each tip linked to its nearest neighbours -> looks like hyphae, not dots |
| webGeom=new THREE.BufferGeometry(); |
| webGeom.setAttribute('position',new THREE.BufferAttribute(new Float32Array(MAXE*2*3),3)); |
| webGeom.setAttribute('color',new THREE.BufferAttribute(new Float32Array(MAXE*2*3),3)); |
| web=new THREE.LineSegments(webGeom,new THREE.LineBasicMaterial({vertexColors:true,transparent:true,opacity:0.55})); |
| scene.add(web); |
| // growth trails: each tip's path over the growth steps (the hypha extending this token) |
| const TS=(F[0]&&F[0].phases&&F[0].phases.traj)?F[0].phases.traj.length:0; |
| if(TS>1){trailGeom=new THREE.BufferGeometry(); |
| trailGeom.setAttribute('position',new THREE.BufferAttribute(new Float32Array(NT*(TS-1)*2*3),3)); |
| trail=new THREE.LineSegments(trailGeom,new THREE.LineBasicMaterial({color:0x8b949e,transparent:true,opacity:0.35})); |
| scene.add(trail);} |
| geom=new THREE.BufferGeometry(); |
| geom.setAttribute('position',new THREE.BufferAttribute(new Float32Array(NT*3),3)); |
| geom.setAttribute('color',new THREE.BufferAttribute(new Float32Array(NT*3),3)); |
| points=new THREE.Points(geom,new THREE.PointsMaterial({size:BND*0.07,vertexColors:true})); |
| scene.add(points); |
| (M.stations||[]).forEach(s=>{const mesh=new THREE.Mesh(new THREE.SphereGeometry(BND*0.06,10,10), |
| new THREE.MeshBasicMaterial({color:0xf0883e,wireframe:true})); |
| mesh.position.set(s[0]||0,s[1]||0,s[2]||0); scene.add(mesh);}); |
| window.addEventListener('resize',()=>{const w=el.clientWidth||600;camera.aspect=w/H;camera.updateProjectionMatrix();renderer.setSize(w,H);}); |
| (function loop(){requestAnimationFrame(loop);controls.update();renderer.render(scene,camera);})(); |
| } |
| function updateColony(f){ |
| if(!geom||!f.phases)return; |
| const th=f.phases.theta,tp=f.phases.tip_psi,tr=f.phases.tip_r; |
| const pos=geom.attributes.position.array,col=geom.attributes.color.array; |
| const P=[]; for(let t=0;t<NT;t++)P.push([th[t*PD],th[t*PD+1],PD>2?th[t*PD+2]:0]); |
| let colr; |
| if(tp){ // colour by ring phase (synchronization view) |
| colr=P.map((_,t)=>phaseRGB(tp[t],tr?tr[t]:0.6)); |
| }else{ // colour by local density (no rings) |
| const r2=(BND*0.25)**2, dens=P.map(a=>{let d=0;P.forEach(b=>{const dx=a[0]-b[0],dy=a[1]-b[1],dz=a[2]-b[2];if(dx*dx+dy*dy+dz*dz<r2)d++;});return d;}); |
| const mx=Math.max(...dens,1); colr=dens.map(d=>colRGB(d/mx)); |
| } |
| for(let t=0;t<NT;t++){pos[t*3]=P[t][0];pos[t*3+1]=P[t][1];pos[t*3+2]=P[t][2]; |
| const c=colr[t];col[t*3]=c[0];col[t*3+1]=c[1];col[t*3+2]=c[2];} |
| geom.attributes.position.needsUpdate=true; geom.attributes.color.needsUpdate=true; |
| // rebuild the filament web: connect each tip to its KNN nearest neighbours |
| if(webGeom){const wp=webGeom.attributes.position.array,wc=webGeom.attributes.color.array;let e=0; |
| for(let a=0;a<NT&&e<MAXE;a++){ |
| const nb=[]; for(let b=0;b<NT;b++){if(b===a)continue; |
| const dx=P[a][0]-P[b][0],dy=P[a][1]-P[b][1],dz=P[a][2]-P[b][2];nb.push([dx*dx+dy*dy+dz*dz,b]);} |
| nb.sort((x,y)=>x[0]-y[0]); |
| for(let n=0;n<Math.min(KNN,nb.length)&&e<MAXE;n++){const b=nb[n][1],ca=colr[a],cb=colr[b]; |
| wp[e*6]=P[a][0];wp[e*6+1]=P[a][1];wp[e*6+2]=P[a][2]; |
| wp[e*6+3]=P[b][0];wp[e*6+4]=P[b][1];wp[e*6+5]=P[b][2]; |
| wc[e*6]=ca[0];wc[e*6+1]=ca[1];wc[e*6+2]=ca[2];wc[e*6+3]=cb[0];wc[e*6+4]=cb[1];wc[e*6+5]=cb[2];e++;}} |
| webGeom.setDrawRange(0,e*2);webGeom.attributes.position.needsUpdate=true;webGeom.attributes.color.needsUpdate=true;} |
| // growth trails: draw each tip's path across the captured growth steps |
| if(trail&&f.phases.traj){const tj=f.phases.traj,TS=tj.length,tp2=trailGeom.attributes.position.array;let g=0; |
| for(let t=0;t<NT;t++)for(let s=0;s<TS-1;s++){ |
| const A=tj[s],Bp=tj[s+1]; |
| tp2[g++]=A[t*PD];tp2[g++]=A[t*PD+1];tp2[g++]=PD>2?A[t*PD+2]:0; |
| tp2[g++]=Bp[t*PD];tp2[g++]=Bp[t*PD+1];tp2[g++]=PD>2?Bp[t*PD+2]:0;} |
| trailGeom.setDrawRange(0,NT*(TS-1)*2);trailGeom.attributes.position.needsUpdate=true;} |
| } |
| |
| const stream=$("stream"); |
| D.prompt_chars.forEach(c=>{const s=document.createElement("span");s.className="tk prompt";s.textContent=esc(c);stream.appendChild(s);}); |
| F.forEach((f,k)=>{const s=document.createElement("span");s.className="tk gen";s.textContent=esc(f.char);s.onclick=()=>{i=k;render()};s.dataset.k=k;stream.appendChild(s);}); |
| function esc(c){return c.replace(/\n/g,"⏎").replace(/ /g,"·");} |
| function heat(f){ |
| let h=`<div class="grid" style="grid-template-columns:auto repeat(${NL},1fr)"><div class="small"></div>`; |
| for(let l=0;l<NL;l++)h+=`<div class="small" style="text-align:center">L${l}</div>`; |
| h+=`<div class="small">density</div>`; const mx=Math.max(...f.rings.map(r=>r.R[0]),1e-6); |
| for(let l=0;l<NL;l++){const s=f.rings[l]?f.rings[l].R[0]:0;h+=`<div class="cell" title="L${l} ${s.toFixed(3)}" style="background:${hue2(s/mx)}"></div>`;} |
| $("heat").innerHTML=h+"</div>"; |
| } |
| function render(){ |
| const f=F[i]; $("pos").textContent=`${i+1}/${F.length}`; seek.value=i; |
| document.querySelectorAll(".tk.gen").forEach(s=>s.classList.toggle("cur",+s.dataset.k===i)); |
| updateColony(f); heat(f); |
| const qn=f.quaz_norm,mx=Math.max(...qn,1e-6);let L=qn.map((_,l)=>"grow L"+l),V=qn.map(v=>v/mx); |
| if(f.traits.hrm!=null){L.push("HRM");V.push(Math.min(1,f.traits.hrm/mx));} |
| if(f.traits.moe!=null){L.push("MoE");V.push(Math.min(1,f.traits.moe/mx));} |
| let th="<table>";V.forEach((v,k)=>{th+=`<tr><td class="lbl">${L[k]}</td><td style="width:100%"><div class="bar"><div style="width:${(v*100).toFixed(0)}%;background:${hue2(v)}"></div></div></td></tr>`;}); |
| $("traits").innerHTML=th+"</table>"; |
| let tp="<table>";f.top.forEach(([t,p])=>{tp+=`<tr><td class="lbl">${p.toFixed(2)}</td><td><div class="bar"><div style="width:${(p*100).toFixed(0)}%;background:var(--ac)"></div></div></td><td class="small">id ${t}</td></tr>`;}); |
| $("top").innerHTML=tp+"</table>"; |
| if(f.attn){const w=f.attn.w,m=Math.max(...w,1e-6);let h="<div style='display:flex;flex-wrap:wrap;gap:1px'>"; |
| w.forEach((a,p)=>{h+=`<div title="pos ${p}: ${a.toFixed(3)}" style="width:8px;height:14px;background:${hue2(a/m)}"></div>`;}); |
| $("attn").innerHTML=h+`</div><div class='small'>${w.length} context positions</div>`;} |
| else $("attn").innerHTML="<span class='small'>n/a</span>"; |
| $("notes").innerHTML=`Tips: ${NT} · stations: ${(M.stations||[]).length}<br>Green = sparse growing edge, orange = dense core.<br>Filaments link each tip to its nearest neighbours (the mycelial web); faint grey trails are each tip's growth path this token.<br>Orange wire-spheres = trait stations. Drag to orbit, scroll to zoom.`; |
| } |
| seek.oninput=()=>{i=+seek.value;render()}; |
| $("play").onclick=()=>{playing=!playing;$("play").textContent=playing?"⏸ pause":"▶ play";if(playing)timer=setInterval(()=>{i=(i+1)%F.length;render();},400);else clearInterval(timer);}; |
| document.onkeydown=e=>{if(e.key==="ArrowRight"){i=Math.min(F.length-1,i+1);render();}if(e.key==="ArrowLeft"){i=Math.max(0,i-1);render();}}; |
| if(window.THREE){init3d();} else {$("colony3d").innerHTML="<div class='small' style='padding:20px'>Three.js failed to load (needs network for the CDN).</div>";} |
| render(); |
| </script></body></html>""" |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|