everydaytok commited on
Commit
cff549e
Β·
verified Β·
1 Parent(s): ea2bd79

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +504 -413
app.py CHANGED
@@ -6,157 +6,249 @@ from fastapi.middleware.cors import CORSMiddleware
6
  app = FastAPI()
7
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
8
 
9
- FWD_K = 2.2
10
- DAMPING = 0.55
11
- DT = 0.12
12
- MICRO = 6
13
- SETTLE = 0.004
14
- CONV_THRESH = 0.02
15
- BRIDGE_K = 0.20 # passive bridge spring constant β€” soft coupling, not learned
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  class SimEngine:
 
 
19
  def __init__(self):
20
- self.mode = 'training'
21
- self.architecture = 'additive'
22
- self.dataset_type = 'housing'
23
- self.n_inputs = 1
24
- self.n_upper = 3
25
- self.n_lower = 3
26
- self.back_alpha = 0.45
27
- self.cross_connect = False
28
- self.running = False
29
- self.batch_queue = collections.deque()
30
- self.logs = []
31
- self.iteration = 0
32
- self.current_error = 0.0
 
 
 
33
  self.current_prediction = 0.0
34
- self.history = []
35
- self.bridge_springs = {}
36
- self._init_mesh()
 
 
 
 
 
 
 
 
37
 
38
  # ── TOPOLOGY ──────────────────────────────────────────────────────────────
39
 
40
- def _build_layers(self):
41
- n, nu, nl = self.n_inputs, self.n_upper, self.n_lower
42
- return [
43
- [f'A{d}' for d in range(1, n+1)],
44
- [f'U{d}_{j}' for d in range(1, n+1) for j in range(1, nu+1)],
45
- [f'C{d}' for d in range(1, n+1)],
46
- [f'L{d}_{j}' for d in range(1, n+1) for j in range(1, nl+1)],
47
- [f'B{d}' for d in range(1, n+1)],
48
- ]
49
-
50
- def _init_mesh(self):
51
- self.iteration = 0
52
- self.current_error = 0.0
53
  self.current_prediction = 0.0
54
- self.history = []
55
- self.bridge_springs = {}
56
- self.layers = self._build_layers()
57
- n = self.n_inputs
58
-
59
- self.nodes = {}
60
- for layer in self.layers:
61
- for nid in layer:
62
- anchored = nid[0] in ('A', 'B')
63
- self.nodes[nid] = {'x': 0.0, 'vel': 0.0, 'anchored': anchored}
64
- for d in range(1, n+1):
65
- self.nodes[f'A{d}']['x'] = 2.0
66
- self.nodes[f'B{d}']['x'] = 3.0
67
-
68
- self.springs = {}
69
- for d in range(1, n+1):
70
- for j in range(1, self.n_upper+1):
71
- uid = f'U{d}_{j}'
72
- self.springs[(f'A{d}', uid)] = round(random.uniform(0.85, 1.15), 4)
73
- self.springs[(uid, f'C{d}')] = round(random.uniform(0.85, 1.15), 4)
74
- for j in range(1, self.n_lower+1):
75
- lid = f'L{d}_{j}'
76
- self.springs[(f'B{d}', lid)] = round(random.uniform(0.85, 1.15), 4)
77
- self.springs[(lid, f'C{d}')] = round(random.uniform(0.85, 1.15), 4)
78
-
79
- if self.cross_connect:
80
- self._add_bridge_nodes()
81
-
82
- # ── BRIDGE NODES ──────────────────────────────────────────────────────────
83
-
84
- def _add_bridge_nodes(self):
85
- """
86
- For each adjacent dimension pair (d, d+1), insert two passive bridge
87
- vertices:
88
- XU{d} β€” sits between U{d}_{n_upper} and U{d+1}_1 (upper hidden layer)
89
- XL{d} β€” sits between L{d}_{n_lower} and L{d+1}_1 (lower hidden layer)
90
-
91
- Bridge springs are FIXED at BRIDGE_K and never touched by LMS.
92
- Each bridge node settles to a position driven by its two neighbours,
93
- and in turn exerts a soft reaction pull on those neighbours β€” creating
94
- a passive physical information channel between dimensions without
95
- interfering with per-dimension gradient descent.
96
- """
97
- n = self.n_inputs
98
- if n < 2:
99
  return
100
- for d in range(1, n):
101
- xuid = f'XU{d}'
102
- self.nodes[xuid] = {'x': 0.0, 'vel': 0.0, 'anchored': False}
103
- self.bridge_springs[(xuid, f'U{d}_{self.n_upper}')] = BRIDGE_K
104
- self.bridge_springs[(xuid, f'U{d+1}_1')] = BRIDGE_K
105
-
106
- xlid = f'XL{d}'
107
- self.nodes[xlid] = {'x': 0.0, 'vel': 0.0, 'anchored': False}
108
- self.bridge_springs[(xlid, f'L{d}_{self.n_lower}')] = BRIDGE_K
109
- self.bridge_springs[(xlid, f'L{d+1}_1')] = BRIDGE_K
110
-
111
- # ── CROSS CONNECT TOGGLE ──────────────────────────────────────────────────
112
-
113
- def toggle_cross_connect(self):
114
- self.cross_connect = not self.cross_connect
115
- self.running = False
116
- self._init_mesh()
117
- self.logs = []
118
- nb = len(self.bridge_springs) // 2 # each bridge has 2 springs
119
- if self.cross_connect:
120
- self.add_log(
121
- f"Cross-connect ON β€” {nb} passive bridge "
122
- f"{'vertex' if nb == 1 else 'vertices'} "
123
- f"(k={BRIDGE_K}, not learned)"
124
- )
125
- else:
126
- self.add_log("Cross-connect OFF β€” independent parallel hourglasses")
127
-
128
- # ── LOGGING ───────────────────────────────────────────────────────────────
129
-
130
- def reset(self):
131
- self.running = False
132
- self.batch_queue.clear()
133
- self.logs = []
134
- self._init_mesh()
135
 
136
- def add_log(self, msg):
137
- self.logs.insert(0, f"[{self.iteration:05d}] {msg}")
138
- if len(self.logs) > 50:
139
- self.logs.pop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- # ── HELPERS ───────────────────────────────────────────────────────────────
142
 
143
  def _to_vec(self, val, n):
144
  if isinstance(val, (list, tuple)):
145
  v = [float(x) for x in val]
146
- if len(v) >= n: return v[:n]
147
- return v + [v[-1]] * (n - len(v))
148
  return [float(val)] * n
149
 
150
- # ── DATASET ───────────────────────────────────────────────────────────────
151
-
152
  def ground_truth(self, a_vec, b_vec):
153
- n = self.n_inputs
154
- a_vec = self._to_vec(a_vec, n)
155
- b_vec = self._to_vec(b_vec, n)
156
- t = self.dataset_type
157
  result = []
158
- for i in range(n):
159
- a, b = a_vec[i], b_vec[i]
160
  if t == 'housing': result.append(round(a*2.5 + b*1.2, 4))
161
  elif t == 'subtraction': result.append(round(a - b, 4))
162
  elif t == 'multiplication': result.append(round(a * b, 4))
@@ -167,225 +259,213 @@ class SimEngine:
167
  # ── PROBLEM SETUP ─────────────────────────────────────────────────────────
168
 
169
  def set_problem(self, a, b, c_target=None):
170
- n = self.n_inputs
171
- a_vec = self._to_vec(a, n)
172
- b_vec = self._to_vec(b, n)
173
- for d in range(1, n+1):
174
- self.nodes[f'A{d}']['x'] = a_vec[d-1]
175
- self.nodes[f'B{d}']['x'] = b_vec[d-1]
176
- # Reset all hidden + bridge nodes
177
- for nid, nd in self.nodes.items():
178
- if nid[0] in ('U', 'L', 'X'):
179
- nd['x'] = 0.0; nd['vel'] = 0.0
180
- c_vec = self._to_vec(c_target, n) if c_target is not None else None
181
- for d in range(1, n+1):
182
- c = self.nodes[f'C{d}']
183
- c['vel'] = 0.0
184
- if self.mode == 'training' and c_vec is not None:
185
- c['x'] = c_vec[d-1]
186
- c['anchored'] = True
187
- else:
188
- c['anchored'] = False
189
- c['x'] = 0.0
190
-
191
- # ── ELASTIC STEP ──────────────────────────────────────────────────────────
192
-
193
- def _elastic_step(self, n_steps):
194
- alpha = self.back_alpha
195
- n = self.n_inputs
196
-
197
- for _ in range(n_steps):
198
- forces = {nid: 0.0 for nid, nd in self.nodes.items()
199
- if not nd['anchored']}
200
-
201
- # Standard hourglass forces (unchanged β€” no merge resolution needed)
202
- for d in range(1, n+1):
203
- A_val = self.nodes[f'A{d}']['x']
204
- B_val = self.nodes[f'B{d}']['x']
205
- C_val = self.nodes[f'C{d}']['x']
206
-
207
- for j in range(1, self.n_upper+1):
208
- uid = f'U{d}_{j}'
209
- ak = self.springs[(f'A{d}', uid)]
210
- f = FWD_K * (ak * A_val - self.nodes[uid]['x'])
211
- if alpha > 0:
212
- kuc = self.springs[(uid, f'C{d}')]
213
- f += alpha * kuc * (C_val - self.nodes[uid]['x'])
214
- forces[uid] += f
215
-
216
- for j in range(1, self.n_lower+1):
217
- lid = f'L{d}_{j}'
218
- bk = self.springs[(f'B{d}', lid)]
219
- f = FWD_K * (bk * B_val - self.nodes[lid]['x'])
220
- if alpha > 0:
221
- klc = self.springs[(lid, f'C{d}')]
222
- f += alpha * klc * (C_val - self.nodes[lid]['x'])
223
- forces[lid] += f
224
-
225
- c = self.nodes[f'C{d}']
226
- if not c['anchored']:
227
- rest_c = (
228
- sum(self.springs[(f'U{d}_{j}', f'C{d}')] *
229
- self.nodes[f'U{d}_{j}']['x']
230
- for j in range(1, self.n_upper+1)) +
231
- sum(self.springs[(f'L{d}_{j}', f'C{d}')] *
232
- self.nodes[f'L{d}_{j}']['x']
233
- for j in range(1, self.n_lower+1))
234
- )
235
- forces[f'C{d}'] = forces.get(f'C{d}', 0.0) + \
236
- FWD_K * (rest_c - c['x'])
237
-
238
- # Bridge forces β€” symmetric Hooke's law, passive (not learned)
239
- # Bridge node pulled toward both neighbours; each neighbour gets
240
- # an equal-and-opposite soft reaction pull toward the bridge.
241
- for (bnode, peer), k in self.bridge_springs.items():
242
- xb = self.nodes[bnode]['x']
243
- xp = self.nodes[peer]['x']
244
- spring_f = FWD_K * k * (xp - xb)
245
- if bnode in forces: forces[bnode] += spring_f
246
- if peer in forces: forces[peer] -= spring_f # weak reaction
247
 
248
- max_v = 0.0
249
- for nid, f in forces.items():
250
- nd = self.nodes[nid]
251
- nd['vel'] = nd['vel'] * DAMPING + f * DT
252
- nd['x'] += nd['vel'] * DT
253
- max_v = max(max_v, abs(nd['vel']))
254
- if max_v < SETTLE:
255
- break
256
 
257
- # ── FEEDFORWARD ───────────────────────────────────────────────────────────
258
-
259
- def _feedforward(self):
260
- n = self.n_inputs
261
- preds = []
262
- ff = {}
263
-
264
- for d in range(1, n+1):
265
- A_val = self.nodes[f'A{d}']['x']
266
- B_val = self.nodes[f'B{d}']['x']
267
-
268
- for j in range(1, self.n_upper+1):
269
- uid = f'U{d}_{j}'
270
- # With cross_connect the node's settled position already
271
- # encodes the soft bridge influence β€” use it directly.
272
- ff[uid] = (self.nodes[uid]['x'] if self.cross_connect
273
- else self.springs[(f'A{d}', uid)] * A_val)
274
-
275
- for j in range(1, self.n_lower+1):
276
- lid = f'L{d}_{j}'
277
- ff[lid] = (self.nodes[lid]['x'] if self.cross_connect
278
- else self.springs[(f'B{d}', lid)] * B_val)
279
-
280
- if self.architecture == 'multiplicative':
281
- nm = max(self.n_upper, self.n_lower)
282
- pred = 0.0
283
- for i in range(nm):
284
- uid = f'U{d}_{(i % self.n_upper)+1}'
285
- lid = f'L{d}_{(i % self.n_lower)+1}'
286
- ku = self.springs[(uid, f'C{d}')]
287
- kl = self.springs[(lid, f'C{d}')]
288
- pred += ku * ff[uid] * kl * ff[lid]
289
- else:
290
- pred = (
291
- sum(self.springs[(f'U{d}_{j}', f'C{d}')] * ff[f'U{d}_{j}']
292
- for j in range(1, self.n_upper+1)) +
293
- sum(self.springs[(f'L{d}_{j}', f'C{d}')] * ff[f'L{d}_{j}']
294
- for j in range(1, self.n_lower+1))
295
- )
296
- preds.append(pred)
297
-
298
- return preds, ff
299
-
300
- # ── LMS UPDATE ────────────────────────────────────────────────────────────
301
- # Bridge springs are never in self.springs so they are never touched here.
302
- # Each dimension's LMS update is fully independent β€” no cross-gradient
303
- # bleed, no error inflation.
304
-
305
- def _lms_update(self, errors, ff):
306
- n = self.n_inputs
307
-
308
- for d in range(1, n+1):
309
- err = errors[d-1]
310
- A_val = self.nodes[f'A{d}']['x']
311
- B_val = self.nodes[f'B{d}']['x']
312
- grads = {}
313
-
314
- if self.architecture == 'additive':
315
- for j in range(1, self.n_upper+1):
316
- uid = f'U{d}_{j}'
317
- ak_key = (f'A{d}', uid)
318
- uc_key = (uid, f'C{d}')
319
- grads[ak_key] = self.springs[uc_key] * A_val
320
- grads[uc_key] = self.springs[ak_key] * A_val
321
- for j in range(1, self.n_lower+1):
322
- lid = f'L{d}_{j}'
323
- bk_key = (f'B{d}', lid)
324
- lc_key = (lid, f'C{d}')
325
- grads[bk_key] = self.springs[lc_key] * B_val
326
- grads[lc_key] = self.springs[bk_key] * B_val
327
  else:
328
- nm = max(self.n_upper, self.n_lower)
329
- for i in range(nm):
330
- uid = f'U{d}_{(i % self.n_upper)+1}'
331
- lid = f'L{d}_{(i % self.n_lower)+1}'
332
- ku = self.springs[(uid, f'C{d}')]
333
- kl = self.springs[(lid, f'C{d}')]
334
- Uv = ff[uid]; Lv = ff[lid]
335
- grads[(f'A{d}', uid)] = ku * A_val * kl * Lv
336
- grads[(f'B{d}', lid)] = kl * B_val * ku * Uv
337
- grads[(uid, f'C{d}')] = Uv * kl * Lv
338
- grads[(lid, f'C{d}')] = Lv * ku * Uv
339
-
340
- norm_sq = sum(g*g for g in grads.values()) + 1e-10
341
- mu = err / norm_sq
342
- for key, g in grads.items():
343
- if key in self.springs:
344
- self.springs[key] -= mu * g
345
- self.springs[key] = max(-30.0, min(30.0, self.springs[key]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
  # ── PHYSICS STEP ──────────────────────────────────────────────────────────
348
 
349
  def physics_step(self):
350
- self._elastic_step(MICRO)
351
- preds, ff = self._feedforward()
352
- n = self.n_inputs
353
-
354
- errors = []
355
- for d in range(1, n+1):
356
- c = self.nodes[f'C{d}']
357
- if c['anchored']:
358
- errors.append(preds[d-1] - c['x'])
359
- else:
360
- c['x'] = round(preds[d-1], 4)
361
- errors.append(0.0)
 
 
 
 
362
 
363
- self.current_prediction = round(sum(preds) / n, 5)
364
- self.current_error = round(sum(abs(e) for e in errors) / n, 5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  self.history.append(self.current_error)
366
- if len(self.history) > 200:
367
- self.history.pop(0)
368
 
369
  if self.current_error < CONV_THRESH:
370
- a_vec = [self.nodes[f'A{d}']['x'] for d in range(1, n+1)]
371
- b_vec = [self.nodes[f'B{d}']['x'] for d in range(1, n+1)]
372
- gt = self.ground_truth(a_vec, b_vec)
373
- delta = sum(abs(preds[d-1] - gt[d-1]) for d in range(1, n+1)) / n
374
- if n == 1:
375
- self.add_log(
376
- f"βœ“ A={a_vec[0]:.2f} B={b_vec[0]:.2f} "
377
- f"P={preds[0]:.4f} GT={gt[0]:.4f} Ξ”={delta:.4f}"
378
- )
379
- else:
380
- p_str = ' '.join(f'{p:.3f}' for p in preds)
381
- g_str = ' '.join(f'{g:.3f}' for g in gt)
382
- self.add_log(f"βœ“ D={n} P=[{p_str}] GT=[{g_str}] Ξ”={delta:.4f}")
383
  return self._next_or_stop()
384
 
385
- if self.mode == 'training' and any(
386
- self.nodes[f'C{d}']['anchored'] for d in range(1, n+1)
387
- ):
388
- self._lms_update(errors, ff)
389
 
390
  self.iteration += 1
391
  return True
@@ -402,32 +482,41 @@ class SimEngine:
402
 
403
  def generate_batch(self, count=30):
404
  self.batch_queue.clear()
405
- n = self.n_inputs
406
- nb = len(self.bridge_springs) // 2
407
  for _ in range(count):
408
  a_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
409
  b_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
410
- c_vec = self.ground_truth(a_vec, b_vec)
 
 
 
 
 
 
 
411
  self.batch_queue.append({'a': a_vec, 'b': b_vec, 'c': c_vec})
412
  p = self.batch_queue.popleft()
413
  self.set_problem(p['a'], p['b'], p.get('c'))
414
  self.running = True
415
- tag = f'B{nb}' if self.cross_connect and nb else 'Β·'
416
  self.add_log(
417
  f"β–Ά {count} | {self.dataset_type} | "
418
- f"D={n} U{self.n_upper}Β·L{self.n_lower} [{tag}]"
 
419
  )
420
 
421
 
422
  # ── SERVER ────────────────────────────────────────────────────────────────────
423
  engine = SimEngine()
424
 
 
425
  def run_loop():
426
  while True:
427
  if engine.running:
428
  engine.physics_step()
429
  time.sleep(0.028)
430
 
 
431
  threading.Thread(target=run_loop, daemon=True).start()
432
 
433
 
@@ -438,33 +527,42 @@ async def get_ui():
438
 
439
  @app.get("/state")
440
  async def get_state():
441
- springs_out = {f"{u}β†’{v}": round(k, 5) for (u, v), k in engine.springs.items()}
442
- bridge_out = {f"{u}β†’{v}": round(k, 5) for (u, v), k in engine.bridge_springs.items()}
443
- n = engine.n_inputs
444
- nb = len(engine.bridge_springs) // 2
 
 
 
 
445
  return {
446
- 'nodes': engine.nodes,
447
- 'springs': springs_out,
448
- 'bridge_springs': bridge_out,
449
- 'layers': engine.layers,
450
- 'error': engine.current_error,
451
- 'prediction': engine.current_prediction,
452
- 'predictions': [round(engine.nodes[f'C{d}']['x'], 4) for d in range(1, n+1)],
453
- 'iter': engine.iteration,
454
- 'logs': engine.logs,
455
- 'history': engine.history[-80:],
456
- 'running': engine.running,
457
- 'mode': engine.mode,
458
- 'architecture': engine.architecture,
459
- 'dataset_type': engine.dataset_type,
460
- 'n_inputs': n,
461
- 'n_upper': engine.n_upper,
462
- 'n_lower': engine.n_lower,
463
- 'back_alpha': engine.back_alpha,
464
- 'cross_connect': engine.cross_connect,
465
- 'n_bridges': nb,
466
- 'bridge_k': BRIDGE_K,
467
- 'queue_size': len(engine.batch_queue),
 
 
 
 
 
468
  }
469
 
470
 
@@ -476,76 +574,69 @@ async def set_mode(data: dict):
476
  return {"ok": True}
477
 
478
 
479
- @app.post("/toggle_cross")
480
- async def toggle_cross():
481
- engine.toggle_cross_connect()
482
- return {
483
- "ok": True,
484
- "cross_connect": engine.cross_connect,
485
- "n_springs": len(engine.springs),
486
- "n_bridges": len(engine.bridge_springs) // 2,
487
- "bridge_k": BRIDGE_K,
488
- }
489
-
490
-
491
  @app.post("/config")
492
  async def config(data: dict):
493
- new_ni = max(1, min(8, int(data.get('n_inputs', engine.n_inputs))))
494
- new_nu = max(1, min(16, int(data.get('n_upper', engine.n_upper))))
495
- new_nl = max(1, min(16, int(data.get('n_lower', engine.n_lower))))
496
-
497
- topo_changed = (
498
- new_ni != engine.n_inputs or
499
- new_nu != engine.n_upper or
500
- new_nl != engine.n_lower
501
- )
502
-
503
- engine.architecture = data.get('architecture', engine.architecture)
504
- engine.dataset_type = data.get('dataset', engine.dataset_type)
505
- engine.back_alpha = max(0.0, min(1.0, float(data.get('back_alpha', engine.back_alpha))))
506
- engine.n_inputs = new_ni
507
- engine.n_upper = new_nu
508
- engine.n_lower = new_nl
 
 
509
  if 'mode' in data:
510
- engine.mode = data['mode']
511
 
512
  if topo_changed:
513
  engine.running = False
514
- engine._init_mesh()
515
  engine.logs = []
516
- nb = len(engine.bridge_springs) // 2
517
  engine.add_log(
518
- f"Mesh rebuilt: D={new_ni} U{new_nu}Β·L{new_nl} "
519
- f"cross={'ON ('+str(nb)+' bridges)' if engine.cross_connect else 'OFF'}"
 
520
  )
521
  else:
522
  engine.add_log(
523
  f"Config: {engine.mode}|{engine.architecture}"
524
- f"|D={new_ni}|Ξ±={engine.back_alpha:.2f}"
525
  )
526
 
527
- return {"ok": True, "topo_changed": topo_changed}
528
 
529
 
530
  @app.post("/set_layer")
531
  async def set_layer(data: dict):
532
  layer = data.get('layer', '')
533
  delta = int(data.get('delta', 0))
534
- if layer == 'inputs': engine.n_inputs = max(1, min(8, engine.n_inputs + delta))
535
- elif layer == 'upper': engine.n_upper = max(1, min(16, engine.n_upper + delta))
536
- elif layer == 'lower': engine.n_lower = max(1, min(16, engine.n_lower + delta))
 
537
  engine.running = False
538
- engine._init_mesh()
539
- nb = len(engine.bridge_springs) // 2
540
  engine.add_log(
541
  f"Topology β†’ D={engine.n_inputs} U{engine.n_upper}Β·L{engine.n_lower} "
542
- f"cross={'ON ('+str(nb)+' bridges)' if engine.cross_connect else 'OFF'}"
543
  )
544
  return {
545
- "ok": True,
546
- "n_inputs": engine.n_inputs,
547
- "n_upper": engine.n_upper,
548
- "n_lower": engine.n_lower,
 
 
549
  }
550
 
551
 
 
6
  app = FastAPI()
7
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
8
 
9
+ FWD_K = 2.2
10
+ DAMPING = 0.55
11
+ DT = 0.12
12
+ MICRO = 6
13
+ SETTLE = 0.004
14
+ CONV_THRESH = 0.02
15
+ STIFF_EPS = 0.0005
16
+ STIFF_INTERVAL = 1.0
17
+
18
+
19
+ # ── ATOMIC UNIT ───────────────────────────────────────────────────────────────
20
+
21
+ class HourglassUnit:
22
+ def __init__(self, uid, n_upper, n_lower, architecture='additive'):
23
+ self.uid = uid
24
+ self.n_upper = n_upper
25
+ self.n_lower = n_lower
26
+ self.architecture = architecture
27
+ self.nodes = {}
28
+ self.springs = {}
29
+ self._prev_springs = {}
30
+ self.stiffness_delta = 0.0
31
+ self.c_val = 0.0
32
+ self.a_val = 0.0
33
+ self.b_val = 0.0
34
+ self._rebuild()
35
+
36
+ def _rebuild(self):
37
+ self.nodes = {}
38
+ self.springs = {}
39
+ for j in range(1, self.n_upper + 1):
40
+ self.nodes[f'U{j}'] = {'x': 0.0, 'vel': 0.0}
41
+ for j in range(1, self.n_lower + 1):
42
+ self.nodes[f'L{j}'] = {'x': 0.0, 'vel': 0.0}
43
+ for j in range(1, self.n_upper + 1):
44
+ self.springs[('A', f'U{j}')] = round(random.uniform(0.85, 1.15), 4)
45
+ self.springs[(f'U{j}', 'C')] = round(random.uniform(0.85, 1.15), 4)
46
+ for j in range(1, self.n_lower + 1):
47
+ self.springs[('B', f'L{j}')] = round(random.uniform(0.85, 1.15), 4)
48
+ self.springs[(f'L{j}', 'C')] = round(random.uniform(0.85, 1.15), 4)
49
+
50
+ def reset_hidden(self):
51
+ for nd in self.nodes.values():
52
+ nd['x'] = 0.0; nd['vel'] = 0.0
53
+
54
+ def elastic_step(self, a_val, b_val, c_target, c_anchored, alpha):
55
+ self.a_val = a_val
56
+ self.b_val = b_val
57
+ for _ in range(MICRO):
58
+ forces = {nid: 0.0 for nid in self.nodes}
59
+ for j in range(1, self.n_upper + 1):
60
+ uid = f'U{j}'
61
+ ak = self.springs[('A', uid)]
62
+ f = FWD_K * (ak * a_val - self.nodes[uid]['x'])
63
+ if alpha > 0 and c_anchored:
64
+ f += alpha * self.springs[(uid, 'C')] * (c_target - self.nodes[uid]['x'])
65
+ forces[uid] += f
66
+ for j in range(1, self.n_lower + 1):
67
+ lid = f'L{j}'
68
+ bk = self.springs[('B', lid)]
69
+ f = FWD_K * (bk * b_val - self.nodes[lid]['x'])
70
+ if alpha > 0 and c_anchored:
71
+ f += alpha * self.springs[(lid, 'C')] * (c_target - self.nodes[lid]['x'])
72
+ forces[lid] += f
73
+ max_v = 0.0
74
+ for nid, fv in forces.items():
75
+ nd = self.nodes[nid]
76
+ nd['vel'] = nd['vel'] * DAMPING + fv * DT
77
+ nd['x'] += nd['vel'] * DT
78
+ max_v = max(max_v, abs(nd['vel']))
79
+ if max_v < SETTLE:
80
+ break
81
 
82
+ def feedforward(self, a_val, b_val):
83
+ ff = {}
84
+ for j in range(1, self.n_upper + 1):
85
+ uid = f'U{j}'; ff[uid] = self.springs[('A', uid)] * a_val
86
+ for j in range(1, self.n_lower + 1):
87
+ lid = f'L{j}'; ff[lid] = self.springs[('B', lid)] * b_val
88
+
89
+ if self.architecture == 'multiplicative':
90
+ nm = max(self.n_upper, self.n_lower); pred = 0.0
91
+ for i in range(nm):
92
+ uid = f'U{(i % self.n_upper)+1}'; lid = f'L{(i % self.n_lower)+1}'
93
+ pred += self.springs[(uid,'C')] * ff[uid] * self.springs[(lid,'C')] * ff[lid]
94
+ else:
95
+ pred = (sum(self.springs[(f'U{j}','C')] * ff[f'U{j}'] for j in range(1, self.n_upper+1)) +
96
+ sum(self.springs[(f'L{j}','C')] * ff[f'L{j}'] for j in range(1, self.n_lower+1)))
97
+ self.c_val = pred
98
+ return pred, ff
99
+
100
+ def lms_update(self, error, a_val, b_val, ff):
101
+ grads = {}
102
+ if self.architecture == 'additive':
103
+ for j in range(1, self.n_upper + 1):
104
+ uid = f'U{j}'
105
+ grads[('A', uid)] = self.springs[(uid,'C')] * a_val
106
+ grads[(uid, 'C')] = self.springs[('A',uid)] * a_val
107
+ for j in range(1, self.n_lower + 1):
108
+ lid = f'L{j}'
109
+ grads[('B', lid)] = self.springs[(lid,'C')] * b_val
110
+ grads[(lid, 'C')] = self.springs[('B',lid)] * b_val
111
+ else:
112
+ nm = max(self.n_upper, self.n_lower)
113
+ for i in range(nm):
114
+ uid = f'U{(i % self.n_upper)+1}'; lid = f'L{(i % self.n_lower)+1}'
115
+ ku = self.springs[(uid,'C')]; kl = self.springs[(lid,'C')]
116
+ Uv = ff[uid]; Lv = ff[lid]
117
+ grads[('A',uid)] = grads.get(('A',uid),0.0) + ku*a_val*kl*Lv
118
+ grads[('B',lid)] = grads.get(('B',lid),0.0) + kl*b_val*ku*Uv
119
+ grads[(uid,'C')] = grads.get((uid,'C'), 0.0) + Uv*kl*Lv
120
+ grads[(lid,'C')] = grads.get((lid,'C'), 0.0) + Lv*ku*Uv
121
+
122
+ norm_sq = sum(g*g for g in grads.values()) + 1e-10
123
+ mu = error / norm_sq
124
+ for key, g in grads.items():
125
+ if key in self.springs:
126
+ self.springs[key] -= mu * g
127
+ self.springs[key] = max(-30.0, min(30.0, self.springs[key]))
128
+
129
+ def sensitivity_to_a(self):
130
+ if self.architecture == 'additive':
131
+ return sum(self.springs[('A',f'U{j}')] * self.springs[(f'U{j}','C')]
132
+ for j in range(1, self.n_upper+1))
133
+ return 1.0
134
+
135
+ def sensitivity_to_b(self):
136
+ if self.architecture == 'additive':
137
+ return sum(self.springs[('B',f'L{j}')] * self.springs[(f'L{j}','C')]
138
+ for j in range(1, self.n_lower+1))
139
+ return 1.0
140
+
141
+ def take_stiffness_snapshot(self):
142
+ if not self._prev_springs:
143
+ self._prev_springs = dict(self.springs); return 0.0
144
+ changed = sum(1 for k in self.springs
145
+ if abs(self.springs[k] - self._prev_springs.get(k, self.springs[k])) > STIFF_EPS)
146
+ self._prev_springs = dict(self.springs)
147
+ self.stiffness_delta = (changed / len(self.springs) * 100.0) if self.springs else 0.0
148
+ return self.stiffness_delta
149
+
150
+ def to_dict(self):
151
+ return {
152
+ 'springs': {f"{u}β†’{v}": round(k, 5) for (u,v),k in self.springs.items()},
153
+ 'nodes': {nid: {'x': round(nd['x'],5), 'vel': round(nd['vel'],5)}
154
+ for nid, nd in self.nodes.items()},
155
+ 'c_val': round(self.c_val, 4),
156
+ 'a_val': round(self.a_val, 4),
157
+ 'b_val': round(self.b_val, 4),
158
+ 'stiffness_delta': round(self.stiffness_delta, 1),
159
+ 'n_upper': self.n_upper,
160
+ 'n_lower': self.n_lower,
161
+ }
162
+
163
+
164
+ # ── ENGINE ────────────────────────────────────────────────────────────────────
165
 
166
  class SimEngine:
167
+ CREDIT_MODES = ['elastic_backprop', 'greedy', 'param_hg', 'independent']
168
+
169
  def __init__(self):
170
+ self.mode = 'training'
171
+ self.architecture = 'additive'
172
+ self.dataset_type = 'housing'
173
+ self.n_inputs = 1
174
+ self.n_upper = 3
175
+ self.n_lower = 3
176
+ self.back_alpha = 0.45
177
+ self.stack_levels = 0
178
+ self.credit_mode = 'elastic_backprop'
179
+ self.reverse_mode = False
180
+ self.individual_train = False
181
+ self.running = False
182
+ self.batch_queue = collections.deque()
183
+ self.logs = []
184
+ self.iteration = 0
185
+ self.current_error = 0.0
186
  self.current_prediction = 0.0
187
+ self.history = []
188
+ self.units = {}
189
+ self.topology = []
190
+ self.connections = []
191
+ self._a_vec = [2.0]
192
+ self._b_vec = [3.0]
193
+ self._c_vec = []
194
+ self._last_stiff_time = time.time()
195
+ self.stiffness_active = 0.0
196
+ self.stiffness_history = []
197
+ self._init_stack()
198
 
199
  # ── TOPOLOGY ──────────────────────────────────────────────────────────────
200
 
201
+ def _init_stack(self):
202
+ self.iteration = 0
203
+ self.current_error = 0.0
 
 
 
 
 
 
 
 
 
 
204
  self.current_prediction = 0.0
205
+ self.history = []
206
+ self.units = {}
207
+ self.topology = []
208
+ self.connections = []
209
+
210
+ n = max(1, self.n_inputs)
211
+ nu = self.n_upper
212
+ nl = self.n_lower
213
+ arch = self.architecture
214
+
215
+ if self.reverse_mode:
216
+ uid = 'HG_REV'
217
+ self.units[uid] = HourglassUnit(uid, nu, nl, arch)
218
+ self.topology = [[uid]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
+ leaves = []
222
+ for i in range(n):
223
+ uid = f'HG_L0_{i}'
224
+ self.units[uid] = HourglassUnit(uid, nu, nl, arch)
225
+ leaves.append(uid)
226
+ self.topology.append(leaves)
227
+
228
+ for lv in range(1, self.stack_levels + 1):
229
+ prev = self.topology[-1]; curr = []
230
+ for pi in range(0, len(prev), 2):
231
+ ua = prev[pi]
232
+ ub = prev[pi+1] if pi+1 < len(prev) else prev[pi]
233
+ uid = f'HG_L{lv}_{pi//2}'
234
+ self.units[uid] = HourglassUnit(uid, nu, nl, arch)
235
+ curr.append(uid)
236
+ self.connections.append({'from_uid': ua, 'to_uid': uid, 'to_port': 'A'})
237
+ self.connections.append({'from_uid': ub, 'to_uid': uid, 'to_port': 'B'})
238
+ self.topology.append(curr)
239
 
240
+ # ── DATASET ───────────────────────────────────────────────────────────────
241
 
242
  def _to_vec(self, val, n):
243
  if isinstance(val, (list, tuple)):
244
  v = [float(x) for x in val]
245
+ return (v[:n] if len(v) >= n else v + [v[-1]] * (n - len(v)))
 
246
  return [float(val)] * n
247
 
 
 
248
  def ground_truth(self, a_vec, b_vec):
 
 
 
 
249
  result = []
250
+ for a, b in zip(a_vec, b_vec):
251
+ t = self.dataset_type
252
  if t == 'housing': result.append(round(a*2.5 + b*1.2, 4))
253
  elif t == 'subtraction': result.append(round(a - b, 4))
254
  elif t == 'multiplication': result.append(round(a * b, 4))
 
259
  # ── PROBLEM SETUP ─────────────────────────────────────────────────────────
260
 
261
  def set_problem(self, a, b, c_target=None):
262
+ n = max(1, self.n_inputs)
263
+ self._a_vec = self._to_vec(a, n)
264
+ self._b_vec = self._to_vec(b, n)
265
+ self._c_vec = list(self._to_vec(c_target, n)) if c_target is not None else []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
+ # ── LOGGING ───────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
268
 
269
+ def add_log(self, msg):
270
+ self.logs.insert(0, f"[{self.iteration:05d}] {msg}")
271
+ if len(self.logs) > 50: self.logs.pop()
272
+
273
+ def reset(self):
274
+ self.running = False; self.batch_queue.clear()
275
+ self.logs = []; self._init_stack()
276
+
277
+ # ── FORWARD PASS ──────────────────────────────────────────────────────────
278
+
279
+ def _forward_pass(self, training):
280
+ a_vec = self._a_vec; b_vec = self._b_vec; c_vec = self._c_vec
281
+ alpha = self.back_alpha; io = {}
282
+ only_level = (len(self.topology) == 1)
283
+
284
+ for i, uid in enumerate(self.topology[0]):
285
+ av = a_vec[i] if i < len(a_vec) else a_vec[-1]
286
+ bv = b_vec[i] if i < len(b_vec) else b_vec[-1]
287
+ u = self.units[uid]; u.reset_hidden()
288
+ anchor = training and bool(c_vec) and (only_level or self.individual_train)
289
+ if anchor:
290
+ if self.individual_train and not only_level:
291
+ c_tgt = self.ground_truth([av], [bv])[0]
292
+ else:
293
+ c_tgt = c_vec[i] if i < len(c_vec) else c_vec[-1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  else:
295
+ c_tgt = 0.0
296
+ u.elastic_step(av, bv, c_tgt, anchor, alpha)
297
+ pred, ff = u.feedforward(av, bv)
298
+ io[uid] = (av, bv, pred, ff)
299
+
300
+ for lv in range(1, len(self.topology)):
301
+ for uid in self.topology[lv]:
302
+ conns = [c for c in self.connections if c['to_uid'] == uid]
303
+ a_src = next((c['from_uid'] for c in conns if c['to_port'] == 'A'), None)
304
+ b_src = next((c['from_uid'] for c in conns if c['to_port'] == 'B'), None)
305
+ av = io[a_src][2] if a_src in io else 0.0
306
+ bv = io[b_src][2] if b_src in io else 0.0
307
+ u = self.units[uid]; u.reset_hidden()
308
+ is_root = (lv == len(self.topology) - 1)
309
+ anchor = training and bool(c_vec) and is_root
310
+ c_tgt = c_vec[0] if anchor else 0.0
311
+ u.elastic_step(av, bv, c_tgt, anchor, alpha)
312
+ pred, ff = u.feedforward(av, bv)
313
+ io[uid] = (av, bv, pred, ff)
314
+
315
+ return io[self.topology[-1][-1]][2], io
316
+
317
+ def _forward_reverse(self, training):
318
+ uid = self.topology[0][0]; u = self.units[uid]
319
+ c_in = self._c_vec[0] if self._c_vec else 0.0
320
+ a_gt = self._a_vec[0] if self._a_vec else 0.0
321
+ u.reset_hidden()
322
+ u.elastic_step(c_in, c_in, a_gt if training else 0.0, training, self.back_alpha)
323
+ pred, ff = u.feedforward(c_in, c_in)
324
+ if training:
325
+ u.lms_update(pred - a_gt, c_in, c_in, ff)
326
+ return pred, {uid: (c_in, c_in, pred, ff)}
327
+
328
+ # ── CREDIT ASSIGNMENT ─────────────────────────────────────────────────────
329
+
330
+ def _backprop(self, root_err, io):
331
+ errors = {self.topology[-1][-1]: root_err}
332
+ for lv in range(len(self.topology)-1, -1, -1):
333
+ for uid in self.topology[lv]:
334
+ if uid not in errors: continue
335
+ err = errors[uid]; av, bv, _, ff = io[uid]
336
+ self.units[uid].lms_update(err, av, bv, ff)
337
+ if lv > 0:
338
+ u = self.units[uid]
339
+ conns = [c for c in self.connections if c['to_uid'] == uid]
340
+ a_src = next((c['from_uid'] for c in conns if c['to_port'] == 'A'), None)
341
+ b_src = next((c['from_uid'] for c in conns if c['to_port'] == 'B'), None)
342
+ if a_src:
343
+ errors[a_src] = errors.get(a_src, 0.0) + err * u.sensitivity_to_a()
344
+ if b_src and b_src != a_src:
345
+ errors[b_src] = errors.get(b_src, 0.0) + err * u.sensitivity_to_b()
346
+
347
+ def _greedy(self, root_err, io):
348
+ n = len(self.topology)
349
+ for lv in range(n-1, -1, -1):
350
+ scale = 1.0 / max(1, n - lv)
351
+ for uid in self.topology[lv]:
352
+ av, bv, _, ff = io[uid]
353
+ self.units[uid].lms_update(root_err * scale, av, bv, ff)
354
+
355
+ def _param_hg(self, root_err, io):
356
+ for level in self.topology:
357
+ for uid in level:
358
+ av, bv, _, ff = io[uid]
359
+ self.units[uid].lms_update(root_err, av, bv, ff)
360
+
361
+ def _independent(self, root_err, io):
362
+ errors = {self.topology[-1][-1]: root_err}
363
+ for lv in range(len(self.topology)-1, 0, -1):
364
+ for uid in self.topology[lv]:
365
+ if uid not in errors: continue
366
+ err = errors[uid]; av, bv, _, ff = io[uid]
367
+ self.units[uid].lms_update(err, av, bv, ff)
368
+ u = self.units[uid]
369
+ conns = [c for c in self.connections if c['to_uid'] == uid]
370
+ a_src = next((c['from_uid'] for c in conns if c['to_port'] == 'A'), None)
371
+ b_src = next((c['from_uid'] for c in conns if c['to_port'] == 'B'), None)
372
+ if a_src:
373
+ errors[a_src] = errors.get(a_src, 0.0) + err * u.sensitivity_to_a()
374
+ if b_src and b_src != a_src:
375
+ errors[b_src] = errors.get(b_src, 0.0) + err * u.sensitivity_to_b()
376
+ # Leaves: independent GT
377
+ for i, uid in enumerate(self.topology[0]):
378
+ av, bv, c_pred, ff = io[uid]
379
+ gt_i = self.ground_truth([av], [bv])[0]
380
+ self.units[uid].lms_update(c_pred - gt_i, av, bv, ff)
381
+
382
+ def _train(self, root_err, io):
383
+ m = self.credit_mode
384
+ if m == 'elastic_backprop': self._backprop(root_err, io)
385
+ elif m == 'greedy': self._greedy(root_err, io)
386
+ elif m == 'param_hg': self._param_hg(root_err, io)
387
+ elif m == 'independent': self._independent(root_err, io)
388
+
389
+ # ── STIFFNESS MONITOR ─────────────────────────────────────────────────────
390
+
391
+ def _update_stiffness(self):
392
+ now = time.time()
393
+ if now - self._last_stiff_time < STIFF_INTERVAL:
394
+ return
395
+ self._last_stiff_time = now
396
+ deltas = [u.take_stiffness_snapshot() for u in self.units.values()]
397
+ self.stiffness_active = round(sum(deltas) / len(deltas), 1) if deltas else 0.0
398
+ self.stiffness_history.append(self.stiffness_active)
399
+ if len(self.stiffness_history) > 60:
400
+ self.stiffness_history.pop(0)
401
 
402
  # ── PHYSICS STEP ──────────────────────────────────────────────────────────
403
 
404
  def physics_step(self):
405
+ training = (self.mode == 'training')
406
+
407
+ # Reverse mode: C→A reconstruction
408
+ if self.reverse_mode:
409
+ pred, io = self._forward_reverse(training)
410
+ a_gt = self._a_vec[0] if self._a_vec else 0.0
411
+ self.current_error = round(abs(pred - a_gt), 5)
412
+ self.current_prediction = round(pred, 5)
413
+ self.iteration += 1
414
+ self.history.append(self.current_error)
415
+ if len(self.history) > 200: self.history.pop(0)
416
+ self._update_stiffness()
417
+ if self.current_error < CONV_THRESH:
418
+ self.add_log(f"βœ“ REV C={self._c_vec[0]:.2f} β†’ A_pred={pred:.4f} gt={a_gt:.4f}")
419
+ return self._next_or_stop()
420
+ return True
421
 
422
+ # Normal forward/stacked pass
423
+ pred, io = self._forward_pass(training)
424
+
425
+ # Flat multi-unit: average error across all leaf outputs vs their own GT
426
+ if len(self.topology) == 1 and len(self.topology[0]) > 1:
427
+ errors = []
428
+ for i, uid in enumerate(self.topology[0]):
429
+ av, bv, c_pred, _ = io[uid]
430
+ gt_i = (self._c_vec[i] if i < len(self._c_vec) else
431
+ self.ground_truth([av], [bv])[0])
432
+ errors.append(c_pred - gt_i)
433
+ mean_err = sum(abs(e) for e in errors) / len(errors)
434
+ self.current_error = round(mean_err, 5)
435
+ self.current_prediction = round(sum(io[uid][2]
436
+ for uid in self.topology[0]) / len(self.topology[0]), 5)
437
+ self.history.append(self.current_error)
438
+ if len(self.history) > 200: self.history.pop(0)
439
+ self._update_stiffness()
440
+ if self.current_error < CONV_THRESH:
441
+ self.add_log(f"βœ“ FLAT D={len(self.topology[0])} avg_err={mean_err:.4f}")
442
+ return self._next_or_stop()
443
+ if training:
444
+ for i, uid in enumerate(self.topology[0]):
445
+ av, bv, _, ff = io[uid]
446
+ self.units[uid].lms_update(errors[i], av, bv, ff)
447
+ self.iteration += 1
448
+ return True
449
+
450
+ # Stacked: single root output vs single GT
451
+ c_gt = self._c_vec[0] if self._c_vec else None
452
+ root_err = (pred - c_gt) if c_gt is not None else 0.0
453
+ self.current_error = round(abs(root_err), 5)
454
+ self.current_prediction = round(pred, 5)
455
  self.history.append(self.current_error)
456
+ if len(self.history) > 200: self.history.pop(0)
457
+ self._update_stiffness()
458
 
459
  if self.current_error < CONV_THRESH:
460
+ n_units = len(self.units)
461
+ self.add_log(
462
+ f"βœ“ STACK L{self.stack_levels} [{n_units}u] "
463
+ f"P={pred:.4f} GT={c_gt:.4f} Ξ”={abs(root_err):.4f}"
464
+ )
 
 
 
 
 
 
 
 
465
  return self._next_or_stop()
466
 
467
+ if training and c_gt is not None:
468
+ self._train(root_err, io)
 
 
469
 
470
  self.iteration += 1
471
  return True
 
482
 
483
  def generate_batch(self, count=30):
484
  self.batch_queue.clear()
485
+ n = max(1, self.n_inputs)
 
486
  for _ in range(count):
487
  a_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
488
  b_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
489
+ if self.reverse_mode:
490
+ c_vec = self.ground_truth(a_vec, b_vec)
491
+ else:
492
+ if self.stack_levels == 0:
493
+ c_vec = self.ground_truth(a_vec, b_vec)
494
+ else:
495
+ # Stacked: single output GT is the function of the first pair
496
+ c_vec = [self.ground_truth([a_vec[0]], [b_vec[0]])[0]]
497
  self.batch_queue.append({'a': a_vec, 'b': b_vec, 'c': c_vec})
498
  p = self.batch_queue.popleft()
499
  self.set_problem(p['a'], p['b'], p.get('c'))
500
  self.running = True
501
+ tag = f'L{self.stack_levels}' if self.stack_levels else 'flat'
502
  self.add_log(
503
  f"β–Ά {count} | {self.dataset_type} | "
504
+ f"D={n} U{self.n_upper}Β·L{self.n_lower} "
505
+ f"[{tag}|{self.credit_mode[:4]}]"
506
  )
507
 
508
 
509
  # ── SERVER ────────────────────────────────────────────────────────────────────
510
  engine = SimEngine()
511
 
512
+
513
  def run_loop():
514
  while True:
515
  if engine.running:
516
  engine.physics_step()
517
  time.sleep(0.028)
518
 
519
+
520
  threading.Thread(target=run_loop, daemon=True).start()
521
 
522
 
 
527
 
528
  @app.get("/state")
529
  async def get_state():
530
+ units_out = {uid: u.to_dict() for uid, u in engine.units.items()}
531
+ # Flatten all springs for display (prefixed with unit id)
532
+ all_springs = {}
533
+ for uid, u in engine.units.items():
534
+ short = uid.replace('HG_', '')
535
+ for (a, b), k in u.springs.items():
536
+ all_springs[f"{short}:{a}β†’{b}"] = round(k, 5)
537
+
538
  return {
539
+ 'units': units_out,
540
+ 'topology': engine.topology,
541
+ 'connections': engine.connections,
542
+ 'springs': all_springs,
543
+ 'error': engine.current_error,
544
+ 'prediction': engine.current_prediction,
545
+ 'iter': engine.iteration,
546
+ 'logs': engine.logs,
547
+ 'history': engine.history[-80:],
548
+ 'running': engine.running,
549
+ 'mode': engine.mode,
550
+ 'architecture': engine.architecture,
551
+ 'dataset_type': engine.dataset_type,
552
+ 'n_inputs': engine.n_inputs,
553
+ 'n_upper': engine.n_upper,
554
+ 'n_lower': engine.n_lower,
555
+ 'back_alpha': engine.back_alpha,
556
+ 'stack_levels': engine.stack_levels,
557
+ 'credit_mode': engine.credit_mode,
558
+ 'reverse_mode': engine.reverse_mode,
559
+ 'individual_train': engine.individual_train,
560
+ 'queue_size': len(engine.batch_queue),
561
+ 'stiffness_active': engine.stiffness_active,
562
+ 'stiffness_history': engine.stiffness_history,
563
+ 'a_vec': engine._a_vec,
564
+ 'b_vec': engine._b_vec,
565
+ 'c_vec': engine._c_vec,
566
  }
567
 
568
 
 
574
  return {"ok": True}
575
 
576
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  @app.post("/config")
578
  async def config(data: dict):
579
+ new_ni = max(1, min(8, int(data.get('n_inputs', engine.n_inputs))))
580
+ new_nu = max(1, min(16, int(data.get('n_upper', engine.n_upper))))
581
+ new_nl = max(1, min(16, int(data.get('n_lower', engine.n_lower))))
582
+ new_sl = max(0, min(4, int(data.get('stack_levels', engine.stack_levels))))
583
+
584
+ topo_changed = (new_ni != engine.n_inputs or new_nu != engine.n_upper or
585
+ new_nl != engine.n_lower or new_sl != engine.stack_levels)
586
+
587
+ engine.architecture = data.get('architecture', engine.architecture)
588
+ engine.dataset_type = data.get('dataset', engine.dataset_type)
589
+ engine.back_alpha = max(0.0, min(1.0, float(data.get('back_alpha', engine.back_alpha))))
590
+ engine.credit_mode = data.get('credit_mode', engine.credit_mode)
591
+ engine.reverse_mode = bool(data.get('reverse_mode', engine.reverse_mode))
592
+ engine.individual_train= bool(data.get('individual_train', engine.individual_train))
593
+ engine.n_inputs = new_ni
594
+ engine.n_upper = new_nu
595
+ engine.n_lower = new_nl
596
+ engine.stack_levels = new_sl
597
  if 'mode' in data:
598
+ engine.mode = data['mode']
599
 
600
  if topo_changed:
601
  engine.running = False
602
+ engine._init_stack()
603
  engine.logs = []
604
+ n_units = len(engine.units)
605
  engine.add_log(
606
+ f"Stack rebuilt: D={new_ni} U{new_nu}Β·L{new_nl} "
607
+ f"levels={new_sl} units={n_units} "
608
+ f"credit={engine.credit_mode}"
609
  )
610
  else:
611
  engine.add_log(
612
  f"Config: {engine.mode}|{engine.architecture}"
613
+ f"|Ξ±={engine.back_alpha:.2f}|{engine.credit_mode}"
614
  )
615
 
616
+ return {"ok": True, "topo_changed": topo_changed, "n_units": len(engine.units)}
617
 
618
 
619
  @app.post("/set_layer")
620
  async def set_layer(data: dict):
621
  layer = data.get('layer', '')
622
  delta = int(data.get('delta', 0))
623
+ if layer == 'inputs': engine.n_inputs = max(1, min(8, engine.n_inputs + delta))
624
+ elif layer == 'upper': engine.n_upper = max(1, min(16, engine.n_upper + delta))
625
+ elif layer == 'lower': engine.n_lower = max(1, min(16, engine.n_lower + delta))
626
+ elif layer == 'stack': engine.stack_levels = max(0, min(4, engine.stack_levels + delta))
627
  engine.running = False
628
+ engine._init_stack()
 
629
  engine.add_log(
630
  f"Topology β†’ D={engine.n_inputs} U{engine.n_upper}Β·L{engine.n_lower} "
631
+ f"stack={engine.stack_levels} units={len(engine.units)}"
632
  )
633
  return {
634
+ "ok": True,
635
+ "n_inputs": engine.n_inputs,
636
+ "n_upper": engine.n_upper,
637
+ "n_lower": engine.n_lower,
638
+ "stack_levels": engine.stack_levels,
639
+ "n_units": len(engine.units),
640
  }
641
 
642