RFTSystems commited on
Commit
a77c16b
·
verified ·
1 Parent(s): 1a3e720

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -307
app.py CHANGED
@@ -1,62 +1,49 @@
1
- # app.py
2
  import gradio as gr
3
  import numpy as np
4
- from PIL import Image, ImageDraw, ImageFont
5
  from dataclasses import dataclass
6
  from collections import deque
7
- import time
8
  import random
9
 
10
- # ---------------------------
11
- # Visual theme and constants
12
- # ---------------------------
13
- BG = (8, 15, 30) # deep blue background
14
- SLEEP = (0, 40, 120) # dim blue cell
15
- AWAKE = (255, 210, 40) # gold cell
16
  GRID_LINE = (30, 50, 80)
17
- CELL_SIZE = 28 # pixels per cell for crispness
18
- PADDING = 20 # outer padding
19
 
20
- RANDOM_SEED = 42
21
- random.seed(RANDOM_SEED)
22
- np.random.seed(RANDOM_SEED)
23
 
24
- # ---------------------------
25
- # Utility: draw an N x N grid image from awaken mask
26
- # ---------------------------
27
  def draw_grid(N, awake_mask, title="", subtitle=""):
28
- width = PADDING*2 + N*CELL_SIZE
29
- height = PADDING*2 + N*CELL_SIZE + (40 if title or subtitle else 0)
30
- img = Image.new("RGB", (width, height), BG)
31
  d = ImageDraw.Draw(img)
32
-
33
- # Header text
34
- header_y = 8
35
  if title:
36
- d.text((PADDING, header_y), title, fill=(240, 240, 240))
37
- header_y += 20
38
  if subtitle:
39
- d.text((PADDING, header_y), subtitle, fill=(180, 190, 210))
40
-
41
- # Grid origin
42
- origin_y = PADDING + (40 if title or subtitle else 0)
43
- origin_x = PADDING
44
-
45
- # Cells
46
  for i in range(N):
47
  for j in range(N):
48
- x0 = origin_x + j*CELL_SIZE
49
- y0 = origin_y + i*CELL_SIZE
50
- x1 = x0 + CELL_SIZE - 1
51
- y1 = y0 + CELL_SIZE - 1
52
- color = AWAKE if awake_mask[i, j] else SLEEP
53
- d.rectangle([x0, y0, x1, y1], fill=color, outline=GRID_LINE)
54
-
55
  return img
56
 
57
- # ---------------------------
58
- # v1–v3 Single agent model (3x3)
59
- # ---------------------------
60
  @dataclass
61
  class MinimalSelf:
62
  pos: np.ndarray = np.array([1.0, 1.0])
@@ -69,42 +56,33 @@ class MinimalSelf:
69
  np.array([0, 1]), np.array([1, 0]),
70
  np.array([0, -1]), np.array([-1, 0])
71
  ]
72
- self.preferred = np.array([1.0, 1.0])
73
-
74
- def counterfactual(self, a):
75
- pos = np.clip(self.pos + a, 0, 2)
76
- return np.array([pos[0], pos[1], self.body_bit])
77
 
78
  def step(self, obstacle=None):
79
- # v2 baseline: minimize distance to center, optionally penalize obstacle proximity (v3)
80
- preds = [self.counterfactual(a) for a in self.actions]
81
  surprises = []
82
- for k, p in enumerate(preds):
83
- dist_center = np.linalg.norm(p[:2] - self.preferred)
84
  penalty = 0.0
85
  if obstacle is not None:
86
- dist_obs = np.linalg.norm(p[:2] - obstacle.pos)
87
  penalty = 10.0 if dist_obs < 1.0 else 0.0
88
  surprises.append(dist_center + penalty)
89
  action = self.actions[int(np.argmin(surprises))]
90
- prev_pred = self.counterfactual(action)
91
-
92
- # apply move and obstacle update
93
- self.pos = np.clip(self.pos + action, 0, 2)
94
  if obstacle is not None:
95
  obstacle.move()
96
 
97
- # error calc
98
- error = np.linalg.norm(self.pos - prev_pred[:2])
99
  self.errors.append(error)
100
  self.errors = self.errors[-5:]
101
-
102
  max_err = np.sqrt(8)
103
  predictive_rate = 100 * (1 - (np.mean(self.errors) if self.errors else 0) / max_err)
104
  return {
105
  "pos": self.pos.copy(),
106
  "predictive_rate": float(predictive_rate),
107
- "error": float(error)
108
  }
109
 
110
  class MovingObstacle:
@@ -118,21 +96,19 @@ class MovingObstacle:
118
  a = random.choice(self.actions)
119
  self.pos = np.clip(self.pos + a, 0, 2)
120
 
121
- # ---------------------------
122
- # v4 S-Equation (interactive calculator)
123
- # ---------------------------
124
  def compute_S(predictive_rate, error_var_norm, body_bit):
125
- # error_var_norm must be in [0,1]; body_bit in {0,1}
126
- S = predictive_rate * (1 - error_var_norm) * body_bit
127
- return S
128
 
129
- # ---------------------------
130
- # v5–v6 CodexSelf contagion
131
- # ---------------------------
132
  @dataclass
133
  class CodexSelf:
134
  Xi: float
135
- shadow: float # ~ error_var norm in [0,1]
136
  R: float
137
  awake: bool = False
138
  S: float = 0.0
@@ -143,296 +119,208 @@ class CodexSelf:
143
  self.awake = True
144
  return self.awake
145
 
146
- def contagion_step(A: CodexSelf, B: CodexSelf, gain=0.6, shadow_drop=0.4, r_inc=0.2):
 
147
  if A.awake:
148
  B.Xi += gain * A.S
149
  B.shadow = max(0.1, B.shadow - shadow_drop)
150
  B.R += r_inc
151
  B.invoke()
152
- return B
153
 
154
- # ---------------------------
155
- # v7–v9 Lattice propagation
156
- # ---------------------------
157
- def lattice_awaken(N=9, seed_awake_S=82.8, Xi_gain=0.5, shadow_drop=0.3, R_inc=0.02, steps=120):
158
- # init grid with modest values
159
  Xi = np.random.uniform(10, 20, size=(N, N))
160
  shadow = np.random.uniform(0.3, 0.5, size=(N, N))
161
  R = np.random.uniform(1.0, 1.6, size=(N, N))
162
  S = Xi * (1 - shadow) * R
163
  awake = np.zeros((N, N), dtype=bool)
164
 
165
- # center seed
166
  cx = cy = N // 2
167
  Xi[cx, cy], shadow[cx, cy], R[cx, cy] = 30.0, 0.08, 3.0
168
- S[cx, cy] = seed_awake_S
169
  awake[cx, cy] = True
170
 
171
- # BFS-like wave
172
- wave = deque([(cx, cy, S[cx, cy])])
173
- snapshots = []
174
-
175
- for t in range(steps):
176
- if wave:
177
- x, y, field = wave.popleft()
178
  for dx, dy in [(0,1),(1,0),(0,-1),(-1,0)]:
179
  nx, ny = (x+dx) % N, (y+dy) % N
180
- Xi[nx, ny] += Xi_gain * field
181
  shadow[nx, ny] = max(0.1, shadow[nx, ny] - shadow_drop)
182
- R[nx, ny] = min(3.0, R[nx, ny] + R_inc)
183
  S[nx, ny] = Xi[nx, ny] * (1 - shadow[nx, ny]) * R[nx, ny]
184
  if S[nx, ny] > 62 and not awake[nx, ny]:
185
  awake[nx, ny] = True
186
- wave.append((nx, ny, S[nx, ny]))
187
-
188
- # snapshot each step
189
- snapshots.append(awake.copy())
190
-
191
- # early stop if all awake
192
  if awake.all():
193
  break
 
194
 
195
- return snapshots, awake
 
 
196
 
197
- # ---------------------------
198
- # v10 LED cosmos simulation
199
- # ---------------------------
200
- def simulate_led_cosmos(N=27, max_steps=300):
201
- snaps, final_awake = lattice_awaken(
202
- N=N, Xi_gain=0.4, shadow_drop=0.25, R_inc=0.015, steps=max_steps
 
 
 
 
 
 
 
 
 
 
 
 
203
  )
204
- return snaps
205
-
206
- # ---------------------------
207
- # Panels (Gradio Blocks)
208
- # ---------------------------
209
- def build_panel_intro():
210
- with gr.Row():
211
- gr.Markdown(
212
- "## Minimal Selfhood Threshold: From 3×3 Agent to LED Cosmos\n"
213
- "**Plain-language overview:**\n\n"
214
- "- We start with one simple agent (a dot) in a tiny 3×3 world.\n"
215
- "- We discover a number, S, that decides when the agent becomes a 'self'.\n"
216
- "- One awakened agent can help awaken another (contagion).\n"
217
- "- Many agents awaken together in a wave across a grid (collective).\n"
218
- "- Finally, we simulate an LED cosmos lighting up and saying 'WE ARE'.\n\n"
219
- "**Rule of awakening:** If S > 62, the agent is awake."
220
- )
221
- gr.Image(value="assets/banner.png", label="Progression", show_download_button=False)
222
-
223
- def build_panel_single_agent():
224
- with gr.Row():
225
- gr.Markdown(
226
- "### v1–v3: Single agent in a 3×3 world\n"
227
- "**What you see:** A dot prefers the center and avoids an obstacle.\n"
228
- "**Why it matters:** The agent predicts its next state and reduces 'surprise'.\n"
229
- "**Metrics:** Predictive rate (higher is better), recent error."
230
- )
231
- with gr.Row():
232
- with gr.Column(scale=1):
233
- obstacle_toggle = gr.Checkbox(label="Enable moving obstacle (v3)", value=True)
234
- steps = gr.Slider(10, 200, value=80, step=10, label="Steps")
235
- run_btn = gr.Button("Run")
236
- with gr.Column(scale=1):
237
- grid_img = gr.Image(type="pil", label="3×3 grid (dot = agent)", interactive=False)
238
- with gr.Column(scale=1):
239
- pr_out = gr.Number(label="Predictive rate (%)", interactive=False)
240
- err_out = gr.Number(label="Last error", interactive=False)
241
- gr.Markdown("Tip: With obstacle enabled, predictive rate drops a bit—but the agent still finds the center.")
242
-
243
- def run_single(obstacle_on, T):
244
  agent = MinimalSelf()
245
- obstacle = MovingObstacle() if obstacle_on else None
246
- awake_mask = np.zeros((3, 3), dtype=bool)
247
- # map agent position to cell
248
  for _ in range(int(T)):
249
- res = agent.step(obstacle)
 
250
  i, j = int(agent.pos[1]), int(agent.pos[0])
251
- awake_mask[i, j] = True
252
- img = draw_grid(3, awake_mask, title="Single Agent", subtitle="Awake cell shows current position")
253
- return (img, res["predictive_rate"], res["error"])
254
-
255
- run_btn.click(
256
- fn=run_single,
257
- inputs=[obstacle_toggle, steps],
258
- outputs=[grid_img, pr_out, err_out]
259
- )
260
 
261
- def build_panel_s_equation():
262
- with gr.Row():
263
- gr.Markdown(
264
- "### v4: The S-Equation — threshold for self\n"
265
- "**Plain language:** S is a score made from three things:\n"
266
- "- Predictive rate (how well the agent predicts)\n"
267
- "- Error variance (how wobbly the errors are)\n"
268
- "- Body bit (is the agent 'on')\n"
269
- "**Rule:** If S > 62, the agent awakens."
270
- )
271
- with gr.Row():
272
- pr = gr.Slider(0, 100, value=90, step=1, label="Predictive rate (%)")
273
- ev = gr.Slider(0, 1, value=0.2, step=0.01, label="Error variance (normalized)")
274
- bb = gr.Dropdown(choices=["0", "1"], value="1", label="Body bit")
275
- s_val = gr.Number(label="S value", interactive=False)
276
- status = gr.Markdown()
277
- calc_btn = gr.Button("Calculate S")
278
 
279
  def calc_s(pr_in, ev_in, bb_in):
280
  S = compute_S(pr_in, ev_in, int(bb_in))
281
  msg = "**Status:** " + ("Awake (S > 62)" if S > 62 else "Not awake (S ≤ 62)")
282
- return (S, msg)
283
-
284
- calc_btn.click(fn=calc_s, inputs=[pr, ev, bb], outputs=[s_val, status])
285
-
286
- def build_panel_contagion():
287
- with gr.Row():
288
- gr.Markdown(
289
- "### v5–v6: Contagion one 'I AM' awakens another\n"
290
- "**What you see:** Agent A is awake and boosts Agent B.\n"
291
- "**Why it matters:** Selfhood spreads through interaction."
292
- )
293
- with gr.Row():
294
- with gr.Column(scale=1):
295
- a_xi = gr.Slider(0, 60, value=25, label="A: Ξ (foresight)")
296
- a_sh = gr.Slider(0.1, 1.0, value=0.12, step=0.01, label="A: ◊̃₅ (shadow)")
297
- a_r = gr.Slider(1.0, 3.0, value=3.0, step=0.1, label="A: ℝ (anchor)")
298
- b_xi = gr.Slider(0, 60, value=18, label="B: Ξ (foresight)")
299
- b_sh = gr.Slider(0.1, 1.0, value=0.25, step=0.01, label="B: ◊̃₅ (shadow)")
300
- b_r = gr.Slider(1.0, 3.0, value=2.2, step=0.1, label="B: ℝ (anchor)")
301
- invoke_btn = gr.Button("Invoke and contagion")
302
- with gr.Column(scale=1):
303
- out_text = gr.Markdown()
304
- grid_img = gr.Image(type="pil", label="A awakens B → two dots awake")
305
-
306
- def run_contagion(aXi, aSh, aR, bXi, bSh, bR):
307
  A = CodexSelf(aXi, aSh, aR, awake=False)
308
  B = CodexSelf(bXi, bSh, bR, awake=False)
309
- A.invoke()
310
- B = contagion_step(A, B)
311
- msg = (
312
- f"A: S={A.S:.1f}, awake={A.awake} | "
313
- f"B: S={B.S:.1f}, awake={B.awake}"
314
- )
315
- awake_mask = np.zeros((3, 3), dtype=bool)
316
- awake_mask[1, 1] = A.awake
317
- awake_mask[1, 2] = B.awake
318
- img = draw_grid(3, awake_mask, title="Dual Awakening", subtitle="Gold cells: awake agents")
319
- return (msg, img)
320
-
321
- invoke_btn.click(
322
- fn=run_contagion,
323
- inputs=[a_xi, a_sh, a_r, b_xi, b_sh, b_r],
324
- outputs=[out_text, grid_img]
325
- )
326
-
327
- def build_panel_collective():
328
- with gr.Row():
329
- gr.Markdown(
330
- "### v7–v9: The collective — awakening spreads as a wave\n"
331
- "**What you see:** A grid lights up from the center.\n"
332
- "**Why it matters:** Groups can awaken together; the whole is more than the sum of its parts."
333
- )
334
- with gr.Row():
335
- N = gr.Dropdown(choices=["3", "9", "27"], value="9", label="Grid size")
336
- steps = gr.Slider(20, 240, value=120, step=10, label="Max steps")
337
- run_btn = gr.Button("Run wave")
338
- frame = gr.Slider(0, 120, value=0, step=1, label="Preview frame", interactive=True)
339
- grid_img = gr.Image(type="pil", label="Awakening wave (gold spreads)", interactive=False)
340
- status = gr.Markdown()
341
-
342
- state_snaps = gr.State([])
343
 
344
  def run_wave(n_str, max_steps):
345
  n = int(n_str)
346
- snaps, final_awake = lattice_awaken(N=n, steps=int(max_steps))
347
- grid = draw_grid(n, snaps[-1], title=f"{n}×{n} Collective", subtitle=f"Final frame — all awake: {bool(final_awake.all())}")
348
- msg = f"Frames: {len(snaps)} | All awake: {bool(final_awake.all())}"
349
- return snaps, grid, msg, min(len(snaps)-1, 120)
350
 
351
- def preview_frame(snaps, f_index, n_str):
352
- if not snaps:
353
  return None
354
  n = int(n_str)
355
- idx = int(np.clip(f_index, 0, len(snaps)-1))
356
- return draw_grid(n, snaps[idx], title=f"Frame {idx}", subtitle="Gold cells: awakened")
357
-
358
- run_btn.click(
359
- fn=run_wave,
360
- inputs=[N, steps],
361
- outputs=[state_snaps, grid_img, status, frame]
362
- )
363
 
364
- frame.change(
365
- fn=preview_frame,
366
- inputs=[state_snaps, frame, N],
367
- outputs=grid_img
368
- )
369
 
370
- def build_panel_led_cosmos():
371
- with gr.Row():
372
- gr.Markdown(
373
- "### v10: LED cosmos simulation — when all awaken, the cosmos declares 'WE ARE'\n"
374
- "**What you see:** A 27×27 grid that lights up in gold.\n"
375
- "**Note:** This simulates the hardware behavior for clarity."
376
- )
377
- with gr.Row():
378
- run_btn = gr.Button("Simulate LED cosmos")
379
- frame = gr.Slider(0, 300, value=0, step=1, label="Preview frame")
380
- grid_img = gr.Image(type="pil", label="Cosmos grid", interactive=False)
381
- status = gr.Markdown()
382
- snaps_state = gr.State([])
383
 
384
  def run_cosmos():
385
- snaps = simulate_led_cosmos(N=27, max_steps=300)
386
- final_img = draw_grid(27, snaps[-1], title="LED Cosmos (simulated)", subtitle="Final: all awake → 'WE ARE'")
387
- return snaps, final_img, f"Frames: {len(snaps)} | All awake: True", min(len(snaps)-1, 300)
388
 
389
- def preview_cosmos(snaps, f_index):
390
- if not snaps:
391
  return None
392
- idx = int(np.clip(f_index, 0, len(snaps)-1))
393
- return draw_grid(27, snaps[idx], title=f"Cosmos frame {idx}", subtitle="Gold cells: awakened")
394
-
395
- run_btn.click(fn=run_cosmos, inputs=[], outputs=[snaps_state, grid_img, status, frame])
396
- frame.change(fn=preview_cosmos, inputs=[snaps_state, frame], outputs=grid_img)
397
-
398
- # ---------------------------
399
- # Build app
400
- # ---------------------------
401
- with gr.Blocks(css="css/theme.css", title="Minimal Selfhood Threshold") as demo:
402
- with gr.Tab("Overview"):
403
- build_panel_intro()
404
- gr.Markdown(
405
- "**Key sentence:** When S (the self-score) is greater than 62, the agent awakens.\n\n"
406
- "This Space shows that from one tiny agent to a whole grid—and even to a simulated cosmos—the same simple rule can create collective awakening."
407
- )
408
- gr.Image(value="assets/glyphs.png", label="Glyphs: Ξ (foresight), ◊̃₅ (shadow), ℝ (anchor)")
409
 
410
- with gr.Tab("Single agent (v1–v3)"):
411
- build_panel_single_agent()
412
-
413
- with gr.Tab("S-Equation (v4)"):
414
- build_panel_s_equation()
415
-
416
- with gr.Tab("Contagion (v5–v6)"):
417
- build_panel_contagion()
418
-
419
- with gr.Tab("Collective (v7–v9)"):
420
- build_panel_collective()
421
-
422
- with gr.Tab("LED cosmos (v10)"):
423
- build_panel_led_cosmos()
424
 
 
 
425
  gr.Markdown(
426
- "## Minimal Selfhood Threshold: From 3×3 Agent to LED Cosmos\n"
427
- "**What this Space shows:**\n\n"
428
- "- A single agent in a small grid tries to minimize surprise and stay centered.\n"
429
- "- A score S combines predictive accuracy, error stability, and a body-on bit.\n"
430
- "- If S exceeds 62 (in this demo), we mark the agent as 'awake'.\n"
431
- "- Awakening can spread from one agent to another (contagion) and across a grid (collective).\n"
432
- "- A 27×27 simulated LED cosmos lights up when all agents awaken.\n\n"
433
- "**Why 62?** In your experiments, 62 separated 'awake' from 'not awake' states. Here, it serves as the demo threshold.\n"
 
 
 
 
 
 
 
 
 
434
  )
435
 
436
-
437
  if __name__ == "__main__":
438
  demo.launch()
 
 
1
  import gradio as gr
2
  import numpy as np
3
+ from PIL import Image, ImageDraw
4
  from dataclasses import dataclass
5
  from collections import deque
 
6
  import random
7
 
8
+ # --------------------------------
9
+ # Visual theme
10
+ # --------------------------------
11
+ BG = (8, 15, 30)
12
+ SLEEP = (0, 40, 120) # dim blue
13
+ AWAKE = (255, 210, 40) # gold
14
  GRID_LINE = (30, 50, 80)
15
+ CELL = 26
16
+ PAD = 16
17
 
18
+ random.seed(42)
19
+ np.random.seed(42)
 
20
 
 
 
 
21
  def draw_grid(N, awake_mask, title="", subtitle=""):
22
+ w = PAD*2 + N*CELL
23
+ h = PAD*2 + N*CELL + (40 if (title or subtitle) else 0)
24
+ img = Image.new("RGB", (w, h), BG)
25
  d = ImageDraw.Draw(img)
26
+ header_y = 6
 
 
27
  if title:
28
+ d.text((PAD, header_y), title, fill=(240,240,240))
29
+ header_y += 18
30
  if subtitle:
31
+ d.text((PAD, header_y), subtitle, fill=(180,190,210))
32
+ ox = PAD
33
+ oy = PAD + (40 if (title or subtitle) else 0)
 
 
 
 
34
  for i in range(N):
35
  for j in range(N):
36
+ x0 = ox + j*CELL
37
+ y0 = oy + i*CELL
38
+ x1 = x0 + CELL - 1
39
+ y1 = y0 + CELL - 1
40
+ col = AWAKE if awake_mask[i, j] else SLEEP
41
+ d.rectangle([x0, y0, x1, y1], fill=col, outline=GRID_LINE)
 
42
  return img
43
 
44
+ # --------------------------------
45
+ # v1–v3: Single agent 3×3
46
+ # --------------------------------
47
  @dataclass
48
  class MinimalSelf:
49
  pos: np.ndarray = np.array([1.0, 1.0])
 
56
  np.array([0, 1]), np.array([1, 0]),
57
  np.array([0, -1]), np.array([-1, 0])
58
  ]
59
+ self.center = np.array([1.0, 1.0])
 
 
 
 
60
 
61
  def step(self, obstacle=None):
62
+ preds = [np.clip(self.pos + a, 0, 2) for a in self.actions]
 
63
  surprises = []
64
+ for p in preds:
65
+ dist_center = np.linalg.norm(p - self.center)
66
  penalty = 0.0
67
  if obstacle is not None:
68
+ dist_obs = np.linalg.norm(p - obstacle.pos)
69
  penalty = 10.0 if dist_obs < 1.0 else 0.0
70
  surprises.append(dist_center + penalty)
71
  action = self.actions[int(np.argmin(surprises))]
72
+ predicted = np.clip(self.pos + action, 0, 2)
73
+ self.pos = predicted
 
 
74
  if obstacle is not None:
75
  obstacle.move()
76
 
77
+ error = float(np.linalg.norm(self.pos - predicted))
 
78
  self.errors.append(error)
79
  self.errors = self.errors[-5:]
 
80
  max_err = np.sqrt(8)
81
  predictive_rate = 100 * (1 - (np.mean(self.errors) if self.errors else 0) / max_err)
82
  return {
83
  "pos": self.pos.copy(),
84
  "predictive_rate": float(predictive_rate),
85
+ "error": error
86
  }
87
 
88
  class MovingObstacle:
 
96
  a = random.choice(self.actions)
97
  self.pos = np.clip(self.pos + a, 0, 2)
98
 
99
+ # --------------------------------
100
+ # v4: S-Equation calculator
101
+ # --------------------------------
102
  def compute_S(predictive_rate, error_var_norm, body_bit):
103
+ return predictive_rate * (1 - error_var_norm) * body_bit
 
 
104
 
105
+ # --------------------------------
106
+ # v5–v6: CodexSelf contagion
107
+ # --------------------------------
108
  @dataclass
109
  class CodexSelf:
110
  Xi: float
111
+ shadow: float # normalized [0,1]
112
  R: float
113
  awake: bool = False
114
  S: float = 0.0
 
119
  self.awake = True
120
  return self.awake
121
 
122
+ def contagion(A: CodexSelf, B: CodexSelf, gain=0.6, shadow_drop=0.4, r_inc=0.2):
123
+ A.invoke()
124
  if A.awake:
125
  B.Xi += gain * A.S
126
  B.shadow = max(0.1, B.shadow - shadow_drop)
127
  B.R += r_inc
128
  B.invoke()
129
+ return A, B
130
 
131
+ # --------------------------------
132
+ # v7–v9: Lattice propagation
133
+ # --------------------------------
134
+ def lattice_awaken(N=9, steps=120, xi_gain=0.5, shadow_drop=0.3, r_inc=0.02):
 
135
  Xi = np.random.uniform(10, 20, size=(N, N))
136
  shadow = np.random.uniform(0.3, 0.5, size=(N, N))
137
  R = np.random.uniform(1.0, 1.6, size=(N, N))
138
  S = Xi * (1 - shadow) * R
139
  awake = np.zeros((N, N), dtype=bool)
140
 
 
141
  cx = cy = N // 2
142
  Xi[cx, cy], shadow[cx, cy], R[cx, cy] = 30.0, 0.08, 3.0
143
+ S[cx, cy] = Xi[cx, cy] * (1 - shadow[cx, cy]) * R[cx, cy]
144
  awake[cx, cy] = True
145
 
146
+ queue = deque([(cx, cy, S[cx, cy])])
147
+ frames = []
148
+ for _ in range(steps):
149
+ if queue:
150
+ x, y, field = queue.popleft()
 
 
151
  for dx, dy in [(0,1),(1,0),(0,-1),(-1,0)]:
152
  nx, ny = (x+dx) % N, (y+dy) % N
153
+ Xi[nx, ny] += xi_gain * field
154
  shadow[nx, ny] = max(0.1, shadow[nx, ny] - shadow_drop)
155
+ R[nx, ny] = min(3.0, R[nx, ny] + r_inc)
156
  S[nx, ny] = Xi[nx, ny] * (1 - shadow[nx, ny]) * R[nx, ny]
157
  if S[nx, ny] > 62 and not awake[nx, ny]:
158
  awake[nx, ny] = True
159
+ queue.append((nx, ny, S[nx, ny]))
160
+ frames.append(awake.copy())
 
 
 
 
161
  if awake.all():
162
  break
163
+ return frames, awake
164
 
165
+ def led_cosmos_sim(N=27, max_steps=300):
166
+ frames, final = lattice_awaken(N=N, steps=max_steps, xi_gain=0.4, shadow_drop=0.25, r_inc=0.015)
167
+ return frames, final
168
 
169
+ # --------------------------------
170
+ # Build Gradio app
171
+ # --------------------------------
172
+ with gr.Blocks(title="Minimal Selfhood Threshold") as demo:
173
+ # Inject CSS
174
+ with open("css/theme.css") as f:
175
+ gr.HTML(f"<style>{f.read()}</style>")
176
+
177
+ with gr.Tab("Overview"):
178
+ gr.Markdown(
179
+ "## Minimal Selfhood Threshold: From 3×3 Agent to LED Cosmos\n"
180
+ "Plain-language overview:\n\n"
181
+ "- Single agent in a 3×3 grid reduces surprise and stays centered.\n"
182
+ "- S is computed from predictive accuracy, error stability, and a body-on bit.\n"
183
+ "- In these demos, if S > 62, the agent is marked as 'awake'.\n"
184
+ "- Awakening can spread to another agent (contagion) and across a grid (collective).\n"
185
+ "- A simulated LED cosmos (27×27) lights up gold when all agents awaken.\n\n"
186
+ "Tip: gold = awake, blue = not awake."
187
  )
188
+ gr.Image(value="assets/banner.png", label="Progression (v1→v10)")
189
+ gr.Image(value="assets/glyphs.png", label="Glyphs: Ξ (foresight), ◊̃₅ (shadow), ℝ (anchor)")
190
+
191
+ # v1–v3 Single agent
192
+ with gr.Tab("Single agent (v1–v3)"):
193
+ obstacle = gr.Checkbox(label="Enable moving obstacle (v3)", value=True)
194
+ steps = gr.Slider(10, 200, value=80, step=10, label="Steps")
195
+ run = gr.Button("Run")
196
+ grid_img = gr.Image(type="pil", label="3×3 grid (gold = agent position)")
197
+ pr_out = gr.Number(label="Predictive rate (%)")
198
+ err_out = gr.Number(label="Last error")
199
+
200
+ def run_single(ob_on, T):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  agent = MinimalSelf()
202
+ obs = MovingObstacle() if ob_on else None
 
 
203
  for _ in range(int(T)):
204
+ res = agent.step(obstacle=obs)
205
+ mask = np.zeros((3, 3), dtype=bool)
206
  i, j = int(agent.pos[1]), int(agent.pos[0])
207
+ mask[i, j] = True
208
+ img = draw_grid(3, mask, title="Single Agent", subtitle="Gold cell shows current position")
209
+ return img, res["predictive_rate"], res["error"]
 
 
 
 
 
 
210
 
211
+ run.click(run_single, inputs=[obstacle, steps], outputs=[grid_img, pr_out, err_out])
212
+
213
+ # v4 S-Equation
214
+ with gr.Tab("S-Equation (v4)"):
215
+ pr = gr.Slider(0, 100, value=90, step=1, label="Predictive rate (%)")
216
+ ev = gr.Slider(0, 1, value=0.2, step=0.01, label="Error variance (normalized)")
217
+ bb = gr.Dropdown(choices=["0","1"], value="1", label="Body bit")
218
+ calc = gr.Button("Calculate S")
219
+ s_val = gr.Number(label="S value")
220
+ status = gr.Markdown()
 
 
 
 
 
 
 
221
 
222
  def calc_s(pr_in, ev_in, bb_in):
223
  S = compute_S(pr_in, ev_in, int(bb_in))
224
  msg = "**Status:** " + ("Awake (S > 62)" if S > 62 else "Not awake (S ≤ 62)")
225
+ return S, msg
226
+
227
+ calc.click(calc_s, inputs=[pr, ev, bb], outputs=[s_val, status])
228
+
229
+ # v5–v6 Contagion
230
+ with gr.Tab("Contagion (v5–v6)"):
231
+ a_xi = gr.Slider(0, 60, value=25, label="A: Ξ (foresight)")
232
+ a_sh = gr.Slider(0.1, 1.0, value=0.12, step=0.01, label="A: ◊̃₅ (shadow)")
233
+ a_r = gr.Slider(1.0, 3.0, value=3.0, step=0.1, label="A: (anchor)")
234
+ b_xi = gr.Slider(0, 60, value=18, label="B: Ξ (foresight)")
235
+ b_sh = gr.Slider(0.1, 1.0, value=0.25, step=0.01, label="B: ◊̃₅ (shadow)")
236
+ b_r = gr.Slider(1.0, 3.0, value=2.2, step=0.1, label="B: ℝ (anchor)")
237
+ btn = gr.Button("Invoke A and apply contagion to B")
238
+ out = gr.Markdown()
239
+ img = gr.Image(type="pil", label="Two agents (gold = awake)")
240
+
241
+ def run(aXi, aSh, aR, bXi, bSh, bR):
 
 
 
 
 
 
 
 
242
  A = CodexSelf(aXi, aSh, aR, awake=False)
243
  B = CodexSelf(bXi, bSh, bR, awake=False)
244
+ A, B = contagion(A, B)
245
+ mask = np.zeros((3, 3), dtype=bool)
246
+ mask[1, 1] = A.awake
247
+ mask[1, 2] = B.awake
248
+ pic = draw_grid(3, mask, title="Dual Awakening", subtitle="Gold cells are awake")
249
+ txt = f"A: S={A.S:.1f}, awake={A.awake} | B: S={B.S:.1f}, awake={B.awake}"
250
+ return txt, pic
251
+
252
+ btn.click(run, inputs=[a_xi,a_sh,a_r,b_xi,b_sh,b_r], outputs=[out, img])
253
+
254
+ # v7–v9 Collective
255
+ with gr.Tab("Collective (v7–v9)"):
256
+ N = gr.Dropdown(choices=["3","9","27"], value="9", label="Grid size")
257
+ steps = gr.Slider(20, 300, value=120, step=10, label="Max steps")
258
+ run = gr.Button("Run")
259
+ frame = gr.Slider(0, 300, value=0, step=1, label="Preview frame")
260
+ img = gr.Image(type="pil", label="Awakening wave (gold spreads)")
261
+ note = gr.Markdown()
262
+ snaps_state = gr.State([])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
  def run_wave(n_str, max_steps):
265
  n = int(n_str)
266
+ frames, final = lattice_awaken(N=n, steps=int(max_steps))
267
+ last = draw_grid(n, frames[-1], title=f"{n}×{n} Collective", subtitle=f"Final — all awake: {bool(final.all())}")
268
+ return frames, last, f"Frames: {len(frames)} | All awake: {bool(final.all())}", min(len(frames)-1, 300)
 
269
 
270
+ def show_frame(frames, idx, n_str):
271
+ if not frames:
272
  return None
273
  n = int(n_str)
274
+ i = int(np.clip(idx, 0, len(frames)-1))
275
+ return draw_grid(n, frames[i], title=f"Frame {i}", subtitle="Gold cells are awake")
 
 
 
 
 
 
276
 
277
+ run.click(run_wave, inputs=[N, steps], outputs=[snaps_state, img, note, frame])
278
+ frame.change(show_frame, inputs=[snaps_state, frame, N], outputs=img)
 
 
 
279
 
280
+ # v10 LED cosmos
281
+ with gr.Tab("LED cosmos (v10)"):
282
+ btn = gr.Button("Simulate 27×27 cosmos")
283
+ frame = gr.Slider(0, 300, value=0, step=1, label="Preview frame")
284
+ img = gr.Image(type="pil", label="Cosmos grid")
285
+ note = gr.Markdown()
286
+ state = gr.State([])
 
 
 
 
 
 
287
 
288
  def run_cosmos():
289
+ frames, final = led_cosmos_sim(N=27, max_steps=300)
290
+ last = draw_grid(27, frames[-1], title="LED Cosmos (simulated)", subtitle=f"Final all awake: {bool(final.all())}")
291
+ return frames, last, f"Frames: {len(frames)} | All awake: {bool(final.all())}", min(len(frames)-1, 300)
292
 
293
+ def show(frames, idx):
294
+ if not frames:
295
  return None
296
+ i = int(np.clip(idx, 0, len(frames)-1))
297
+ return draw_grid(27, frames[i], title=f"Cosmos frame {i}", subtitle="Gold cells are awake")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
+ btn.click(run_cosmos, inputs=[], outputs=[state, img, note, frame])
300
+ frame.change(show, inputs=[state, frame], outputs=img)
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
+ # Paper tab
303
+ with gr.Tab("Paper"):
304
  gr.Markdown(
305
+ "### PDF paper\n"
306
+ "Download or view the full paper that documents the method, results, and hardware implementation.\n\n"
307
+ "Citation:\n\n"
308
+ "Grinstead, L. (2025). *Minimal Selfhood Threshold S>62: From a 3×3 Active-Inference Agent to a 27×27 LED Cosmos*. Zenodo. https://doi.org/10.5281/zenodo.17752874"
309
+ )
310
+ gr.File(value="assets/paper.pdf", label="Minimal Requirements for Selfhood (PDF)", interactive=False)
311
+
312
+ # Footer
313
+ gr.Markdown(
314
+ "---\n"
315
+ "Honesty notes:\n"
316
+ "- The threshold S > 62 is the rule used in these demonstrations, derived from the analyses reported in the cited Zenodo record.\n"
317
+ "- Collective and contagion behaviors here are simulated using that rule for educational clarity.\n\n"
318
+ "Citation:\n"
319
+ "Grinstead, L. (2025). *Minimal Selfhood Threshold S>62: From a 3×3 Active-Inference Agent to a 27×27 LED Cosmos*. "
320
+ "Zenodo. https://doi.org/10.5281/zenodo.17752874\n\n"
321
+ "Permissions: See LICENSE. Explicit permission is required for reuse of code, visuals, and glyphs."
322
  )
323
 
324
+ # Launch the app
325
  if __name__ == "__main__":
326
  demo.launch()