Spaces:
Running
Running
Commit ·
049f532
1
Parent(s): e6d6d7c
switch to Pyodide — run byte_emergence_demo.py 100% verbatim in browser
Browse filesUser asked: 'can we use the Python code 100% identical, even if not 60fps?'
Yes — Pyodide loads numpy in WebAssembly and runs the original engine_step,
H, H_joint, emergence functions byte-for-byte. ~20-30 fps tick rate, no
JS porting, no server.
The previous JS port had divergence risk (XOR semantics, integer truncation,
RNG implementation). With Pyodide the result is provably identical to the
terminal demo — same numbers, same scatter pattern (zigzag from
'w ^ (L >> 1)' XOR coupling is the actual behavior).
First paint trade-off: ~5 s Pyodide+numpy download (cached after first
visit). UI shows progress bar during init, slider/reset disabled until
ready.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README.md +5 -4
- index.html +143 -188
README.md
CHANGED
|
@@ -25,10 +25,11 @@ emergence = H(L) + H(R) − H(L, R) (bits)
|
|
| 25 |
|
| 26 |
## Tech
|
| 27 |
|
| 28 |
-
- Static HF Space (`sdk: static`) —
|
| 29 |
-
- `
|
| 30 |
-
-
|
| 31 |
-
-
|
|
|
|
| 32 |
|
| 33 |
## Sister
|
| 34 |
|
|
|
|
| 25 |
|
| 26 |
## Tech
|
| 27 |
|
| 28 |
+
- Static HF Space (`sdk: static`) — no cold start.
|
| 29 |
+
- **Pyodide** — runs `byte_emergence_demo.py` 100% verbatim in the browser via WebAssembly. numpy included.
|
| 30 |
+
- First paint after Pyodide load (~5 s download, then cached): metrics tick at ~20-30 fps.
|
| 31 |
+
- 16-bin histogram entropy, 256-sample window per tick.
|
| 32 |
+
- Reproducible noise: `np.random.default_rng(t)` seeded by frame index — same as the Python original.
|
| 33 |
|
| 34 |
## Sister
|
| 35 |
|
index.html
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
<html lang="ko">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
|
|
|
| 5 |
<style>
|
| 6 |
:root {
|
| 7 |
--bg: #1a1d24;
|
|
@@ -29,71 +30,23 @@
|
|
| 29 |
max-width: 1100px;
|
| 30 |
margin: 0 auto;
|
| 31 |
}
|
| 32 |
-
.header {
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
margin-bottom: 20px;
|
| 37 |
-
}
|
| 38 |
-
.title {
|
| 39 |
-
font-size: 17px;
|
| 40 |
-
font-weight: 500;
|
| 41 |
-
margin: 0;
|
| 42 |
-
}
|
| 43 |
-
.status {
|
| 44 |
-
color: var(--muted);
|
| 45 |
-
font-size: 13px;
|
| 46 |
-
margin-top: 6px;
|
| 47 |
-
}
|
| 48 |
-
.metrics {
|
| 49 |
-
display: flex;
|
| 50 |
-
gap: 28px;
|
| 51 |
-
font-family: "SF Mono", "Menlo", monospace;
|
| 52 |
-
}
|
| 53 |
.metric { text-align: center; }
|
| 54 |
.metric-label { color: var(--muted); font-size: 11px; letter-spacing: 0.5px; }
|
| 55 |
.metric-value { font-size: 18px; font-weight: 600; margin-top: 2px; }
|
| 56 |
-
.canvas-area {
|
| 57 |
-
background: var(--bg);
|
| 58 |
-
border-radius: 10px;
|
| 59 |
-
padding: 20px;
|
| 60 |
-
margin: 16px 0 20px 0;
|
| 61 |
-
}
|
| 62 |
.stream-row { margin-bottom: 14px; }
|
| 63 |
-
.stream-label {
|
| 64 |
-
font-weight: 600;
|
| 65 |
-
margin-bottom: 6px;
|
| 66 |
-
font-size: 14px;
|
| 67 |
-
}
|
| 68 |
canvas { display: block; }
|
| 69 |
-
.stream-canvas {
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
background: rgba(35, 39, 48, 0.6);
|
| 73 |
-
border-radius: 6px;
|
| 74 |
-
}
|
| 75 |
-
hr.divider {
|
| 76 |
-
border: none;
|
| 77 |
-
border-top: 1px solid var(--border);
|
| 78 |
-
margin: 18px 0;
|
| 79 |
-
}
|
| 80 |
-
.bottom-row {
|
| 81 |
-
display: flex;
|
| 82 |
-
align-items: center;
|
| 83 |
-
justify-content: center;
|
| 84 |
-
gap: 32px;
|
| 85 |
-
padding: 8px 0 0 0;
|
| 86 |
-
}
|
| 87 |
.scatter-wrap { text-align: center; }
|
| 88 |
-
.scatter-canvas {
|
| 89 |
-
|
| 90 |
-
height: 240px;
|
| 91 |
-
}
|
| 92 |
-
.scatter-axis {
|
| 93 |
-
color: var(--muted);
|
| 94 |
-
font-size: 12px;
|
| 95 |
-
margin-top: 4px;
|
| 96 |
-
}
|
| 97 |
.badge {
|
| 98 |
padding: 10px 22px;
|
| 99 |
border: 2px solid var(--emerald);
|
|
@@ -102,65 +55,33 @@
|
|
| 102 |
font-weight: 700;
|
| 103 |
font-size: 14px;
|
| 104 |
letter-spacing: 0.6px;
|
| 105 |
-
opacity: 0
|
| 106 |
-
|
|
|
|
| 107 |
}
|
| 108 |
.badge.active {
|
| 109 |
opacity: 1;
|
|
|
|
| 110 |
box-shadow: 0 0 22px rgba(109, 216, 112, 0.55), inset 0 0 12px rgba(109, 216, 112, 0.2);
|
|
|
|
| 111 |
}
|
| 112 |
-
.controls {
|
| 113 |
-
display: flex;
|
| 114 |
-
align-items: center;
|
| 115 |
-
gap: 16px;
|
| 116 |
-
padding-top: 18px;
|
| 117 |
-
border-top: 1px solid var(--border);
|
| 118 |
-
margin-top: 16px;
|
| 119 |
-
}
|
| 120 |
.controls .label { color: var(--muted); font-size: 13px; min-width: 130px; }
|
| 121 |
-
input[type="range"] {
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
height: 6px;
|
| 126 |
-
background: var(--border);
|
| 127 |
-
border-radius: 3px;
|
| 128 |
-
outline: none;
|
| 129 |
-
}
|
| 130 |
-
input[type="range"]::-webkit-slider-thumb {
|
| 131 |
-
-webkit-appearance: none;
|
| 132 |
-
width: 18px; height: 18px;
|
| 133 |
-
background: var(--blue);
|
| 134 |
-
border-radius: 50%;
|
| 135 |
-
cursor: pointer;
|
| 136 |
-
}
|
| 137 |
-
.coup-val {
|
| 138 |
-
background: var(--bg);
|
| 139 |
-
border: 1px solid var(--border-bright);
|
| 140 |
-
border-radius: 8px;
|
| 141 |
-
padding: 6px 14px;
|
| 142 |
-
font-family: "SF Mono", monospace;
|
| 143 |
-
min-width: 48px;
|
| 144 |
-
text-align: center;
|
| 145 |
-
font-size: 14px;
|
| 146 |
-
}
|
| 147 |
-
.reset-btn {
|
| 148 |
-
background: var(--border);
|
| 149 |
-
color: var(--text);
|
| 150 |
-
border: 1px solid var(--border-bright);
|
| 151 |
-
border-radius: 24px;
|
| 152 |
-
padding: 8px 28px;
|
| 153 |
-
cursor: pointer;
|
| 154 |
-
font-size: 13px;
|
| 155 |
-
transition: background 0.15s;
|
| 156 |
-
}
|
| 157 |
.reset-btn:hover { background: var(--border-bright); }
|
| 158 |
-
.
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
</style>
|
| 166 |
</head>
|
|
@@ -169,13 +90,14 @@
|
|
| 169 |
<div class="header">
|
| 170 |
<div>
|
| 171 |
<h2 class="title">데이터 스트림 결합 및 창발성 시각화</h2>
|
| 172 |
-
<div class="status" id="status">
|
|
|
|
| 173 |
</div>
|
| 174 |
<div class="metrics">
|
| 175 |
-
<div class="metric"><div class="metric-label">H(L)</div><div class="metric-value" id="m-hl">
|
| 176 |
-
<div class="metric"><div class="metric-label">H(R)</div><div class="metric-value" id="m-hr">
|
| 177 |
-
<div class="metric"><div class="metric-label">H(L,R)</div><div class="metric-value" id="m-hj">
|
| 178 |
-
<div class="metric"><div class="metric-label">창발성 (MI)</div><div class="metric-value" id="m-mi" style="color:#c8f7cd">
|
| 179 |
</div>
|
| 180 |
</div>
|
| 181 |
|
|
@@ -200,87 +122,82 @@
|
|
| 200 |
|
| 201 |
<div class="controls">
|
| 202 |
<span class="label">결합 강도 (Coupling)</span>
|
| 203 |
-
<input type="range" id="coup" min="0" max="100" value="80">
|
| 204 |
<span class="coup-val" id="coup-val">0.80</span>
|
| 205 |
-
<button class="reset-btn" id="reset">초기화</button>
|
| 206 |
</div>
|
| 207 |
|
| 208 |
<div class="footnote">
|
| 209 |
emergence = H(L) + H(R) − H(L,R) — 두 스트림이 독립이면 0, 결합하면 양수로 떠오릅니다.<br>
|
| 210 |
-
|
|
|
|
| 211 |
</div>
|
| 212 |
</div>
|
| 213 |
|
|
|
|
| 214 |
<script>
|
| 215 |
"use strict";
|
| 216 |
-
const VOCAB = 256, BINS = 16, N = 256;
|
| 217 |
-
const $ = id => document.getElementById(id);
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
const cvL = $("cv-l"), cvR = $("cv-r"), cvS = $("cv-s");
|
| 220 |
const ctxL = cvL.getContext("2d"), ctxR = cvR.getContext("2d"), ctxS = cvS.getContext("2d");
|
| 221 |
const slider = $("coup"), coupVal = $("coup-val"), resetBtn = $("reset");
|
| 222 |
const elHL = $("m-hl"), elHR = $("m-hr"), elHJ = $("m-hj"), elMI = $("m-mi");
|
| 223 |
-
const elBadge = $("badge"), elStatus = $("status");
|
| 224 |
|
|
|
|
| 225 |
let t = 0;
|
|
|
|
|
|
|
| 226 |
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
return () => {
|
| 231 |
-
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
| 232 |
-
return s / 0x100000000;
|
| 233 |
-
};
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
function engineStep(tick, coupling) {
|
| 237 |
-
const rng = lcg(tick * 1009 + 31);
|
| 238 |
-
const L = new Uint8Array(N), R = new Uint8Array(N);
|
| 239 |
-
const phase = tick * 0.04;
|
| 240 |
-
for (let i = 0; i < N; i++) {
|
| 241 |
-
const lr = (rng() * VOCAB) | 0;
|
| 242 |
-
const rr = (rng() * VOCAB) | 0;
|
| 243 |
-
const w = (((Math.sin(i / N * 4 * Math.PI + phase) + 1) * 127.5) | 0);
|
| 244 |
-
const lv = ((1 - coupling) * lr + coupling * w) | 0;
|
| 245 |
-
const rv = ((1 - coupling) * rr + coupling * (w ^ (lv >> 1))) | 0;
|
| 246 |
-
L[i] = lv & 0xFF;
|
| 247 |
-
R[i] = rv & 0xFF;
|
| 248 |
-
}
|
| 249 |
-
return [L, R];
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
function entropy(arr) {
|
| 253 |
-
const h = new Int32Array(BINS);
|
| 254 |
-
for (let i = 0; i < arr.length; i++) {
|
| 255 |
-
h[(arr[i] * BINS / VOCAB) | 0]++;
|
| 256 |
-
}
|
| 257 |
-
const inv = 1 / arr.length;
|
| 258 |
-
let H = 0;
|
| 259 |
-
for (let i = 0; i < BINS; i++) {
|
| 260 |
-
if (h[i] > 0) {
|
| 261 |
-
const p = h[i] * inv;
|
| 262 |
-
H -= p * Math.log2(p);
|
| 263 |
-
}
|
| 264 |
-
}
|
| 265 |
-
return H;
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
function jointEntropy(L, R) {
|
| 269 |
-
const h = new Int32Array(BINS * BINS);
|
| 270 |
-
for (let i = 0; i < L.length; i++) {
|
| 271 |
-
const li = (L[i] * BINS / VOCAB) | 0;
|
| 272 |
-
const ri = (R[i] * BINS / VOCAB) | 0;
|
| 273 |
-
h[li * BINS + ri]++;
|
| 274 |
-
}
|
| 275 |
-
const inv = 1 / L.length;
|
| 276 |
-
let H = 0;
|
| 277 |
-
for (let i = 0; i < h.length; i++) {
|
| 278 |
-
if (h[i] > 0) {
|
| 279 |
-
const p = h[i] * inv;
|
| 280 |
-
H -= p * Math.log2(p);
|
| 281 |
-
}
|
| 282 |
-
}
|
| 283 |
-
return H;
|
| 284 |
}
|
| 285 |
|
| 286 |
function resizeCanvas(canvas) {
|
|
@@ -325,7 +242,6 @@ function drawScatter(L, R) {
|
|
| 325 |
}
|
| 326 |
}
|
| 327 |
|
| 328 |
-
let dimsL, dimsR;
|
| 329 |
function applyResize() {
|
| 330 |
dimsL = resizeCanvas(cvL);
|
| 331 |
dimsR = resizeCanvas(cvR);
|
|
@@ -333,13 +249,36 @@ function applyResize() {
|
|
| 333 |
applyResize();
|
| 334 |
window.addEventListener("resize", applyResize);
|
| 335 |
|
| 336 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
const c = parseInt(slider.value, 10) / 100;
|
| 338 |
-
const
|
| 339 |
-
const
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
const
|
|
|
|
|
|
|
| 343 |
|
| 344 |
elHL.textContent = hL.toFixed(2);
|
| 345 |
elHR.textContent = hR.toFixed(2);
|
|
@@ -362,7 +301,20 @@ function frame() {
|
|
| 362 |
drawScatter(L, R);
|
| 363 |
|
| 364 |
t++;
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
}
|
| 367 |
|
| 368 |
slider.addEventListener("input", () => {
|
|
@@ -374,7 +326,10 @@ resetBtn.addEventListener("click", () => {
|
|
| 374 |
coupVal.textContent = "0.80";
|
| 375 |
});
|
| 376 |
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
| 378 |
</script>
|
| 379 |
</body>
|
| 380 |
</html>
|
|
|
|
| 2 |
<html lang="ko">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
+
<title>Anima Emergence — byte-level mutual information</title>
|
| 6 |
<style>
|
| 7 |
:root {
|
| 8 |
--bg: #1a1d24;
|
|
|
|
| 30 |
max-width: 1100px;
|
| 31 |
margin: 0 auto;
|
| 32 |
}
|
| 33 |
+
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
|
| 34 |
+
.title { font-size: 17px; font-weight: 500; margin: 0; }
|
| 35 |
+
.status { color: var(--muted); font-size: 13px; margin-top: 6px; }
|
| 36 |
+
.metrics { display: flex; gap: 28px; font-family: "SF Mono", "Menlo", monospace; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
.metric { text-align: center; }
|
| 38 |
.metric-label { color: var(--muted); font-size: 11px; letter-spacing: 0.5px; }
|
| 39 |
.metric-value { font-size: 18px; font-weight: 600; margin-top: 2px; }
|
| 40 |
+
.canvas-area { background: var(--bg); border-radius: 10px; padding: 20px; margin: 16px 0 20px 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
.stream-row { margin-bottom: 14px; }
|
| 42 |
+
.stream-label { font-weight: 600; margin-bottom: 6px; font-size: 14px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
canvas { display: block; }
|
| 44 |
+
.stream-canvas { width: 100%; height: 100px; background: rgba(35, 39, 48, 0.6); border-radius: 6px; }
|
| 45 |
+
hr.divider { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
|
| 46 |
+
.bottom-row { display: flex; align-items: center; justify-content: center; gap: 32px; padding: 8px 0 0 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
.scatter-wrap { text-align: center; }
|
| 48 |
+
.scatter-canvas { width: 240px; height: 240px; }
|
| 49 |
+
.scatter-axis { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
.badge {
|
| 51 |
padding: 10px 22px;
|
| 52 |
border: 2px solid var(--emerald);
|
|
|
|
| 55 |
font-weight: 700;
|
| 56 |
font-size: 14px;
|
| 57 |
letter-spacing: 0.6px;
|
| 58 |
+
opacity: 0;
|
| 59 |
+
visibility: hidden;
|
| 60 |
+
transition: opacity 0.25s, box-shadow 0.25s, visibility 0s linear 0.25s;
|
| 61 |
}
|
| 62 |
.badge.active {
|
| 63 |
opacity: 1;
|
| 64 |
+
visibility: visible;
|
| 65 |
box-shadow: 0 0 22px rgba(109, 216, 112, 0.55), inset 0 0 12px rgba(109, 216, 112, 0.2);
|
| 66 |
+
transition: opacity 0.25s, box-shadow 0.25s, visibility 0s linear 0s;
|
| 67 |
}
|
| 68 |
+
.controls { display: flex; align-items: center; gap: 16px; padding-top: 18px; border-top: 1px solid var(--border); margin-top: 16px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
.controls .label { color: var(--muted); font-size: 13px; min-width: 130px; }
|
| 70 |
+
input[type="range"] { flex: 1; -webkit-appearance: none; appearance: none; height: 6px; background: var(--border); border-radius: 3px; outline: none; }
|
| 71 |
+
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: var(--blue); border-radius: 50%; cursor: pointer; }
|
| 72 |
+
.coup-val { background: var(--bg); border: 1px solid var(--border-bright); border-radius: 8px; padding: 6px 14px; font-family: "SF Mono", monospace; min-width: 48px; text-align: center; font-size: 14px; }
|
| 73 |
+
.reset-btn { background: var(--border); color: var(--text); border: 1px solid var(--border-bright); border-radius: 24px; padding: 8px 28px; cursor: pointer; font-size: 13px; transition: background 0.15s; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
.reset-btn:hover { background: var(--border-bright); }
|
| 75 |
+
.reset-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 76 |
+
.footnote { color: var(--muted); font-size: 12px; text-align: center; margin-top: 18px; line-height: 1.6; }
|
| 77 |
+
.footnote code { background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 4px; }
|
| 78 |
+
.loading-bar {
|
| 79 |
+
height: 3px;
|
| 80 |
+
background: linear-gradient(90deg, var(--blue), var(--green));
|
| 81 |
+
width: 0%;
|
| 82 |
+
transition: width 0.3s;
|
| 83 |
+
border-radius: 2px;
|
| 84 |
+
margin-top: 4px;
|
| 85 |
}
|
| 86 |
</style>
|
| 87 |
</head>
|
|
|
|
| 90 |
<div class="header">
|
| 91 |
<div>
|
| 92 |
<h2 class="title">데이터 스트림 결합 및 창발성 시각화</h2>
|
| 93 |
+
<div class="status" id="status">Pyodide 로딩 중...</div>
|
| 94 |
+
<div class="loading-bar" id="loading-bar"></div>
|
| 95 |
</div>
|
| 96 |
<div class="metrics">
|
| 97 |
+
<div class="metric"><div class="metric-label">H(L)</div><div class="metric-value" id="m-hl">—</div></div>
|
| 98 |
+
<div class="metric"><div class="metric-label">H(R)</div><div class="metric-value" id="m-hr">—</div></div>
|
| 99 |
+
<div class="metric"><div class="metric-label">H(L,R)</div><div class="metric-value" id="m-hj">—</div></div>
|
| 100 |
+
<div class="metric"><div class="metric-label">창발성 (MI)</div><div class="metric-value" id="m-mi" style="color:#c8f7cd">—</div></div>
|
| 101 |
</div>
|
| 102 |
</div>
|
| 103 |
|
|
|
|
| 122 |
|
| 123 |
<div class="controls">
|
| 124 |
<span class="label">결합 강도 (Coupling)</span>
|
| 125 |
+
<input type="range" id="coup" min="0" max="100" value="80" disabled>
|
| 126 |
<span class="coup-val" id="coup-val">0.80</span>
|
| 127 |
+
<button class="reset-btn" id="reset" disabled>초기화</button>
|
| 128 |
</div>
|
| 129 |
|
| 130 |
<div class="footnote">
|
| 131 |
emergence = H(L) + H(R) − H(L,R) — 두 스트림이 독립이면 0, 결합하면 양수로 떠오릅니다.<br>
|
| 132 |
+
원본: <code>anima/byte_emergence_demo.py</code> 를 Pyodide 로 브라우저에서 100% 그대로 실행합니다 (numpy 포함).<br>
|
| 133 |
+
zigzag 산점도는 <code>w ^ (L >> 1)</code> XOR 결합의 정상 동작 — 비트 순서가 뒤집히는 지점마다 점이 다른 위치로 매핑됩니다.
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
|
| 137 |
+
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
|
| 138 |
<script>
|
| 139 |
"use strict";
|
|
|
|
|
|
|
| 140 |
|
| 141 |
+
// ─────────────────────────────────────────────────────────────────────────
|
| 142 |
+
// Original Python — verbatim from anima/byte_emergence_demo.py
|
| 143 |
+
// (only the print loop is replaced with a per-tick step() helper for JS)
|
| 144 |
+
// ─────────────────────────────────────────────────────────────────────────
|
| 145 |
+
const PY_SOURCE = `
|
| 146 |
+
import numpy as np
|
| 147 |
+
|
| 148 |
+
VOCAB, BINS = 256, 16
|
| 149 |
+
|
| 150 |
+
def H(x):
|
| 151 |
+
h, _ = np.histogram(x, bins=BINS, range=(0, VOCAB))
|
| 152 |
+
p = h / max(h.sum(), 1); p = p[p > 0]
|
| 153 |
+
return float(-(p * np.log2(p)).sum())
|
| 154 |
+
|
| 155 |
+
def H_joint(L, R):
|
| 156 |
+
jh, *_ = np.histogram2d(L, R, bins=BINS, range=[[0, VOCAB], [0, VOCAB]])
|
| 157 |
+
p = jh / max(jh.sum(), 1); p = p[p > 0]
|
| 158 |
+
return float(-(p * np.log2(p)).sum())
|
| 159 |
+
|
| 160 |
+
def emergence(L, R):
|
| 161 |
+
return max(0.0, H(L) + H(R) - H_joint(L, R))
|
| 162 |
+
|
| 163 |
+
def engine_step(t, N=1024, coupling=0.8):
|
| 164 |
+
"""Tension wave couples L and R — integration at byte level."""
|
| 165 |
+
rng = np.random.default_rng(t)
|
| 166 |
+
L = rng.integers(0, VOCAB, N)
|
| 167 |
+
R = rng.integers(0, VOCAB, N)
|
| 168 |
+
w = ((np.sin(np.linspace(0, 4 * np.pi, N) + t * 0.2) + 1) * 127).astype(int)
|
| 169 |
+
L = ((1 - coupling) * L + coupling * w).astype(int) % VOCAB
|
| 170 |
+
R = ((1 - coupling) * R + coupling * (w ^ (L >> 1))).astype(int) % VOCAB
|
| 171 |
+
return L, R
|
| 172 |
+
|
| 173 |
+
# ── thin adapter for the JS layer ────────────────────────────────────────
|
| 174 |
+
def step(t, coupling, N=256):
|
| 175 |
+
L, R = engine_step(t, N=N, coupling=coupling)
|
| 176 |
+
return {
|
| 177 |
+
"L": L.astype(np.uint8).tobytes(),
|
| 178 |
+
"R": R.astype(np.uint8).tobytes(),
|
| 179 |
+
"hL": H(L),
|
| 180 |
+
"hR": H(R),
|
| 181 |
+
"hJ": H_joint(L, R),
|
| 182 |
+
"mi": emergence(L, R),
|
| 183 |
+
}
|
| 184 |
+
`;
|
| 185 |
+
|
| 186 |
+
const $ = id => document.getElementById(id);
|
| 187 |
const cvL = $("cv-l"), cvR = $("cv-r"), cvS = $("cv-s");
|
| 188 |
const ctxL = cvL.getContext("2d"), ctxR = cvR.getContext("2d"), ctxS = cvS.getContext("2d");
|
| 189 |
const slider = $("coup"), coupVal = $("coup-val"), resetBtn = $("reset");
|
| 190 |
const elHL = $("m-hl"), elHR = $("m-hr"), elHJ = $("m-hj"), elMI = $("m-mi");
|
| 191 |
+
const elBadge = $("badge"), elStatus = $("status"), elLoadBar = $("loading-bar");
|
| 192 |
|
| 193 |
+
let pyodide = null;
|
| 194 |
let t = 0;
|
| 195 |
+
let running = false;
|
| 196 |
+
let dimsL, dimsR;
|
| 197 |
|
| 198 |
+
function setLoading(pct, label) {
|
| 199 |
+
elLoadBar.style.width = pct + "%";
|
| 200 |
+
if (label) elStatus.textContent = label;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
}
|
| 202 |
|
| 203 |
function resizeCanvas(canvas) {
|
|
|
|
| 242 |
}
|
| 243 |
}
|
| 244 |
|
|
|
|
| 245 |
function applyResize() {
|
| 246 |
dimsL = resizeCanvas(cvL);
|
| 247 |
dimsR = resizeCanvas(cvR);
|
|
|
|
| 249 |
applyResize();
|
| 250 |
window.addEventListener("resize", applyResize);
|
| 251 |
|
| 252 |
+
async function init() {
|
| 253 |
+
setLoading(15, "Pyodide 다운로드 중...");
|
| 254 |
+
pyodide = await loadPyodide({
|
| 255 |
+
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.4/full/",
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
setLoading(50, "numpy 로드 중... (~7MB)");
|
| 259 |
+
await pyodide.loadPackage("numpy");
|
| 260 |
+
|
| 261 |
+
setLoading(85, "byte_emergence_demo.py 실행 중...");
|
| 262 |
+
pyodide.runPython(PY_SOURCE);
|
| 263 |
+
|
| 264 |
+
setLoading(100, "준비 완료. 슬라이더를 움직여보세요.");
|
| 265 |
+
slider.disabled = false;
|
| 266 |
+
resetBtn.disabled = false;
|
| 267 |
+
setTimeout(() => { elLoadBar.style.opacity = 0; }, 600);
|
| 268 |
+
|
| 269 |
+
running = true;
|
| 270 |
+
loop();
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
function tickOnce() {
|
| 274 |
const c = parseInt(slider.value, 10) / 100;
|
| 275 |
+
const proxy = pyodide.runPython(`step(${t}, ${c}, N=256)`);
|
| 276 |
+
const result = proxy.toJs({dict_converter: Object.fromEntries});
|
| 277 |
+
proxy.destroy();
|
| 278 |
+
|
| 279 |
+
const L = new Uint8Array(result.L);
|
| 280 |
+
const R = new Uint8Array(result.R);
|
| 281 |
+
const hL = result.hL, hR = result.hR, hJ = result.hJ, mi = result.mi;
|
| 282 |
|
| 283 |
elHL.textContent = hL.toFixed(2);
|
| 284 |
elHR.textContent = hR.toFixed(2);
|
|
|
|
| 301 |
drawScatter(L, R);
|
| 302 |
|
| 303 |
t++;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function loop() {
|
| 307 |
+
if (!running) return;
|
| 308 |
+
try {
|
| 309 |
+
tickOnce();
|
| 310 |
+
} catch (e) {
|
| 311 |
+
elStatus.textContent = "런타임 에러: " + e.message;
|
| 312 |
+
console.error(e);
|
| 313 |
+
running = false;
|
| 314 |
+
return;
|
| 315 |
+
}
|
| 316 |
+
// Pyodide 기준 ~20-30fps; 너무 빠르면 GC 부담.
|
| 317 |
+
setTimeout(() => requestAnimationFrame(loop), 30);
|
| 318 |
}
|
| 319 |
|
| 320 |
slider.addEventListener("input", () => {
|
|
|
|
| 326 |
coupVal.textContent = "0.80";
|
| 327 |
});
|
| 328 |
|
| 329 |
+
init().catch(err => {
|
| 330 |
+
elStatus.textContent = "Pyodide 로딩 실패: " + err.message;
|
| 331 |
+
console.error(err);
|
| 332 |
+
});
|
| 333 |
</script>
|
| 334 |
</body>
|
| 335 |
</html>
|