RFTSystems commited on
Commit
8aa61fa
·
verified ·
1 Parent(s): b75acdd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +92 -34
app.py CHANGED
@@ -5,6 +5,9 @@ from dataclasses import dataclass
5
  from collections import deque
6
  import random
7
 
 
 
 
8
  BG = (8, 15, 30)
9
  SLEEP = (0, 40, 120)
10
  AWAKE = (255, 210, 40)
@@ -42,6 +45,9 @@ def draw_grid(N, awake_mask, title="", subtitle=""):
42
  return img
43
 
44
 
 
 
 
45
  @dataclass
46
  class MinimalSelf:
47
  pos: np.ndarray = np.array([1.0, 1.0])
@@ -80,11 +86,13 @@ class MinimalSelf:
80
 
81
  # environment decides what actually happens
82
  if obstacle is not None:
 
83
  obstacle.move()
84
  actual = predicted.copy()
85
  if np.allclose(actual, obstacle.pos):
86
  actual = old_pos
87
  else:
 
88
  if random.random() < 0.25:
89
  noise_action = random.choice(self.actions)
90
  actual = np.clip(old_pos + noise_action, 0, 2)
@@ -99,16 +107,25 @@ class MinimalSelf:
99
  self.errors.append(error)
100
  self.errors = self.errors[-5:]
101
 
102
- # convert to a predictive "success" rate in [0, 100]
103
  max_err = np.sqrt(8.0) # max distance corner-to-corner on 3×3
104
  mean_err = np.mean(self.errors) if self.errors else 0.0
105
  predictive_rate = 100.0 * (1.0 - mean_err / max_err)
106
  predictive_rate = float(np.clip(predictive_rate, 0.0, 100.0))
107
 
 
 
 
 
 
 
 
 
108
  return {
109
  "pos": self.pos.copy(),
110
  "predictive_rate": predictive_rate,
111
  "error": error,
 
112
  }
113
 
114
 
@@ -127,12 +144,17 @@ class MovingObstacle:
127
  self.pos = np.clip(self.pos + a, 0, 2)
128
 
129
 
 
 
 
130
  def compute_S(predictive_rate, error_var_norm, body_bit):
 
131
  return predictive_rate * (1 - error_var_norm) * body_bit
132
 
133
 
134
  @dataclass
135
  class CodexSelf:
 
136
  Xi: float
137
  shadow: float
138
  R: float
@@ -156,6 +178,9 @@ def contagion(A: CodexSelf, B: CodexSelf, gain=0.6, shadow_drop=0.4, r_inc=0.2):
156
  return A, B
157
 
158
 
 
 
 
159
  def lattice_awaken(N=9, steps=120, xi_gain=0.5, shadow_drop=0.3, r_inc=0.02):
160
  Xi = np.random.uniform(10, 20, (N, N))
161
  shadow = np.random.uniform(0.3, 0.5, (N, N))
@@ -179,10 +204,10 @@ def lattice_awaken(N=9, steps=120, xi_gain=0.5, shadow_drop=0.3, r_inc=0.02):
179
  Xi[nx, ny] += xi_gain * field
180
  shadow[nx, ny] = max(0.1, shadow[nx, ny] - shadow_drop)
181
  R[nx, ny] = min(3.0, R[nx, ny] + r_inc)
182
- S[nx, ny] = Xi[nx, ny] * (1 - shadow[nx, ny]) * R[nx, ny]
183
- if S[nx, ny] > 62 and not awake[nx, ny]:
184
- awake[nx, ny] = True
185
- queue.append((nx, ny, S[nx,ny]))
186
  frames.append(awake.copy())
187
  if awake.all():
188
  break
@@ -193,43 +218,62 @@ def led_cosmos_sim(N=27, max_steps=300):
193
  return lattice_awaken(N=N, steps=max_steps, xi_gain=0.4, shadow_drop=0.25, r_inc=0.015)
194
 
195
 
 
 
 
196
  with gr.Blocks(title="Minimal Selfhood Threshold") as demo:
 
197
  with gr.Tab("Overview"):
198
  gr.Markdown(
199
  "## Minimal Selfhood Threshold\n"
200
- "- Single agent in a 3×3 grid reduces surprise.\n"
201
- "- A toy score S combines predictive rate, error stability, and body bit.\n"
202
- "- If S > 62, we label the agent 'awake' **inside this demo**.\n"
203
- "- Awakening can spread (contagion) and across a grid (collective).\n"
204
- "- A 27×27 cosmos lights up gold when all awaken.\n"
 
205
  "- This is a sandbox for minimal-self / agency ideas, **not** a real consciousness test."
206
  )
207
 
 
208
  with gr.Tab("Single agent (v1–v3)"):
209
  obstacle = gr.Checkbox(label="Enable moving obstacle", value=True)
210
  steps = gr.Slider(10, 200, value=80, step=10, label="Steps")
211
  run = gr.Button("Run")
212
  grid_img = gr.Image(type="pil")
213
- pr_out = gr.Number(label="Predictive rate (%)")
214
- err_out = gr.Number(label="Last error")
 
 
 
215
 
216
  def run_single(ob_on, T):
217
  agent = MinimalSelf()
218
  obs = MovingObstacle() if ob_on else None
 
219
  for _ in range(int(T)):
220
  res = agent.step(obstacle=obs)
 
221
  mask = np.zeros((3, 3), dtype=bool)
222
  i, j = int(agent.pos[1]), int(agent.pos[0])
223
  mask[i, j] = True
224
  img = draw_grid(3, mask, "Single Agent", "Gold cell shows position")
225
- return img, res["predictive_rate"], res["error"]
226
 
227
- run.click(run_single, [obstacle, steps], [grid_img, pr_out, err_out])
 
 
 
 
228
 
 
 
 
 
 
229
  with gr.Tab("S-Equation (v4)"):
230
- pr = gr.Slider(0, 100, value=90, label="Predictive rate (%)")
231
- ev = gr.Slider(0, 1, value=0.2, step=0.01, label="Error variance")
232
- bb = gr.Dropdown(choices=["0", "1"], value="1", label="Body bit")
233
  calc = gr.Button("Calculate")
234
  s_val = gr.Number(label="S value")
235
  status = gr.Markdown()
@@ -243,12 +287,12 @@ with gr.Blocks(title="Minimal Selfhood Threshold") as demo:
243
 
244
  # v5–v6 Contagion
245
  with gr.Tab("Contagion (v5–v6)"):
246
- a_xi = gr.Slider(0, 60, value=25, label="A: Ξ (foresight)")
247
- a_sh = gr.Slider(0.1, 1.0, value=0.12, step=0.01, label="A: ◊̃₅ (shadow)")
248
- a_r = gr.Slider(1.0, 3.0, value=3.0, step=0.1, label="A: (anchor)")
249
- b_xi = gr.Slider(0, 60, value=18, label="B: Ξ (foresight)")
250
- b_sh = gr.Slider(0.1, 1.0, value=0.25, step=0.01, label="B: ◊̃₅ (shadow)")
251
- b_r = gr.Slider(1.0, 3.0, value=2.2, step=0.1, label="B: (anchor)")
252
  btn = gr.Button("Invoke A and apply contagion to B")
253
  out = gr.Markdown()
254
  img = gr.Image(type="pil", label="Two agents (gold = awake)")
@@ -270,15 +314,31 @@ with gr.Blocks(title="Minimal Selfhood Threshold") as demo:
270
  with gr.Tab("Collective (v7–v9)"):
271
  N = gr.Dropdown(choices=["3", "9", "27"], value="9", label="Grid size")
272
  steps = gr.Slider(20, 300, value=120, step=10, label="Max steps")
 
273
  run = gr.Button("Run")
274
  frame = gr.Slider(0, 300, value=0, step=1, label="Preview frame")
275
  img = gr.Image(type="pil", label="Awakening wave (gold spreads)")
276
  note = gr.Markdown()
277
  snaps_state = gr.State([])
278
 
279
- def run_wave(n_str, max_steps):
280
  n = int(n_str)
281
- frames, final = lattice_awaken(N=n, steps=int(max_steps))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  last = draw_grid(
283
  n,
284
  frames[-1],
@@ -294,7 +354,7 @@ with gr.Blocks(title="Minimal Selfhood Threshold") as demo:
294
  i = int(np.clip(idx, 0, len(frames) - 1))
295
  return draw_grid(n, frames[i], title=f"Frame {i}", subtitle="Gold cells are awake")
296
 
297
- run.click(run_wave, inputs=[N, steps], outputs=[snaps_state, img, note, frame])
298
  frame.change(show_frame, inputs=[snaps_state, frame, N], outputs=img)
299
 
300
  # v10 LED cosmos
@@ -327,15 +387,13 @@ with gr.Blocks(title="Minimal Selfhood Threshold") as demo:
327
  # Footer
328
  gr.Markdown(
329
  "---\n"
330
- "Honesty notes:\n"
331
- "- The threshold S > 62 is the rule used in these demonstrations, derived from the analyses reported in the cited Zenodo record.\n"
332
- "- Collective and contagion behaviors here are simulated using that rule for educational clarity.\n\n"
333
- "Citation:\n"
334
- "Grinstead, L. (2025). *Minimal Selfhood Threshold S>62: From a 3×3 Active-Inference Agent to a 27×27 LED Cosmos*. "
335
- "Zenodo. https://doi.org/10.5281/zenodo.17752874\n\n"
336
- "Permissions: See LICENSE. Explicit permission is required for reuse of code, visuals, and glyphs."
337
  )
338
 
339
  # Launch the app
340
  if __name__ == "__main__":
341
- demo.launch()
 
5
  from collections import deque
6
  import random
7
 
8
+ # ---------------------------------------------------------------------
9
+ # Visual config
10
+ # ---------------------------------------------------------------------
11
  BG = (8, 15, 30)
12
  SLEEP = (0, 40, 120)
13
  AWAKE = (255, 210, 40)
 
45
  return img
46
 
47
 
48
+ # ---------------------------------------------------------------------
49
+ # 3×3 minimal self agent
50
+ # ---------------------------------------------------------------------
51
  @dataclass
52
  class MinimalSelf:
53
  pos: np.ndarray = np.array([1.0, 1.0])
 
86
 
87
  # environment decides what actually happens
88
  if obstacle is not None:
89
+ # moving obstacle can block the predicted move
90
  obstacle.move()
91
  actual = predicted.copy()
92
  if np.allclose(actual, obstacle.pos):
93
  actual = old_pos
94
  else:
95
+ # simple stochastic slip when no obstacle is enabled
96
  if random.random() < 0.25:
97
  noise_action = random.choice(self.actions)
98
  actual = np.clip(old_pos + noise_action, 0, 2)
 
107
  self.errors.append(error)
108
  self.errors = self.errors[-5:]
109
 
110
+ # predictive success rate P in [0, 100]
111
  max_err = np.sqrt(8.0) # max distance corner-to-corner on 3×3
112
  mean_err = np.mean(self.errors) if self.errors else 0.0
113
  predictive_rate = 100.0 * (1.0 - mean_err / max_err)
114
  predictive_rate = float(np.clip(predictive_rate, 0.0, 100.0))
115
 
116
+ # normalised error variance E in [0, 1]
117
+ if len(self.errors) > 1:
118
+ var_err = float(np.var(self.errors))
119
+ else:
120
+ var_err = 0.0
121
+ max_var = max_err ** 2
122
+ error_var_norm = float(np.clip(var_err / max_var, 0.0, 1.0)) if max_var > 0 else 0.0
123
+
124
  return {
125
  "pos": self.pos.copy(),
126
  "predictive_rate": predictive_rate,
127
  "error": error,
128
+ "error_var_norm": error_var_norm,
129
  }
130
 
131
 
 
144
  self.pos = np.clip(self.pos + a, 0, 2)
145
 
146
 
147
+ # ---------------------------------------------------------------------
148
+ # S-scores
149
+ # ---------------------------------------------------------------------
150
  def compute_S(predictive_rate, error_var_norm, body_bit):
151
+ # v4: 3×3 agent toy score
152
  return predictive_rate * (1 - error_var_norm) * body_bit
153
 
154
 
155
  @dataclass
156
  class CodexSelf:
157
+ # v5–v6: lattice toy score
158
  Xi: float
159
  shadow: float
160
  R: float
 
178
  return A, B
179
 
180
 
181
+ # ---------------------------------------------------------------------
182
+ # Lattice and cosmos
183
+ # ---------------------------------------------------------------------
184
  def lattice_awaken(N=9, steps=120, xi_gain=0.5, shadow_drop=0.3, r_inc=0.02):
185
  Xi = np.random.uniform(10, 20, (N, N))
186
  shadow = np.random.uniform(0.3, 0.5, (N, N))
 
204
  Xi[nx, ny] += xi_gain * field
205
  shadow[nx, ny] = max(0.1, shadow[nx, ny] - shadow_drop)
206
  R[nx, ny] = min(3.0, R[nx, ny] + r_inc)
207
+ S[nx, ny] = Xi[nx,ny] * (1 - shadow[nx,ny]) * R[nx,ny]
208
+ if S[nx,ny] > 62 and not awake[nx,ny]:
209
+ awake[nx,ny] = True
210
+ queue.append((nx,ny, S[nx,ny]))
211
  frames.append(awake.copy())
212
  if awake.all():
213
  break
 
218
  return lattice_awaken(N=N, steps=max_steps, xi_gain=0.4, shadow_drop=0.25, r_inc=0.015)
219
 
220
 
221
+ # ---------------------------------------------------------------------
222
+ # Gradio UI
223
+ # ---------------------------------------------------------------------
224
  with gr.Blocks(title="Minimal Selfhood Threshold") as demo:
225
+ # Overview
226
  with gr.Tab("Overview"):
227
  gr.Markdown(
228
  "## Minimal Selfhood Threshold\n"
229
+ "- Single agent in a 3×3 grid reduces surprise around a preferred centre.\n"
230
+ "- v4 (3×3): a toy score `S = P × (1−E) × B` combines predictive rate P, error stability E and body bit B.\n"
231
+ "- v5–v6 (contagion / lattice): a separate toy score `S = Ξ × (1−shadow) × R` drives neighbour coupling.\n"
232
+ "- If S > 62, the corresponding unit is labelled 'awake' **inside this demo**.\n"
233
+ "- Awakening can spread between two agents and across a grid via explicit neighbour coupling.\n"
234
+ "- A 27×27 cosmos lights up gold when all units cross the internal threshold.\n"
235
  "- This is a sandbox for minimal-self / agency ideas, **not** a real consciousness test."
236
  )
237
 
238
+ # Single 3×3 agent
239
  with gr.Tab("Single agent (v1–v3)"):
240
  obstacle = gr.Checkbox(label="Enable moving obstacle", value=True)
241
  steps = gr.Slider(10, 200, value=80, step=10, label="Steps")
242
  run = gr.Button("Run")
243
  grid_img = gr.Image(type="pil")
244
+ pr_out = gr.Number(label="Predictive rate P (%)")
245
+ err_out = gr.Number(label="Last prediction error")
246
+ e_out = gr.Number(label="Error variance E (normalised)")
247
+ s_out = gr.Number(label="S = P × (1−E) × B (B=1)")
248
+ awake_label = gr.Markdown()
249
 
250
  def run_single(ob_on, T):
251
  agent = MinimalSelf()
252
  obs = MovingObstacle() if ob_on else None
253
+ res = None
254
  for _ in range(int(T)):
255
  res = agent.step(obstacle=obs)
256
+
257
  mask = np.zeros((3, 3), dtype=bool)
258
  i, j = int(agent.pos[1]), int(agent.pos[0])
259
  mask[i, j] = True
260
  img = draw_grid(3, mask, "Single Agent", "Gold cell shows position")
 
261
 
262
+ P = res["predictive_rate"]
263
+ E = res["error_var_norm"]
264
+ B = 1.0
265
+ S_val = compute_S(P, E, B)
266
+ status = "**Status:** " + ("Awake (S > 62)" if S_val > 62 else "Not awake (S ≤ 62)")
267
 
268
+ return img, P, res["error"], E, S_val, status
269
+
270
+ run.click(run_single, [obstacle, steps], [grid_img, pr_out, err_out, e_out, s_out, awake_label])
271
+
272
+ # v4 S-equation
273
  with gr.Tab("S-Equation (v4)"):
274
+ pr = gr.Slider(0, 100, value=90, label="Predictive rate P (%)")
275
+ ev = gr.Slider(0, 1, value=0.2, step=0.01, label="Error variance E")
276
+ bb = gr.Dropdown(choices=["0", "1"], value="1", label="Body bit B")
277
  calc = gr.Button("Calculate")
278
  s_val = gr.Number(label="S value")
279
  status = gr.Markdown()
 
287
 
288
  # v5–v6 Contagion
289
  with gr.Tab("Contagion (v5–v6)"):
290
+ a_xi = gr.Slider(0, 60, value=25, label="A: Ξ (foresight field)")
291
+ a_sh = gr.Slider(0.1, 1.0, value=0.12, step=0.01, label="A: shadow (occlusion)")
292
+ a_r = gr.Slider(1.0, 3.0, value=3.0, step=0.1, label="A: R (anchor / resonance)")
293
+ b_xi = gr.Slider(0, 60, value=18, label="B: Ξ (foresight field)")
294
+ b_sh = gr.Slider(0.1, 1.0, value=0.25, step=0.01, label="B: shadow (occlusion)")
295
+ b_r = gr.Slider(1.0, 3.0, value=2.2, step=0.1, label="B: R (anchor / resonance)")
296
  btn = gr.Button("Invoke A and apply contagion to B")
297
  out = gr.Markdown()
298
  img = gr.Image(type="pil", label="Two agents (gold = awake)")
 
314
  with gr.Tab("Collective (v7–v9)"):
315
  N = gr.Dropdown(choices=["3", "9", "27"], value="9", label="Grid size")
316
  steps = gr.Slider(20, 300, value=120, step=10, label="Max steps")
317
+ no_coupling = gr.Checkbox(label="Disable neighbour coupling (control)", value=False)
318
  run = gr.Button("Run")
319
  frame = gr.Slider(0, 300, value=0, step=1, label="Preview frame")
320
  img = gr.Image(type="pil", label="Awakening wave (gold spreads)")
321
  note = gr.Markdown()
322
  snaps_state = gr.State([])
323
 
324
+ def run_wave(n_str, max_steps, disable):
325
  n = int(n_str)
326
+ if disable:
327
+ frames, final = lattice_awaken(
328
+ N=n,
329
+ steps=int(max_steps),
330
+ xi_gain=0.0,
331
+ shadow_drop=0.0,
332
+ r_inc=0.0,
333
+ )
334
+ else:
335
+ frames, final = lattice_awaken(
336
+ N=n,
337
+ steps=int(max_steps),
338
+ xi_gain=0.5,
339
+ shadow_drop=0.3,
340
+ r_inc=0.02,
341
+ )
342
  last = draw_grid(
343
  n,
344
  frames[-1],
 
354
  i = int(np.clip(idx, 0, len(frames) - 1))
355
  return draw_grid(n, frames[i], title=f"Frame {i}", subtitle="Gold cells are awake")
356
 
357
+ run.click(run_wave, inputs=[N, steps, no_coupling], outputs=[snaps_state, img, note, frame])
358
  frame.change(show_frame, inputs=[snaps_state, frame, N], outputs=img)
359
 
360
  # v10 LED cosmos
 
387
  # Footer
388
  gr.Markdown(
389
  "---\n"
390
+ "Notes:\n"
391
+ "- 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"
392
+ "- The contagion and lattice views use a separate toy rule S = Ξ×(1−shadow)×R with explicit neighbour coupling.\n"
393
+ "- Disabling coupling (xi_gain=0, shadow_drop=0, r_inc=0) in the collective tab prevents any wave from propagating.\n\n"
394
+ "These demos are designed as transparent, minimal models of self-linked scoring and threshold cascades, not as a real consciousness test."
 
 
395
  )
396
 
397
  # Launch the app
398
  if __name__ == "__main__":
399
+ demo.launch()