Files

137 lines
6.8 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
# build_field_viz.py
# Reads the REAL captured live field (predict_capture.npz) and bakes it into a
# self-contained HTML file you open in a browser. No mockup — this renders the
# actual 500 frames of the substrate the observer captured.
#
# Shows: all 6 channels (rho, ux, uy, sxx, syy, sxy) as 32x32 heatmaps, animated
# through the real frames, PLUS a 7th panel = per-tile linear-prediction error
# (where the field surprises the predictor). Play/pause/scrub.
import sys, io, json, base64
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
import numpy as np
NPZ = "predict_capture.npz"
OUT = "field_viz.html"
TILES, CH = 32, 6
CH_NAMES = ["rho","ux","uy","sxx","syy","sxy"]
d = np.load(NPZ, allow_pickle=True)
F = d["frames"].astype(np.float32) # (T, 1024, 6)
C = d["cycles"]
T = F.shape[0]
print(f"Loaded {T} frames, cycles {C[0]}..{C[-1]}", flush=True)
Fg = F.reshape(T, TILES, TILES, CH) # (T, y, x, ch)
# Per-tile linear-prediction error field: |truth - (2*prev - prev2)| per tile, summed over channels
# aligned to frames 2..T-1
pred = 2*Fg[1:-1] - Fg[0:-2]
truth = Fg[2:]
err = np.sqrt(np.mean((pred-truth)**2, axis=3)) # (T-2, y, x)
# pad front so err index matches frame index
err_full = np.zeros((T, TILES, TILES), dtype=np.float32)
err_full[2:] = err
# Per-channel normalization for display (diverging around each channel's mean)
disp = np.zeros_like(Fg)
ranges = []
for c in range(CH):
ch = Fg[...,c]
mu = ch.mean(); sd = ch.std() + 1e-9
z = (ch - mu) / sd
z = np.clip(z, -3, 3) / 3.0 # -> [-1,1]
disp[...,c] = z
ranges.append((float(mu), float(sd)))
# error display: normalize to [0,1]
emax = np.percentile(err_full[2:], 99) + 1e-9
edisp = np.clip(err_full / emax, 0, 1)
# Quantize to uint8 to keep the HTML small: disp in [-1,1]->[0,255], edisp [0,1]->[0,255]
disp_q = ((disp*0.5+0.5)*255).astype(np.uint8) # (T,y,x,ch)
edisp_q = (edisp*255).astype(np.uint8) # (T,y,x)
# Pack as base64: frames as (T, ch, y, x) then err (T,y,x)
buf = disp_q.transpose(0,3,1,2).tobytes() # T*6*32*32
ebuf = edisp_q.tobytes() # T*32*32
b64 = base64.b64encode(buf).decode()
eb64 = base64.b64encode(ebuf).decode()
cyc_list = [int(x) for x in C]
html = f"""<!doctype html><html><head><meta charset="utf-8"><title>Khra'gixx live field</title>
<style>
body{{margin:0;background:#0a0a0f;color:#c8c8d0;font-family:ui-monospace,Menlo,monospace}}
.wrap{{max-width:1100px;margin:0 auto;padding:18px}}
h1{{font-size:15px;font-weight:600;color:#e6e6ee;margin:0 0 2px}}
.sub{{font-size:11px;color:#7a7a88;margin-bottom:14px}}
.grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}}
.panel{{background:#12121a;border:1px solid #22222e;border-radius:8px;padding:8px}}
.panel h2{{font-size:11px;margin:0 0 6px;color:#9a9aae;font-weight:600;letter-spacing:.04em}}
canvas{{width:100%;image-rendering:pixelated;border-radius:4px;background:#000;aspect-ratio:1}}
.err h2{{color:#ff9a6a}}
.ctl{{display:flex;align-items:center;gap:12px;margin:16px 0 6px}}
button{{background:#1c1c28;color:#d0d0dc;border:1px solid #33334a;border-radius:6px;padding:7px 14px;font:inherit;cursor:pointer}}
button:hover{{background:#26263a}}
input[type=range]{{flex:1;accent-color:#ff9a6a}}
.meta{{font-size:11px;color:#7a7a88}}
.cyc{{color:#ff9a6a;font-weight:600}}
.legend{{font-size:10px;color:#66667a;margin-top:8px;line-height:1.5}}
</style></head><body><div class="wrap">
<h1>Khra'gixx — the live field, rendered</h1>
<div class="sub">{T} real frames captured from port 5561 · cycles <span id="cr"></span> · 32×32 coarse grid · this is the substrate moving, not nine averages</div>
<div class="grid" id="panels"></div>
<div class="ctl">
<button id="play">⏸ pause</button>
<input type="range" id="scrub" min="0" max="{T-1}" value="0">
<span class="meta">frame <span id="fn">0</span>/{T-1} · cycle <span class="cyc" id="cyc"></span></span>
</div>
<div class="legend">
Six macroscopic channels + the linear-prediction error (orange). Blue↔red = each channel's own deviation (±3σ).
The <b style="color:#ff9a6a">error panel</b> lights up where the field's next state SURPRISES the constant-velocity predictor —
i.e. where something non-trivial is happening. Smooth dark error = predictable flow. Bright spots = the interesting bits.
Passive predict-test verdict: linear beats persistence by 21% — the field has real short-horizon dynamics.
</div></div>
<script>
const T={T}, N={TILES}, CH={CH}, names={json.dumps(CH_NAMES)}, cycles={json.dumps(cyc_list)};
const raw=Uint8Array.from(atob("{b64}"),c=>c.charCodeAt(0)); // T*CH*N*N
const eraw=Uint8Array.from(atob("{eb64}"),c=>c.charCodeAt(0)); // T*N*N
document.getElementById('cr').textContent=cycles[0]+""+cycles[T-1];
const panels=document.getElementById('panels'); const cvs=[];
function mk(title,cls){{const d=document.createElement('div');d.className='panel'+(cls?' '+cls:'');d.innerHTML='<h2>'+title+'</h2>';const c=document.createElement('canvas');c.width=N;c.height=N;d.appendChild(c);panels.appendChild(d);return c;}}
for(let c=0;c<CH;c++)cvs.push(mk(names[c]));
const ecv=mk('prediction error','err');
const ctxs=cvs.map(c=>c.getContext('2d')); const ectx=ecv.getContext('2d');
const imgs=cvs.map(()=>ctxs[0].createImageData(N,N)); const eimg=ectx.createImageData(N,N);
// diverging blue-white-red
function div(v){{ // v in [0,255] -> rgb
const t=(v/255)*2-1; // -1..1
if(t<0){{const a=-t;return[Math.round(30+ (1-a)*200),Math.round(60+(1-a)*180),Math.round(120+a*135)];}}
else{{return[Math.round(230),Math.round(230-t*180),Math.round(230-t*200)];}}
}}
function heat(v){{ // 0..255 -> black->orange->white
const t=v/255; const r=Math.min(255,t*3*255), g=Math.min(255,Math.max(0,(t-0.33)*3*255)), b=Math.min(255,Math.max(0,(t-0.66)*3*255));
return[r,g,b];
}}
function draw(f){{
for(let c=0;c<CH;c++){{const off=(f*CH+c)*N*N; const im=imgs[c];
for(let i=0;i<N*N;i++){{const [r,g,b]=div(raw[off+i]);im.data[i*4]=r;im.data[i*4+1]=g;im.data[i*4+2]=b;im.data[i*4+3]=255;}}
ctxs[c].putImageData(im,0,0);}}
const eoff=f*N*N; for(let i=0;i<N*N;i++){{const [r,g,b]=heat(eraw[eoff+i]);eimg.data[i*4]=r;eimg.data[i*4+1]=g;eimg.data[i*4+2]=b;eimg.data[i*4+3]=255;}}
ectx.putImageData(eimg,0,0);
document.getElementById('fn').textContent=f; document.getElementById('cyc').textContent=cycles[f];
document.getElementById('scrub').value=f;
}}
let f=0,playing=true;
const scrub=document.getElementById('scrub'),playb=document.getElementById('play');
scrub.oninput=()=>{{f=+scrub.value;draw(f);}};
playb.onclick=()=>{{playing=!playing;playb.textContent=playing?'⏸ pause':'▶ play';}};
function loop(){{if(playing){{f=(f+1)%T;draw(f);}}setTimeout(loop,60);}}
draw(0);loop();
</script></body></html>"""
with open(OUT,"w") as fp:
fp.write(html)
print(f"Wrote {OUT} ({len(html)//1024} KB). Open it in a browser.", flush=True)