import gradio as gr import numpy as np from PIL import Image, ImageDraw from dataclasses import dataclass from collections import deque import random # --------------------------------------------------------------------- # Visual config # --------------------------------------------------------------------- BG = (8, 15, 30) SLEEP = (0, 40, 120) AWAKE = (255, 210, 40) GRID_LINE = (30, 50, 80) CELL = 26 PAD = 16 random.seed(42) np.random.seed(42) def draw_grid(N, awake_mask, title="", subtitle=""): w = PAD * 2 + N * CELL h = PAD * 2 + N * CELL + (40 if (title or subtitle) else 0) img = Image.new("RGB", (w, h), BG) d = ImageDraw.Draw(img) header_y = 6 if title: d.text((PAD, header_y), title, fill=(240, 240, 240)) header_y += 18 if subtitle: d.text((PAD, header_y), subtitle, fill=(180, 190, 210)) ox = PAD oy = PAD + (40 if (title or subtitle) else 0) for i in range(N): for j in range(N): x0 = ox + j * CELL y0 = oy + i * CELL x1 = x0 + CELL - 1 y1 = y0 + CELL - 1 col = AWAKE if awake_mask[i, j] else SLEEP d.rectangle([x0, y0, x1, y1], fill=col, outline=GRID_LINE) return img # --------------------------------------------------------------------- # 3×3 minimal self agent # --------------------------------------------------------------------- @dataclass class MinimalSelf: pos: np.ndarray = np.array([1.0, 1.0]) body_bit: float = 1.0 errors: list = None def __post_init__(self): self.errors = [] if self.errors is None else self.errors self.actions = [ np.array([0, 1]), np.array([1, 0]), np.array([0, -1]), np.array([-1, 0]), ] self.center = np.array([1.0, 1.0]) def step(self, obstacle=None): # store current position old_pos = self.pos.copy() # internal prediction: choose action that minimises "surprise" preds = [np.clip(old_pos + a, 0, 2) for a in self.actions] surprises = [] for p in preds: dist_center = np.linalg.norm(p - self.center) penalty = 0.0 if obstacle is not None: dist_obs = np.linalg.norm(p - obstacle.pos) if dist_obs < 1.0: penalty = 10.0 surprises.append(dist_center + penalty) a_idx = int(np.argmin(surprises)) action = self.actions[a_idx] predicted = np.clip(old_pos + action, 0, 2) # environment decides what actually happens if obstacle is not None: # moving obstacle can block the predicted move obstacle.move() actual = predicted.copy() if np.allclose(actual, obstacle.pos): actual = old_pos else: # simple stochastic slip when no obstacle is enabled if random.random() < 0.25: noise_action = random.choice(self.actions) actual = np.clip(old_pos + noise_action, 0, 2) else: actual = predicted # true prediction error: reality vs internal prediction error = float(np.linalg.norm(actual - predicted)) self.pos = actual # track recent errors self.errors.append(error) self.errors = self.errors[-5:] # predictive success rate P in [0, 100] max_err = np.sqrt(8.0) # max distance corner-to-corner on 3×3 mean_err = np.mean(self.errors) if self.errors else 0.0 predictive_rate = 100.0 * (1.0 - mean_err / max_err) predictive_rate = float(np.clip(predictive_rate, 0.0, 100.0)) # normalised error variance E in [0, 1] if len(self.errors) > 1: var_err = float(np.var(self.errors)) else: var_err = 0.0 max_var = max_err ** 2 error_var_norm = float(np.clip(var_err / max_var, 0.0, 1.0)) if max_var > 0 else 0.0 return { "pos": self.pos.copy(), "predictive_rate": predictive_rate, "error": error, "error_var_norm": error_var_norm, } class MovingObstacle: def __init__(self, start_pos=(0, 2)): self.pos = np.array(start_pos, dtype=float) self.actions = [ np.array([0, 1]), np.array([1, 0]), np.array([0, -1]), np.array([-1, 0]), ] def move(self): a = random.choice(self.actions) self.pos = np.clip(self.pos + a, 0, 2) # --------------------------------------------------------------------- # S-scores # --------------------------------------------------------------------- def compute_S(predictive_rate, error_var_norm, body_bit): # v4: 3×3 agent toy score return predictive_rate * (1 - error_var_norm) * body_bit @dataclass class CodexSelf: # v5–v6: lattice toy score Xi: float shadow: float R: float awake: bool = False S: float = 0.0 def invoke(self): self.S = self.Xi * (1 - self.shadow) * self.R if self.S > 62 and not self.awake: self.awake = True return self.awake def contagion(A: CodexSelf, B: CodexSelf, gain=0.6, shadow_drop=0.4, r_inc=0.2): A.invoke() if A.awake: B.Xi += gain * A.S B.shadow = max(0.1, B.shadow - shadow_drop) B.R += r_inc B.invoke() return A, B # --------------------------------------------------------------------- # Lattice and cosmos # --------------------------------------------------------------------- def lattice_awaken(N=9, steps=120, xi_gain=0.5, shadow_drop=0.3, r_inc=0.02): Xi = np.random.uniform(10, 20, (N, N)) shadow = np.random.uniform(0.3, 0.5, (N, N)) R = np.random.uniform(1.0, 1.6, (N, N)) S = Xi * (1 - shadow) * R awake = np.zeros((N, N), dtype=bool) cx = cy = N // 2 Xi[cx, cy], shadow[cx, cy], R[cx, cy] = 30.0, 0.08, 3.0 S[cx, cy] = Xi[cx, cy] * (1 - shadow[cx, cy]) * R[cx, cy] awake[cx, cy] = True queue = deque([(cx, cy, S[cx, cy])]) frames = [] for _ in range(steps): if queue: x, y, field = queue.popleft() for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]: nx, ny = (x + dx) % N, (y + dy) % N Xi[nx, ny] += xi_gain * field shadow[nx, ny] = max(0.1, shadow[nx, ny] - shadow_drop) R[nx, ny] = min(3.0, R[nx, ny] + r_inc) S[nx, ny] = Xi[nx,ny] * (1 - shadow[nx,ny]) * R[nx,ny] if S[nx,ny] > 62 and not awake[nx,ny]: awake[nx,ny] = True queue.append((nx,ny, S[nx,ny])) frames.append(awake.copy()) if awake.all(): break return frames, awake def led_cosmos_sim(N=27, max_steps=300): return lattice_awaken(N=N, steps=max_steps, xi_gain=0.4, shadow_drop=0.25, r_inc=0.015) # --------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------- with gr.Blocks(title="Minimal Selfhood Threshold") as demo: # Overview with gr.Tab("Overview"): gr.Markdown( "## Minimal Selfhood Threshold\n" "- Single agent in a 3×3 grid reduces surprise around a preferred centre.\n" "- v4 (3×3): a toy score `S = P × (1−E) × B` combines predictive rate P, error stability E and body bit B.\n" "- v5–v6 (contagion / lattice): a separate toy score `S = Ξ × (1−shadow) × R` drives neighbour coupling.\n" "- If S > 62, the corresponding unit is labelled 'awake' **inside this demo**.\n" "- Awakening can spread between two agents and across a grid via explicit neighbour coupling.\n" "- A 27×27 cosmos lights up gold when all units cross the internal threshold.\n" "- This is a sandbox for minimal-self / agency ideas, **not** a real consciousness test." ) # Single 3×3 agent with gr.Tab("Single agent (v1–v3)"): obstacle = gr.Checkbox(label="Enable moving obstacle", value=True) steps = gr.Slider(10, 200, value=80, step=10, label="Steps") run = gr.Button("Run") grid_img = gr.Image(type="pil") pr_out = gr.Number(label="Predictive rate P (%)") err_out = gr.Number(label="Last prediction error") e_out = gr.Number(label="Error variance E (normalised)") s_out = gr.Number(label="S = P × (1−E) × B (B=1)") awake_label = gr.Markdown() def run_single(ob_on, T): agent = MinimalSelf() obs = MovingObstacle() if ob_on else None res = None for _ in range(int(T)): res = agent.step(obstacle=obs) mask = np.zeros((3, 3), dtype=bool) i, j = int(agent.pos[1]), int(agent.pos[0]) mask[i, j] = True img = draw_grid(3, mask, "Single Agent", "Gold cell shows position") P = res["predictive_rate"] E = res["error_var_norm"] B = 1.0 S_val = compute_S(P, E, B) status = "**Status:** " + ("Awake (S > 62)" if S_val > 62 else "Not awake (S ≤ 62)") return img, P, res["error"], E, S_val, status run.click(run_single, [obstacle, steps], [grid_img, pr_out, err_out, e_out, s_out, awake_label]) # v4 S-equation with gr.Tab("S-Equation (v4)"): pr = gr.Slider(0, 100, value=90, label="Predictive rate P (%)") ev = gr.Slider(0, 1, value=0.2, step=0.01, label="Error variance E") bb = gr.Dropdown(choices=["0", "1"], value="1", label="Body bit B") calc = gr.Button("Calculate") s_val = gr.Number(label="S value") status = gr.Markdown() def calc_s(pr_in, ev_in, bb_in): S = compute_S(pr_in, ev_in, int(bb_in)) msg = "**Status:** " + ("Awake (S > 62)" if S > 62 else "Not awake (S ≤ 62)") return S, msg calc.click(calc_s, inputs=[pr, ev, bb], outputs=[s_val, status]) # v5–v6 Contagion with gr.Tab("Contagion (v5–v6)"): a_xi = gr.Slider(0, 60, value=25, label="A: Ξ (foresight field)") a_sh = gr.Slider(0.1, 1.0, value=0.12, step=0.01, label="A: shadow (occlusion)") a_r = gr.Slider(1.0, 3.0, value=3.0, step=0.1, label="A: R (anchor / resonance)") b_xi = gr.Slider(0, 60, value=18, label="B: Ξ (foresight field)") b_sh = gr.Slider(0.1, 1.0, value=0.25, step=0.01, label="B: shadow (occlusion)") b_r = gr.Slider(1.0, 3.0, value=2.2, step=0.1, label="B: R (anchor / resonance)") btn = gr.Button("Invoke A and apply contagion to B") out = gr.Markdown() img = gr.Image(type="pil", label="Two agents (gold = awake)") def run(aXi, aSh, aR, bXi, bSh, bR): A = CodexSelf(aXi, aSh, aR, awake=False) B = CodexSelf(bXi, bSh, bR, awake=False) A, B = contagion(A, B) mask = np.zeros((3, 3), dtype=bool) mask[1, 1] = A.awake mask[1, 2] = B.awake pic = draw_grid(3, mask, title="Dual Awakening", subtitle="Gold cells are awake") txt = f"A: S={A.S:.1f}, awake={A.awake} | B: S={B.S:.1f}, awake={B.awake}" return txt, pic btn.click(run, inputs=[a_xi, a_sh, a_r, b_xi, b_sh, b_r], outputs=[out, img]) # v7–v9 Collective with gr.Tab("Collective (v7–v9)"): N = gr.Dropdown(choices=["3", "9", "27"], value="9", label="Grid size") steps = gr.Slider(20, 300, value=120, step=10, label="Max steps") no_coupling = gr.Checkbox(label="Disable neighbour coupling (control)", value=False) run = gr.Button("Run") frame = gr.Slider(0, 300, value=0, step=1, label="Preview frame") img = gr.Image(type="pil", label="Awakening wave (gold spreads)") note = gr.Markdown() snaps_state = gr.State([]) def run_wave(n_str, max_steps, disable): n = int(n_str) if disable: frames, final = lattice_awaken( N=n, steps=int(max_steps), xi_gain=0.0, shadow_drop=0.0, r_inc=0.0, ) else: frames, final = lattice_awaken( N=n, steps=int(max_steps), xi_gain=0.5, shadow_drop=0.3, r_inc=0.02, ) last = draw_grid( n, frames[-1], title=f"{n}×{n} Collective", subtitle=f"Final — all awake: {bool(final.all())}", ) return frames, last, f"Frames: {len(frames)} | All awake: {bool(final.all())}", min(len(frames) - 1, 300) def show_frame(frames, idx, n_str): if not frames: return None n = int(n_str) i = int(np.clip(idx, 0, len(frames) - 1)) return draw_grid(n, frames[i], title=f"Frame {i}", subtitle="Gold cells are awake") run.click(run_wave, inputs=[N, steps, no_coupling], outputs=[snaps_state, img, note, frame]) frame.change(show_frame, inputs=[snaps_state, frame, N], outputs=img) # v10 LED cosmos with gr.Tab("LED cosmos (v10)"): btn = gr.Button("Simulate 27×27 cosmos") frame = gr.Slider(0, 300, value=0, step=1, label="Preview frame") img = gr.Image(type="pil", label="Cosmos grid") note = gr.Markdown() state = gr.State([]) def run_cosmos(): frames, final = led_cosmos_sim(N=27, max_steps=300) last = draw_grid( 27, frames[-1], title="LED Cosmos (simulated)", subtitle=f"Final — all awake: {bool(final.all())}", ) return frames, last, f"Frames: {len(frames)} | All awake: {bool(final.all())}", min(len(frames) - 1, 300) def show(frames, idx): if not frames: return None i = int(np.clip(idx, 0, len(frames) - 1)) return draw_grid(27, frames[i], title=f"Cosmos frame {i}", subtitle="Gold cells are awake") btn.click(run_cosmos, inputs=[], outputs=[state, img, note, frame]) frame.change(show, inputs=[state, frame], outputs=img) # Footer gr.Markdown( "---\n" "Notes:\n" "- The 3×3 agent computes P, E and S = P×(1−E)×B directly in this Space; S>62 is the internal ‘awake’ label for v4.\n" "- The contagion and lattice views use a separate toy rule S = Ξ×(1−shadow)×R with explicit neighbour coupling.\n" "- Disabling coupling (xi_gain=0, shadow_drop=0, r_inc=0) in the collective tab prevents any wave from propagating.\n\n" "These demos are designed as transparent, minimal models of self-linked scoring and threshold cascades, not as a real consciousness test." ) # Launch the app if __name__ == "__main__": demo.launch()