Files
2026-07-16 11:57:36 +07:00

137 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)