everydaytok commited on
Commit
d30e10a
·
verified ·
1 Parent(s): cf8b1cc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +401 -295
app.py CHANGED
@@ -1,5 +1,4 @@
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
@@ -7,321 +6,428 @@ from fastapi.middleware.cors import CORSMiddleware
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
327
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ import time, collections, threading, random
 
2
  from fastapi import FastAPI
3
  from fastapi.responses import HTMLResponse, FileResponse
4
  from fastapi.middleware.cors import CORSMiddleware
 
6
  app = FastAPI()
7
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
8
 
9
+ # ── ELASTIC CONSTANTS ─────────────────────────────────────────────────────────
10
+ FWD_K = 2.2 # spring pull toward rest position
11
+ DAMPING = 0.55 # velocity retention (lower = more visible oscillation)
12
+ DT = 0.12 # micro-step size
13
+ MICRO = 6 # micro-steps per physics_step (keeps UI responsive)
14
+ SETTLE = 0.004 # velocity threshold for early exit
15
+
16
+
17
+ class SimEngine:
18
+ def __init__(self):
19
+ self.mode = 'training'
20
+ self.architecture = 'additive'
21
+ self.dataset_type = 'housing'
22
+ self.n_upper = 3 # nodes in upper bulge (A-side)
23
+ self.n_lower = 3 # nodes in lower bulge (B-side)
24
+ self.back_alpha = 0.45 # backward tension coupling
25
+ self.running = False
26
+ self.batch_queue = collections.deque()
27
+ self.logs = []
28
+ self.iteration = 0
29
+ self.current_error = 0.0
30
+ self.current_prediction = 0.0
31
+ self.history = []
32
+ self._init_mesh()
33
+
34
+ # ── TOPOLOGY ──────────────────────────────────────────────────────────────
35
+ # Physical layout (top→bottom):
36
+ # A → U1..Un C L1..Ln B
37
+ #
38
+ # Layers for display (top to bottom):
39
+ # 0: [A]
40
+ # 1: [U1..Un] upper bulge
41
+ # 2: [C] CENTER — the waist
42
+ # 3: [L1..Ln] lower bulge
43
+ # 4: [B]
44
+ #
45
+ # Springs:
46
+ # A → each Ui (K_aui)
47
+ # Ui C (K_uic)
48
+ # B → each Li (K_bli)
49
+ # Li → C (K_lic)
50
+
51
+ def _build_layers(self):
52
+ return [
53
+ ['A'],
54
+ [f'U{i}' for i in range(1, self.n_upper + 1)],
55
+ ['C'],
56
+ [f'L{i}' for i in range(1, self.n_lower + 1)],
57
+ ['B'],
58
+ ]
59
+
60
+ def _init_mesh(self):
61
+ self.iteration = 0
62
+ self.current_error = 0.0
63
+ self.current_prediction = 0.0
64
+ self.history = []
65
+ self.layers = self._build_layers()
66
+
67
  self.nodes = {}
68
+ for layer in self.layers:
69
+ for nid in layer:
70
+ anchored = nid in ('A', 'B')
71
+ self.nodes[nid] = {'x': 0.0, 'vel': 0.0, 'anchored': anchored}
72
+ self.nodes['A']['x'] = 2.0
73
+ self.nodes['B']['x'] = 3.0
74
+
75
+ # Springs — one per directed edge
76
  self.springs = {}
77
+ # A-side: A → Ui → C
78
+ for i in range(1, self.n_upper + 1):
79
+ uid = f'U{i}'
80
+ self.springs[('A', uid)] = round(random.uniform(0.85, 1.15), 4)
81
+ self.springs[(uid, 'C')] = round(random.uniform(0.85, 1.15), 4)
82
+ # B-side: B → Li → C
83
+ for i in range(1, self.n_lower + 1):
84
+ lid = f'L{i}'
85
+ self.springs[('B', lid)] = round(random.uniform(0.85, 1.15), 4)
86
+ self.springs[(lid, 'C')] = round(random.uniform(0.85, 1.15), 4)
87
+
88
+ def reset(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  self.running = False
90
+ self.batch_queue.clear()
91
  self.logs = []
92
+ self._init_mesh()
 
 
 
 
 
 
93
 
94
+ # ── LOGGING ───────────────────────────────────────────────────────────────
95
  def add_log(self, msg):
96
+ self.logs.insert(0, f"[{self.iteration:04d}] {msg}")
97
+ if len(self.logs) > 40:
98
+ self.logs.pop()
99
+
100
+ # ── DATASET ───────────────────────────────────────────────────────────────
101
+ def ground_truth(self, a, b):
102
+ t = self.dataset_type
103
+ if t == 'housing': return round(a * 2.5 + b * 1.2, 4)
104
+ elif t == 'subtraction': return round(a - b, 4)
105
+ elif t == 'multiplication': return round(a * b, 4)
106
+ elif t == 'quadratic': return round(a * a + b, 4)
107
+ return round(a + b, 4)
108
+
109
+ # ── PROBLEM SETUP ─────────────────────────────────────────────────────────
110
+ def set_problem(self, a, b, c_target=None):
111
+ self.nodes['A']['x'] = float(a)
112
+ self.nodes['B']['x'] = float(b)
113
+ # Reset hidden nodes so elastic wave is visible from scratch
114
+ for layer in self.layers[1:4]:
115
+ for nid in layer:
116
+ if nid != 'C':
117
+ self.nodes[nid]['x'] = 0.0
118
+ self.nodes[nid]['vel'] = 0.0
119
+ c = self.nodes['C']
120
+ c['vel'] = 0.0
121
+ if self.mode == 'training' and c_target is not None:
122
+ c['x'] = float(c_target)
123
+ c['anchored'] = True
124
  else:
125
+ c['anchored'] = False
126
+ c['x'] = 0.0
127
+
128
+ # ── FEEDFORWARD REST POSITION ─────────────────────────────────────────────
129
+ def _rest_upper(self, uid):
130
+ """Rest position of an upper hidden node — driven by A."""
131
+ k = self.springs[('A', uid)]
132
+ xa = self.nodes['A']['x']
133
+ return k * xa # additive or used as scale for mult
134
+
135
+ def _rest_lower(self, lid):
136
+ """Rest position of a lower hidden node — driven by B."""
137
+ k = self.springs[('B', lid)]
138
+ xb = self.nodes['B']['x']
139
+ return k * xb
140
+
141
+ def _rest_c(self):
142
+ """Rest position of C — sum of contributions from both bulges."""
143
+ total = 0.0
144
+ for i in range(1, self.n_upper + 1):
145
+ uid = f'U{i}'
146
+ hu = self.nodes[uid]['x']
147
+ total += self.springs[(uid, 'C')] * hu
148
+ for i in range(1, self.n_lower + 1):
149
+ lid = f'L{i}'
150
+ hl = self.nodes[lid]['x']
151
+ total += self.springs[(lid, 'C')] * hl
152
+ return total
153
+
154
+ # ── ELASTIC RELAXATION (DISPLAY PHYSICS) ─────────────────────────────────
155
+ def _elastic_step(self, n_steps):
156
+ """
157
+ Damped-oscillator spring dynamics for visualisation.
158
+
159
+ Upper hidden nodes Ui are pulled toward K(A,Ui)·A by a forward spring.
160
+ Lower hidden nodes Li are pulled toward K(B,Li)·B by a forward spring.
161
+ C is pulled toward Σ K(Xi,C)·Xi from both sides.
162
+
163
+ Back-tension (alpha): if alpha>0, each node also feels a restoring pull
164
+ from downstream — i.e. C's anchored position creates tension that travels
165
+ back up through Ui and down through Li, making the elastic wave visible.
166
+ """
167
+ alpha = self.back_alpha
168
+ for _ in range(n_steps):
169
+ max_v = 0.0
170
+
171
+ # Upper hidden nodes
172
+ for i in range(1, self.n_upper + 1):
173
+ uid = f'U{i}'
174
+ n = self.nodes[uid]
175
+ f = FWD_K * (self._rest_upper(uid) - n['x'])
176
+ if alpha > 0:
177
+ kuc = self.springs[(uid, 'C')]
178
+ f += alpha * kuc * (self.nodes['C']['x'] - n['x'])
179
+ n['vel'] = n['vel'] * DAMPING + f * DT
180
+ n['x'] += n['vel'] * DT
181
+ max_v = max(max_v, abs(n['vel']))
182
+
183
+ # Lower hidden nodes
184
+ for i in range(1, self.n_lower + 1):
185
+ lid = f'L{i}'
186
+ n = self.nodes[lid]
187
+ f = FWD_K * (self._rest_lower(lid) - n['x'])
188
+ if alpha > 0:
189
+ klc = self.springs[(lid, 'C')]
190
+ f += alpha * klc * (self.nodes['C']['x'] - n['x'])
191
+ n['vel'] = n['vel'] * DAMPING + f * DT
192
+ n['x'] += n['vel'] * DT
193
+ max_v = max(max_v, abs(n['vel']))
194
+
195
+ # C node (only moves in inference)
196
+ c = self.nodes['C']
197
+ if not c['anchored']:
198
+ f = FWD_K * (self._rest_c() - c['x'])
199
+ c['vel'] = c['vel'] * DAMPING + f * DT
200
+ c['x'] += c['vel'] * DT
201
+ max_v = max(max_v, abs(c['vel']))
202
+
203
+ if max_v < SETTLE:
204
+ break
205
+
206
+ # ── EXACT FEEDFORWARD (LEARNING) ─────────────────────────────────────────
207
+ def _feedforward(self):
208
+ """
209
+ Exact prediction, independent of elastic node positions.
210
+ Additive: Ui = K(A,Ui)·A Li = K(B,Li)·B
211
+ Multiplicative: same shape but U·L cross-product at C boundary.
212
+ Returns (prediction, hidden_values_dict).
213
+ """
214
+ A, B = self.nodes['A']['x'], self.nodes['B']['x']
215
+ ff = {}
216
+
217
+ for i in range(1, self.n_upper + 1):
218
+ uid = f'U{i}'
219
+ ff[uid] = self.springs[('A', uid)] * A
220
+
221
+ for i in range(1, self.n_lower + 1):
222
+ lid = f'L{i}'
223
+ ff[lid] = self.springs[('B', lid)] * B
224
+
225
+ if self.architecture == 'multiplicative':
226
+ # Each Ui pairs with a Li (by index, wrap if unequal counts)
227
+ pred = 0.0
228
+ n = max(self.n_upper, self.n_lower)
229
+ for i in range(n):
230
+ uid = f'U{(i % self.n_upper) + 1}'
231
+ lid = f'L{(i % self.n_lower) + 1}'
232
+ ku = self.springs[(uid, 'C')]
233
+ kl = self.springs[(lid, 'C')]
234
+ pred += ku * ff[uid] * kl * ff[lid]
235
+ else:
236
+ # Additive: both sides simply sum at C
237
+ pred = (
238
+ sum(self.springs[(f'U{i}', 'C')] * ff[f'U{i}'] for i in range(1, self.n_upper + 1)) +
239
+ sum(self.springs[(f'L{i}', 'C')] * ff[f'L{i}'] for i in range(1, self.n_lower + 1))
240
+ )
241
+
242
+ return pred, ff
243
+
244
+ # ── LMS OPTIMAL-STEP UPDATE ───────────────────────────────────────────────
245
+ def _lms_update(self, error, ff):
246
+ """
247
+ LMS optimal step μ = error / ||∇pred||²
248
+ Gradients via backprop through feedforward values.
249
+ """
250
+ grads = {k: 0.0 for k in self.springs}
251
+ A, B = self.nodes['A']['x'], self.nodes['B']['x']
252
+
253
+ if self.architecture == 'additive':
254
+ for i in range(1, self.n_upper + 1):
255
+ uid = f'U{i}'
256
+ kau = self.springs[('A', uid)]
257
+ kuc = self.springs[(uid, 'C')]
258
+ # ∂pred/∂K(A,Ui) = K(Ui,C)·A
259
+ grads[('A', uid)] = kuc * A
260
+ # ∂pred/∂K(Ui,C) = K(A,Ui)·A
261
+ grads[(uid, 'C')] = kau * A
262
+ for i in range(1, self.n_lower + 1):
263
+ lid = f'L{i}'
264
+ kbl = self.springs[('B', lid)]
265
+ klc = self.springs[(lid, 'C')]
266
+ grads[('B', lid)] = klc * B
267
+ grads[(lid, 'C')] = kbl * B
268
+ else:
269
+ n = max(self.n_upper, self.n_lower)
270
+ for i in range(n):
271
+ uid = f'U{(i % self.n_upper) + 1}'
272
+ lid = f'L{(i % self.n_lower) + 1}'
273
+ kau = self.springs[('A', uid)]
274
+ kbl = self.springs[('B', lid)]
275
+ ku = self.springs[(uid, 'C')]
276
+ kl = self.springs[(lid, 'C')]
277
+ Uv, Lv = ff[uid], ff[lid]
278
+ # ∂pred/∂K(A,Ui) = K(Ui,C)·A · K(Li,C)·Lv
279
+ grads[('A', uid)] = ku * A * kl * Lv
280
+ grads[('B', lid)] = kl * B * ku * Uv
281
+ grads[(uid, 'C')] = Uv * kl * Lv
282
+ grads[(lid, 'C')] = Lv * ku * Uv
283
+
284
+ norm_sq = sum(g * g for g in grads.values()) + 1e-10
285
+ mu = error / norm_sq
286
+ for key, g in grads.items():
287
+ self.springs[key] -= mu * g
288
+ self.springs[key] = max(-30.0, min(30.0, self.springs[key]))
289
+
290
+ # ── MAIN PHYSICS STEP ─────────────────────────────────────────────────────
291
+ def physics_step(self):
292
+ self._elastic_step(MICRO)
293
+
294
+ pred, ff = self._feedforward()
295
+ self.current_prediction = round(pred, 5)
296
+
297
+ c = self.nodes['C']
298
+ if c['anchored']:
299
+ self.current_error = pred - c['x']
300
+ else:
301
+ c['x'] = round(pred, 4)
302
+ self.current_error = 0.0
303
+
304
+ self.history.append(round(self.current_error, 4))
305
+ if len(self.history) > 200:
306
+ self.history.pop(0)
307
+
308
+ if abs(self.current_error) < 0.02:
309
+ a, b = self.nodes['A']['x'], self.nodes['B']['x']
310
+ gt = self.ground_truth(a, b)
311
+ self.add_log(
312
+ f"✓ A={a:.2f} B={b:.2f} → P={pred:.4f} GT={gt:.4f} Δ={abs(pred-gt):.4f}"
313
+ )
314
+ return self._next_or_stop()
315
+
316
+ if self.mode == 'training' and c['anchored']:
317
+ self._lms_update(self.current_error, ff)
318
+
319
+ self.iteration += 1
320
+ return True
321
+
322
+ def _next_or_stop(self):
323
+ if self.batch_queue:
324
+ p = self.batch_queue.popleft()
325
+ self.set_problem(p['a'], p['b'], p.get('c'))
326
+ self.add_log(f"→ Next ({len(self.batch_queue)} queued)")
327
+ return True
328
  self.running = False
329
+ self.add_log("◼ Batch complete.")
330
+ return False
331
+
332
+ def generate_batch(self, count=30):
333
+ self.batch_queue.clear()
334
+ for _ in range(count):
335
+ a = round(random.uniform(1.0, 10.0), 2)
336
+ b = round(random.uniform(1.0, 10.0), 2)
337
+ self.batch_queue.append({'a': a, 'b': b, 'c': self.ground_truth(a, b)})
338
+ p = self.batch_queue.popleft()
339
+ self.set_problem(p['a'], p['b'], p.get('c'))
340
+ self.running = True
341
+ self.add_log(f"▶ {count} samples | {self.dataset_type} | U{self.n_upper}·L{self.n_lower}")
342
+
343
+
344
+ # ── SERVER ────────────────────────────────────────────────────────────────────
345
+ engine = SimEngine()
346
+
347
+ def run_loop():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  while True:
349
+ if engine.running:
350
+ engine.physics_step()
351
+ time.sleep(0.028)
352
+
353
+ threading.Thread(target=run_loop, daemon=True).start()
354
 
355
  @app.get("/", response_class=HTMLResponse)
356
+ async def get_ui():
357
+ return FileResponse("index.html")
358
 
359
  @app.get("/state")
360
+ async def get_state():
361
+ springs_out = {f"{u}→{v}": round(k, 5) for (u, v), k in engine.springs.items()}
362
  return {
363
+ 'nodes': engine.nodes,
364
+ 'springs': springs_out,
365
+ 'layers': engine.layers,
366
+ 'error': engine.current_error,
367
+ 'prediction': engine.current_prediction,
368
+ 'iter': engine.iteration,
369
+ 'logs': engine.logs,
370
+ 'history': engine.history[-80:],
371
+ 'running': engine.running,
372
+ 'mode': engine.mode,
373
+ 'architecture': engine.architecture,
374
+ 'dataset_type': engine.dataset_type,
375
+ 'n_upper': engine.n_upper,
376
+ 'n_lower': engine.n_lower,
377
+ 'back_alpha': engine.back_alpha,
378
+ 'queue_size': len(engine.batch_queue),
379
  }
380
 
381
+ @app.post("/config")
382
+ async def config(data: dict):
383
+ engine.mode = data.get('mode', engine.mode)
384
+ engine.architecture = data.get('architecture', engine.architecture)
385
+ engine.dataset_type = data.get('dataset', engine.dataset_type)
386
+ engine.n_upper = max(1, min(8, int(data.get('n_upper', engine.n_upper))))
387
+ engine.n_lower = max(1, min(8, int(data.get('n_lower', engine.n_lower))))
388
+ engine.back_alpha = max(0.0, min(1.0, float(data.get('back_alpha', engine.back_alpha))))
389
+ engine.running = False
390
+ engine._init_mesh()
391
+ engine.logs = []
392
+ engine.add_log(
393
+ f"Config: {engine.mode}|{engine.architecture}|U{engine.n_upper}·L{engine.n_lower}|α={engine.back_alpha:.2f}"
394
+ )
395
  return {"ok": True}
396
 
397
+ @app.post("/set_layer")
398
+ async def set_layer(data: dict):
399
+ layer = data.get('layer', '')
400
+ delta = int(data.get('delta', 0))
401
+ if layer == 'upper': engine.n_upper = max(1, min(8, engine.n_upper + delta))
402
+ elif layer == 'lower': engine.n_lower = max(1, min(8, engine.n_lower + delta))
403
+ engine.running = False
404
+ engine._init_mesh()
405
+ engine.add_log(f"Topology → U{engine.n_upper}·L{engine.n_lower}")
406
+ return {"ok": True, "n_upper": engine.n_upper, "n_lower": engine.n_lower}
407
+
408
+ @app.post("/generate")
409
+ async def generate(data: dict):
410
+ engine.generate_batch(int(data.get('count', 30)))
411
  return {"ok": True}
412
 
413
+ @app.post("/run_custom")
414
+ async def run_custom(data: dict):
415
+ a = float(data['a'])
416
+ b = float(data['b'])
417
+ c = float(data['c']) if data.get('c') not in (None, '', 'null') else None
418
+ if c is None and engine.mode == 'training':
419
+ c = engine.ground_truth(a, b)
420
+ engine.batch_queue.clear()
421
+ engine.set_problem(a, b, c)
422
+ engine.running = True
423
+ engine.add_log(f"Custom: A={a} B={b} target={c}")
424
+ return {"ok": True}
 
 
 
425
 
426
  @app.post("/halt")
427
+ async def halt():
428
+ engine.running = False
429
+ return {"ok": True}
430
 
431
  if __name__ == "__main__":
432
  import uvicorn
433
+ uvicorn.run(app, host="0.0.0.0", port=7860)