everydaytok commited on
Commit
ff87c16
·
verified ·
1 Parent(s): 9a5e85b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +268 -349
app.py CHANGED
@@ -1,15 +1,18 @@
1
- import time
2
  import math
3
  import random
 
4
  import threading
5
  import collections
6
  from dataclasses import dataclass, asdict
 
7
  from typing import Optional, List, Dict, Any, Literal
8
 
9
- from fastapi import FastAPI
10
- from fastapi.responses import HTMLResponse, FileResponse
11
  from fastapi.middleware.cors import CORSMiddleware
12
- from pydantic import BaseModel
 
 
13
 
14
  app = FastAPI()
15
  app.add_middleware(
@@ -22,15 +25,15 @@ app.add_middleware(
22
 
23
  @dataclass
24
  class EngineConfig:
25
- architecture: str = "additive" # additive | multiplicative | affine | bilinear | gated
26
- coeff_mode: str = "single_k" # single_k | triple_k | per_edge_k
27
- topology: str = "single_cell" # single_cell | chain | mesh
28
- dataset_family: str = "housing" # housing | subtraction | multiplication | mixed | symbolic
29
- mode: str = "training" # training | inference
30
- num_cells: int = 3
31
  learning_rate: float = 0.01
32
- damping: float = 0.12
33
  coupling: float = 0.05
 
34
  batch_size: int = 24
35
  sample_noise: float = 0.0
36
 
@@ -49,7 +52,7 @@ class CellState:
49
  kc: float = 0.0
50
  prediction: float = 0.0
51
  error: float = 0.0
52
- energy: float = 0.0
53
  force: float = 0.0
54
  anchored: bool = False
55
 
@@ -61,36 +64,37 @@ class SimEngine:
61
  def __init__(self):
62
  self.lock = threading.Lock()
63
  self.config = EngineConfig()
64
-
65
  self.running = False
66
  self.iteration = 0
67
  self.current_error = 0.0
68
  self.current_loss = 0.0
69
- self.logs = []
70
-
71
  self.cells: List[CellState] = []
72
  self.batch_queue = collections.deque()
73
  self.current_sample: Optional[Dict[str, Any]] = None
74
  self.last_sample: Optional[Dict[str, Any]] = None
75
-
76
  self.loss_history = collections.deque(maxlen=120)
77
  self.error_history = collections.deque(maxlen=120)
78
-
79
  self.reset_state()
80
 
 
 
 
 
 
81
  def reset_state(self):
82
- with self.lock:
83
- self.iteration = 0
84
- self.current_error = 0.0
85
- self.current_loss = 0.0
86
- self.logs = []
87
- self.batch_queue.clear()
88
- self.current_sample = None
89
- self.last_sample = None
90
- self.loss_history.clear()
91
- self.error_history.clear()
92
- self._build_cells()
93
- self.add_log("Engine reset.")
94
 
95
  def _build_cells(self):
96
  count = 1 if self.config.topology == "single_cell" else max(2, int(self.config.num_cells))
@@ -99,434 +103,349 @@ class SimEngine:
99
  self.cells.append(
100
  CellState(
101
  id=i,
102
- a=0.0,
103
- b=0.0,
104
- c=0.0,
105
- k=random.uniform(0.35, 1.25),
106
- ka=random.uniform(0.35, 1.25),
107
- kb=random.uniform(0.35, 1.25),
108
- kc=random.uniform(-0.25, 0.25),
109
  )
110
  )
111
 
112
- def add_log(self, msg: str):
113
- stamp = f"[{self.iteration}] {msg}"
114
- self.logs.insert(0, stamp)
115
- if len(self.logs) > 20:
116
- self.logs.pop()
117
-
118
  def configure(self, payload: Dict[str, Any]):
119
  with self.lock:
 
120
  self.config.architecture = payload.get("architecture", self.config.architecture)
121
  self.config.coeff_mode = payload.get("coeff_mode", self.config.coeff_mode)
122
  self.config.topology = payload.get("topology", self.config.topology)
123
  self.config.dataset_family = payload.get("dataset_family", self.config.dataset_family)
124
- self.config.mode = payload.get("mode", self.config.mode)
125
  self.config.num_cells = int(payload.get("num_cells", self.config.num_cells))
126
  self.config.learning_rate = float(payload.get("learning_rate", self.config.learning_rate))
127
- self.config.damping = float(payload.get("damping", self.config.damping))
128
  self.config.coupling = float(payload.get("coupling", self.config.coupling))
 
129
  self.config.batch_size = int(payload.get("batch_size", self.config.batch_size))
130
  self.config.sample_noise = float(payload.get("sample_noise", self.config.sample_noise))
131
 
132
  self.running = False
133
  self.reset_state()
134
  self.add_log(
135
- f"Config applied: {self.config.architecture} | {self.config.coeff_mode} | "
136
- f"{self.config.topology} | {self.config.dataset_family} | {self.config.mode}"
137
  )
138
 
139
- def _sample_housing(self):
140
- a = random.uniform(2, 10)
141
- b = random.uniform(2, 10)
142
- c = (2.5 * a) + (1.2 * b) + random.uniform(-self.config.sample_noise, self.config.sample_noise)
143
- return a, b, c, "housing_affine"
144
-
145
- def _sample_subtraction(self):
146
- a = random.uniform(2, 10)
147
- b = random.uniform(2, 10)
148
- c = (1.0 * a) + (-1.0 * b) + random.uniform(-self.config.sample_noise, self.config.sample_noise)
149
- return a, b, c, "signed_subtraction"
150
-
151
- def _sample_multiplication(self):
152
  a = random.uniform(2, 10)
153
  b = random.uniform(2, 10)
154
- c = (a * b) + random.uniform(-self.config.sample_noise, self.config.sample_noise)
155
- return a, b, c, "multiplicative"
156
-
157
- def _sample_symbolic(self):
158
- a = random.uniform(1, 12)
159
- b = random.uniform(1, 12)
160
- branch = random.choice(["affine", "signed_affine", "hybrid"])
161
- if branch == "affine":
162
- c = (1.7 * a) + (0.9 * b)
163
- elif branch == "signed_affine":
164
- c = (0.8 * a) + (-1.4 * b) + 2.0
165
- else:
166
- c = (a * 0.6) + (b * 0.4) + ((a * b) * 0.2)
167
- c += random.uniform(-self.config.sample_noise, self.config.sample_noise)
168
- return a, b, c, f"symbolic_{branch}"
169
 
170
- def generate_sample(self, family: Optional[str] = None) -> Dict[str, Any]:
171
- family = family or self.config.dataset_family
172
  if family == "housing":
173
- a, b, c, label = self._sample_housing()
 
174
  elif family == "subtraction":
175
- a, b, c, label = self._sample_subtraction()
 
176
  elif family == "multiplication":
177
- a, b, c, label = self._sample_multiplication()
 
178
  elif family == "symbolic":
179
- a, b, c, label = self._sample_symbolic()
 
 
 
 
 
 
 
180
  elif family == "mixed":
181
- pick = random.choice(["housing", "subtraction", "multiplication", "symbolic"])
182
- return self.generate_sample(pick)
183
  else:
184
- a, b = random.uniform(2, 10), random.uniform(2, 10)
185
- c, label = a + b, "default_add"
186
- return {"a": float(a), "b": float(b), "c": float(c), "label": label}
 
 
187
 
188
- def _apply_sample_to_cells(self, sample: Dict[str, Any], anchor_output: bool):
 
 
 
 
189
  self.current_sample = sample
190
  self.last_sample = sample
191
-
192
  for cell in self.cells:
193
  cell.a = float(sample["a"])
194
  cell.b = float(sample["b"])
195
  cell.target = float(sample["c"]) if sample.get("c") is not None else None
196
  cell.label = sample.get("label", "")
197
  cell.anchored = anchor_output
198
-
199
- if anchor_output:
200
- cell.c = float(sample["c"])
201
- else:
202
- cell.c = 0.0
203
-
204
  cell.prediction = 0.0
205
  cell.error = 0.0
206
- cell.energy = 0.0
207
  cell.force = 0.0
208
 
209
  def load_sample(self, sample: Dict[str, Any], anchor_output: Optional[bool] = None):
210
- with self.lock:
211
- if anchor_output is None:
212
- anchor_output = self.config.mode == "training"
213
- self._apply_sample_to_cells(sample, anchor_output=anchor_output)
214
- self.add_log(
215
- f"Sample loaded: a={sample['a']:.3f}, b={sample['b']:.3f}, "
216
- f"c={sample['c']:.3f}, label={sample.get('label', '')}"
217
- )
218
 
219
- def _coefficient_snapshot(self, cell: CellState):
220
- if self.config.coeff_mode == "single_k":
221
- return {"ka": cell.k, "kb": cell.k, "kc": cell.k}
222
- if self.config.coeff_mode == "per_edge_k":
223
- return {"ka": cell.ka, "kb": cell.kb, "kc": 0.0}
224
- return {"ka": cell.ka, "kb": cell.kb, "kc": cell.kc}
225
-
226
- def _set_trainable_param(self, cell: CellState, name: str, value: float):
227
- value = max(-20.0, min(20.0, value))
228
- if name == "k":
229
- cell.k = value
230
- elif name == "ka":
231
- cell.ka = value
232
- elif name == "kb":
233
- cell.kb = value
234
- elif name == "kc":
235
- cell.kc = value
236
-
237
- def _get_trainable_params(self):
238
- if self.config.coeff_mode == "single_k":
239
- return ["k"]
240
- if self.config.coeff_mode == "per_edge_k":
241
- return ["ka", "kb"]
242
- return ["ka", "kb", "kc"]
243
 
244
- def _predict_cell(self, cell: CellState) -> float:
245
- coeffs = self._coefficient_snapshot(cell)
246
  a, b = cell.a, cell.b
247
- arch = self.config.architecture
248
- ka, kb, kc = coeffs["ka"], coeffs["kb"], coeffs["kc"]
249
 
250
  if self.config.coeff_mode == "single_k":
251
  k = cell.k
252
- if arch == "additive":
253
  return k * (a + b)
254
- if arch == "multiplicative":
255
  return k * (a * b)
256
- if arch == "affine":
257
- return (k * a) + (k * b) + k
258
- if arch == "bilinear":
259
- return k * (a + b + (a * b))
260
- if arch == "gated":
261
  gate = 1.0 / (1.0 + math.exp(-k))
262
  return gate * (a + b) + (1.0 - gate) * (a * b)
263
  return k * (a + b)
264
 
265
- if arch == "additive":
 
266
  return (ka * a) + (kb * b) + kc
267
- if arch == "multiplicative":
268
  return (ka * a) * (kb * b) + kc
269
- if arch == "affine":
270
  return (ka * a) + (kb * b) + kc
271
- if arch == "bilinear":
272
- return (ka * a) + (kb * b) + (kc * a * b)
273
- if arch == "gated":
274
  gate = 1.0 / (1.0 + math.exp(-kc))
275
  return gate * ((ka * a) + (kb * b)) + (1.0 - gate) * (a * b)
276
  return (ka * a) + (kb * b) + kc
277
 
278
- def _neighbors(self, idx: int):
279
- if self.config.topology == "single_cell":
280
- return []
281
- if self.config.topology == "chain":
282
- n = []
283
- if idx - 1 >= 0:
284
- n.append(idx - 1)
285
- if idx + 1 < len(self.cells):
286
- n.append(idx + 1)
287
- return n
288
- if self.config.topology == "mesh":
289
- return [j for j in range(len(self.cells)) if j != idx]
290
- return []
291
-
292
- def _cell_loss(self, idx: int, preds: List[float]) -> float:
293
- cell = self.cells[idx]
294
- pred = preds[idx]
295
- loss = 0.0
296
- if cell.target is not None:
297
- loss += (pred - cell.target) ** 2
298
-
299
- neighbors = self._neighbors(idx)
300
- if neighbors:
301
- neighbor_mean = sum(preds[j] for j in neighbors) / len(neighbors)
302
- loss += self.config.coupling * ((pred - neighbor_mean) ** 2)
303
-
304
- return loss
305
-
306
- def _numeric_gradient(self, idx: int, param_name: str, eps: float = 1e-4) -> float:
307
- cell = self.cells[idx]
308
- old = getattr(cell, param_name)
309
-
310
- def local_loss() -> float:
311
- pred = self._predict_cell(cell)
312
- loss = 0.0
313
- if cell.target is not None:
314
- loss += (pred - cell.target) ** 2
315
- neighbors = self._neighbors(idx)
316
- if neighbors:
317
- neighbor_preds = [self._predict_cell(self.cells[j]) for j in neighbors]
318
- neighbor_mean = sum(neighbor_preds) / len(neighbor_preds)
319
- loss += self.config.coupling * ((pred - neighbor_mean) ** 2)
320
- return loss
321
-
322
- self._set_trainable_param(cell, param_name, old + eps)
323
- plus = local_loss()
324
-
325
- self._set_trainable_param(cell, param_name, old - eps)
326
- minus = local_loss()
327
-
328
- self._set_trainable_param(cell, param_name, old)
329
- return (plus - minus) / (2.0 * eps)
330
-
331
- def _mean(self, xs: List[float]) -> float:
332
- return sum(xs) / max(1, len(xs))
333
-
334
- def _load_next_sample_from_batch(self):
335
  if self.batch_queue:
336
- sample = self.batch_queue.popleft()
337
- self._apply_sample_to_cells(sample, anchor_output=(self.config.mode == "training"))
338
- self.add_log(f"Next batch sample: {sample.get('label', '')}")
339
  return True
340
  return False
341
 
342
- def physics_step(self):
343
- with self.lock:
344
- if not self.running or not self.cells:
345
- return False
346
 
347
- preds = []
348
- for cell in self.cells:
349
- pred = self._predict_cell(cell)
350
- cell.prediction = pred
351
- preds.append(pred)
352
 
353
- global_pred = self._mean(preds)
354
- target_available = self.current_sample is not None and self.current_sample.get("c") is not None
355
- target = self._mean([c.target for c in self.cells if c.target is not None]) if target_available else None
356
 
357
  if self.config.mode == "training":
358
- total_loss = 0.0
359
-
360
- for idx, cell in enumerate(self.cells):
361
- cell.target = target if target is not None else cell.target
362
- cell.error = (cell.prediction - cell.target) if cell.target is not None else 0.0
363
- cell.energy = cell.error ** 2
364
-
365
- for param_name in self._get_trainable_params():
366
- grad = self._numeric_gradient(idx, param_name)
367
- old = getattr(cell, param_name)
368
- new_val = old - (self.config.learning_rate * grad)
369
- new_val = (1.0 - self.config.damping) * new_val + (self.config.damping * old)
370
- self._set_trainable_param(cell, param_name, new_val)
371
-
372
- total_loss += self._cell_loss(idx, preds)
373
-
374
- self.current_loss = total_loss / max(1, len(self.cells))
375
- self.current_error = (global_pred - target) if target is not None else global_pred
376
- self.loss_history.append(self.current_loss)
377
- self.error_history.append(self.current_error)
378
-
379
- if target_available and abs(self.current_error) < 0.05 and self.current_loss < 0.01:
380
- self.add_log("Converged on current sample.")
381
- if self._load_next_sample_from_batch():
382
- self.iteration += 1
383
- return True
384
- self.running = False
385
- self.add_log("Batch complete.")
386
- self.iteration += 1
387
- return False
388
 
389
  else:
390
- # Inference mode: output node(s) drift toward the predicted state.
391
- drift_values = []
392
- for idx, cell in enumerate(self.cells):
393
- neighbors = self._neighbors(idx)
394
- neighbor_mean = self._mean([preds[j] for j in neighbors]) if neighbors else pred
395
-
396
- drift = (pred - cell.c)
397
- drift += self.config.coupling * (neighbor_mean - cell.c)
398
-
399
- cell.force = drift
400
- cell.c += 0.15 * drift
401
- cell.error = pred - cell.c
402
- cell.energy = cell.error ** 2
403
- drift_values.append(abs(drift))
404
-
405
- self.current_error = self._mean([cell.error for cell in self.cells])
406
- self.current_loss = self._mean([cell.energy for cell in self.cells])
407
- self.loss_history.append(self.current_loss)
408
- self.error_history.append(self.current_error)
409
-
410
- if self.current_sample and abs(self.current_error) < 0.05 and self.current_loss < 0.01:
411
- self.add_log("Inference settled.")
412
- if self._load_next_sample_from_batch():
413
- self.iteration += 1
414
- return True
415
- self.running = False
416
- self.add_log("Task complete.")
417
- self.iteration += 1
418
- return False
419
-
420
- # If no target exists, stop when drift is tiny.
421
- if not target_available and self._mean(drift_values) < 0.002:
422
- self.running = False
423
- self.add_log("Inference drift stabilized.")
424
- self.iteration += 1
425
- return False
426
-
427
- self.iteration += 1
428
- return True
429
 
430
  def start_batch(self, count: int):
431
- with self.lock:
432
- self.batch_queue.clear()
433
- for _ in range(count):
434
- self.batch_queue.append(self.generate_sample())
435
- first = self._load_next_sample_from_batch()
436
- self.running = first
437
- self.add_log(f"Batch started with {count} samples.")
438
- return first
439
-
440
- def set_custom_sample(self, a: float, b: float, c: Optional[float] = None):
441
- with self.lock:
442
- sample = {"a": float(a), "b": float(b), "c": float(c) if c is not None else None, "label": "custom"}
443
- self._apply_sample_to_cells(sample, anchor_output=(self.config.mode == "training" and c is not None))
444
- self.current_sample = sample
445
- self.last_sample = sample
446
- self.running = True
447
- self.add_log(f"Custom sample loaded: a={a}, b={b}, c={c}")
448
- return sample
449
 
450
  def halt(self):
451
- with self.lock:
452
- self.running = False
453
- self.add_log("Engine halted.")
454
-
455
- def snapshot(self) -> Dict[str, Any]:
456
- with self.lock:
457
- return {
458
- "config": asdict(self.config),
459
- "running": self.running,
460
- "iteration": self.iteration,
461
- "current_error": self.current_error,
462
- "current_loss": self.current_loss,
463
- "cells": [c.to_dict() for c in self.cells],
464
- "logs": self.logs,
465
- "last_sample": self.last_sample,
466
- "current_sample": self.current_sample,
467
- "batch_remaining": len(self.batch_queue),
468
- "loss_history": list(self.loss_history),
469
- "error_history": list(self.error_history),
470
- }
471
 
472
 
473
  engine = SimEngine()
474
 
475
 
476
- def run_loop():
477
  while True:
478
- if engine.running:
479
- engine.physics_step()
 
 
 
 
 
480
  time.sleep(0.04)
481
 
482
 
483
- threading.Thread(target=run_loop, daemon=True).start()
484
 
485
 
486
- @app.get("/", response_class=HTMLResponse)
487
- async def get_ui():
488
- return FileResponse("index.html")
489
 
490
 
491
  @app.get("/state")
492
- async def get_state():
493
- return engine.snapshot()
494
 
495
 
496
  @app.post("/config")
497
- async def config(data: dict):
498
- engine.configure(data)
499
- return {"ok": True}
 
500
 
501
 
502
  @app.post("/example")
503
- async def example(data: dict):
504
- family = data.get("dataset_family", engine.config.dataset_family)
505
- sample = engine.generate_sample(family)
506
- return sample
507
 
508
 
509
  @app.post("/generate_batch")
510
- async def generate_batch(data: dict):
511
- count = int(data.get("count", engine.config.batch_size))
 
512
  engine.start_batch(count)
513
- return {"ok": True, "count": count}
514
 
515
 
516
  @app.post("/test_custom")
517
- async def test_custom(data: dict):
518
- a = float(data["a"])
519
- b = float(data["b"])
520
- c = data.get("c", None)
521
- c_val = float(c) if c not in [None, "", "null"] else None
522
- engine.set_custom_sample(a, b, c_val)
523
- return {"ok": True}
 
524
 
525
 
526
  @app.post("/halt")
527
  async def halt():
528
  engine.halt()
529
- return {"ok": True}
530
 
531
 
532
  if __name__ == "__main__":
 
 
1
  import math
2
  import random
3
+ import time
4
  import threading
5
  import collections
6
  from dataclasses import dataclass, asdict
7
+ from pathlib import Path
8
  from typing import Optional, List, Dict, Any, Literal
9
 
10
+ from fastapi import FastAPI, Request
11
+ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
12
  from fastapi.middleware.cors import CORSMiddleware
13
+
14
+ BASE_DIR = Path(__file__).resolve().parent
15
+ INDEX_PATH = BASE_DIR / "index.html"
16
 
17
  app = FastAPI()
18
  app.add_middleware(
 
25
 
26
  @dataclass
27
  class EngineConfig:
28
+ mode: Literal["training", "inference"] = "training"
29
+ architecture: Literal["additive", "multiplicative", "affine", "gated"] = "additive"
30
+ coeff_mode: Literal["single_k", "triple_k"] = "single_k"
31
+ topology: Literal["single_cell", "chain", "mesh"] = "single_cell"
32
+ dataset_family: Literal["housing", "subtraction", "multiplication", "symbolic", "mixed"] = "housing"
33
+ num_cells: int = 4
34
  learning_rate: float = 0.01
 
35
  coupling: float = 0.05
36
+ damping: float = 0.08
37
  batch_size: int = 24
38
  sample_noise: float = 0.0
39
 
 
52
  kc: float = 0.0
53
  prediction: float = 0.0
54
  error: float = 0.0
55
+ loss: float = 0.0
56
  force: float = 0.0
57
  anchored: bool = False
58
 
 
64
  def __init__(self):
65
  self.lock = threading.Lock()
66
  self.config = EngineConfig()
 
67
  self.running = False
68
  self.iteration = 0
69
  self.current_error = 0.0
70
  self.current_loss = 0.0
71
+ self.last_action = "Idle"
72
+ self.logs: List[str] = []
73
  self.cells: List[CellState] = []
74
  self.batch_queue = collections.deque()
75
  self.current_sample: Optional[Dict[str, Any]] = None
76
  self.last_sample: Optional[Dict[str, Any]] = None
 
77
  self.loss_history = collections.deque(maxlen=120)
78
  self.error_history = collections.deque(maxlen=120)
 
79
  self.reset_state()
80
 
81
+ def add_log(self, msg: str):
82
+ self.logs.insert(0, f"[{self.iteration}] {msg}")
83
+ self.logs = self.logs[:20]
84
+ self.last_action = msg
85
+
86
  def reset_state(self):
87
+ self.iteration = 0
88
+ self.current_error = 0.0
89
+ self.current_loss = 0.0
90
+ self.logs = []
91
+ self.batch_queue.clear()
92
+ self.current_sample = None
93
+ self.last_sample = None
94
+ self.loss_history.clear()
95
+ self.error_history.clear()
96
+ self._build_cells()
97
+ self.add_log("Engine reset.")
 
98
 
99
  def _build_cells(self):
100
  count = 1 if self.config.topology == "single_cell" else max(2, int(self.config.num_cells))
 
103
  self.cells.append(
104
  CellState(
105
  id=i,
106
+ k=random.uniform(0.6, 1.4),
107
+ ka=random.uniform(0.6, 1.4),
108
+ kb=random.uniform(0.6, 1.4),
109
+ kc=random.uniform(-0.3, 0.3),
 
 
 
110
  )
111
  )
112
 
 
 
 
 
 
 
113
  def configure(self, payload: Dict[str, Any]):
114
  with self.lock:
115
+ self.config.mode = payload.get("mode", self.config.mode)
116
  self.config.architecture = payload.get("architecture", self.config.architecture)
117
  self.config.coeff_mode = payload.get("coeff_mode", self.config.coeff_mode)
118
  self.config.topology = payload.get("topology", self.config.topology)
119
  self.config.dataset_family = payload.get("dataset_family", self.config.dataset_family)
 
120
  self.config.num_cells = int(payload.get("num_cells", self.config.num_cells))
121
  self.config.learning_rate = float(payload.get("learning_rate", self.config.learning_rate))
 
122
  self.config.coupling = float(payload.get("coupling", self.config.coupling))
123
+ self.config.damping = float(payload.get("damping", self.config.damping))
124
  self.config.batch_size = int(payload.get("batch_size", self.config.batch_size))
125
  self.config.sample_noise = float(payload.get("sample_noise", self.config.sample_noise))
126
 
127
  self.running = False
128
  self.reset_state()
129
  self.add_log(
130
+ f"Config: {self.config.mode} | {self.config.architecture} | "
131
+ f"{self.config.coeff_mode} | {self.config.topology} | {self.config.dataset_family}"
132
  )
133
 
134
+ def _make_sample(self, family: str):
 
 
 
 
 
 
 
 
 
 
 
 
135
  a = random.uniform(2, 10)
136
  b = random.uniform(2, 10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
 
 
138
  if family == "housing":
139
+ c = 2.5 * a + 1.2 * b
140
+ label = "affine_housing"
141
  elif family == "subtraction":
142
+ c = 1.0 * a - 1.0 * b
143
+ label = "signed_subtraction"
144
  elif family == "multiplication":
145
+ c = a * b
146
+ label = "product"
147
  elif family == "symbolic":
148
+ mode = random.choice(["affine", "hybrid", "gated"])
149
+ if mode == "affine":
150
+ c = 1.7 * a + 0.9 * b + 1.0
151
+ elif mode == "hybrid":
152
+ c = 0.5 * a + 0.4 * b + 0.2 * a * b
153
+ else:
154
+ c = (a + b) / (1.0 + math.exp(-0.4 * (a - b)))
155
+ label = f"symbolic_{mode}"
156
  elif family == "mixed":
157
+ sub = random.choice(["housing", "subtraction", "multiplication", "symbolic"])
158
+ return self._make_sample(sub)
159
  else:
160
+ c = a + b
161
+ label = "default_add"
162
+
163
+ noise = random.uniform(-self.config.sample_noise, self.config.sample_noise)
164
+ return {"a": float(a), "b": float(b), "c": float(c + noise), "label": label}
165
 
166
+ def generate_example(self, family: Optional[str] = None):
167
+ with self.lock:
168
+ return self._make_sample(family or self.config.dataset_family)
169
+
170
+ def _apply_sample(self, sample: Dict[str, Any], anchor_output: bool):
171
  self.current_sample = sample
172
  self.last_sample = sample
 
173
  for cell in self.cells:
174
  cell.a = float(sample["a"])
175
  cell.b = float(sample["b"])
176
  cell.target = float(sample["c"]) if sample.get("c") is not None else None
177
  cell.label = sample.get("label", "")
178
  cell.anchored = anchor_output
179
+ cell.c = float(sample["c"]) if anchor_output and sample.get("c") is not None else 0.0
 
 
 
 
 
180
  cell.prediction = 0.0
181
  cell.error = 0.0
182
+ cell.loss = 0.0
183
  cell.force = 0.0
184
 
185
  def load_sample(self, sample: Dict[str, Any], anchor_output: Optional[bool] = None):
186
+ if anchor_output is None:
187
+ anchor_output = self.config.mode == "training" and sample.get("c") is not None
188
+ self._apply_sample(sample, anchor_output)
189
+ self.add_log(
190
+ f"Loaded sample: a={sample['a']:.3f}, b={sample['b']:.3f}, "
191
+ f"c={sample.get('c', None)}, label={sample.get('label', '')}"
192
+ )
 
193
 
194
+ def _neighbors(self, idx: int):
195
+ if self.config.topology == "single_cell":
196
+ return []
197
+ if self.config.topology == "chain":
198
+ n = []
199
+ if idx - 1 >= 0:
200
+ n.append(idx - 1)
201
+ if idx + 1 < len(self.cells):
202
+ n.append(idx + 1)
203
+ return n
204
+ return [j for j in range(len(self.cells)) if j != idx]
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
+ def _predict(self, cell: CellState) -> float:
 
207
  a, b = cell.a, cell.b
 
 
208
 
209
  if self.config.coeff_mode == "single_k":
210
  k = cell.k
211
+ if self.config.architecture == "additive":
212
  return k * (a + b)
213
+ if self.config.architecture == "multiplicative":
214
  return k * (a * b)
215
+ if self.config.architecture == "affine":
216
+ return k * a + k * b + k
217
+ if self.config.architecture == "gated":
 
 
218
  gate = 1.0 / (1.0 + math.exp(-k))
219
  return gate * (a + b) + (1.0 - gate) * (a * b)
220
  return k * (a + b)
221
 
222
+ ka, kb, kc = cell.ka, cell.kb, cell.kc
223
+ if self.config.architecture == "additive":
224
  return (ka * a) + (kb * b) + kc
225
+ if self.config.architecture == "multiplicative":
226
  return (ka * a) * (kb * b) + kc
227
+ if self.config.architecture == "affine":
228
  return (ka * a) + (kb * b) + kc
229
+ if self.config.architecture == "gated":
 
 
230
  gate = 1.0 / (1.0 + math.exp(-kc))
231
  return gate * ((ka * a) + (kb * b)) + (1.0 - gate) * (a * b)
232
  return (ka * a) + (kb * b) + kc
233
 
234
+ def _train_cell(self, cell: CellState):
235
+ err = cell.prediction - (cell.target if cell.target is not None else 0.0)
236
+
237
+ if self.config.coeff_mode == "single_k":
238
+ if self.config.architecture == "additive":
239
+ grad = 2.0 * err * (cell.a + cell.b)
240
+ elif self.config.architecture == "multiplicative":
241
+ grad = 2.0 * err * (cell.a * cell.b)
242
+ elif self.config.architecture == "affine":
243
+ grad = 2.0 * err * (cell.a + cell.b + 1.0)
244
+ else:
245
+ gate = 1.0 / (1.0 + math.exp(-cell.k))
246
+ grad = 2.0 * err * ((cell.a + cell.b) - (cell.a * cell.b)) * gate * (1.0 - gate)
247
+
248
+ cell.k -= self.config.learning_rate * grad
249
+ cell.k = max(-20.0, min(20.0, cell.k))
250
+ return
251
+
252
+ if self.config.architecture == "additive":
253
+ da = 2.0 * err * cell.a
254
+ db = 2.0 * err * cell.b
255
+ dc = 2.0 * err * 1.0
256
+ cell.ka -= self.config.learning_rate * da
257
+ cell.kb -= self.config.learning_rate * db
258
+ cell.kc -= self.config.learning_rate * dc
259
+ elif self.config.architecture == "multiplicative":
260
+ da = 2.0 * err * (cell.kb * cell.b * cell.a)
261
+ db = 2.0 * err * (cell.ka * cell.a * cell.b)
262
+ dc = 2.0 * err * 1.0
263
+ cell.ka -= self.config.learning_rate * da
264
+ cell.kb -= self.config.learning_rate * db
265
+ cell.kc -= self.config.learning_rate * dc
266
+ elif self.config.architecture == "gated":
267
+ gate = 1.0 / (1.0 + math.exp(-cell.kc))
268
+ mix = (cell.ka * cell.a) + (cell.kb * cell.b)
269
+ da = 2.0 * err * gate * cell.a
270
+ db = 2.0 * err * gate * cell.b
271
+ dc = 2.0 * err * gate * (1.0 - gate) * (mix - (cell.a * cell.b))
272
+ cell.ka -= self.config.learning_rate * da
273
+ cell.kb -= self.config.learning_rate * db
274
+ cell.kc -= self.config.learning_rate * dc
275
+ else:
276
+ da = 2.0 * err * cell.a
277
+ db = 2.0 * err * cell.b
278
+ dc = 2.0 * err
279
+ cell.ka -= self.config.learning_rate * da
280
+ cell.kb -= self.config.learning_rate * db
281
+ cell.kc -= self.config.learning_rate * dc
282
+
283
+ cell.ka = max(-20.0, min(20.0, cell.ka))
284
+ cell.kb = max(-20.0, min(20.0, cell.kb))
285
+ cell.kc = max(-20.0, min(20.0, cell.kc))
286
+
287
+ def _pull_next_batch(self):
 
 
 
288
  if self.batch_queue:
289
+ nxt = self.batch_queue.popleft()
290
+ self.load_sample(nxt, anchor_output=(self.config.mode == "training" and nxt.get("c") is not None))
 
291
  return True
292
  return False
293
 
294
+ def step(self):
295
+ if not self.running or not self.cells:
296
+ return False
 
297
 
298
+ preds = []
299
+ for cell in self.cells:
300
+ cell.prediction = self._predict(cell)
301
+ preds.append(cell.prediction)
 
302
 
303
+ for idx, cell in enumerate(self.cells):
304
+ neigh = self._neighbors(idx)
305
+ neigh_mean = sum(preds[j] for j in neigh) / len(neigh) if neigh else cell.prediction
306
 
307
  if self.config.mode == "training":
308
+ if cell.target is None:
309
+ continue
310
+
311
+ cell.error = cell.prediction - cell.target
312
+ cell.loss = cell.error ** 2 + self.config.coupling * ((cell.prediction - neigh_mean) ** 2)
313
+ self._train_cell(cell)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
  else:
316
+ if cell.target is None:
317
+ cell.force = cell.prediction - cell.c
318
+ cell.c += 0.18 * cell.force
319
+ cell.error = cell.prediction - cell.c
320
+ cell.loss = cell.error ** 2
321
+ else:
322
+ cell.force = (cell.prediction - cell.c) + self.config.coupling * (neigh_mean - cell.c)
323
+ cell.c += 0.16 * cell.force
324
+ cell.error = cell.prediction - cell.c
325
+ cell.loss = cell.error ** 2
326
+
327
+ self.current_error = sum(c.error for c in self.cells) / len(self.cells)
328
+ self.current_loss = sum(c.loss for c in self.cells) / len(self.cells)
329
+ self.loss_history.append(self.current_loss)
330
+ self.error_history.append(self.current_error)
331
+
332
+ self.iteration += 1
333
+
334
+ converged = abs(self.current_error) < 0.05 and self.current_loss < 0.01
335
+ if converged:
336
+ if self._pull_next_batch():
337
+ self.add_log("Switched to next batch item.")
338
+ return True
339
+ self.running = False
340
+ self.add_log("Converged.")
341
+ return False
342
+
343
+ return True
 
 
 
 
 
 
 
 
 
 
 
344
 
345
  def start_batch(self, count: int):
346
+ self.batch_queue.clear()
347
+ for _ in range(count):
348
+ self.batch_queue.append(self.generate_example())
349
+ first = self.batch_queue.popleft()
350
+ self.load_sample(first, anchor_output=(self.config.mode == "training"))
351
+ self.running = True
352
+ self.add_log(f"Batch started: {count} samples.")
353
+ return first
354
+
355
+ def custom_input(self, a: float, b: float, c: Optional[float]):
356
+ sample = {"a": float(a), "b": float(b), "c": float(c) if c is not None else None, "label": "custom"}
357
+ self.load_sample(sample, anchor_output=(self.config.mode == "training" and c is not None))
358
+ self.running = True
359
+ self.add_log("Custom input loaded.")
360
+ return sample
 
 
 
361
 
362
  def halt(self):
363
+ self.running = False
364
+ self.add_log("Halted.")
365
+
366
+ def snapshot(self):
367
+ return {
368
+ "config": asdict(self.config),
369
+ "running": self.running,
370
+ "iteration": self.iteration,
371
+ "current_error": self.current_error,
372
+ "current_loss": self.current_loss,
373
+ "last_action": self.last_action,
374
+ "cells": [c.to_dict() for c in self.cells],
375
+ "logs": self.logs,
376
+ "current_sample": self.current_sample,
377
+ "last_sample": self.last_sample,
378
+ "batch_remaining": len(self.batch_queue),
379
+ "loss_history": list(self.loss_history),
380
+ "error_history": list(self.error_history),
381
+ }
 
382
 
383
 
384
  engine = SimEngine()
385
 
386
 
387
+ def loop():
388
  while True:
389
+ try:
390
+ if engine.running:
391
+ engine.step()
392
+ except Exception as e:
393
+ with engine.lock:
394
+ engine.running = False
395
+ engine.add_log(f"Engine error: {e}")
396
  time.sleep(0.04)
397
 
398
 
399
+ threading.Thread(target=loop, daemon=True).start()
400
 
401
 
402
+ @app.get("/")
403
+ async def root():
404
+ return FileResponse(INDEX_PATH)
405
 
406
 
407
  @app.get("/state")
408
+ async def state():
409
+ return JSONResponse(engine.snapshot())
410
 
411
 
412
  @app.post("/config")
413
+ async def config(req: Request):
414
+ payload = await req.json()
415
+ engine.configure(payload)
416
+ return {"ok": True, "message": "Config applied."}
417
 
418
 
419
  @app.post("/example")
420
+ async def example(req: Request):
421
+ payload = await req.json()
422
+ family = payload.get("dataset_family", engine.config.dataset_family)
423
+ return engine.generate_example(family)
424
 
425
 
426
  @app.post("/generate_batch")
427
+ async def generate_batch(req: Request):
428
+ payload = await req.json()
429
+ count = int(payload.get("count", engine.config.batch_size))
430
  engine.start_batch(count)
431
+ return {"ok": True, "message": f"Batch started with {count} samples."}
432
 
433
 
434
  @app.post("/test_custom")
435
+ async def test_custom(req: Request):
436
+ payload = await req.json()
437
+ a = float(payload["a"])
438
+ b = float(payload["b"])
439
+ c_val = payload.get("c", None)
440
+ c = None if c_val in [None, "", "null"] else float(c_val)
441
+ engine.custom_input(a, b, c)
442
+ return {"ok": True, "message": "Custom input loaded."}
443
 
444
 
445
  @app.post("/halt")
446
  async def halt():
447
  engine.halt()
448
+ return {"ok": True, "message": "Engine halted."}
449
 
450
 
451
  if __name__ == "__main__":