everydaytok commited on
Commit
88e9e88
Β·
verified Β·
1 Parent(s): d30e10a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +341 -265
app.py CHANGED
@@ -7,11 +7,12 @@ 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:
@@ -19,9 +20,10 @@ class SimEngine:
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 = []
@@ -32,29 +34,25 @@ class SimEngine:
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):
@@ -63,27 +61,28 @@ class SimEngine:
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
@@ -91,230 +90,238 @@ class SimEngine:
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
@@ -331,19 +338,25 @@ class SimEngine:
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:
@@ -352,19 +365,23 @@ def run_loop():
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:],
@@ -372,62 +389,121 @@ async def get_state():
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)
 
7
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
8
 
9
  # ── ELASTIC CONSTANTS ─────────────────────────────────────────────────────────
10
+ FWD_K = 2.2
11
+ DAMPING = 0.55
12
+ DT = 0.12
13
+ MICRO = 6
14
+ SETTLE = 0.004
15
+ CONV_THRESH = 0.02
16
 
17
 
18
  class SimEngine:
 
20
  self.mode = 'training'
21
  self.architecture = 'additive'
22
  self.dataset_type = 'housing'
23
+ self.n_inputs = 1 # ← number of input/output dimensions
24
+ self.n_upper = 3 # hidden nodes per dim, A-side
25
+ self.n_lower = 3 # hidden nodes per dim, B-side
26
+ self.back_alpha = 0.45
27
  self.running = False
28
  self.batch_queue = collections.deque()
29
  self.logs = []
 
34
  self._init_mesh()
35
 
36
  # ── TOPOLOGY ──────────────────────────────────────────────────────────────
37
+ # For n_inputs=N, n_upper=U, n_lower=L:
38
+ # Layer 0 : [A1..AN]
39
+ # Layer 1 : [U1_1..U1_U, U2_1..UN_U] upper bulge, grouped by dim
40
+ # Layer 2 : [C1..CN] center waist
41
+ # Layer 3 : [L1_1..L1_L, L2_1..LN_L] lower bulge
42
+ # Layer 4 : [B1..BN]
43
  #
44
+ # Springs per dimension d:
45
+ # (Ad, Ud_j) and (Ud_j, Cd) for j in 1..n_upper
46
+ # (Bd, Ld_j) and (Ld_j, Cd) for j in 1..n_lower
 
 
 
 
 
 
 
 
 
47
 
48
  def _build_layers(self):
49
+ n, nu, nl = self.n_inputs, self.n_upper, self.n_lower
50
  return [
51
+ [f'A{d}' for d in range(1, n+1)],
52
+ [f'U{d}_{j}' for d in range(1, n+1) for j in range(1, nu+1)],
53
+ [f'C{d}' for d in range(1, n+1)],
54
+ [f'L{d}_{j}' for d in range(1, n+1) for j in range(1, nl+1)],
55
+ [f'B{d}' for d in range(1, n+1)],
56
  ]
57
 
58
  def _init_mesh(self):
 
61
  self.current_prediction = 0.0
62
  self.history = []
63
  self.layers = self._build_layers()
64
+ n = self.n_inputs
65
 
66
  self.nodes = {}
67
  for layer in self.layers:
68
  for nid in layer:
69
+ anchored = nid[0] in ('A', 'B')
70
  self.nodes[nid] = {'x': 0.0, 'vel': 0.0, 'anchored': anchored}
 
 
71
 
72
+ for d in range(1, n+1):
73
+ self.nodes[f'A{d}']['x'] = 2.0
74
+ self.nodes[f'B{d}']['x'] = 3.0
75
+
76
  self.springs = {}
77
+ for d in range(1, n+1):
78
+ for j in range(1, self.n_upper+1):
79
+ uid = f'U{d}_{j}'
80
+ self.springs[(f'A{d}', uid)] = round(random.uniform(0.85, 1.15), 4)
81
+ self.springs[(uid, f'C{d}')] = round(random.uniform(0.85, 1.15), 4)
82
+ for j in range(1, self.n_lower+1):
83
+ lid = f'L{d}_{j}'
84
+ self.springs[(f'B{d}', lid)] = round(random.uniform(0.85, 1.15), 4)
85
+ self.springs[(lid, f'C{d}')] = round(random.uniform(0.85, 1.15), 4)
 
86
 
87
  def reset(self):
88
  self.running = False
 
90
  self.logs = []
91
  self._init_mesh()
92
 
 
93
  def add_log(self, msg):
94
+ self.logs.insert(0, f"[{self.iteration:05d}] {msg}")
95
+ if len(self.logs) > 50:
96
  self.logs.pop()
97
 
98
+ # ── HELPERS ───────────────────────────────────────────────────────────────
99
+
100
+ def _to_vec(self, val, n):
101
+ """Ensure val is a list of n floats. Scalars are broadcast."""
102
+ if isinstance(val, (list, tuple)):
103
+ v = [float(x) for x in val]
104
+ if len(v) >= n:
105
+ return v[:n]
106
+ return v + [v[-1]] * (n - len(v))
107
+ return [float(val)] * n
108
+
109
  # ── DATASET ───────────────────────────────────────────────────────────────
110
+
111
+ def ground_truth(self, a_vec, b_vec):
112
+ """Per-dimension ground truth. Same formula applied independently."""
113
+ n = self.n_inputs
114
+ a_vec = self._to_vec(a_vec, n)
115
+ b_vec = self._to_vec(b_vec, n)
116
  t = self.dataset_type
117
+ result = []
118
+ for i in range(n):
119
+ a, b = a_vec[i], b_vec[i]
120
+ if t == 'housing': result.append(round(a * 2.5 + b * 1.2, 4))
121
+ elif t == 'subtraction': result.append(round(a - b, 4))
122
+ elif t == 'multiplication': result.append(round(a * b, 4))
123
+ elif t == 'quadratic': result.append(round(a*a + b, 4))
124
+ else: result.append(round(a + b, 4))
125
+ return result
126
 
127
  # ── PROBLEM SETUP ─────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ def set_problem(self, a, b, c_target=None):
130
+ n = self.n_inputs
131
+ a_vec = self._to_vec(a, n)
132
+ b_vec = self._to_vec(b, n)
133
+
134
+ for d in range(1, n+1):
135
+ self.nodes[f'A{d}']['x'] = a_vec[d-1]
136
+ self.nodes[f'B{d}']['x'] = b_vec[d-1]
137
+
138
+ # Reset hidden nodes only
139
+ for nid, nd in self.nodes.items():
140
+ if nid[0] in ('U', 'L'):
141
+ nd['x'] = 0.0
142
+ nd['vel'] = 0.0
143
+
144
+ c_vec = self._to_vec(c_target, n) if c_target is not None else None
145
+ for d in range(1, n+1):
146
+ c = self.nodes[f'C{d}']
147
+ c['vel'] = 0.0
148
+ if self.mode == 'training' and c_vec is not None:
149
+ c['x'] = c_vec[d-1]
150
+ c['anchored'] = True
151
+ else:
152
+ c['anchored'] = False
153
+ c['x'] = 0.0
154
+
155
+ # ── ELASTIC DISPLAY PHYSICS ───────────────────────────────────────────────
156
 
157
+ def _elastic_step(self, n_steps):
 
 
 
158
  alpha = self.back_alpha
159
+ n = self.n_inputs
160
  for _ in range(n_steps):
161
  max_v = 0.0
162
+ for d in range(1, n+1):
163
+ A_val = self.nodes[f'A{d}']['x']
164
+ B_val = self.nodes[f'B{d}']['x']
165
+ C_val = self.nodes[f'C{d}']['x']
166
+
167
+ for j in range(1, self.n_upper+1):
168
+ uid = f'U{d}_{j}'
169
+ nd = self.nodes[uid]
170
+ rest = self.springs[(f'A{d}', uid)] * A_val
171
+ f = FWD_K * (rest - nd['x'])
172
+ if alpha > 0:
173
+ kuc = self.springs[(uid, f'C{d}')]
174
+ f += alpha * kuc * (C_val - nd['x'])
175
+ nd['vel'] = nd['vel'] * DAMPING + f * DT
176
+ nd['x'] += nd['vel'] * DT
177
+ max_v = max(max_v, abs(nd['vel']))
178
+
179
+ for j in range(1, self.n_lower+1):
180
+ lid = f'L{d}_{j}'
181
+ nd = self.nodes[lid]
182
+ rest = self.springs[(f'B{d}', lid)] * B_val
183
+ f = FWD_K * (rest - nd['x'])
184
+ if alpha > 0:
185
+ klc = self.springs[(lid, f'C{d}')]
186
+ f += alpha * klc * (C_val - nd['x'])
187
+ nd['vel'] = nd['vel'] * DAMPING + f * DT
188
+ nd['x'] += nd['vel'] * DT
189
+ max_v = max(max_v, abs(nd['vel']))
190
+
191
+ c = self.nodes[f'C{d}']
192
+ if not c['anchored']:
193
+ rest_c = (
194
+ sum(self.springs[(f'U{d}_{j}', f'C{d}')] * self.nodes[f'U{d}_{j}']['x']
195
+ for j in range(1, self.n_upper+1)) +
196
+ sum(self.springs[(f'L{d}_{j}', f'C{d}')] * self.nodes[f'L{d}_{j}']['x']
197
+ for j in range(1, self.n_lower+1))
198
+ )
199
+ f = FWD_K * (rest_c - c['x'])
200
+ c['vel'] = c['vel'] * DAMPING + f * DT
201
+ c['x'] += c['vel'] * DT
202
+ max_v = max(max_v, abs(c['vel']))
203
 
204
  if max_v < SETTLE:
205
  break
206
 
207
+ # ── FEEDFORWARD ───────────────────────────────────────────────────────────
208
+
209
  def _feedforward(self):
210
+ n = self.n_inputs
211
+ preds = []
212
+ ff = {}
213
+ for d in range(1, n+1):
214
+ A_val = self.nodes[f'A{d}']['x']
215
+ B_val = self.nodes[f'B{d}']['x']
216
+
217
+ for j in range(1, self.n_upper+1):
218
+ uid = f'U{d}_{j}'
219
+ ff[uid] = self.springs[(f'A{d}', uid)] * A_val
220
+
221
+ for j in range(1, self.n_lower+1):
222
+ lid = f'L{d}_{j}'
223
+ ff[lid] = self.springs[(f'B{d}', lid)] * B_val
224
+
225
+ if self.architecture == 'multiplicative':
226
+ nm = max(self.n_upper, self.n_lower)
227
+ pred = 0.0
228
+ for i in range(nm):
229
+ uid = f'U{d}_{(i % self.n_upper) + 1}'
230
+ lid = f'L{d}_{(i % self.n_lower) + 1}'
231
+ ku = self.springs[(uid, f'C{d}')]
232
+ kl = self.springs[(lid, f'C{d}')]
233
+ pred += ku * ff[uid] * kl * ff[lid]
234
+ else:
235
+ pred = (
236
+ sum(self.springs[(f'U{d}_{j}', f'C{d}')] * ff[f'U{d}_{j}']
237
+ for j in range(1, self.n_upper+1)) +
238
+ sum(self.springs[(f'L{d}_{j}', f'C{d}')] * ff[f'L{d}_{j}']
239
+ for j in range(1, self.n_lower+1))
240
+ )
241
+ preds.append(pred)
242
+ return preds, ff
243
+
244
+ # ── LMS UPDATE ────────────────────────────────────────────────────────────
245
+
246
+ def _lms_update(self, errors, ff):
247
+ n = self.n_inputs
248
+ for d in range(1, n+1):
249
+ err = errors[d-1]
250
+ A_val = self.nodes[f'A{d}']['x']
251
+ B_val = self.nodes[f'B{d}']['x']
252
+ grads = {}
253
+
254
+ if self.architecture == 'additive':
255
+ for j in range(1, self.n_upper+1):
256
+ uid = f'U{d}_{j}'
257
+ grads[(f'A{d}', uid)] = self.springs[(uid, f'C{d}')] * A_val
258
+ grads[(uid, f'C{d}')] = self.springs[(f'A{d}', uid)] * A_val
259
+ for j in range(1, self.n_lower+1):
260
+ lid = f'L{d}_{j}'
261
+ grads[(f'B{d}', lid)] = self.springs[(lid, f'C{d}')] * B_val
262
+ grads[(lid, f'C{d}')] = self.springs[(f'B{d}', lid)] * B_val
263
+ else:
264
+ nm = max(self.n_upper, self.n_lower)
265
+ for i in range(nm):
266
+ uid = f'U{d}_{(i % self.n_upper) + 1}'
267
+ lid = f'L{d}_{(i % self.n_lower) + 1}'
268
+ ku = self.springs[(uid, f'C{d}')]
269
+ kl = self.springs[(lid, f'C{d}')]
270
+ Uv = ff[uid]; Lv = ff[lid]
271
+ grads[(f'A{d}', uid)] = ku * A_val * kl * Lv
272
+ grads[(f'B{d}', lid)] = kl * B_val * ku * Uv
273
+ grads[(uid, f'C{d}')] = Uv * kl * Lv
274
+ grads[(lid, f'C{d}')] = Lv * ku * Uv
275
+
276
+ norm_sq = sum(g * g for g in grads.values()) + 1e-10
277
+ mu = err / norm_sq
278
+ for key, g in grads.items():
279
+ self.springs[key] -= mu * g
280
+ self.springs[key] = max(-30.0, min(30.0, self.springs[key]))
281
+
282
+ # ── PHYSICS STEP ──────────────────────────────────────────────────────────
283
+
 
 
 
 
 
 
 
 
 
284
  def physics_step(self):
285
  self._elastic_step(MICRO)
286
+ preds, ff = self._feedforward()
287
+ n = self.n_inputs
288
+
289
+ errors = []
290
+ for d in range(1, n+1):
291
+ c = self.nodes[f'C{d}']
292
+ if c['anchored']:
293
+ errors.append(preds[d-1] - c['x'])
294
+ else:
295
+ c['x'] = round(preds[d-1], 4)
296
+ errors.append(0.0)
297
+
298
+ self.current_prediction = round(sum(preds) / n, 5)
299
+ self.current_error = round(sum(abs(e) for e in errors) / n, 5)
300
+
301
+ self.history.append(self.current_error)
302
  if len(self.history) > 200:
303
  self.history.pop(0)
304
 
305
+ if self.current_error < CONV_THRESH:
306
+ a_vec = [self.nodes[f'A{d}']['x'] for d in range(1, n+1)]
307
+ b_vec = [self.nodes[f'B{d}']['x'] for d in range(1, n+1)]
308
+ gt = self.ground_truth(a_vec, b_vec)
309
+ delta = sum(abs(preds[d-1] - gt[d-1]) for d in range(1, n+1)) / n
310
+ if n == 1:
311
+ self.add_log(
312
+ f"βœ“ A={a_vec[0]:.2f} B={b_vec[0]:.2f} "
313
+ f"P={preds[0]:.4f} GT={gt[0]:.4f} Ξ”={delta:.4f}"
314
+ )
315
+ else:
316
+ p_str = ' '.join(f'{p:.3f}' for p in preds)
317
+ g_str = ' '.join(f'{g:.3f}' for g in gt)
318
+ self.add_log(f"βœ“ D={n} P=[{p_str}] GT=[{g_str}] Ξ”={delta:.4f}")
319
  return self._next_or_stop()
320
 
321
+ if self.mode == 'training' and any(
322
+ self.nodes[f'C{d}']['anchored'] for d in range(1, n+1)
323
+ ):
324
+ self._lms_update(errors, ff)
325
 
326
  self.iteration += 1
327
  return True
 
338
 
339
  def generate_batch(self, count=30):
340
  self.batch_queue.clear()
341
+ n = self.n_inputs
342
  for _ in range(count):
343
+ a_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
344
+ b_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
345
+ c_vec = self.ground_truth(a_vec, b_vec)
346
+ self.batch_queue.append({'a': a_vec, 'b': b_vec, 'c': c_vec})
347
  p = self.batch_queue.popleft()
348
  self.set_problem(p['a'], p['b'], p.get('c'))
349
  self.running = True
350
+ self.add_log(
351
+ f"β–Ά {count} samples | {self.dataset_type} "
352
+ f"| D={n} U{self.n_upper}Β·L{self.n_lower}"
353
+ )
354
 
355
 
356
  # ── SERVER ────────────────────────────────────────────────────────────────────
357
  engine = SimEngine()
358
 
359
+
360
  def run_loop():
361
  while True:
362
  if engine.running:
 
365
 
366
  threading.Thread(target=run_loop, daemon=True).start()
367
 
368
+
369
  @app.get("/", response_class=HTMLResponse)
370
  async def get_ui():
371
  return FileResponse("index.html")
372
 
373
+
374
  @app.get("/state")
375
  async def get_state():
376
  springs_out = {f"{u}β†’{v}": round(k, 5) for (u, v), k in engine.springs.items()}
377
+ n = engine.n_inputs
378
  return {
379
  'nodes': engine.nodes,
380
  'springs': springs_out,
381
  'layers': engine.layers,
382
  'error': engine.current_error,
383
  'prediction': engine.current_prediction,
384
+ 'predictions': [round(engine.nodes[f'C{d}']['x'], 4) for d in range(1, n+1)],
385
  'iter': engine.iteration,
386
  'logs': engine.logs,
387
  'history': engine.history[-80:],
 
389
  'mode': engine.mode,
390
  'architecture': engine.architecture,
391
  'dataset_type': engine.dataset_type,
392
+ 'n_inputs': n,
393
  'n_upper': engine.n_upper,
394
  'n_lower': engine.n_lower,
395
  'back_alpha': engine.back_alpha,
396
  'queue_size': len(engine.batch_queue),
397
  }
398
 
399
+
400
+ @app.post("/set_mode")
401
+ async def set_mode(data: dict):
402
+ """Switch training ↔ inference without touching springs or mesh."""
403
+ engine.mode = data.get('mode', engine.mode)
404
+ engine.running = False
405
+ engine.add_log(f"Mode β†’ {engine.mode} (springs preserved)")
406
+ return {"ok": True}
407
+
408
+
409
  @app.post("/config")
410
  async def config(data: dict):
411
+ new_ni = max(1, min(8, int(data.get('n_inputs', engine.n_inputs))))
412
+ new_nu = max(1, min(16, int(data.get('n_upper', engine.n_upper))))
413
+ new_nl = max(1, min(16, int(data.get('n_lower', engine.n_lower))))
414
+
415
+ topo_changed = (
416
+ new_ni != engine.n_inputs or
417
+ new_nu != engine.n_upper or
418
+ new_nl != engine.n_lower
419
+ )
420
+
421
+ # Non-topology settings β€” always apply
422
  engine.architecture = data.get('architecture', engine.architecture)
423
  engine.dataset_type = data.get('dataset', engine.dataset_type)
 
 
424
  engine.back_alpha = max(0.0, min(1.0, float(data.get('back_alpha', engine.back_alpha))))
425
+ engine.n_inputs = new_ni
426
+ engine.n_upper = new_nu
427
+ engine.n_lower = new_nl
428
+
429
+ # Mode via /set_mode is preferred; config can also carry it
430
+ if 'mode' in data:
431
+ engine.mode = data['mode']
432
+
433
+ if topo_changed:
434
+ engine.running = False
435
+ engine._init_mesh()
436
+ engine.logs = []
437
+ engine.add_log(
438
+ f"Mesh rebuilt: D={new_ni} U{new_nu}Β·L{new_nl} | {engine.architecture}"
439
+ )
440
+ else:
441
+ engine.add_log(
442
+ f"Config: {engine.mode}|{engine.architecture}"
443
+ f"|D={new_ni}|Ξ±={engine.back_alpha:.2f}"
444
+ )
445
+
446
+ return {"ok": True, "topo_changed": topo_changed}
447
+
448
 
449
  @app.post("/set_layer")
450
  async def set_layer(data: dict):
451
  layer = data.get('layer', '')
452
  delta = int(data.get('delta', 0))
453
+ if layer == 'inputs': engine.n_inputs = max(1, min(8, engine.n_inputs + delta))
454
+ elif layer == 'upper': engine.n_upper = max(1, min(16, engine.n_upper + delta))
455
+ elif layer == 'lower': engine.n_lower = max(1, min(16, engine.n_lower + delta))
456
  engine.running = False
457
  engine._init_mesh()
458
+ engine.add_log(
459
+ f"Topology β†’ D={engine.n_inputs} U{engine.n_upper}Β·L{engine.n_lower}"
460
+ )
461
+ return {
462
+ "ok": True,
463
+ "n_inputs": engine.n_inputs,
464
+ "n_upper": engine.n_upper,
465
+ "n_lower": engine.n_lower,
466
+ }
467
+
468
 
469
  @app.post("/generate")
470
  async def generate(data: dict):
471
  engine.generate_batch(int(data.get('count', 30)))
472
  return {"ok": True}
473
 
474
+
475
  @app.post("/run_custom")
476
  async def run_custom(data: dict):
477
+ def parse(v):
478
+ if v is None or v in ('', 'null'): return None
479
+ if isinstance(v, list): return [float(x) for x in v]
480
+ s = str(v)
481
+ if ',' in s:
482
+ return [float(x.strip()) for x in s.split(',') if x.strip()]
483
+ try: return float(s)
484
+ except: return None
485
+
486
+ a = parse(data.get('a')) or 5.0
487
+ b = parse(data.get('b')) or 3.0
488
+ c = parse(data.get('c'))
489
  if c is None and engine.mode == 'training':
490
+ c = engine.ground_truth(
491
+ engine._to_vec(a, engine.n_inputs),
492
+ engine._to_vec(b, engine.n_inputs),
493
+ )
494
  engine.batch_queue.clear()
495
  engine.set_problem(a, b, c)
496
  engine.running = True
497
+ engine.add_log(f"Custom: A={a} B={b} C={c}")
498
  return {"ok": True}
499
 
500
+
501
  @app.post("/halt")
502
  async def halt():
503
  engine.running = False
504
  return {"ok": True}
505
 
506
+
507
  if __name__ == "__main__":
508
  import uvicorn
509
+ uvicorn.run(app, host="0.0.0.0", port=7860)