everydaytok commited on
Commit
aa26b54
Β·
verified Β·
1 Parent(s): c8ba274

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -154
app.py CHANGED
@@ -12,6 +12,7 @@ DT = 0.12
12
  MICRO = 6
13
  SETTLE = 0.004
14
  CONV_THRESH = 0.02
 
15
 
16
 
17
  class SimEngine:
@@ -31,7 +32,7 @@ class SimEngine:
31
  self.current_error = 0.0
32
  self.current_prediction = 0.0
33
  self.history = []
34
- self.merge_map = {}
35
  self._init_mesh()
36
 
37
  # ── TOPOLOGY ──────────────────────────────────────────────────────────────
@@ -51,6 +52,7 @@ class SimEngine:
51
  self.current_error = 0.0
52
  self.current_prediction = 0.0
53
  self.history = []
 
54
  self.layers = self._build_layers()
55
  n = self.n_inputs
56
 
@@ -74,86 +76,51 @@ class SimEngine:
74
  self.springs[(f'B{d}', lid)] = round(random.uniform(0.85, 1.15), 4)
75
  self.springs[(lid, f'C{d}')] = round(random.uniform(0.85, 1.15), 4)
76
 
77
- # Structural merge: co-located nodes become shared vertices
78
- self.merge_map = self._compute_merge_map() if self.cross_connect else {}
79
- if self.merge_map:
80
- self._apply_merge()
81
 
82
- # ── NODE MERGE ────────────────────────────────────────────────────────────
83
 
84
- def _compute_merge_map(self):
85
  """
86
- The rightmost upper hidden node of dim d and the leftmost upper
87
- hidden node of dim d+1 are visually co-located β†’ one shared vertex.
88
- Same for lower. Only applies when β‰₯2 hidden nodes per side so the
89
- boundary node is distinct from the centre node.
90
- Returns {removed_id: canonical_id}.
 
 
 
 
 
91
  """
92
- mm = {}
93
- n = self.n_inputs
94
  if n < 2:
95
- return mm
96
  for d in range(1, n):
97
- if self.n_upper >= 2:
98
- mm[f'U{d+1}_1'] = f'U{d}_{self.n_upper}'
99
- if self.n_lower >= 2:
100
- mm[f'L{d+1}_1'] = f'L{d}_{self.n_lower}'
101
- return mm
102
 
103
- def _apply_merge(self):
104
- """
105
- Retarget all spring keys through merge_map and remove duplicate nodes.
106
- e.g. (A2, U2_1) β†’ (A2, U1_3). If two remapped keys collide
107
- (should not happen with this rule) their constants are averaged.
108
- """
109
- mm = self.merge_map
110
- new_springs = {}
111
- for (u, v), k in self.springs.items():
112
- key = (mm.get(u, u), mm.get(v, v))
113
- if key in new_springs:
114
- new_springs[key] = (new_springs[key] + k) / 2.0
115
- else:
116
- new_springs[key] = k
117
- self.springs = new_springs
118
-
119
- removed = set(mm.keys())
120
- for rid in removed:
121
- self.nodes.pop(rid, None)
122
- self.layers = [
123
- [nid for nid in layer if nid not in removed]
124
- for layer in self.layers
125
- ]
126
-
127
- # ── MERGE HELPERS ─────────────────────────────────────────────────────────
128
-
129
- def _resolve(self, nid):
130
- """Resolve a node ID to its canonical (possibly merged) ID."""
131
- return self.merge_map.get(nid, nid)
132
-
133
- def _spring(self, u, v):
134
- """Spring constant lookup with automatic merge-map resolution."""
135
- return self.springs[(self._resolve(u), self._resolve(v))]
136
 
137
  # ── CROSS CONNECT TOGGLE ──────────────────────────────────────────────────
138
 
139
  def toggle_cross_connect(self):
140
- """
141
- Toggle structural node merging ON/OFF.
142
- ON β†’ overlapping boundary hidden nodes become one shared vertex
143
- with springs to both neighbouring inputs/outputs.
144
- OFF β†’ fully independent parallel hourglasses (original behaviour).
145
- Rebuilds the mesh (topology change); spring values reset.
146
- """
147
  self.cross_connect = not self.cross_connect
148
  self.running = False
149
  self._init_mesh()
150
  self.logs = []
151
- ns = len(self.merge_map)
152
  if self.cross_connect:
153
  self.add_log(
154
- f"Cross-connect ON β€” {ns} shared "
155
- f"{'vertex' if ns == 1 else 'vertices'} "
156
- f"(structural merge, no extra springs)"
157
  )
158
  else:
159
  self.add_log("Cross-connect OFF β€” independent parallel hourglasses")
@@ -206,8 +173,9 @@ class SimEngine:
206
  for d in range(1, n+1):
207
  self.nodes[f'A{d}']['x'] = a_vec[d-1]
208
  self.nodes[f'B{d}']['x'] = b_vec[d-1]
 
209
  for nid, nd in self.nodes.items():
210
- if nid[0] in ('U', 'L'):
211
  nd['x'] = 0.0; nd['vel'] = 0.0
212
  c_vec = self._to_vec(c_target, n) if c_target is not None else None
213
  for d in range(1, n+1):
@@ -221,10 +189,6 @@ class SimEngine:
221
  c['x'] = 0.0
222
 
223
  # ── ELASTIC STEP ──────────────────────────────────────────────────────────
224
- # Forces are accumulated first, then integrated β€” clean slot for any
225
- # future force contributions. Merge-aware: all node lookups resolve
226
- # through merge_map so shared vertices accumulate forces from every
227
- # dimension that owns them.
228
 
229
  def _elastic_step(self, n_steps):
230
  alpha = self.back_alpha
@@ -234,46 +198,53 @@ class SimEngine:
234
  forces = {nid: 0.0 for nid, nd in self.nodes.items()
235
  if not nd['anchored']}
236
 
 
237
  for d in range(1, n+1):
238
  A_val = self.nodes[f'A{d}']['x']
239
  B_val = self.nodes[f'B{d}']['x']
240
  C_val = self.nodes[f'C{d}']['x']
241
 
242
  for j in range(1, self.n_upper+1):
243
- uid_raw = f'U{d}_{j}'
244
- uid = self._resolve(uid_raw)
245
- ak = self._spring(f'A{d}', uid_raw)
246
- f = FWD_K * (ak * A_val - self.nodes[uid]['x'])
247
  if alpha > 0:
248
- kuc = self._spring(uid_raw, f'C{d}')
249
  f += alpha * kuc * (C_val - self.nodes[uid]['x'])
250
- if uid in forces:
251
- forces[uid] += f
252
 
253
  for j in range(1, self.n_lower+1):
254
- lid_raw = f'L{d}_{j}'
255
- lid = self._resolve(lid_raw)
256
- bk = self._spring(f'B{d}', lid_raw)
257
- f = FWD_K * (bk * B_val - self.nodes[lid]['x'])
258
  if alpha > 0:
259
- klc = self._spring(lid_raw, f'C{d}')
260
  f += alpha * klc * (C_val - self.nodes[lid]['x'])
261
- if lid in forces:
262
- forces[lid] += f
263
 
264
  c = self.nodes[f'C{d}']
265
  if not c['anchored']:
266
  rest_c = (
267
- sum(self._spring(f'U{d}_{j}', f'C{d}') *
268
- self.nodes[self._resolve(f'U{d}_{j}')]['x']
269
  for j in range(1, self.n_upper+1)) +
270
- sum(self._spring(f'L{d}_{j}', f'C{d}') *
271
- self.nodes[self._resolve(f'L{d}_{j}')]['x']
272
  for j in range(1, self.n_lower+1))
273
  )
274
  forces[f'C{d}'] = forces.get(f'C{d}', 0.0) + \
275
  FWD_K * (rest_c - c['x'])
276
 
 
 
 
 
 
 
 
 
 
 
277
  max_v = 0.0
278
  for nid, f in forces.items():
279
  nd = self.nodes[nid]
@@ -284,10 +255,6 @@ class SimEngine:
284
  break
285
 
286
  # ── FEEDFORWARD ───────────────────────────────────────────────────────────
287
- # cross_connect=False β†’ analytic K*input (original behaviour)
288
- # cross_connect=True β†’ settled node positions used; shared vertices
289
- # already encode cross-dimensional mixing from
290
- # receiving forces from both neighbouring inputs.
291
 
292
  def _feedforward(self):
293
  n = self.n_inputs
@@ -299,31 +266,31 @@ class SimEngine:
299
  B_val = self.nodes[f'B{d}']['x']
300
 
301
  for j in range(1, self.n_upper+1):
302
- uid_raw = f'U{d}_{j}'
303
- uid = self._resolve(uid_raw)
304
- ff[uid_raw] = (self.nodes[uid]['x'] if self.cross_connect
305
- else self._spring(f'A{d}', uid_raw) * A_val)
 
306
 
307
  for j in range(1, self.n_lower+1):
308
- lid_raw = f'L{d}_{j}'
309
- lid = self._resolve(lid_raw)
310
- ff[lid_raw] = (self.nodes[lid]['x'] if self.cross_connect
311
- else self._spring(f'B{d}', lid_raw) * B_val)
312
 
313
  if self.architecture == 'multiplicative':
314
  nm = max(self.n_upper, self.n_lower)
315
  pred = 0.0
316
  for i in range(nm):
317
- uid_raw = f'U{d}_{(i % self.n_upper)+1}'
318
- lid_raw = f'L{d}_{(i % self.n_lower)+1}'
319
- ku = self._spring(uid_raw, f'C{d}')
320
- kl = self._spring(lid_raw, f'C{d}')
321
- pred += ku * ff[uid_raw] * kl * ff[lid_raw]
322
  else:
323
  pred = (
324
- sum(self._spring(f'U{d}_{j}', f'C{d}') * ff[f'U{d}_{j}']
325
  for j in range(1, self.n_upper+1)) +
326
- sum(self._spring(f'L{d}_{j}', f'C{d}') * ff[f'L{d}_{j}']
327
  for j in range(1, self.n_lower+1))
328
  )
329
  preds.append(pred)
@@ -331,10 +298,9 @@ class SimEngine:
331
  return preds, ff
332
 
333
  # ── LMS UPDATE ────────────────────────────────────────────────────────────
334
- # Each dimension's error drives its own spring gradients independently.
335
- # Shared vertices have springs from both dimensions updated separately
336
- # β€” the merge means those canonical spring keys already exist in
337
- # self.springs, so the updates land correctly with no special casing.
338
 
339
  def _lms_update(self, errors, ff):
340
  n = self.n_inputs
@@ -347,29 +313,25 @@ class SimEngine:
347
 
348
  if self.architecture == 'additive':
349
  for j in range(1, self.n_upper+1):
350
- uid_raw = f'U{d}_{j}'
351
- uid = self._resolve(uid_raw)
352
  ak_key = (f'A{d}', uid)
353
  uc_key = (uid, f'C{d}')
354
- grads[ak_key] = self._spring(uid_raw, f'C{d}') * A_val
355
- grads[uc_key] = self._spring(f'A{d}', uid_raw) * A_val
356
  for j in range(1, self.n_lower+1):
357
- lid_raw = f'L{d}_{j}'
358
- lid = self._resolve(lid_raw)
359
  bk_key = (f'B{d}', lid)
360
  lc_key = (lid, f'C{d}')
361
- grads[bk_key] = self._spring(lid_raw, f'C{d}') * B_val
362
- grads[lc_key] = self._spring(f'B{d}', lid_raw) * B_val
363
  else:
364
  nm = max(self.n_upper, self.n_lower)
365
  for i in range(nm):
366
- uid_raw = f'U{d}_{(i % self.n_upper)+1}'
367
- lid_raw = f'L{d}_{(i % self.n_lower)+1}'
368
- uid = self._resolve(uid_raw)
369
- lid = self._resolve(lid_raw)
370
- ku = self._spring(uid_raw, f'C{d}')
371
- kl = self._spring(lid_raw, f'C{d}')
372
- Uv = ff[uid_raw]; Lv = ff[lid_raw]
373
  grads[(f'A{d}', uid)] = ku * A_val * kl * Lv
374
  grads[(f'B{d}', lid)] = kl * B_val * ku * Uv
375
  grads[(uid, f'C{d}')] = Uv * kl * Lv
@@ -441,7 +403,7 @@ class SimEngine:
441
  def generate_batch(self, count=30):
442
  self.batch_queue.clear()
443
  n = self.n_inputs
444
- ns = len(self.merge_map)
445
  for _ in range(count):
446
  a_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
447
  b_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
@@ -450,7 +412,7 @@ class SimEngine:
450
  p = self.batch_queue.popleft()
451
  self.set_problem(p['a'], p['b'], p.get('c'))
452
  self.running = True
453
- tag = f'M{ns}' if self.cross_connect and ns else 'Β·'
454
  self.add_log(
455
  f"β–Ά {count} | {self.dataset_type} | "
456
  f"D={n} U{self.n_upper}Β·L{self.n_lower} [{tag}]"
@@ -477,29 +439,32 @@ async def get_ui():
477
  @app.get("/state")
478
  async def get_state():
479
  springs_out = {f"{u}β†’{v}": round(k, 5) for (u, v), k in engine.springs.items()}
 
480
  n = engine.n_inputs
481
- ns = len(engine.merge_map)
482
  return {
483
- 'nodes': engine.nodes,
484
- 'springs': springs_out,
485
- 'layers': engine.layers,
486
- 'error': engine.current_error,
487
- 'prediction': engine.current_prediction,
488
- 'predictions': [round(engine.nodes[f'C{d}']['x'], 4) for d in range(1, n+1)],
489
- 'iter': engine.iteration,
490
- 'logs': engine.logs,
491
- 'history': engine.history[-80:],
492
- 'running': engine.running,
493
- 'mode': engine.mode,
494
- 'architecture': engine.architecture,
495
- 'dataset_type': engine.dataset_type,
496
- 'n_inputs': n,
497
- 'n_upper': engine.n_upper,
498
- 'n_lower': engine.n_lower,
499
- 'back_alpha': engine.back_alpha,
500
- 'cross_connect': engine.cross_connect,
501
- 'n_shared': ns,
502
- 'queue_size': len(engine.batch_queue),
 
 
503
  }
504
 
505
 
@@ -518,7 +483,8 @@ async def toggle_cross():
518
  "ok": True,
519
  "cross_connect": engine.cross_connect,
520
  "n_springs": len(engine.springs),
521
- "n_shared": len(engine.merge_map),
 
522
  }
523
 
524
 
@@ -547,10 +513,10 @@ async def config(data: dict):
547
  engine.running = False
548
  engine._init_mesh()
549
  engine.logs = []
550
- ns = len(engine.merge_map)
551
  engine.add_log(
552
  f"Mesh rebuilt: D={new_ni} U{new_nu}Β·L{new_nl} "
553
- f"cross={'ON ('+str(ns)+' shared)' if engine.cross_connect else 'OFF'}"
554
  )
555
  else:
556
  engine.add_log(
@@ -570,13 +536,13 @@ async def set_layer(data: dict):
570
  elif layer == 'lower': engine.n_lower = max(1, min(16, engine.n_lower + delta))
571
  engine.running = False
572
  engine._init_mesh()
573
- ns = len(engine.merge_map)
574
  engine.add_log(
575
  f"Topology β†’ D={engine.n_inputs} U{engine.n_upper}Β·L{engine.n_lower} "
576
- f"cross={'ON ('+str(ns)+' shared)' if engine.cross_connect else 'OFF'}"
577
  )
578
  return {
579
- "ok": True,
580
  "n_inputs": engine.n_inputs,
581
  "n_upper": engine.n_upper,
582
  "n_lower": engine.n_lower,
 
12
  MICRO = 6
13
  SETTLE = 0.004
14
  CONV_THRESH = 0.02
15
+ BRIDGE_K = 0.20 # passive bridge spring constant β€” soft coupling, not learned
16
 
17
 
18
  class SimEngine:
 
32
  self.current_error = 0.0
33
  self.current_prediction = 0.0
34
  self.history = []
35
+ self.bridge_springs = {}
36
  self._init_mesh()
37
 
38
  # ── TOPOLOGY ──────────────────────────────────────────────────────────────
 
52
  self.current_error = 0.0
53
  self.current_prediction = 0.0
54
  self.history = []
55
+ self.bridge_springs = {}
56
  self.layers = self._build_layers()
57
  n = self.n_inputs
58
 
 
76
  self.springs[(f'B{d}', lid)] = round(random.uniform(0.85, 1.15), 4)
77
  self.springs[(lid, f'C{d}')] = round(random.uniform(0.85, 1.15), 4)
78
 
79
+ if self.cross_connect:
80
+ self._add_bridge_nodes()
 
 
81
 
82
+ # ── BRIDGE NODES ──────────────────────────────────────────────────────────
83
 
84
+ def _add_bridge_nodes(self):
85
  """
86
+ For each adjacent dimension pair (d, d+1), insert two passive bridge
87
+ vertices:
88
+ XU{d} β€” sits between U{d}_{n_upper} and U{d+1}_1 (upper hidden layer)
89
+ XL{d} β€” sits between L{d}_{n_lower} and L{d+1}_1 (lower hidden layer)
90
+
91
+ Bridge springs are FIXED at BRIDGE_K and never touched by LMS.
92
+ Each bridge node settles to a position driven by its two neighbours,
93
+ and in turn exerts a soft reaction pull on those neighbours β€” creating
94
+ a passive physical information channel between dimensions without
95
+ interfering with per-dimension gradient descent.
96
  """
97
+ n = self.n_inputs
 
98
  if n < 2:
99
+ return
100
  for d in range(1, n):
101
+ xuid = f'XU{d}'
102
+ self.nodes[xuid] = {'x': 0.0, 'vel': 0.0, 'anchored': False}
103
+ self.bridge_springs[(xuid, f'U{d}_{self.n_upper}')] = BRIDGE_K
104
+ self.bridge_springs[(xuid, f'U{d+1}_1')] = BRIDGE_K
 
105
 
106
+ xlid = f'XL{d}'
107
+ self.nodes[xlid] = {'x': 0.0, 'vel': 0.0, 'anchored': False}
108
+ self.bridge_springs[(xlid, f'L{d}_{self.n_lower}')] = BRIDGE_K
109
+ self.bridge_springs[(xlid, f'L{d+1}_1')] = BRIDGE_K
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  # ── CROSS CONNECT TOGGLE ──────────────────────────────────────────────────
112
 
113
  def toggle_cross_connect(self):
 
 
 
 
 
 
 
114
  self.cross_connect = not self.cross_connect
115
  self.running = False
116
  self._init_mesh()
117
  self.logs = []
118
+ nb = len(self.bridge_springs) // 2 # each bridge has 2 springs
119
  if self.cross_connect:
120
  self.add_log(
121
+ f"Cross-connect ON β€” {nb} passive bridge "
122
+ f"{'vertex' if nb == 1 else 'vertices'} "
123
+ f"(k={BRIDGE_K}, not learned)"
124
  )
125
  else:
126
  self.add_log("Cross-connect OFF β€” independent parallel hourglasses")
 
173
  for d in range(1, n+1):
174
  self.nodes[f'A{d}']['x'] = a_vec[d-1]
175
  self.nodes[f'B{d}']['x'] = b_vec[d-1]
176
+ # Reset all hidden + bridge nodes
177
  for nid, nd in self.nodes.items():
178
+ if nid[0] in ('U', 'L', 'X'):
179
  nd['x'] = 0.0; nd['vel'] = 0.0
180
  c_vec = self._to_vec(c_target, n) if c_target is not None else None
181
  for d in range(1, n+1):
 
189
  c['x'] = 0.0
190
 
191
  # ── ELASTIC STEP ──────────────────────────────────────────────────────────
 
 
 
 
192
 
193
  def _elastic_step(self, n_steps):
194
  alpha = self.back_alpha
 
198
  forces = {nid: 0.0 for nid, nd in self.nodes.items()
199
  if not nd['anchored']}
200
 
201
+ # Standard hourglass forces (unchanged β€” no merge resolution needed)
202
  for d in range(1, n+1):
203
  A_val = self.nodes[f'A{d}']['x']
204
  B_val = self.nodes[f'B{d}']['x']
205
  C_val = self.nodes[f'C{d}']['x']
206
 
207
  for j in range(1, self.n_upper+1):
208
+ uid = f'U{d}_{j}'
209
+ ak = self.springs[(f'A{d}', uid)]
210
+ f = FWD_K * (ak * A_val - self.nodes[uid]['x'])
 
211
  if alpha > 0:
212
+ kuc = self.springs[(uid, f'C{d}')]
213
  f += alpha * kuc * (C_val - self.nodes[uid]['x'])
214
+ forces[uid] += f
 
215
 
216
  for j in range(1, self.n_lower+1):
217
+ lid = f'L{d}_{j}'
218
+ bk = self.springs[(f'B{d}', lid)]
219
+ f = FWD_K * (bk * B_val - self.nodes[lid]['x'])
 
220
  if alpha > 0:
221
+ klc = self.springs[(lid, f'C{d}')]
222
  f += alpha * klc * (C_val - self.nodes[lid]['x'])
223
+ forces[lid] += f
 
224
 
225
  c = self.nodes[f'C{d}']
226
  if not c['anchored']:
227
  rest_c = (
228
+ sum(self.springs[(f'U{d}_{j}', f'C{d}')] *
229
+ self.nodes[f'U{d}_{j}']['x']
230
  for j in range(1, self.n_upper+1)) +
231
+ sum(self.springs[(f'L{d}_{j}', f'C{d}')] *
232
+ self.nodes[f'L{d}_{j}']['x']
233
  for j in range(1, self.n_lower+1))
234
  )
235
  forces[f'C{d}'] = forces.get(f'C{d}', 0.0) + \
236
  FWD_K * (rest_c - c['x'])
237
 
238
+ # Bridge forces β€” symmetric Hooke's law, passive (not learned)
239
+ # Bridge node pulled toward both neighbours; each neighbour gets
240
+ # an equal-and-opposite soft reaction pull toward the bridge.
241
+ for (bnode, peer), k in self.bridge_springs.items():
242
+ xb = self.nodes[bnode]['x']
243
+ xp = self.nodes[peer]['x']
244
+ spring_f = FWD_K * k * (xp - xb)
245
+ if bnode in forces: forces[bnode] += spring_f
246
+ if peer in forces: forces[peer] -= spring_f # weak reaction
247
+
248
  max_v = 0.0
249
  for nid, f in forces.items():
250
  nd = self.nodes[nid]
 
255
  break
256
 
257
  # ── FEEDFORWARD ───────────────────────────────────────────────────────────
 
 
 
 
258
 
259
  def _feedforward(self):
260
  n = self.n_inputs
 
266
  B_val = self.nodes[f'B{d}']['x']
267
 
268
  for j in range(1, self.n_upper+1):
269
+ uid = f'U{d}_{j}'
270
+ # With cross_connect the node's settled position already
271
+ # encodes the soft bridge influence β€” use it directly.
272
+ ff[uid] = (self.nodes[uid]['x'] if self.cross_connect
273
+ else self.springs[(f'A{d}', uid)] * A_val)
274
 
275
  for j in range(1, self.n_lower+1):
276
+ lid = f'L{d}_{j}'
277
+ ff[lid] = (self.nodes[lid]['x'] if self.cross_connect
278
+ else self.springs[(f'B{d}', lid)] * B_val)
 
279
 
280
  if self.architecture == 'multiplicative':
281
  nm = max(self.n_upper, self.n_lower)
282
  pred = 0.0
283
  for i in range(nm):
284
+ uid = f'U{d}_{(i % self.n_upper)+1}'
285
+ lid = f'L{d}_{(i % self.n_lower)+1}'
286
+ ku = self.springs[(uid, f'C{d}')]
287
+ kl = self.springs[(lid, f'C{d}')]
288
+ pred += ku * ff[uid] * kl * ff[lid]
289
  else:
290
  pred = (
291
+ sum(self.springs[(f'U{d}_{j}', f'C{d}')] * ff[f'U{d}_{j}']
292
  for j in range(1, self.n_upper+1)) +
293
+ sum(self.springs[(f'L{d}_{j}', f'C{d}')] * ff[f'L{d}_{j}']
294
  for j in range(1, self.n_lower+1))
295
  )
296
  preds.append(pred)
 
298
  return preds, ff
299
 
300
  # ── LMS UPDATE ────────────────────────────────────────────────────────────
301
+ # Bridge springs are never in self.springs so they are never touched here.
302
+ # Each dimension's LMS update is fully independent β€” no cross-gradient
303
+ # bleed, no error inflation.
 
304
 
305
  def _lms_update(self, errors, ff):
306
  n = self.n_inputs
 
313
 
314
  if self.architecture == 'additive':
315
  for j in range(1, self.n_upper+1):
316
+ uid = f'U{d}_{j}'
 
317
  ak_key = (f'A{d}', uid)
318
  uc_key = (uid, f'C{d}')
319
+ grads[ak_key] = self.springs[uc_key] * A_val
320
+ grads[uc_key] = self.springs[ak_key] * A_val
321
  for j in range(1, self.n_lower+1):
322
+ lid = f'L{d}_{j}'
 
323
  bk_key = (f'B{d}', lid)
324
  lc_key = (lid, f'C{d}')
325
+ grads[bk_key] = self.springs[lc_key] * B_val
326
+ grads[lc_key] = self.springs[bk_key] * B_val
327
  else:
328
  nm = max(self.n_upper, self.n_lower)
329
  for i in range(nm):
330
+ uid = f'U{d}_{(i % self.n_upper)+1}'
331
+ lid = f'L{d}_{(i % self.n_lower)+1}'
332
+ ku = self.springs[(uid, f'C{d}')]
333
+ kl = self.springs[(lid, f'C{d}')]
334
+ Uv = ff[uid]; Lv = ff[lid]
 
 
335
  grads[(f'A{d}', uid)] = ku * A_val * kl * Lv
336
  grads[(f'B{d}', lid)] = kl * B_val * ku * Uv
337
  grads[(uid, f'C{d}')] = Uv * kl * Lv
 
403
  def generate_batch(self, count=30):
404
  self.batch_queue.clear()
405
  n = self.n_inputs
406
+ nb = len(self.bridge_springs) // 2
407
  for _ in range(count):
408
  a_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
409
  b_vec = [round(random.uniform(1.0, 10.0), 2) for _ in range(n)]
 
412
  p = self.batch_queue.popleft()
413
  self.set_problem(p['a'], p['b'], p.get('c'))
414
  self.running = True
415
+ tag = f'B{nb}' if self.cross_connect and nb else 'Β·'
416
  self.add_log(
417
  f"β–Ά {count} | {self.dataset_type} | "
418
  f"D={n} U{self.n_upper}Β·L{self.n_lower} [{tag}]"
 
439
  @app.get("/state")
440
  async def get_state():
441
  springs_out = {f"{u}β†’{v}": round(k, 5) for (u, v), k in engine.springs.items()}
442
+ bridge_out = {f"{u}β†’{v}": round(k, 5) for (u, v), k in engine.bridge_springs.items()}
443
  n = engine.n_inputs
444
+ nb = len(engine.bridge_springs) // 2
445
  return {
446
+ 'nodes': engine.nodes,
447
+ 'springs': springs_out,
448
+ 'bridge_springs': bridge_out,
449
+ 'layers': engine.layers,
450
+ 'error': engine.current_error,
451
+ 'prediction': engine.current_prediction,
452
+ 'predictions': [round(engine.nodes[f'C{d}']['x'], 4) for d in range(1, n+1)],
453
+ 'iter': engine.iteration,
454
+ 'logs': engine.logs,
455
+ 'history': engine.history[-80:],
456
+ 'running': engine.running,
457
+ 'mode': engine.mode,
458
+ 'architecture': engine.architecture,
459
+ 'dataset_type': engine.dataset_type,
460
+ 'n_inputs': n,
461
+ 'n_upper': engine.n_upper,
462
+ 'n_lower': engine.n_lower,
463
+ 'back_alpha': engine.back_alpha,
464
+ 'cross_connect': engine.cross_connect,
465
+ 'n_bridges': nb,
466
+ 'bridge_k': BRIDGE_K,
467
+ 'queue_size': len(engine.batch_queue),
468
  }
469
 
470
 
 
483
  "ok": True,
484
  "cross_connect": engine.cross_connect,
485
  "n_springs": len(engine.springs),
486
+ "n_bridges": len(engine.bridge_springs) // 2,
487
+ "bridge_k": BRIDGE_K,
488
  }
489
 
490
 
 
513
  engine.running = False
514
  engine._init_mesh()
515
  engine.logs = []
516
+ nb = len(engine.bridge_springs) // 2
517
  engine.add_log(
518
  f"Mesh rebuilt: D={new_ni} U{new_nu}Β·L{new_nl} "
519
+ f"cross={'ON ('+str(nb)+' bridges)' if engine.cross_connect else 'OFF'}"
520
  )
521
  else:
522
  engine.add_log(
 
536
  elif layer == 'lower': engine.n_lower = max(1, min(16, engine.n_lower + delta))
537
  engine.running = False
538
  engine._init_mesh()
539
+ nb = len(engine.bridge_springs) // 2
540
  engine.add_log(
541
  f"Topology β†’ D={engine.n_inputs} U{engine.n_upper}Β·L{engine.n_lower} "
542
+ f"cross={'ON ('+str(nb)+' bridges)' if engine.cross_connect else 'OFF'}"
543
  )
544
  return {
545
+ "ok": True,
546
  "n_inputs": engine.n_inputs,
547
  "n_upper": engine.n_upper,
548
  "n_lower": engine.n_lower,