everydaytok commited on
Commit
e5df198
·
verified ·
1 Parent(s): 924c838

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +205 -583
app.py CHANGED
@@ -1,41 +1,9 @@
1
  """
2
- main.py — Scalar Triangulated Hourglass Mesh v4
3
-
4
- WHAT CHANGED FROM v3:
5
- • Every node is a single scalar float (not a D-vector)
6
- • Every spring K is a single scalar (not a D×D matrix)
7
- • Topology is a triangulated row-grid (matches the sketch)
8
- • Physics: Hooke's law F_i = Σ_j K_ij(x_j − x_i) − ε·x_i
9
- • Learning: Contrastive Hebbian (CHL) — two-phase equilibrium
10
- • No layer norm, no matrix multiply, no embedding vectors
11
-
12
- TOPOLOGY (default n_half=3, max_w=4):
13
-
14
- A Row 0 width 1 ← scalar input, anchored
15
- / \
16
- · · Row 1 width 2
17
- / \ / \
18
- · · · · Row 2 width 4 ← widest upper
19
- \ / \ / \ /
20
- · · · Row 3 width 3? ...
21
- ...
22
- [C] neck row ← scalar output, anchored in training
23
- ...
24
- · · ·
25
- / \ / \ / \
26
- · · · · widest lower
27
- \ / \ /
28
- · ·
29
- \ /
30
- B last row ← scalar input, anchored
31
-
32
- SPRINGS: one scalar K per undirected edge. K can be negative (repulsive).
33
- PHYSICS: Hooke's law with small grounding force F = K(x_j−x_i), F_ground = −ε·x_i
34
- LEARNING: CHL ΔK_ij = η·(x_i_clamp·x_j_clamp − x_i_free·x_j_free)
35
  """
36
-
37
  import numpy as np
38
- import time, collections, threading, json, random
39
  from fastapi import FastAPI
40
  from fastapi.responses import HTMLResponse, FileResponse
41
  from fastapi.middleware.cors import CORSMiddleware
@@ -43,586 +11,240 @@ from fastapi.middleware.cors import CORSMiddleware
43
  app = FastAPI()
44
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
45
 
46
- # ── PHYSICS CONSTANTS ─────────────────────────────────────────────────────────
47
- DT = 0.10
48
- DAMPING = 0.65
49
- GROUND_EPS = 0.04 # weak restoring force → 0, prevents drift/explosion
50
- SETTLE_N = 60 # steps to near-equilibrium per CHL phase
51
- CONV_THRESH = 0.03
52
- MAX_STEPS = 400
53
- LR = 0.006
54
- K_CLIP = 12.0
55
- MICRO = 10 # display steps per server tick
56
-
57
-
58
- # ── ROW WIDTH HELPER ──────────────────────────────────────────────────────────
59
-
60
- def make_hourglass_widths(n_half=3, max_w=4):
61
- """
62
- Symmetric hourglass:
63
- apex(1) → expand → max_w → contract → neck(1) → expand → max_w → contract → apex(1)
64
-
65
- Returns (widths_list, neck_row_index)
66
- """
67
- n = n_half + 2 # rows in the upper half (including apex + neck)
68
- upper = []
69
- for i in range(n):
70
- frac = i / (n - 1)
71
- if frac <= 0.5:
72
- w = round(1 + 2 * frac * (max_w - 1))
73
- else:
74
- w = round(max_w - 2 * (frac - 0.5) * (max_w - 1))
75
- upper.append(max(1, w))
76
- upper[0] = 1 # apex
77
- upper[-1] = 1 # neck
78
-
79
- full = upper + upper[::-1][1:] # mirror; neck shared, not duplicated
80
- neck_row = len(upper) - 1
81
- return full, neck_row
82
-
83
-
84
- # ── MESH ──────────────────────────────────────────────────────────────────────
85
-
86
- class TriMesh:
87
- """
88
- Scalar triangulated hourglass mesh.
89
-
90
- Each node holds: x (scalar position), vel (scalar velocity)
91
- Each edge holds: K (scalar spring stiffness, signed)
92
-
93
- The mesh learns K values via Contrastive Hebbian Learning so that,
94
- at equilibrium, the free C node matches the target C value.
95
- """
96
-
97
- def __init__(self, n_half=3, max_w=4):
98
- self.n_half = n_half
99
- self.max_w = max_w
100
- self._build()
101
-
102
- # ── BUILD ─────────────────────────────────────────────────────────────────
103
-
104
- def _build(self):
105
- widths, neck_row = make_hourglass_widths(self.n_half, self.max_w)
106
- self.row_widths = widths
107
- self.neck_row = neck_row
108
- n_rows = len(widths)
109
-
110
- self.layers = []
111
- self.nodes = {} # name → dict
112
- self.row_of = {}
113
-
114
- counter = [0]
115
- def mk(ri, ci, kind):
116
- if kind == 'A':
117
- name = f'A{ci}'
118
- elif kind == 'B':
119
- name = f'B{ci}'
120
- elif kind == 'C':
121
- name = f'C{ci}'
122
- else:
123
- name = f'H{counter[0]}'
124
- counter[0] += 1
125
- self.nodes[name] = {
126
- 'x': 0.0,
127
- 'vel': 0.0,
128
- 'anchored': kind in ('A', 'B'),
129
- 'row': ri,
130
- 'col': ci,
131
- 'kind': kind,
132
- }
133
- self.row_of[name] = ri
134
- return name
135
-
136
- for ri, w in enumerate(widths):
137
- if ri == 0:
138
- row = [mk(ri, ci, 'A') for ci in range(w)]
139
- elif ri == neck_row:
140
- row = [mk(ri, ci, 'C') for ci in range(w)]
141
- elif ri == n_rows - 1:
142
- row = [mk(ri, ci, 'B') for ci in range(w)]
143
- else:
144
- row = [mk(ri, ci, 'H') for ci in range(w)]
145
- self.layers.append(row)
146
-
147
- self.a_nodes = self.layers[0]
148
- self.c_nodes = self.layers[neck_row]
149
- self.b_nodes = self.layers[-1]
150
-
151
- # Springs: undirected, key = (u,v) with u < v lexicographically
152
  self.springs = {}
153
- self.adj = {n: [] for n in self.nodes}
154
-
155
- for ri in range(n_rows):
156
- row = self.layers[ri]
157
- # Horizontal springs
158
- for ci in range(len(row) - 1):
159
- self._add_spring(row[ci], row[ci + 1])
160
- # Inter-row springs to next row
161
- if ri < n_rows - 1:
162
- self._connect_rows(self.layers[ri], self.layers[ri + 1])
163
-
164
- # Init K: small positive, slightly randomised
165
- for key in self.springs:
166
- self.springs[key] = random.uniform(0.25, 0.55)
167
-
168
- # Precompute 2-D display positions (row-based, centred)
169
- self._positions = {}
170
- for ri, row in enumerate(self.layers):
171
- y = 1.0 - 2.0 * ri / max(n_rows - 1, 1)
172
- w = len(row)
173
- for ci, nid in enumerate(row):
174
- x = (2 * ci - (w - 1)) / max(w - 1, 1) if w > 1 else 0.0
175
- self._positions[nid] = (round(x * 4.5, 3), round(y * 4.5, 3))
176
-
177
- # Precompute triangles for visualisation
178
- self._triangles = self._find_triangles()
179
-
180
- def _ek(self, a, b):
181
- return (a, b) if a < b else (b, a)
182
-
183
- def _add_spring(self, a, b):
184
- key = self._ek(a, b)
185
- if key not in self.springs:
186
- self.springs[key] = 0.4
187
- self.adj[a].append(b)
188
- self.adj[b].append(a)
189
-
190
- def _connect_rows(self, ra, rb):
191
- """Connect two adjacent rows: nearest-neighbour bipartite mapping."""
192
- wa, wb = len(ra), len(rb)
193
- if wa <= wb:
194
- for i, a in enumerate(ra):
195
- j0 = (i * wb) // wa
196
- j1 = max(j0 + 1, ((i + 1) * wb + wa - 1) // wa)
197
- for j in range(min(j1, wb)):
198
- self._add_spring(a, rb[j])
199
- else:
200
- for j, b in enumerate(rb):
201
- i0 = (j * wa) // wb
202
- i1 = max(i0 + 1, ((j + 1) * wa + wb - 1) // wb)
203
- for i in range(min(i1, wa)):
204
- self._add_spring(ra[i], b)
205
-
206
- def _find_triangles(self):
207
- """Find all triangles (3 mutually connected nodes) for visualisation."""
208
- tris = []
209
- adj_set = {n: set(ns) for n, ns in self.adj.items()}
210
- checked = set()
211
- for u, v in self.springs:
212
- for w in adj_set[u] & adj_set[v]:
213
- key = tuple(sorted([u, v, w]))
214
- if key not in checked:
215
- tris.append(key)
216
- checked.add(key)
217
- return tris
218
-
219
- # ── PHYSICS ───────────────────────────────────────────────────────────────
220
-
221
- def _hooke_step(self, n_steps, anchor_c):
222
- """Hooke's law + grounding. anchor_c=True keeps C nodes fixed."""
223
- c_set = set(self.c_nodes)
224
- for _ in range(n_steps):
225
- # Accumulate forces
226
- forces = {nid: 0.0 for nid in self.nodes}
227
  for (u, v), K in self.springs.items():
228
  f = K * (self.nodes[v]['x'] - self.nodes[u]['x'])
229
  forces[u] += f
230
  forces[v] -= f
231
- # Integrate
232
- for nid, nd in self.nodes.items():
233
- if nd['anchored']:
234
- continue
235
- if nid in c_set and anchor_c:
236
- continue
237
- f = forces[nid] - GROUND_EPS * nd['x']
238
- nd['vel'] = nd['vel'] * DAMPING + f * DT
239
- nd['x'] += nd['vel'] * DT
240
-
241
- def settle(self, n=SETTLE_N, anchor_c=True):
242
- self._hooke_step(n, anchor_c)
243
-
244
- # ── PROBLEM SETUP ─────────────────────────────────────────────────────────
245
-
246
- def set_problem(self, a, b, c_target=None, mode='training'):
247
- # Inputs
248
- for nid in self.a_nodes:
249
- self.nodes[nid]['x'] = float(a)
250
- for nid in self.b_nodes:
251
- self.nodes[nid]['x'] = float(b)
252
-
253
- # Reset hidden nodes
254
- c_set = set(self.c_nodes)
255
- for nid, nd in self.nodes.items():
256
- if not nd['anchored'] and nid not in c_set:
257
- nd['x'] = 0.0; nd['vel'] = 0.0
258
-
259
- # C setup
260
- c0 = self.nodes[self.c_nodes[0]]
261
- c0['vel'] = 0.0
262
- if mode == 'training' and c_target is not None:
263
- c0['x'] = float(c_target)
264
- c0['anchored'] = True
265
- self._c_target = float(c_target)
266
- else:
267
- c0['anchored'] = False
268
- c0['x'] = float(a + b) / 2 # warm start near midpoint
269
- c0['vel'] = 0.0
270
- self._c_target = float(c_target) if c_target is not None else None
271
-
272
- def predict(self):
273
- return float(np.mean([self.nodes[n]['x'] for n in self.c_nodes]))
274
-
275
- def free_c(self):
276
- for nid in self.c_nodes:
277
- self.nodes[nid]['anchored'] = False
278
- self.nodes[nid]['vel'] = 0.0
279
-
280
- def reset_hidden(self):
281
- c_set = set(self.c_nodes)
282
- fixed = set(self.a_nodes) | set(self.b_nodes) | c_set
283
- for nid in self.nodes:
284
- if nid not in fixed:
285
- self.nodes[nid]['x'] = 0.0
286
- self.nodes[nid]['vel'] = 0.0
287
-
288
- # ── CHL LEARNING ─────────────────────────────────────────────────────────
289
-
290
- def chl_update(self, a, b, c_target):
291
  """
292
- Contrastive Hebbian Learning.
293
-
294
- Phase 1 — CLAMPED:
295
- A, B, C all anchored. Hidden nodes settle to the state that is
296
- consistent with (A, B, C=target). Record x_clamp.
297
-
298
- Phase 2 — FREE:
299
- Only A, B anchored. C and hidden nodes settle freely.
300
- Record x_free and C_prediction.
301
-
302
- Update: ΔK_ij = η · (x_i_clamp·x_j_clamp − x_i_free·x_j_free)
303
-
304
- Effect: K values shift to make the free equilibrium look more like
305
- the clamped one, i.e., C_free → C_target over many samples.
306
  """
307
- # ── Clamped phase ───────────────────────────────────────────────────
308
- self.set_problem(a, b, c_target, mode='training')
309
- self.reset_hidden()
310
- self.settle(SETTLE_N, anchor_c=True)
311
- x_c = {nid: self.nodes[nid]['x'] for nid in self.nodes}
312
-
313
- # ── Free phase ──────────────────────────────────────────────────────
314
- self.free_c()
315
- self.reset_hidden()
316
- self.settle(SETTLE_N, anchor_c=False)
317
- x_f = {nid: self.nodes[nid]['x'] for nid in self.nodes}
318
- pred = self.predict()
319
- error = pred - c_target
320
-
321
- # ── Weight update ────────────────────────────────────────────────────
322
- for (u, v) in self.springs:
323
- delta = x_c[u] * x_c[v] - x_f[u] * x_f[v]
324
- new_k = self.springs[(u, v)] + LR * delta
325
- self.springs[(u, v)] = max(-K_CLIP, min(K_CLIP, new_k))
326
-
327
- return error, pred
328
-
329
- # ── STATE ─────────────────────────────────────────────────────────────────
330
-
331
- def node_state(self):
332
- out = {}
333
- for nid, nd in self.nodes.items():
334
- out[nid] = {
335
- 'x': round(nd['x'], 4),
336
- 'vel': round(nd['vel'], 4),
337
- 'anchored': nd['anchored'],
338
- 'row': nd['row'],
339
- 'col': nd['col'],
340
- 'kind': nd['kind'],
341
- 'pos': list(self._positions[nid]),
342
- }
343
- return out
344
-
345
- def spring_state(self):
346
- out = {}
347
- for (u, v), K in self.springs.items():
348
- out[f"{u}↔{v}"] = round(K, 4)
349
- return out
350
-
351
- def triangle_state(self):
352
- tris = []
353
- for (u, v, w) in self._triangles:
354
- ku = self.springs.get(self._ek(u,v), 0)
355
- kv = self.springs.get(self._ek(v,w), 0)
356
- kw = self.springs.get(self._ek(u,w), 0)
357
- avg_k = (ku + kv + kw) / 3
358
- tris.append({
359
- 'nodes': [u, v, w],
360
- 'pos': [list(self._positions[u]),
361
- list(self._positions[v]),
362
- list(self._positions[w])],
363
- 'avg_k': round(avg_k, 4),
364
- 'stress': round(abs(self.nodes[u]['x'] - self.nodes[v]['x']) +
365
- abs(self.nodes[v]['x'] - self.nodes[w]['x']) +
366
- abs(self.nodes[u]['x'] - self.nodes[w]['x']), 4),
367
- })
368
- return tris
369
-
370
 
371
- # ── ENGINE ────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  class Engine:
374
  def __init__(self):
375
- self.mesh = TriMesh(n_half=3, max_w=4)
376
- self.mode = 'idle'
377
- self.running = False
378
- self.batch_queue = collections.deque()
379
- self.logs = []
380
- self.iteration = 0
381
- self.step_count = 0
382
- self.error = 0.0
383
- self.prediction = 0.0
384
- self.history = []
385
- self.train_data = []
386
- self.test_data = []
387
- self.c_target = None
388
- self.current_type = '—'
389
- self.test_results = []
390
-
391
- def add_log(self, msg):
392
- self.logs.insert(0, f"[{self.iteration:06d}] {msg}")
393
- if len(self.logs) > 60: self.logs.pop()
394
-
395
- # ── DATA ──────────────────────────────────────────────────────────────────
396
-
397
- def load_data(self, train='data/train.json', test='data/test.json'):
398
- with open(train) as f: self.train_data = json.load(f)
399
- with open(test) as f: self.test_data = json.load(f)
400
- ood = sum(1 for d in self.test_data if d['type'] == 'heavy_b')
401
- seen = len(self.test_data) - ood
402
- self.add_log(f"Data: {len(self.train_data)} train | "
403
- f"{len(self.test_data)} test ({seen} seen / {ood} OOD)")
404
-
405
- # ── VISUAL TRAINING STEP ─────────────────────────────────────────────────
406
-
407
- def physics_step(self):
408
- """One server tick — runs MICRO display steps + optional CHL update."""
409
- self.mesh._hooke_step(MICRO, anchor_c=(self.mode == 'training'))
410
- self.prediction = self.mesh.predict()
411
-
412
- if self.c_target is not None:
413
- self.error = self.prediction - self.c_target
414
- else:
415
- self.error = 0.0
416
-
417
- self.history.append(round(abs(self.error), 5))
418
- if len(self.history) > 200: self.history.pop(0)
419
-
420
- self.step_count += 1
421
- converged = abs(self.error) < CONV_THRESH
422
- timeout = self.step_count >= MAX_STEPS
423
-
424
- if converged or timeout:
425
- tag = '✓' if converged else '⚠'
426
- is_ood = self.current_type == 'heavy_b'
427
- self.add_log(f"{tag}{'[OOD]' if is_ood else ''} [{self.current_type}] "
428
- f"err={self.error:.4f} steps={self.step_count}")
429
- if self.mode == 'inference' and self.c_target is not None:
430
- self.test_results.append({
431
- 'type': self.current_type,
432
- 'abs': round(abs(self.error), 5),
433
- 'ok': converged,
434
- 'steps': self.step_count,
435
- 'ood': is_ood,
436
- })
437
- return self._next_or_stop()
438
-
439
- self.iteration += 1
440
- return True
441
-
442
- def _next_or_stop(self):
443
- if self.batch_queue:
444
- p = self.batch_queue.popleft()
445
- self._load_sample(p)
446
- return True
447
- self.running = False
448
- self.add_log("◼ Queue empty.")
449
- return False
450
-
451
- def _load_sample(self, p):
452
- self.current_type = p['type']
453
- self.c_target = p.get('c')
454
- self.step_count = 0
455
- self.mesh.set_problem(p['a'], p['b'],
456
- c_target=p.get('c'),
457
- mode=self.mode)
458
-
459
- def _fill_queue(self, data):
460
- self.batch_queue.clear()
461
- for d in data:
462
- self.batch_queue.append(d)
463
- if self.batch_queue:
464
- self._load_sample(self.batch_queue.popleft())
465
-
466
- # ── OFFLINE TRAINING (fast, no display) ───────────────────────────────────
467
-
468
- def train_offline(self, epochs=5):
469
  self.running = False
470
- self.mode = 'training'
471
- self.add_log(f"⚡ Offline CHL training: {epochs} epoch(s)…")
 
 
 
 
 
472
 
473
- for ep in range(1, epochs + 1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  random.shuffle(self.train_data)
475
- total_err, conv = 0.0, 0
476
-
477
  for sample in self.train_data:
478
- err, pred = self.mesh.chl_update(
479
- sample['a'], sample['b'], sample['c']
480
- )
481
- total_err += abs(err)
482
- if abs(err) < CONV_THRESH:
483
- conv += 1
484
-
485
- avg = total_err / max(len(self.train_data), 1)
486
- pct = 100 * conv / max(len(self.train_data), 1)
487
- msg = f" Ep {ep}/{epochs}: avg|e|={avg:.4f} conv={pct:.1f}%"
488
- self.add_log(msg); print(msg)
489
-
490
- self.add_log("✓ Offline training complete.")
491
- self.mode = 'idle'
492
-
493
- # ── INFERENCE ─────────────────────────────────────────────────────────────
494
-
495
- def start_inference(self, n=None):
496
- data = self.test_data[:n] if n else self.test_data
497
- self.test_results = []
498
- self._fill_queue(data)
499
- self.mode = 'inference'
500
- self.running = True
501
- ood = sum(1 for d in data if d['type'] == 'heavy_b')
502
- self.add_log(f"▶ Inference: {len(data)} samples ({ood} OOD)")
503
-
504
- def start_training_visual(self, n=None):
505
- data = random.sample(self.train_data,
506
- min(n or len(self.train_data), len(self.train_data)))
507
- self._fill_queue(data)
508
- self.mode = 'training'
509
- self.running = True
510
- self.add_log(f"▶ Visual training: {len(data)} samples")
511
-
512
- # ── ACCURACY SUMMARY ──────────────────────────────────────────────────────
513
-
514
- def accuracy_summary(self):
515
- acc = {}
516
- for r in self.test_results:
517
- t = r['type']
518
- if t not in acc:
519
- acc[t] = {'n':0,'ok':0,'sum_e':0.0,'sum_s':0,'ood':r['ood']}
520
- acc[t]['n'] += 1
521
- acc[t]['ok'] += int(r['ok'])
522
- acc[t]['sum_e'] += r['abs']
523
- acc[t]['sum_s'] += r['steps']
524
- return {
525
- t: {
526
- 'n': v['n'],
527
- 'acc': round(100*v['ok']/max(v['n'],1), 1),
528
- 'avg_err': round(v['sum_e']/max(v['n'],1), 4),
529
- 'avg_steps':round(v['sum_s']/max(v['n'],1), 1),
530
- 'ood': v['ood'],
531
- }
532
- for t, v in acc.items()
533
- }
534
-
535
- # ── STATE DICT ────────────────────────────────────────────────────────────
536
-
537
- def state_dict(self):
538
- return {
539
- 'nodes': self.mesh.node_state(),
540
- 'springs': self.mesh.spring_state(),
541
- 'triangles': self.mesh.triangle_state(),
542
- 'layers': self.mesh.layers,
543
- 'row_widths': self.mesh.row_widths,
544
- 'neck_row': self.mesh.neck_row,
545
- 'error': round(self.error, 5),
546
- 'prediction': round(self.prediction, 5),
547
- 'c_target': round(self.c_target, 5) if self.c_target else None,
548
- 'iter': self.iteration,
549
- 'step_count': self.step_count,
550
- 'logs': self.logs,
551
- 'history': self.history[-120:],
552
- 'running': self.running,
553
- 'mode': self.mode,
554
- 'current_type': self.current_type,
555
- 'queue_size': len(self.batch_queue),
556
- 'train_size': len(self.train_data),
557
- 'test_size': len(self.test_data),
558
- 'type_acc': self.accuracy_summary(),
559
- 'n_test_done': len(self.test_results),
560
- 'n_half': self.mesh.n_half,
561
- 'max_w': self.mesh.max_w,
562
- }
563
-
564
-
565
- # ── SERVER ────────────────────────────────────────────────────────────────────
566
 
567
  engine = Engine()
568
  try:
569
- engine.load_data()
570
- except Exception as e:
571
- engine.add_log(f"⚠ No data — run: python data_gen.py ({e})")
572
-
 
573
 
574
- def run_loop():
575
  while True:
576
- if engine.running:
577
- engine.physics_step()
578
- time.sleep(0.028)
579
-
580
- threading.Thread(target=run_loop, daemon=True).start()
581
-
582
 
583
  @app.get("/", response_class=HTMLResponse)
584
- async def get_ui(): return FileResponse("index.html")
585
 
586
  @app.get("/state")
587
- async def get_state(): return engine.state_dict()
588
-
589
- @app.post("/train_offline")
590
- async def train_offline(data: dict = {}):
591
- ep = int(data.get('epochs', 5))
592
- threading.Thread(target=engine.train_offline, args=(ep,), daemon=True).start()
593
- return {"ok": True}
594
-
595
- @app.post("/train_visual")
596
- async def train_visual(data: dict = {}):
597
- engine.start_training_visual()
 
 
 
598
  return {"ok": True}
599
 
600
  @app.post("/infer")
601
- async def infer(data: dict = {}):
602
- engine.start_inference(n=data.get('n'))
603
- return {"ok": True}
604
-
605
- @app.post("/set_topo")
606
- async def set_topo(data: dict):
607
- engine.running = False
608
- n_half = max(2, min(6, int(data.get('n_half', engine.mesh.n_half))))
609
- max_w = max(2, min(8, int(data.get('max_w', engine.mesh.max_w))))
610
- engine.mesh = TriMesh(n_half=n_half, max_w=max_w)
611
- engine.add_log(f"Mesh rebuilt: n_half={n_half} max_w={max_w} "
612
- f"rows={len(engine.mesh.row_widths)} "
613
- f"springs={len(engine.mesh.springs)}")
614
  return {"ok": True}
615
 
616
  @app.post("/halt")
617
  async def halt(): engine.running = False; return {"ok": True}
618
 
619
- @app.post("/reset")
620
- async def reset():
621
- engine.running = False
622
- engine.mesh = TriMesh(engine.mesh.n_half, engine.mesh.max_w)
623
- engine.add_log("Springs re-initialised.")
624
- return {"ok": True}
625
-
626
  if __name__ == "__main__":
627
  import uvicorn
628
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
+ main.py
3
+ Perfect Triangulated Grid + LMS Learning + Inference EWC
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
 
5
  import numpy as np
6
+ import time, collections, threading, json, random, math
7
  from fastapi import FastAPI
8
  from fastapi.responses import HTMLResponse, FileResponse
9
  from fastapi.middleware.cors import CORSMiddleware
 
11
  app = FastAPI()
12
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
13
 
14
+ DIM = 8
15
+ LR = 0.05
16
+ EWC_LAMBDA = 0.8
17
+ FISHER_DECAY = 0.95
18
+ DAMPING = 0.5
19
+ DT = 0.2
20
+
21
+ class PristineMesh:
22
+ def __init__(self, n_dim=DIM):
23
+ self.n_dim = n_dim
24
+ self.nodes = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  self.springs = {}
26
+ self.fisher = {}
27
+ self.anchor_k = {}
28
+ self._build_lattice()
29
+
30
+ def _build_lattice(self):
31
+ """Builds a perfect geometric honeycomb/diamond lattice."""
32
+ # Define the row widths to create the bulges
33
+ # Example for N=8: A(8) -> 9 -> 10 -> 9 -> C(8) -> 9 -> 10 -> 9 -> B(8)
34
+ self.row_widths = [
35
+ self.n_dim, self.n_dim+1, self.n_dim+2, self.n_dim+1,
36
+ self.n_dim,
37
+ self.n_dim+1, self.n_dim+2, self.n_dim+1, self.n_dim
38
+ ]
39
+
40
+ y_spacing = 0.866 # sqrt(3)/2 for perfect equilateral triangles
41
+
42
+ # 1. Place Nodes
43
+ for r, w in enumerate(self.row_widths):
44
+ y = -r * y_spacing
45
+ x_offset = -(w - 1) / 2.0
46
+
47
+ kind = 'H'
48
+ if r == 0: kind = 'A'
49
+ elif r == len(self.row_widths)-1: kind = 'B'
50
+ elif r == len(self.row_widths)//2: kind = 'C'
51
+
52
+ for c in range(w):
53
+ x = x_offset + c
54
+ nid = f"{kind}_r{r}_c{c}"
55
+ self.nodes[nid] = {
56
+ 'x': 0.0, 'vel': 0.0, 'kind': kind,
57
+ 'pos': (x, y), 'anchored': kind in ['A', 'B']
58
+ }
59
+
60
+ # 2. Connect nearest neighbors perfectly
61
+ node_ids = list(self.nodes.keys())
62
+ self.adj = {n: [] for n in node_ids}
63
+
64
+ for i in range(len(node_ids)):
65
+ for j in range(i + 1, len(node_ids)):
66
+ n1, n2 = node_ids[i], node_ids[j]
67
+ p1, p2 = self.nodes[n1]['pos'], self.nodes[n2]['pos']
68
+ dist = math.hypot(p1[0]-p2[0], p1[1]-p2[1])
69
+
70
+ # If distance is ~1.0, they are adjacent in the lattice
71
+ if dist < 1.05:
72
+ key = tuple(sorted([n1, n2]))
73
+ self.springs[key] = random.uniform(0.4, 0.6)
74
+ self.fisher[key] = 0.0
75
+ self.anchor_k[key] = self.springs[key]
76
+ self.adj[n1].append(n2)
77
+ self.adj[n2].append(n1)
78
+
79
+ self.c_nodes = [n for n in self.nodes if self.nodes[n]['kind'] == 'C']
80
+ self.a_nodes = [n for n in self.nodes if self.nodes[n]['kind'] == 'A']
81
+ self.b_nodes = [n for n in self.nodes if self.nodes[n]['kind'] == 'B']
82
+
83
+ def set_inputs(self, a_vec, b_vec):
84
+ for i, nid in enumerate(self.a_nodes): self.nodes[nid]['x'] = a_vec[i]
85
+ for i, nid in enumerate(self.b_nodes): self.nodes[nid]['x'] = b_vec[i]
86
+ for nid, data in self.nodes.items():
87
+ if data['kind'] not in ['A', 'B']:
88
+ data['x'] = 0.0
89
+ data['vel'] = 0.0
90
+
91
+ def settle(self, steps=40):
92
+ for _ in range(steps):
93
+ forces = {n: 0.0 for n in self.nodes}
 
 
 
 
 
 
94
  for (u, v), K in self.springs.items():
95
  f = K * (self.nodes[v]['x'] - self.nodes[u]['x'])
96
  forces[u] += f
97
  forces[v] -= f
98
+
99
+ for nid, data in self.nodes.items():
100
+ if not data['anchored']:
101
+ # Grounding force to prevent floating
102
+ f = forces[nid] - (0.05 * data['x'])
103
+ data['vel'] = data['vel'] * DAMPING + f * DT
104
+ data['x'] += data['vel'] * DT
105
+
106
+ def get_predictions(self):
107
+ return [self.nodes[n]['x'] for n in self.c_nodes]
108
+
109
+ def lms_update(self, target_vec, mode='train'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  """
111
+ Network-wide LMS: Backpropagates the error from C through the mesh.
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  """
113
+ # 1. Compute errors at C
114
+ errors = {n: 0.0 for n in self.nodes}
115
+ for i, nid in enumerate(self.c_nodes):
116
+ errors[nid] = self.nodes[nid]['x'] - target_vec[i]
117
+
118
+ # 2. Backprop errors to hidden layers topologically
119
+ for _ in range(3): # Smooth error outward
120
+ next_err = dict(errors)
121
+ for (u, v), K in self.springs.items():
122
+ # Error flows proportionally to stiffness
123
+ next_err[u] += K * errors[v] * 0.1
124
+ next_err[v] += K * errors[u] * 0.1
125
+ errors = next_err
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
+ # 3. Update Springs
128
+ for key in self.springs:
129
+ u, v = key
130
+ # The gradient for K_uv is driven by the error difference and displacement
131
+ err_gradient = (errors[u] - errors[v]) * (self.nodes[u]['x'] - self.nodes[v]['x'])
132
+
133
+ if mode == 'train':
134
+ self.springs[key] -= LR * err_gradient
135
+ # Accumulate Fisher info for EWC
136
+ self.fisher[key] = FISHER_DECAY * self.fisher[key] + (1 - FISHER_DECAY) * (err_gradient ** 2)
137
+
138
+ elif mode == 'infer':
139
+ # Real-time inference updating with EWC protection
140
+ penalty = EWC_LAMBDA * self.fisher[key] * (self.springs[key] - self.anchor_k[key])
141
+ self.springs[key] -= (LR * 0.5 * err_gradient) + penalty
142
+
143
+ self.springs[key] = max(-4.0, min(8.0, self.springs[key])) # Clip to stable bounds
144
+
145
+ def save_anchors(self):
146
+ self.anchor_k = dict(self.springs)
147
 
148
  class Engine:
149
  def __init__(self):
150
+ self.mesh = PristineMesh()
151
+ self.mode = 'idle'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  self.running = False
153
+ self.queue = collections.deque()
154
+ self.logs = []
155
+ self.iter = 0
156
+ self.train_data = []
157
+ self.test_data = []
158
+ self.error_hist = []
159
+ self.current_err = 0.0
160
 
161
+ def add_log(self, msg):
162
+ self.logs.insert(0, f"[{self.iter:05d}] {msg}")
163
+ if len(self.logs) > 30: self.logs.pop()
164
+
165
+ def run_step(self):
166
+ if not self.queue:
167
+ self.running = False
168
+ return
169
+
170
+ sample = self.queue.popleft()
171
+ self.mesh.set_inputs(sample['a'], sample['b'])
172
+ self.mesh.settle(steps=30)
173
+ preds = self.mesh.get_predictions()
174
+
175
+ # Mean Absolute Error across all N output dimensions
176
+ err = float(np.mean(np.abs(np.array(preds) - np.array(sample['c']))))
177
+ self.current_err = err
178
+ self.error_hist.append(err)
179
+ if len(self.error_hist) > 100: self.error_hist.pop(0)
180
+
181
+ # Apply LMS + EWC
182
+ self.mesh.lms_update(sample['c'], mode=self.mode)
183
+
184
+ self.iter += 1
185
+ if self.iter % 10 == 0:
186
+ self.add_log(f"[{sample['type']}] err: {err:.4f}")
187
+
188
+ def train_offline(self, epochs):
189
+ self.add_log("⚡ Offline Training Started...")
190
+ for ep in range(epochs):
191
  random.shuffle(self.train_data)
192
+ errs = []
 
193
  for sample in self.train_data:
194
+ self.mesh.set_inputs(sample['a'], sample['b'])
195
+ self.mesh.settle(15)
196
+ self.mesh.lms_update(sample['c'], mode='train')
197
+ preds = self.mesh.get_predictions()
198
+ errs.append(np.mean(np.abs(np.array(preds) - np.array(sample['c']))))
199
+ self.add_log(f"Epoch {ep+1} | Avg Err: {np.mean(errs):.4f}")
200
+ print(f"Epoch {ep+1} | Avg Err: {np.mean(errs):.4f}")
201
+ self.mesh.save_anchors()
202
+ self.add_log("✓ Training Complete. EWC Anchors saved.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
  engine = Engine()
205
  try:
206
+ with open('data/train.json') as f: engine.train_data = json.load(f)
207
+ with open('data/test.json') as f: engine.test_data = json.load(f)
208
+ engine.add_log("Data loaded.")
209
+ except:
210
+ pass
211
 
212
+ def loop():
213
  while True:
214
+ if engine.running: engine.run_step()
215
+ time.sleep(0.05)
216
+ threading.Thread(target=loop, daemon=True).start()
 
 
 
217
 
218
  @app.get("/", response_class=HTMLResponse)
219
+ async def ui(): return FileResponse("index.html")
220
 
221
  @app.get("/state")
222
+ async def state():
223
+ return {
224
+ 'nodes': engine.mesh.nodes,
225
+ 'springs': engine.mesh.springs,
226
+ 'error': engine.current_err,
227
+ 'hist': engine.error_hist,
228
+ 'mode': engine.mode,
229
+ 'running': engine.running,
230
+ 'logs': engine.logs
231
+ }
232
+
233
+ @app.post("/train")
234
+ async def train():
235
+ threading.Thread(target=engine.train_offline, args=(5,), daemon=True).start()
236
  return {"ok": True}
237
 
238
  @app.post("/infer")
239
+ async def infer():
240
+ engine.mode = 'infer'
241
+ engine.queue.extend(engine.test_data)
242
+ engine.running = True
 
 
 
 
 
 
 
 
 
243
  return {"ok": True}
244
 
245
  @app.post("/halt")
246
  async def halt(): engine.running = False; return {"ok": True}
247
 
 
 
 
 
 
 
 
248
  if __name__ == "__main__":
249
  import uvicorn
250
  uvicorn.run(app, host="0.0.0.0", port=7860)