theapemachine commited on
Commit
754cc24
·
verified ·
1 Parent(s): ed77837

Remove legacy: tensegrity/pipeline/iterative.py

Browse files
Files changed (1) hide show
  1. tensegrity/pipeline/iterative.py +0 -466
tensegrity/pipeline/iterative.py DELETED
@@ -1,466 +0,0 @@
1
- """
2
- Iterative cognitive scorer — LLM-free multi-pass settling over choices.
3
-
4
- Single-shot ScoringBridge encodes prompt once, settles NGC once per choice,
5
- fuses sentence + FHRR + NGC scores in one shot. The graft results show this
6
- behaves like an undifferentiated bias field.
7
-
8
- This iterative scorer instead runs an active-inference loop:
9
- 1. Encode prompt context, settle NGC, learn (ground the field).
10
- 2. Initialize uniform belief over choices.
11
- 3. For each iteration up to a budget:
12
- a. Score each choice via NGC free-energy under the *current* field state.
13
- b. Update beliefs by accumulating evidence (Bayesian-style log-odds).
14
- c. Take the leading choice's encoding, learn a small Hebbian step under
15
- it (modulation = belief mass), shaping the field toward that
16
- interpretation.
17
- d. Optionally retrieve from Hopfield with the leading encoding to inject
18
- memory pressure.
19
- e. Check convergence: top-1 belief mass > τ, or marginal change < ε.
20
- 4. Commit argmax.
21
-
22
- The LLM is absent. The cognitive layer alone resolves the choice.
23
- """
24
- from __future__ import annotations
25
-
26
- import re
27
- import logging
28
- from dataclasses import dataclass, field
29
- from typing import Any, Dict, List, Optional, Tuple
30
-
31
- import numpy as np
32
-
33
- logger = logging.getLogger(__name__)
34
-
35
-
36
- @dataclass
37
- class IterationTrace:
38
- iteration: int
39
- energies: List[float]
40
- sentence_sims: List[float]
41
- fhrr_sims: List[float]
42
- log_belief: List[float]
43
- belief: List[float]
44
- top_idx: int
45
- top_p: float
46
-
47
-
48
- @dataclass
49
- class IterativeResult:
50
- scores: List[float] # final fused scores per choice
51
- belief: List[float] # final belief vector per choice
52
- committed_idx: int
53
- iterations_used: int
54
- converged: bool
55
- trace: List[IterationTrace] = field(default_factory=list)
56
-
57
-
58
- class IterativeCognitiveScorer:
59
- """
60
- Multi-pass cognitive scorer over a UnifiedField.
61
-
62
- No LLM in the loop. Operates on prompt+choices via:
63
- - sbert sentence similarity (one-shot, doesn't change across iterations)
64
- - FHRR similarity (one-shot)
65
- - NGC free energy (recomputed each iteration as the field is shaped)
66
- - Hopfield retrieval (cumulative memory pressure across iterations)
67
- """
68
-
69
- def __init__(
70
- self,
71
- field=None,
72
- *,
73
- obs_dim: int = 256,
74
- hidden_dims: Optional[List[int]] = None,
75
- fhrr_dim: int = 2048,
76
- ngc_settle_steps: int = 30,
77
- ngc_learning_rate: float = 0.01,
78
- hopfield_beta: float = 0.05,
79
- # iteration controls
80
- max_iterations: int = 6,
81
- convergence_top_p: float = 0.75,
82
- convergence_delta: float = 1e-3,
83
- # context settling
84
- context_settle_steps: int = 40,
85
- choice_settle_steps: int = 25,
86
- context_learning_epochs: int = 3,
87
- # fusion weights (z-scored)
88
- w_sbert: float = 0.5,
89
- w_fhrr: float = 0.3,
90
- w_ngc: float = 1.0,
91
- w_falsify: float = 0.7,
92
- # belief update step
93
- belief_step: float = 0.6,
94
- # Hebbian shaping is now under the prompt context (not the leading choice),
95
- # so iteration deepens the prompt model rather than reinforcing the leader.
96
- shaping_lr_scale: float = 0.5,
97
- # Hopfield: store leading encoding each iteration; query each iteration
98
- use_hopfield: bool = True,
99
- hopfield_steps: int = 2,
100
- # Episodic memory persists across items in a session. At the start of
101
- # each item we retrieve past episodes whose context matches the current
102
- # prompt and use their stored chosen-answer FHRR vectors to bias the
103
- # current choices — the cross-item learning channel.
104
- use_episodic: bool = True,
105
- episodic_context_dim: int = 64,
106
- episodic_capacity: int = 4096,
107
- episodic_top_k: int = 8,
108
- # Default off: the simple "past-answer FHRR similarity" signal is too
109
- # noisy to help. The wiring (encode/retrieve) stays so smarter signals
110
- # can be plugged in here without re-plumbing.
111
- w_episodic: float = 0.0,
112
- # Minimum cosine match between query and episodic context to trust retrieval.
113
- episodic_ctx_sim_threshold: float = 0.5,
114
- # Seed for NGC `reinitialize` on `reset`; None chooses a random seed each time.
115
- reset_seed: Optional[int] = 12345,
116
- ):
117
- from tensegrity.engine.unified_field import UnifiedField
118
- from tensegrity.memory.episodic import EpisodicMemory
119
- self.field = field or UnifiedField(
120
- obs_dim=obs_dim,
121
- hidden_dims=hidden_dims or [128, 32],
122
- fhrr_dim=fhrr_dim,
123
- hopfield_beta=hopfield_beta,
124
- ngc_settle_steps=ngc_settle_steps,
125
- ngc_learning_rate=ngc_learning_rate,
126
- )
127
- self.max_iterations = max_iterations
128
- self.convergence_top_p = convergence_top_p
129
- self.convergence_delta = convergence_delta
130
- self.context_settle_steps = context_settle_steps
131
- self.choice_settle_steps = choice_settle_steps
132
- self.context_learning_epochs = context_learning_epochs
133
- self.w_sbert = w_sbert
134
- self.w_fhrr = w_fhrr
135
- self.w_ngc = w_ngc
136
- self.w_falsify = w_falsify
137
- self.belief_step = belief_step
138
- self.shaping_lr_scale = shaping_lr_scale
139
- self.use_hopfield = use_hopfield
140
- self.hopfield_steps = hopfield_steps
141
- self.use_episodic = use_episodic
142
- self.episodic_top_k = episodic_top_k
143
- self.w_episodic = w_episodic
144
- self.episodic_ctx_sim_threshold = episodic_ctx_sim_threshold
145
- self.reset_seed = reset_seed
146
- # Dirichlet-style per-channel reliability. Each channel accumulates a
147
- # pseudocount that grows when the channel's top-ranked choice matches
148
- # the committed belief on an item. Fusion weights = normalized counts.
149
- # Uniform prior of 1.0 means we start with equal trust; the system
150
- # discovers which channels are reliable for the current task on its own.
151
- self._channels = ["sbert", "fhrr", "ngc", "falsify", "hop", "episodic"]
152
- self._channel_counts: Dict[str, float] = {c: 1.0 for c in self._channels}
153
- self.episodic = EpisodicMemory(
154
- context_dim=episodic_context_dim,
155
- capacity=episodic_capacity,
156
- drift_rate=0.95,
157
- encoding_strength=0.3,
158
- ) if use_episodic else None
159
-
160
- # ---------- text helpers ----------
161
-
162
- def _tokenize(self, text: str, max_tokens: int = 48) -> List[str]:
163
- return re.findall(r"[a-zA-Z]+(?:'[a-z]+)?|[0-9]+(?:\.[0-9]+)?", text.lower())[-max_tokens:]
164
-
165
- def _encode(self, tokens: List[str]) -> np.ndarray:
166
- if not tokens:
167
- return np.ones(self.field.fhrr_dim, dtype=np.complex64)
168
- return self.field.encoder.encode_sequence(tokens)
169
-
170
- # ---------- one-shot signals (computed once per item) ----------
171
-
172
- def _sbert_similarities(self, prompt: str, choices: List[str]) -> List[float]:
173
- features = self.field.encoder.features
174
- getter = getattr(features, "get_sbert_model", None)
175
- sbert = getter() if callable(getter) else None
176
- if sbert is not None:
177
- embs = sbert.encode([prompt] + choices, show_progress_bar=False)
178
- pe = embs[0]
179
- pn = float(np.linalg.norm(pe))
180
- out = []
181
- for i in range(len(choices)):
182
- ce = embs[i + 1]
183
- cn = float(np.linalg.norm(ce))
184
- out.append(float(np.dot(pe, ce) / (pn * cn)) if pn > 1e-8 and cn > 1e-8 else 0.0)
185
- return out
186
- if self.field.encoder.semantic and callable(getter) and not getattr(
187
- self, "_sbert_unavailable_logged", False
188
- ):
189
- logger.warning("SBERT sentence similarity unavailable; using FHRR cosine similarity.")
190
- setattr(self, "_sbert_unavailable_logged", True)
191
- pf = self._encode(self._tokenize(prompt, 64))
192
- return [
193
- self.field.encoder.similarity(pf, self._encode(self._tokenize(c, 32)))
194
- for c in choices
195
- ]
196
-
197
- def _fhrr_similarities(self, prompt: str, choices: List[str]) -> List[float]:
198
- pf = self._encode(self._tokenize(prompt, 64))
199
- return [
200
- self.field.encoder.similarity(pf, self._encode(self._tokenize(c, 32)))
201
- for c in choices
202
- ]
203
-
204
- # ---------- iterative loop ----------
205
-
206
- def score(self, prompt: str, choices: List[str]) -> IterativeResult:
207
- n = len(choices)
208
- if n == 0:
209
- return IterativeResult(scores=[], belief=[], committed_idx=-1,
210
- iterations_used=0, converged=False)
211
-
212
- # 1. One-shot signals
213
- sbert_sims = np.asarray(self._sbert_similarities(prompt, choices), dtype=np.float64)
214
- fhrr_sims = np.asarray(self._fhrr_similarities(prompt, choices), dtype=np.float64)
215
-
216
- # 2. Encode + settle prompt context, learn it
217
- prompt_tokens = self._tokenize(prompt, max_tokens=64)
218
- for _ in range(max(1, self.context_learning_epochs)):
219
- ctx_obs = self.field._fhrr_to_obs(self._encode(prompt_tokens))
220
- self.field.ngc.settle(ctx_obs, steps=self.context_settle_steps)
221
- self.field.ngc.learn(modulation=1.0)
222
- base_state = self.field.ngc.save_state()
223
-
224
- # Pre-tokenize choice contexts (prompt+choice for joint settling)
225
- choice_token_lists = [self._tokenize(prompt + " " + c, 64) for c in choices]
226
- choice_obs = [self.field._fhrr_to_obs(self._encode(t)) for t in choice_token_lists]
227
- # Choice-only obs (for falsification: settle under choice alone, then predict prompt)
228
- choice_only_obs = [
229
- self.field._fhrr_to_obs(self._encode(self._tokenize(c, 32))) for c in choices
230
- ]
231
- choice_fhrr = [self._encode(self._tokenize(c, 32)) for c in choices]
232
- # Cache prompt observation vector for falsification target
233
- prompt_obs_vec = self.field._fhrr_to_obs(self._encode(prompt_tokens))
234
-
235
- # Episodic retrieval: project current prompt into context space and ask
236
- # the episodic store for similar past episodes. Each retrieved episode
237
- # carries the FHRR of the answer that won there. We bias current
238
- # choices by their similarity to those past winners, weighted by the
239
- # context match. This is the cross-item memory channel.
240
- episodic_bias = np.zeros(n, dtype=np.float64)
241
- if self.use_episodic and self.episodic is not None and len(self.episodic.episodes) > 0:
242
- uniform_belief = np.full(n, 1.0 / n, dtype=np.float64)
243
- try:
244
- query_ctx = self.episodic.compute_item_representation(
245
- prompt_obs_vec, uniform_belief
246
- )
247
- retrieved = self.episodic.retrieve_by_context(
248
- query_context=query_ctx, k=self.episodic_top_k
249
- )
250
- except Exception as e:
251
- logger.debug("episodic retrieval skipped: %s", e)
252
- retrieved = []
253
- if retrieved:
254
- # Real-valued unit-norm choice vectors (cached for reuse)
255
- ch_real = []
256
- for f in choice_fhrr:
257
- v = np.real(f).astype(np.float64)
258
- nrm = np.linalg.norm(v)
259
- ch_real.append(v / nrm if nrm > 1e-10 else v)
260
- # Only trust episodes whose prompt context strongly matches.
261
- # Below this threshold, "similar past answer" is noise, not signal.
262
- for ep in retrieved:
263
- ans_vec = ep.metadata.get("chosen_fhrr_real") if ep.metadata else None
264
- if ans_vec is None:
265
- continue
266
- ctx_sim = float(np.dot(query_ctx, ep.context_vector))
267
- if ctx_sim < self.episodic_ctx_sim_threshold:
268
- continue
269
- # Also discount by past surprise: episodes the agent struggled
270
- # with (low committed confidence) carry less authority.
271
- confidence = max(0.0, 1.0 - float(ep.surprise))
272
- weight = ctx_sim * confidence
273
- if weight <= 0:
274
- continue
275
- for i in range(n):
276
- episodic_bias[i] += weight * float(np.dot(ch_real[i], ans_vec))
277
-
278
- # 3. Initialize belief uniformly in log space
279
- log_belief = np.zeros(n, dtype=np.float64)
280
-
281
- trace: List[IterationTrace] = []
282
- prev_belief = np.ones(n) / n
283
- converged = False
284
- iterations_used = 0
285
- last_channel_scores: Dict[str, np.ndarray] = {}
286
-
287
- def znorm(a: np.ndarray) -> np.ndarray:
288
- s = a.std()
289
- return (a - a.mean()) / s if s > 1e-10 else np.zeros_like(a)
290
-
291
- for it in range(self.max_iterations):
292
- iterations_used = it + 1
293
-
294
- # 3a. Score each choice under current field state.
295
- # Two NGC signals:
296
- # energies: free energy of settling on (prompt+choice) jointly.
297
- # falsify: -prediction_error of (prompt | settled-on-choice-alone).
298
- # This asks "does this choice's state predict the prompt?"
299
- # — a real falsification operation, not a fit score.
300
- energies = np.zeros(n, dtype=np.float64)
301
- falsify = np.zeros(n, dtype=np.float64)
302
- for i in range(n):
303
- self.field.ngc.restore_state(base_state)
304
- r = self.field.ngc.settle(choice_obs[i], steps=self.choice_settle_steps)
305
- energies[i] = float(r["final_energy"])
306
-
307
- # Falsification: settle under choice-only, then ask the field
308
- # to predict the prompt observation. Higher prediction error =
309
- # this choice does a worse job of explaining the prompt.
310
- self.field.ngc.restore_state(base_state)
311
- self.field.ngc.settle(choice_only_obs[i], steps=self.choice_settle_steps)
312
- pe = self.field.ngc.prediction_error(prompt_obs_vec)
313
- falsify[i] = -float(pe)
314
- ngc_score = -energies
315
-
316
- # Hopfield bonus: similarity of choice FHRR to retrieved memory
317
- hop_bonus = np.zeros(n, dtype=np.float64)
318
- if self.use_hopfield and self.field.memory.n_patterns > 0:
319
- for i in range(n):
320
- q = np.real(choice_fhrr[i]).astype(np.float64)
321
- qn = np.linalg.norm(q)
322
- if qn < 1e-8:
323
- continue
324
- q = q / qn
325
- retrieved, _e = self.field.memory.retrieve(q, steps=self.hopfield_steps)
326
- rn = np.linalg.norm(retrieved)
327
- if rn > 1e-8:
328
- hop_bonus[i] = float(np.dot(q, retrieved / rn))
329
-
330
- # 3b. Fuse z-normalized
331
- # Normalized channel weights from accumulated reliability counts.
332
- total = sum(self._channel_counts.values())
333
- w = {c: self._channel_counts[c] / total for c in self._channels}
334
-
335
- channel_scores = {
336
- "sbert": znorm(sbert_sims),
337
- "fhrr": znorm(fhrr_sims),
338
- "ngc": znorm(ngc_score),
339
- "falsify": znorm(falsify),
340
- "hop": znorm(hop_bonus) if self.use_hopfield else np.zeros(n),
341
- "episodic": znorm(episodic_bias) if self.use_episodic else np.zeros(n),
342
- }
343
- fused = sum(w[c] * channel_scores[c] for c in self._channels)
344
- last_channel_scores = channel_scores
345
-
346
- # 3c. Accumulate evidence into log-belief
347
- log_belief = log_belief + self.belief_step * fused
348
- shifted = log_belief - log_belief.max()
349
- belief = np.exp(shifted)
350
- belief = belief / belief.sum() if belief.sum() > 0 else np.ones(n) / n
351
-
352
- top_idx = int(np.argmax(belief))
353
- top_p = float(belief[top_idx])
354
-
355
- trace.append(IterationTrace(
356
- iteration=it,
357
- energies=energies.tolist(),
358
- sentence_sims=sbert_sims.tolist(),
359
- fhrr_sims=fhrr_sims.tolist(),
360
- log_belief=log_belief.tolist(),
361
- belief=belief.tolist(),
362
- top_idx=top_idx,
363
- top_p=top_p,
364
- ))
365
-
366
- # 3d. Hebbian shaping under the PROMPT (not the leading choice).
367
- # This deepens the field's model of the question over iterations
368
- # without injecting a positive-feedback loop on the leader.
369
- self.field.ngc.restore_state(base_state)
370
- self.field.ngc.settle(prompt_obs_vec, steps=self.context_settle_steps)
371
- self.field.ngc.learn(modulation=self.shaping_lr_scale)
372
-
373
- # Re-base on the prompt-grounded state for next iteration's scoring
374
- base_state = self.field.ngc.save_state()
375
-
376
- # 3f. Convergence checks
377
- db = float(np.max(np.abs(belief - prev_belief)))
378
- prev_belief = belief
379
- if top_p >= self.convergence_top_p or db < self.convergence_delta:
380
- converged = True
381
- break
382
-
383
- # Store prompt encoding once for Hopfield cross-item memory (not each iteration).
384
- if self.use_hopfield:
385
- self.field.memory.store(self._encode(prompt_tokens))
386
-
387
- committed_idx = int(np.argmax(prev_belief))
388
-
389
- # Reliability update via *cross-channel agreement* (not agreement with
390
- # the committed belief — that would be self-fulfilling). Each channel
391
- # earns one pseudocount per OTHER active channel that picked the same
392
- # top choice. The consensus structure is the anchor; no single
393
- # channel is privileged. Channels tracking signal grow together;
394
- # noisy outliers don't.
395
- if last_channel_scores and n > 1:
396
- active = []
397
- for c in self._channels:
398
- cs = last_channel_scores.get(c)
399
- if cs is None:
400
- continue
401
- if not np.any(np.abs(cs) > 1e-12):
402
- continue
403
- active.append((c, int(np.argmax(cs))))
404
- for i, (c_i, top_i) in enumerate(active):
405
- agreements = sum(
406
- 1 for j, (_, top_j) in enumerate(active) if j != i and top_j == top_i
407
- )
408
- if agreements > 0:
409
- self._channel_counts[c_i] += float(agreements) / max(len(active) - 1, 1)
410
-
411
- # Episodic encoding: store the prompt context together with the FHRR
412
- # of the chosen answer, so future items can retrieve "what worked
413
- # last time on a similar prompt."
414
- if self.use_episodic and self.episodic is not None:
415
- top_p_final = float(prev_belief[committed_idx]) if n > 0 else 0.0
416
- chosen_real = np.real(choice_fhrr[committed_idx]).astype(np.float64)
417
- chosen_norm = np.linalg.norm(chosen_real)
418
- if chosen_norm > 1e-10:
419
- chosen_real = chosen_real / chosen_norm
420
- try:
421
- self.episodic.encode(
422
- observation=prompt_obs_vec,
423
- morton_code=np.zeros(1, dtype=np.int64),
424
- belief_state=np.asarray(prev_belief, dtype=np.float64),
425
- action=committed_idx,
426
- surprise=float(1.0 - top_p_final),
427
- free_energy=float(np.mean(energies) if n > 0 else 0.0),
428
- metadata={"chosen_fhrr_real": chosen_real},
429
- )
430
- except Exception as e:
431
- logger.debug("episodic encode skipped: %s", e)
432
-
433
- return IterativeResult(
434
- scores=log_belief.tolist(),
435
- belief=prev_belief.tolist(),
436
- committed_idx=committed_idx,
437
- iterations_used=iterations_used,
438
- converged=converged,
439
- trace=trace,
440
- )
441
-
442
- def reset(self):
443
- """Per-item reset. Clears NGC working state but PRESERVES Hopfield
444
- patterns and episodic memory — those carry across items in a session
445
- and provide cross-item learning.
446
-
447
- NGC weights are reinitialized using ``reset_seed``: default ``12345``
448
- matches legacy behavior for reproducibility; pass ``None`` for a random
449
- seed each reset, or any other integer to pin runs.
450
- """
451
- seed = self.reset_seed
452
- if seed is None:
453
- seed = int(np.random.randint(0, 2 ** 31))
454
- self.field.ngc.reinitialize(seed)
455
- self.field.energy_history.clear()
456
- self.field._step_count = 0
457
-
458
- def reset_session(self):
459
- """Full reset. Use at task / session boundaries to clear all memory
460
- and per-channel reliability priors (which are task-specific)."""
461
- self.reset()
462
- self.field.memory.clear()
463
- if self.episodic is not None:
464
- self.episodic.clear()
465
- for c in self._channels:
466
- self._channel_counts[c] = 1.0