everydaytok commited on
Commit
86fab25
Β·
verified Β·
1 Parent(s): 6e01dcd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +530 -308
app.py CHANGED
@@ -1,54 +1,84 @@
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
5
 
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)],
@@ -58,376 +88,568 @@ class SimEngine:
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)
 
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)],
 
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
655
  uvicorn.run(app, host="0.0.0.0", port=7860)