everydaytok commited on
Commit
731b39e
Β·
verified Β·
1 Parent(s): 190d213

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +289 -617
app.py CHANGED
@@ -1,654 +1,326 @@
1
- """
2
- main.py β€” Elastic Mesh Engine + FastAPI server.
3
-
4
- Architecture:
5
- Bilateral hourglass: A (top) ─[U1..Un]─┐
6
- C (center waist)
7
- B (bot) ─[L1..Ln]β”€β”˜
8
-
9
- Each node : x, vel ∈ ℝ^DIM
10
- Each spring: K ∈ ℝ^(DIMΓ—DIM) β€” full linear map per edge
11
-
12
- Forward (additive):
13
- x_Ui = K(A,Ui) @ x_A
14
- x_Li = K(B,Li) @ x_B
15
- x_C = Ξ£ K(Ui,C) @ x_Ui + Ξ£ K(Li,C) @ x_Li
16
-
17
- Training:
18
- C anchored at target β†’ K matrices update via matrix LMS
19
- one-shot zero-residual for linear problems
20
-
21
- Inference:
22
- C free β†’ elastic dynamics settle to equilibrium
23
- EWC regularisation protects weights from catastrophic forgetting
24
- Fisher diagonal accumulates during training
25
- """
26
-
27
  import numpy as np
28
- import time, collections, threading, json, pathlib, random
29
  from fastapi import FastAPI
30
  from fastapi.responses import HTMLResponse, FileResponse
31
  from fastapi.middleware.cors import CORSMiddleware
32
 
33
  app = FastAPI()
34
- app.add_middleware(CORSMiddleware,
35
- allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
36
-
37
- # ── CONSTANTS ─────────────────────────────────────────────────────────────────
38
- DIM = 32 # embedding dimension (scale to 768 for LLM integration)
39
- FWD_K = 1.5 # forward spring stiffness for elastic display
40
- BACK_A = 0.40 # backward tension (C pulls on hidden nodes)
41
- DAMPING = 0.58 # velocity retention per display micro-step
42
- DT = 0.10 # display physics time-step
43
- MICRO = 4 # display micro-steps per server tick
44
- CONV_THRESH = 0.08 # β€–errorβ€– < this β†’ sample converged
45
- MAX_STEPS = 400 # hard cap per sample (prevents infinite loops)
46
- EWC_LAMBDA = 0.6 # EWC penalty strength
47
- FISHER_DECAY= 0.97 # EMA decay for Fisher accumulation
48
-
49
-
50
- class MeshEngine:
51
- """
52
- Elastic hourglass mesh with matrix spring stiffness.
53
-
54
- The mesh learns to produce C = equilibrium(A, B) such that C lies in the
55
- feasibility space satisfying A-constraints while respecting B-objectives.
56
- This is not computed β€” it is converged to.
57
- """
58
-
59
- def __init__(self, dim: int = DIM, n_upper: int = 3, n_lower: int = 3):
60
- self.dim = dim
61
- self.n_upper = n_upper
62
- self.n_lower = n_lower
63
- self.mode = 'idle' # 'training' | 'inference' | 'idle'
64
- self.running = False
65
- self.batch_queue = collections.deque()
66
- self.logs = []
67
- self.iteration = 0
68
- self.step_count = 0 # steps on current sample
69
- self.error_norm = 0.0
70
- self.pred_norm = 0.0
71
- self.history = []
72
- self.train_data = []
73
- self.test_data = []
74
- self.c_target = None # ground-truth C for current sample (inference)
75
- self.current_type = 'unknown'
76
- self.test_errors = [] # list of {type, err, rel} β€” inference results
77
- self._init_mesh()
78
-
79
- # ── TOPOLOGY ──────────────────────────────────────────────────────────────
80
-
81
- def _layers(self):
82
- return [
83
- ['A'],
84
- [f'U{i}' for i in range(1, self.n_upper + 1)],
85
- ['C'],
86
- [f'L{i}' for i in range(1, self.n_lower + 1)],
87
- ['B'],
88
- ]
89
-
90
- def _init_mesh(self):
91
- self.iteration = 0
92
- self.step_count = 0
93
- self.error_norm = 0.0
94
- self.pred_norm = 0.0
95
- self.history = []
96
- self.layers = self._layers()
97
- d = self.dim
98
-
99
- # Nodes β€” each carries a d-vector position and velocity
100
- self.nodes = {
101
- nid: {
102
- 'x': np.zeros(d),
103
- 'vel': np.zeros(d),
104
- 'anchored': nid in ('A', 'B'),
105
- }
106
- for layer in self.layers for nid in layer
107
- }
108
-
109
- # Spring matrices β€” K ∈ ℝ^(dΓ—d) per edge, Xavier init
110
- scale = np.sqrt(2.0 / (d + d))
111
- self.K = {}
112
- for i in range(1, self.n_upper + 1):
113
- uid = f'U{i}'
114
- self.K[('A', uid)] = np.random.normal(0, scale, (d, d))
115
- self.K[(uid, 'C')] = np.random.normal(0, scale, (d, d))
116
- for i in range(1, self.n_lower + 1):
117
- lid = f'L{i}'
118
- self.K[('B', lid)] = np.random.normal(0, scale, (d, d))
119
- self.K[(lid, 'C')] = np.random.normal(0, scale, (d, d))
120
-
121
- # EWC: Fisher diagonal (per element of each K matrix)
122
- self.fisher = {k: np.zeros((d, d)) for k in self.K}
123
- self.K_anchor = {k: v.copy() for k, v in self.K.items()}
124
-
125
- # ── PROBLEM SETUP ────────────────────────────��────────────────────────────
126
-
127
- def set_problem(self, a_vec, b_vec, c_target=None, ptype='unknown'):
128
- d = self.dim
129
- self.nodes['A']['x'] = np.asarray(a_vec, dtype=float)[:d]
130
- self.nodes['B']['x'] = np.asarray(b_vec, dtype=float)[:d]
131
- self.current_type = ptype
132
- self.step_count = 0
133
-
134
- # Reset free nodes for fresh elastic oscillation
135
- for layer in self.layers[1:4]:
136
- for nid in layer:
137
- if nid != 'C':
138
- self.nodes[nid]['x'] = np.zeros(d)
139
- self.nodes[nid]['vel'] = np.zeros(d)
140
-
141
- c = self.nodes['C']
142
- c['vel'] = np.zeros(d)
143
-
144
- if self.mode == 'training' and c_target is not None:
145
- c['x'] = np.asarray(c_target, dtype=float)[:d]
146
- c['anchored'] = True
147
- self.c_target = c['x'].copy()
148
- else:
149
- # Inference: C is free; store target only for accuracy measurement
150
- c['anchored'] = False
151
- c['x'] = np.zeros(d)
152
- self.c_target = (np.asarray(c_target, dtype=float)[:d]
153
- if c_target is not None else None)
154
-
155
- # ── FEEDFORWARD ───────────────────────────────────────────────────────────
156
-
157
- def _forward(self):
158
- """
159
- Exact feedforward pass (used for learning).
160
- Returns (C_pred, hidden_activations).
161
- """
162
- xa, xb = self.nodes['A']['x'], self.nodes['B']['x']
163
- hid = {}
164
-
165
- for i in range(1, self.n_upper + 1):
166
- uid = f'U{i}'
167
- hid[uid] = self.K[('A', uid)] @ xa # ℝ^d
168
-
169
- for i in range(1, self.n_lower + 1):
170
- lid = f'L{i}'
171
- hid[lid] = self.K[('B', lid)] @ xb # ℝ^d
172
-
173
- pred = np.zeros(self.dim)
174
- for i in range(1, self.n_upper + 1):
175
- pred += self.K[(f'U{i}', 'C')] @ hid[f'U{i}']
176
- for i in range(1, self.n_lower + 1):
177
- pred += self.K[(f'L{i}', 'C')] @ hid[f'L{i}']
178
-
179
- return pred, hid
180
-
181
- # ── ELASTIC DISPLAY PHYSICS ───────────────────────────────────────────────
182
-
183
- def _elastic_step(self, n_steps: int = MICRO):
184
- """
185
- Damped-oscillator spring dynamics for visualisation.
186
-
187
- Forward springs pull hidden nodes toward their feedforward rest positions.
188
- Backward tension (BACK_A) lets anchored-C's position propagate upstream β€”
189
- the mesh physically feels the error as strain before any K update.
190
- """
191
- xa, xb = self.nodes['A']['x'], self.nodes['B']['x']
192
-
193
- for _ in range(n_steps):
194
- for i in range(1, self.n_upper + 1):
195
- uid = f'U{i}'
196
- n = self.nodes[uid]
197
- rest = self.K[('A', uid)] @ xa
198
- f = FWD_K * (rest - n['x'])
199
- f += BACK_A * (self.K[(uid, 'C')].T @
200
- (self.nodes['C']['x'] - self.K[(uid, 'C')] @ n['x']))
201
- n['vel'] = n['vel'] * DAMPING + f * DT
202
- n['x'] += n['vel'] * DT
203
-
204
- for i in range(1, self.n_lower + 1):
205
- lid = f'L{i}'
206
- n = self.nodes[lid]
207
- rest = self.K[('B', lid)] @ xb
208
- f = FWD_K * (rest - n['x'])
209
- f += BACK_A * (self.K[(lid, 'C')].T @
210
- (self.nodes['C']['x'] - self.K[(lid, 'C')] @ n['x']))
211
- n['vel'] = n['vel'] * DAMPING + f * DT
212
- n['x'] += n['vel'] * DT
213
-
214
- c = self.nodes['C']
215
- if not c['anchored']:
216
- rest = np.zeros(self.dim)
217
- for i in range(1, self.n_upper + 1):
218
- rest += self.K[(f'U{i}', 'C')] @ self.nodes[f'U{i}']['x']
219
- for i in range(1, self.n_lower + 1):
220
- rest += self.K[(f'L{i}', 'C')] @ self.nodes[f'L{i}']['x']
221
- f = FWD_K * (rest - c['x'])
222
- c['vel'] = c['vel'] * DAMPING + f * DT
223
- c['x'] += c['vel'] * DT
224
-
225
- # ── MATRIX LMS UPDATE ─────────────────────────────────────────────────────
226
-
227
- def _lms_update(self, error: np.ndarray, hid: dict, ewc: bool = False):
228
- """
229
- Matrix LMS with joint optimal step.
230
-
231
- For the output layer (X β†’ C):
232
- grad_K = outer(error, h_X) ∈ ℝ^(dΓ—d)
233
- joint_denom = Ξ£_edges β€–h_Xβ€–Β² (one normaliser for all output-layer edges)
234
- K(X,C) -= grad_K / joint_denom
235
-
236
- This drives β€–errorβ€– β†’ 0 in one step for linear systems (provable).
237
-
238
- For the hidden layer (A/B β†’ U/L):
239
- delta propagates back through K(X,C):
240
- Ξ΄_U = K(U,C)α΅€ @ error
241
- grad_K = outer(Ξ΄_U, x_A)
242
- K(A,U) -= grad_K / β€–x_Aβ€–Β²
243
-
244
- EWC mode: step size reduced by (1 + λ·F) per element, protecting
245
- dimensions with high Fisher importance from past training.
246
- """
247
- eps = 1e-8
248
- xa = self.nodes['A']['x']
249
- xb = self.nodes['B']['x']
250
-
251
- # ── Output-layer joint update ──────────────────────────────────────
252
- joint_denom = eps
253
- for i in range(1, self.n_upper + 1):
254
- joint_denom += float(np.dot(hid[f'U{i}'], hid[f'U{i}']))
255
- for i in range(1, self.n_lower + 1):
256
- joint_denom += float(np.dot(hid[f'L{i}'], hid[f'L{i}']))
257
-
258
- for i in range(1, self.n_upper + 1):
259
- uid = f'U{i}'
260
- key = (uid, 'C')
261
- grad = np.outer(error, hid[uid])
262
- if ewc:
263
- denom = joint_denom * (1.0 + EWC_LAMBDA * self.fisher[key])
264
- else:
265
- denom = joint_denom
266
- self.K[key] -= grad / denom
267
- np.clip(self.K[key], -8.0, 8.0, out=self.K[key])
268
-
269
- for i in range(1, self.n_lower + 1):
270
- lid = f'L{i}'
271
- key = (lid, 'C')
272
- grad = np.outer(error, hid[lid])
273
- if ewc:
274
- denom = joint_denom * (1.0 + EWC_LAMBDA * self.fisher[key])
275
- else:
276
- denom = joint_denom
277
- self.K[key] -= grad / denom
278
- np.clip(self.K[key], -8.0, 8.0, out=self.K[key])
279
-
280
- # ── Hidden-layer update (backprop) ────────────────────────────────
281
- xa_denom = float(np.dot(xa, xa)) + eps
282
- xb_denom = float(np.dot(xb, xb)) + eps
283
-
284
- for i in range(1, self.n_upper + 1):
285
- uid = f'U{i}'
286
- key = ('A', uid)
287
- delta = self.K[(uid, 'C')].T @ error # back-propagated error ∈ ℝ^d
288
- grad = np.outer(delta, xa)
289
- if ewc:
290
- denom = xa_denom * (1.0 + EWC_LAMBDA * self.fisher[key])
291
- else:
292
- denom = xa_denom
293
- self.K[key] -= grad / denom
294
- np.clip(self.K[key], -8.0, 8.0, out=self.K[key])
295
-
296
- for i in range(1, self.n_lower + 1):
297
- lid = f'L{i}'
298
- key = ('B', lid)
299
- delta = self.K[(lid, 'C')].T @ error
300
- grad = np.outer(delta, xb)
301
- if ewc:
302
- denom = xb_denom * (1.0 + EWC_LAMBDA * self.fisher[key])
303
- else:
304
- denom = xb_denom
305
- self.K[key] -= grad / denom
306
- np.clip(self.K[key], -8.0, 8.0, out=self.K[key])
307
-
308
- # ── FISHER ACCUMULATION (EWC) ─────────────────────────────────────────────
309
-
310
- def _update_fisher(self, error: np.ndarray, hid: dict):
311
- """
312
- Accumulate Fisher diagonal via EMA of squared gradient elements.
313
- High Fisher β†’ this weight dimension was important for past problems.
314
- """
315
- xa = self.nodes['A']['x']
316
- xb = self.nodes['B']['x']
317
-
318
- for i in range(1, self.n_upper + 1):
319
- uid = f'U{i}'
320
- g_uc = np.outer(error, hid[uid]) ** 2
321
- g_au = np.outer(self.K[(uid, 'C')].T @ error, xa) ** 2
322
- self.fisher[(uid, 'C')] = (FISHER_DECAY * self.fisher[(uid, 'C')] +
323
- (1 - FISHER_DECAY) * g_uc)
324
- self.fisher[('A', uid)] = (FISHER_DECAY * self.fisher[('A', uid)] +
325
- (1 - FISHER_DECAY) * g_au)
326
-
327
- for i in range(1, self.n_lower + 1):
328
- lid = f'L{i}'
329
- g_lc = np.outer(error, hid[lid]) ** 2
330
- g_bl = np.outer(self.K[(lid, 'C')].T @ error, xb) ** 2
331
- self.fisher[(lid, 'C')] = (FISHER_DECAY * self.fisher[(lid, 'C')] +
332
- (1 - FISHER_DECAY) * g_lc)
333
- self.fisher[('B', lid)] = (FISHER_DECAY * self.fisher[('B', lid)] +
334
- (1 - FISHER_DECAY) * g_bl)
335
-
336
- # ── PHYSICS STEP ──────────────────────────────────────────────────────────
337
-
338
- def physics_step(self) -> bool:
339
- """One server tick: elastic display + LMS update."""
340
- self._elastic_step(MICRO)
341
-
342
- pred, hid = self._forward()
343
- self.pred_norm = float(np.linalg.norm(pred))
344
- self.step_count += 1
345
-
346
- c = self.nodes['C']
347
- if c['anchored']:
348
- error = pred - c['x']
349
- self.error_norm = float(np.linalg.norm(error))
350
- else:
351
- c['x'] = pred.copy()
352
- error = (pred - self.c_target
353
- if self.c_target is not None
354
- else np.zeros(self.dim))
355
- self.error_norm = float(np.linalg.norm(error))
356
-
357
- self.history.append(round(self.error_norm, 5))
358
- if len(self.history) > 300:
359
- self.history.pop(0)
360
-
361
- converged = self.error_norm < CONV_THRESH
362
- timeout = self.step_count >= MAX_STEPS
363
-
364
- if converged or timeout:
365
- tag = 'βœ“' if converged else '⚠'
366
- self.add_log(f"{tag} [{self.current_type}] "
367
- f"err={self.error_norm:.4f} it={self.step_count}")
368
- if self.mode == 'inference' and self.c_target is not None:
369
- ct_norm = float(np.linalg.norm(self.c_target)) + 1e-8
370
- self.test_errors.append({
371
- 'type': self.current_type,
372
- 'abs': round(self.error_norm, 5),
373
- 'rel': round(self.error_norm / ct_norm, 5),
374
- 'ok': converged,
375
- })
376
- self._update_fisher(error, hid)
377
- return self._next_or_stop()
378
-
379
- if c['anchored']:
380
- # Training: update K to reduce error
381
- self._lms_update(error, hid, ewc=False)
382
- elif self.mode == 'inference':
383
- # Inference: EWC-regularised online adaptation
384
- self._lms_update(error, hid, ewc=True)
385
-
386
- self.iteration += 1
387
- return True
388
-
389
- def _next_or_stop(self) -> bool:
390
- if self.batch_queue:
391
- p = self.batch_queue.popleft()
392
- self.set_problem(p['A'], p['B'], p.get('C'), p.get('type', 'unknown'))
393
- return True
394
  self.running = False
395
- self.add_log("β—Ό Queue empty.")
396
- return False
 
 
 
 
 
 
 
397
 
398
- # ── FAST OFFLINE TRAINING ─────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
- def train_offline(self, epochs: int = 5):
401
- """
402
- Run full training at CPU speed (no sleep, no display physics).
403
- Called in a background thread from /train_offline endpoint.
404
- """
405
  self.running = False
406
- self.mode = 'training'
407
- self.add_log(f"⚑ Offline training: {epochs} epoch(s)…")
408
-
409
- for ep in range(1, epochs + 1):
410
  random.shuffle(self.train_data)
411
- total_err = 0.0
412
- converged = 0
413
-
414
  for sample in self.train_data:
415
- d = self.dim
416
- xa = np.asarray(sample['A'], dtype=float)[:d]
417
- xb = np.asarray(sample['B'], dtype=float)[:d]
418
- ct = np.asarray(sample['C'], dtype=float)[:d]
419
- self.nodes['A']['x'] = xa
420
- self.nodes['B']['x'] = xb
421
- self.nodes['C']['x'] = ct
422
-
423
- for _ in range(MAX_STEPS):
424
- pred, hid = self._forward()
425
- err = pred - ct
426
- en = float(np.linalg.norm(err))
427
- if en < CONV_THRESH:
428
- self._update_fisher(err, hid)
429
- converged += 1
430
- break
431
- self._lms_update(err, hid, ewc=False)
432
-
433
- total_err += float(np.linalg.norm(self._forward()[0] - ct))
434
-
435
- avg = total_err / max(len(self.train_data), 1)
436
- pct = 100 * converged / max(len(self.train_data), 1)
437
- self.add_log(f" Ep {ep}/{epochs}: avgβ€–eβ€–={avg:.4f} conv={pct:.1f}%")
438
- print(f" Ep {ep}/{epochs}: avgβ€–eβ€–={avg:.4f} converged={pct:.1f}%")
439
-
440
- # Save anchor weights for EWC
441
- self.K_anchor = {k: v.copy() for k, v in self.K.items()}
442
- self.add_log("βœ“ Offline training complete. EWC anchors saved.")
443
  self.mode = 'idle'
444
 
445
- # ── DATA LOADING ──────────────────────────────────────────────────────────
446
-
447
- def load_data(self, train='data/train.json', test='data/test.json'):
448
- with open(train) as f: self.train_data = json.load(f)
449
- with open(test) as f: self.test_data = json.load(f)
450
- self.add_log(f"Data loaded: {len(self.train_data)} train / "
451
- f"{len(self.test_data)} test")
452
-
453
- # ── QUEUE HELPERS ─────────────────────────────────────────────────────────
454
-
455
- def start_training(self, n=None):
456
- data = random.sample(self.train_data,
457
- min(n or len(self.train_data), len(self.train_data)))
458
- self._fill_queue(data, anchor_c=True)
459
- self.mode = 'training'
460
- self.running = True
461
- self.add_log(f"β–Ά Visual training: {len(data)} samples")
462
-
463
- def start_inference(self, n=None):
464
- data = self.test_data[:n] if n else self.test_data
465
- self.test_errors = []
466
- self._fill_queue(data, anchor_c=False)
467
- self.mode = 'inference'
468
- self.running = True
469
- self.add_log(f"β–Ά Inference: {len(data)} samples")
470
-
471
- def _fill_queue(self, data, anchor_c):
472
- self.batch_queue.clear()
473
- for d in data:
474
- self.batch_queue.append(
475
- {'A': d['A'], 'B': d['B'], 'C': d['C'], 'type': d.get('type','?')}
476
- )
477
- if self.batch_queue:
478
- p = self.batch_queue.popleft()
479
- if anchor_c:
480
- self.set_problem(p['A'], p['B'], p['C'], p['type'])
481
- else:
482
- # Inference: don't anchor but store target
483
- d = self.dim
484
- self.nodes['A']['x'] = np.asarray(p['A'])[:d]
485
- self.nodes['B']['x'] = np.asarray(p['B'])[:d]
486
- self.nodes['C']['x'] = np.zeros(d)
487
- self.nodes['C']['vel'] = np.zeros(d)
488
- self.nodes['C']['anchored'] = False
489
- self.c_target = np.asarray(p['C'])[:d]
490
- self.current_type = p['type']
491
- self.step_count = 0
492
- for layer in self.layers[1:4]:
493
- for nid in layer:
494
- if nid != 'C':
495
- self.nodes[nid]['x'] = np.zeros(d)
496
- self.nodes[nid]['vel'] = np.zeros(d)
497
-
498
- # ── LOGGING ───────────────────────────────────────────────────────────────
499
-
500
- def add_log(self, msg):
501
- self.logs.insert(0, f"[{self.iteration:06d}] {msg}")
502
- if len(self.logs) > 60:
503
- self.logs.pop()
504
-
505
- # ── STATE SERIALISATION ───────────────────────────────────────────────────
506
-
507
- def state_dict(self):
508
- nodes_out = {}
509
- for nid, n in self.nodes.items():
510
- nodes_out[nid] = {
511
- 'norm': round(float(np.linalg.norm(n['x'])), 4),
512
- 'vel_norm': round(float(np.linalg.norm(n['vel'])), 4),
513
- 'anchored': bool(n['anchored']),
514
- 'x_head': [round(float(v), 3) for v in n['x'][:6]],
515
- }
516
-
517
- springs_out = {}
518
- for (u, v), km in self.K.items():
519
- label = f"{u}β†’{v}"
520
- springs_out[label] = {
521
- 'frob': round(float(np.linalg.norm(km)), 4),
522
- 'mean': round(float(np.mean(km)), 4),
523
- 'std': round(float(np.std(km)), 4),
524
- 'fish': round(float(np.mean(self.fisher[(u, v)])), 5),
525
- }
526
-
527
- # Per-type inference accuracy
528
- type_acc = {}
529
- for te in self.test_errors:
530
- t = te['type']
531
- if t not in type_acc:
532
- type_acc[t] = {'n': 0, 'n_ok': 0, 'sum_abs': 0.0}
533
- type_acc[t]['n'] += 1
534
- type_acc[t]['n_ok'] += int(te['ok'])
535
- type_acc[t]['sum_abs'] += te['abs']
536
- acc_summary = {
537
- t: {
538
- 'n': v['n'],
539
- 'acc': round(100 * v['n_ok'] / max(v['n'], 1), 1),
540
- 'avg_err': round(v['sum_abs'] / max(v['n'], 1), 4),
541
- }
542
- for t, v in type_acc.items()
543
- }
544
-
545
- return {
546
- 'nodes': nodes_out,
547
- 'springs': springs_out,
548
- 'error': round(self.error_norm, 5),
549
- 'pred_norm': round(self.pred_norm, 5),
550
- 'iter': self.iteration,
551
- 'step_count': self.step_count,
552
- 'logs': self.logs,
553
- 'history': self.history[-120:],
554
- 'running': self.running,
555
- 'mode': self.mode,
556
- 'n_upper': self.n_upper,
557
- 'n_lower': self.n_lower,
558
- 'layers': self.layers,
559
- 'queue_size': len(self.batch_queue),
560
- 'train_size': len(self.train_data),
561
- 'test_size': len(self.test_data),
562
- 'type_acc': acc_summary,
563
- 'n_test_done': len(self.test_errors),
564
- 'current_type': self.current_type,
565
- 'dim': self.dim,
566
- }
567
-
568
-
569
- # ── SERVER ────────────────────────────────────────────────────────────────────
570
-
571
- engine = MeshEngine(dim=DIM, n_upper=3, n_lower=3)
572
 
 
573
  try:
574
- engine.load_data()
 
 
575
  except Exception as e:
576
- engine.add_log(f"No data found β€” run: python data_gen.py ({e})")
577
 
578
-
579
- def run_loop():
580
  while True:
581
- if engine.running:
582
- engine.physics_step()
583
- time.sleep(0.025)
584
-
585
- threading.Thread(target=run_loop, daemon=True).start()
586
-
587
 
588
  @app.get("/", response_class=HTMLResponse)
589
- async def get_ui():
590
- return FileResponse("index.html")
591
 
592
  @app.get("/state")
593
- async def get_state():
594
- return engine.state_dict()
595
-
596
- # ── Training controls ─────────────────────────────────────────────────────────
597
-
598
- @app.post("/train_visual")
599
- async def train_visual(data: dict = {}):
600
- """Start visual (slow) training β€” shows elastic dynamics in UI."""
601
- engine.start_training(n=data.get('n'))
 
 
 
 
 
 
 
 
 
 
 
602
  return {"ok": True}
603
 
604
- @app.post("/train_offline")
605
- async def train_offline(data: dict = {}):
606
- """Fast offline training in background thread β€” no display."""
607
- epochs = int(data.get('epochs', 5))
608
- threading.Thread(target=engine.train_offline, args=(epochs,), daemon=True).start()
609
- return {"ok": True, "epochs": epochs}
610
-
611
  @app.post("/infer")
612
- async def start_infer(data: dict = {}):
613
- """Run inference on test set, measuring C reconstruction accuracy."""
614
- engine.start_inference(n=data.get('n'))
 
 
 
 
615
  return {"ok": True}
616
 
617
- @app.post("/reload_data")
618
- async def reload_data():
619
  try:
620
- engine.load_data()
 
 
 
 
 
 
 
 
621
  return {"ok": True}
622
  except Exception as e:
623
  return {"ok": False, "error": str(e)}
624
 
625
- # ── Topology controls ────────────────────────────────────────────────────────
626
-
627
- @app.post("/set_layer")
628
- async def set_layer(data: dict):
629
- layer = data.get('layer', '')
630
- delta = int(data.get('delta', 0))
631
- engine.running = False
632
- if layer == 'upper':
633
- engine.n_upper = max(1, min(8, engine.n_upper + delta))
634
- elif layer == 'lower':
635
- engine.n_lower = max(1, min(8, engine.n_lower + delta))
636
- engine._init_mesh()
637
- engine.add_log(f"Topology β†’ U{engine.n_upper} Β· L{engine.n_lower} | springs re-init")
638
- return {"ok": True, "n_upper": engine.n_upper, "n_lower": engine.n_lower}
639
-
640
  @app.post("/halt")
641
- async def halt():
642
- engine.running = False
643
- return {"ok": True}
644
-
645
- @app.post("/reset")
646
- async def reset():
647
- engine.running = False
648
- engine._init_mesh()
649
- engine.add_log("Mesh reset.")
650
- return {"ok": True}
651
-
652
 
653
  if __name__ == "__main__":
654
  import uvicorn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import numpy as np
2
+ import time, collections, threading, json, random, math, os, pathlib
3
  from fastapi import FastAPI
4
  from fastapi.responses import HTMLResponse, FileResponse
5
  from fastapi.middleware.cors import CORSMiddleware
6
 
7
  app = FastAPI()
8
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
9
+
10
+ DIM = 8 # The actual width of the A, C, and B rows!
11
+ LR = 0.05
12
+ EWC_LAMBDA = 0.8
13
+ FISHER_DECAY = 0.95
14
+ DAMPING = 0.6
15
+ DT = 0.2
16
+
17
+ # --- DATA GENERATOR (D-length arrays) ---
18
+ def ensure_data():
19
+ out = pathlib.Path('data')
20
+ out.mkdir(exist_ok=True)
21
+ if os.path.exists('data/train.json') and os.path.exists('data/test.json'):
22
+ return
23
+
24
+ print(f"Generating Scalar Fabric Dataset (D={DIM})...")
25
+ rng = np.random.default_rng(42)
26
+ data = []
27
+ for _ in range(1000):
28
+ a, b = rng.uniform(0.1, 0.9, DIM), rng.uniform(0.1, 0.9, DIM)
29
+
30
+ # 1. Straight blend (Dimension N only talks to Dimension N)
31
+ data.append({'a': a.tolist(), 'b': b.tolist(), 'c': (0.7 * a + 0.3 * b).tolist(), 'type': 'blend'})
32
+
33
+ # 2. Difference (Requires repulsive negative springs)
34
+ data.append({'a': a.tolist(), 'b': b.tolist(), 'c': (0.5 + 0.4 * (a - b)).tolist(), 'type': 'diff'})
35
+
36
+ # 3. Lateral Route (Dimension N talks to N-1 and N+1. Forces lateral springs to activate!)
37
+ data.append({'a': a.tolist(), 'b': b.tolist(), 'c': (0.5 * np.roll(a, 1) + 0.5 * np.roll(b, -1)).tolist(), 'type': 'route'})
38
+
39
+ random.shuffle(data)
40
+ split = int(len(data) * 0.9)
41
+ with open('data/train.json', 'w') as f: json.dump(data[:split], f)
42
+ with open('data/test.json', 'w') as f: json.dump(data[split:], f)
43
+
44
+ ensure_data()
45
+
46
+ # --- SCALAR FABRIC TOPOLOGY ---
47
+ def _widths(d):
48
+ # e.g., D=8 -> 8, 9, 10, 9, 8(C), 9, 10, 9, 8
49
+ return [d, d+1, d+2, d+1, d, d+1, d+2, d+1, d]
50
+
51
+ def _xpos(w):
52
+ # Offsets rows by 0.5 to create flawless equilateral triangles
53
+ return [(2*i - (w-1)) / 2.0 for i in range(w)]
54
+
55
+ class ScalarFabricMesh:
56
+ def __init__(self, n_dim=DIM):
57
+ self.n_dim = n_dim
58
+ self.nodes = {}
59
+ self.springs = {}
60
+ self.fisher = {}
61
+ self.anchor_k = {}
62
+ self._build_lattice()
63
+
64
+ def _build_lattice(self):
65
+ self.row_widths = _widths(self.n_dim)
66
+ y_spacing = 0.866
67
+
68
+ # 1. Place individual scalar nodes
69
+ for r, w in enumerate(self.row_widths):
70
+ y = -r * y_spacing
71
+ xs = _xpos(w)
72
+
73
+ kind = 'H'
74
+ if r == 0: kind = 'A'
75
+ elif r == len(self.row_widths)-1: kind = 'B'
76
+ elif r == len(self.row_widths)//2: kind = 'C'
77
+
78
+ for c in range(w):
79
+ nid = f"{kind}_r{r}_c{c}"
80
+ self.nodes[nid] = {
81
+ 'x': 0.5, 'vel': 0.0, 'kind': kind, 'row': r, 'col': c,
82
+ 'pos': (xs[c], y), 'anchored': kind in ['A', 'B']
83
+ }
84
+
85
+ # 2. Wire the Fabric
86
+ node_ids = list(self.nodes.keys())
87
+ for i in range(len(node_ids)):
88
+ for j in range(i + 1, len(node_ids)):
89
+ n1, n2 = node_ids[i], node_ids[j]
90
+ r1, r2 = self.nodes[n1]['row'], self.nodes[n2]['row']
91
+ x1, x2 = self.nodes[n1]['pos'][0], self.nodes[n2]['pos'][0]
92
+
93
+ # Connect if horizontally adjacent OR diagonally adjacent
94
+ if (r1 == r2 and abs(x1 - x2) == 1.0) or (abs(r1 - r2) == 1 and abs(x1 - x2) == 0.5):
95
+ key = tuple(sorted([n1, n2]))
96
+ self.springs[key] = random.uniform(0.1, 0.4)
97
+ self.fisher[key] = 0.0
98
+ self.anchor_k[key] = self.springs[key]
99
+
100
+ # Sort so array indices map correctly 0 -> D
101
+ self.c_nodes = sorted([n for n in self.nodes if self.nodes[n]['kind'] == 'C'], key=lambda k: self.nodes[k]['col'])
102
+ self.a_nodes = sorted([n for n in self.nodes if self.nodes[n]['kind'] == 'A'], key=lambda k: self.nodes[k]['col'])
103
+ self.b_nodes = sorted([n for n in self.nodes if self.nodes[n]['kind'] == 'B'], key=lambda k: self.nodes[k]['col'])
104
+
105
+ def set_inputs(self, a_vec, b_vec):
106
+ """Pins the D scalar nodes at A and B to the input arrays."""
107
+ for i, nid in enumerate(self.a_nodes): self.nodes[nid]['x'] = a_vec[i]
108
+ for i, nid in enumerate(self.b_nodes): self.nodes[nid]['x'] = b_vec[i]
109
+ for nid, data in self.nodes.items():
110
+ if data['kind'] not in ['A', 'B']:
111
+ data['x'] = 0.5
112
+ data['vel'] = 0.0
113
+
114
+ def settle(self, steps=30):
115
+ """Pure Hookean Physics on Scalars."""
116
+ for _ in range(steps):
117
+ forces = {n: 0.0 for n in self.nodes}
118
+ for (u, v), K in self.springs.items():
119
+ f = K * (self.nodes[v]['x'] - self.nodes[u]['x'])
120
+ forces[u] += f
121
+ forces[v] -= f
122
+
123
+ for nid, data in self.nodes.items():
124
+ if not data['anchored']:
125
+ f = forces[nid] - (0.05 * (data['x'] - 0.5)) # Soft ground
126
+ data['vel'] = data['vel'] * DAMPING + f * DT
127
+ data['x'] += data['vel'] * DT
128
+ # Absolute stability bounding
129
+ data['x'] = max(-1.0, min(2.0, data['x']))
130
+
131
+ def get_predictions(self):
132
+ return [self.nodes[n]['x'] for n in self.c_nodes]
133
+
134
+ def lms_update(self, target_vec, mode='train'):
135
+ """Physical Backprop through the fabric threads."""
136
+ # 1. Measure error at the D center nodes
137
+ errors = {n: 0.0 for n in self.nodes}
138
+ for i, nid in enumerate(self.c_nodes):
139
+ errors[nid] = self.nodes[nid]['x'] - target_vec[i]
140
+
141
+ # 2. Diffuse error outwards based on spring thickness
142
+ for _ in range(5):
143
+ next_err = dict(errors)
144
+ for (u, v), K in self.springs.items():
145
+ weight = min(abs(K) * 0.1, 0.4)
146
+ next_err[u] += weight * errors[v]
147
+ next_err[v] += weight * errors[u]
148
+ for n in errors: next_err[n] *= 0.85
149
+ errors = next_err
150
+
151
+ # 3. Widrow-Hoff Local Thread Update
152
+ for key in self.springs:
153
+ u, v = key
154
+
155
+ # THE CRITICAL SIGN CORRECTION (+).
156
+ # If U is too high, and U pulls V, K must decrease to drop tension.
157
+ err_gradient = (errors[u] - errors[v]) * (self.nodes[u]['x'] - self.nodes[v]['x'])
158
+ norm_mod = 1.0 / ((self.nodes[u]['x'] - self.nodes[v]['x'])**2 + 0.1)
159
+
160
+ step = LR * err_gradient * norm_mod
161
+
162
+ if mode == 'train':
163
+ self.springs[key] += step
164
+ self.fisher[key] = FISHER_DECAY * self.fisher[key] + (1 - FISHER_DECAY) * (err_gradient ** 2)
165
+ elif mode == 'infer':
166
+ # EWC preserves memory of past geometries
167
+ penalty = EWC_LAMBDA * self.fisher[key] * (self.springs[key] - self.anchor_k[key])
168
+ self.springs[key] += (step * 0.2) - penalty
169
+
170
+ self.springs[key] = max(-2.0, min(3.0, self.springs[key]))
171
+
172
+ def save_anchors(self):
173
+ self.anchor_k = dict(self.springs)
174
+
175
+ class Engine:
176
+ def __init__(self):
177
+ self.mesh = ScalarFabricMesh()
178
+ self.mode = 'idle'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  self.running = False
180
+ self.queue = collections.deque()
181
+ self.logs = []
182
+ self.iter = 0
183
+ self.train_data = []
184
+ self.test_data = []
185
+ self.error_hist = []
186
+ self.current_err = 0.0
187
+ self.current_type = 'β€”'
188
+ self.test_results = []
189
 
190
+ def add_log(self, msg):
191
+ self.logs.insert(0, f"[{self.iter:05d}] {msg}")
192
+ if len(self.logs) > 40: self.logs.pop()
193
+
194
+ def run_step(self):
195
+ if not self.queue:
196
+ self.running = False
197
+ self.add_log("Queue empty. Standing by.")
198
+ return
199
+
200
+ sample = self.queue.popleft()
201
+ self.current_type = sample['type']
202
+
203
+ self.mesh.set_inputs(sample['a'], sample['b'])
204
+ self.mesh.settle(steps=25)
205
+ preds = self.mesh.get_predictions()
206
+
207
+ if sample['type'] != 'manual':
208
+ err = float(np.mean(np.abs(np.array(preds) - np.array(sample['c']))))
209
+ if math.isnan(err): err = 1.0
210
+ self.current_err = err
211
+ self.error_hist.append(err)
212
+ if len(self.error_hist) > 100: self.error_hist.pop(0)
213
+
214
+ if self.mode == 'infer':
215
+ self.test_results.append({'type': self.current_type, 'err': err})
216
+
217
+ self.mesh.lms_update(sample['c'], mode=self.mode)
218
+ else:
219
+ self.current_err = 0.0
220
+
221
+ self.iter += 1
222
+ if self.iter % 5 == 0 or sample['type'] == 'manual':
223
+ self.add_log(f"[{self.current_type}] err: {self.current_err:.4f}")
224
 
225
+ def train_offline(self, epochs):
 
 
 
 
226
  self.running = False
227
+ self.mode = 'train'
228
+ self.add_log(f"⚑ Offline Training: {epochs} epochs...")
229
+ for ep in range(epochs):
 
230
  random.shuffle(self.train_data)
231
+ errs = []
 
 
232
  for sample in self.train_data:
233
+ self.mesh.set_inputs(sample['a'], sample['b'])
234
+ self.mesh.settle(20)
235
+ self.mesh.lms_update(sample['c'], mode='train')
236
+ preds = self.mesh.get_predictions()
237
+ e = np.mean(np.abs(np.array(preds) - np.array(sample['c'])))
238
+ if not math.isnan(e): errs.append(e)
239
+
240
+ avg_e = np.mean(errs) if errs else 0.0
241
+ self.add_log(f"Ep {ep+1} | Avg Err: {avg_e:.4f}")
242
+
243
+ self.mesh.save_anchors()
244
+ self.add_log("βœ“ Training Complete.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  self.mode = 'idle'
246
 
247
+ def get_accuracy_summary(self):
248
+ acc = {}
249
+ for r in self.test_results:
250
+ t = r['type']
251
+ if t not in acc: acc[t] = {'n': 0, 'sum_e': 0.0}
252
+ acc[t]['n'] += 1
253
+ acc[t]['sum_e'] += r['err']
254
+ return {t: {'n': v['n'], 'avg_err': round(v['sum_e']/v['n'], 4)} for t, v in acc.items()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ engine = Engine()
257
  try:
258
+ with open('data/train.json') as f: engine.train_data = json.load(f)
259
+ with open('data/test.json') as f: engine.test_data = json.load(f)
260
+ engine.add_log("Data loaded successfully.")
261
  except Exception as e:
262
+ engine.add_log(f"Error loading data: {str(e)}")
263
 
264
+ def loop():
 
265
  while True:
266
+ if engine.running: engine.run_step()
267
+ time.sleep(0.06)
268
+ threading.Thread(target=loop, daemon=True).start()
 
 
 
269
 
270
  @app.get("/", response_class=HTMLResponse)
271
+ async def ui(): return FileResponse("index.html")
 
272
 
273
  @app.get("/state")
274
+ async def state():
275
+ return {
276
+ 'nodes': engine.mesh.nodes,
277
+ # Safely pipe the tuples for JSON
278
+ 'springs': {f"{u}|{v}": k for (u, v), k in engine.mesh.springs.items()},
279
+ 'error': engine.current_err,
280
+ 'hist': engine.error_hist,
281
+ 'mode': engine.mode,
282
+ 'running': engine.running,
283
+ 'logs': engine.logs,
284
+ 'current_type': engine.current_type,
285
+ 'queue_size': len(engine.queue),
286
+ 'type_acc': engine.get_accuracy_summary(),
287
+ 'dim': DIM
288
+ }
289
+
290
+ @app.post("/train")
291
+ async def train(data: dict):
292
+ ep = int(data.get('epochs', 5))
293
+ threading.Thread(target=engine.train_offline, args=(ep,), daemon=True).start()
294
  return {"ok": True}
295
 
 
 
 
 
 
 
 
296
  @app.post("/infer")
297
+ async def infer(data: dict):
298
+ n = int(data.get('n', 200))
299
+ engine.mode = 'infer'
300
+ engine.test_results = []
301
+ engine.queue.clear()
302
+ engine.queue.extend(engine.test_data[:n])
303
+ engine.running = True
304
  return {"ok": True}
305
 
306
+ @app.post("/manual")
307
+ async def manual(data: dict):
308
  try:
309
+ a_vec = [float(x.strip()) for x in data.get('a', '').split(',')]
310
+ b_vec = [float(x.strip()) for x in data.get('b', '').split(',')]
311
+ if len(a_vec) != DIM or len(b_vec) != DIM:
312
+ return {"ok": False, "error": f"Vectors must be exactly length {DIM}"}
313
+
314
+ engine.mode = 'manual'
315
+ engine.queue.clear()
316
+ engine.queue.append({'a': a_vec, 'b': b_vec, 'c': [0]*DIM, 'type': 'manual'})
317
+ engine.running = True
318
  return {"ok": True}
319
  except Exception as e:
320
  return {"ok": False, "error": str(e)}
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  @app.post("/halt")
323
+ async def halt(): engine.running = False; return {"ok": True}
 
 
 
 
 
 
 
 
 
 
324
 
325
  if __name__ == "__main__":
326
  import uvicorn