theapemachine commited on
Commit
46410a2
·
verified ·
1 Parent(s): 27b2258

Remove: tensegrity/legacy/v1/agent.py

Browse files
Files changed (1) hide show
  1. tensegrity/legacy/v1/agent.py +0 -497
tensegrity/legacy/v1/agent.py DELETED
@@ -1,497 +0,0 @@
1
- """
2
- TensegrityAgent: The complete cognitive architecture.
3
-
4
- Integrates all components into a single agent that:
5
- 1. Receives modality-agnostic observations (Morton-encoded)
6
- 2. Updates beliefs via free energy minimization (no gradients)
7
- 3. Maintains three memory systems (epistemic, episodic, associative)
8
- 4. Runs competing causal models in the arena
9
- 5. Selects actions that minimize expected free energy
10
- 6. Generates epistemic actions to resolve model uncertainty
11
-
12
- The name "Tensegrity" comes from the architectural principle where
13
- structural integrity comes from the balance of tension and compression.
14
- Here, the system's cognitive integrity comes from the tension between
15
- competing causal models (compression = model evidence, tension = model
16
- disagreement) balanced by the free energy principle.
17
- """
18
-
19
- import hashlib
20
- import inspect
21
- import numpy as np
22
- from typing import Optional, Dict, List, Any, Tuple
23
- import logging
24
-
25
- from tensegrity.legacy.v1.morton import MortonEncoder
26
- from tensegrity.legacy.v1.blanket import MarkovBlanket
27
- from tensegrity.memory.epistemic import EpistemicMemory
28
- from tensegrity.memory.episodic import EpisodicMemory
29
- from tensegrity.memory.associative import AssociativeMemory
30
- from tensegrity.causal.arena import CausalArena
31
- from tensegrity.causal.scm import StructuralCausalModel
32
- from tensegrity.inference.free_energy import FreeEnergyEngine
33
- from tensegrity.engine.unified_field import UnifiedField
34
-
35
- logger = logging.getLogger(__name__)
36
-
37
- # Default SCM registered in ``_init_default_models`` whose observation vector includes ``cause``.
38
- DEFAULT_MEDIATED_SCM_NAME = "mediated_causal"
39
-
40
-
41
- class TensegrityAgent:
42
- """
43
- A non-gradient cognitive agent.
44
-
45
- The agent perceives the world through Morton-coded observations,
46
- maintains beliefs via Bayesian updates, resolves competing causal
47
- explanations in an adversarial arena, and acts to minimize
48
- expected free energy.
49
-
50
- No backpropagation. No gradient descent. No optimizer state.
51
-
52
- All learning is:
53
- - Dirichlet counting (epistemic memory)
54
- - Context drift (episodic memory)
55
- - Energy minimization via Hopfield dynamics (associative memory)
56
- - Bayesian model comparison (causal arena)
57
- - Fixed-point iteration (belief propagation)
58
- """
59
-
60
- def __init__(self,
61
- n_states: int = 16,
62
- n_observations: int = 32,
63
- n_actions: int = 4,
64
- sensory_dims: int = 4,
65
- sensory_bits: int = 8,
66
- context_dim: int = 64,
67
- associative_dim: int = 128,
68
- planning_horizon: int = 3,
69
- precision: float = 4.0,
70
- zipf_exponent: float = 1.0,
71
- unified_obs_dim: int = 256,
72
- unified_hidden_dims: Optional[List[int]] = None,
73
- unified_fhrr_dim: int = 2048,
74
- unified_hopfield_beta: float = 0.01,
75
- unified_ngc_settle_steps: int = 20,
76
- unified_ngc_learning_rate: float = 0.005,
77
- epistemic_tension_threshold: float = 0.5,
78
- epistemic_info_gain_threshold: float = 0.1):
79
- """
80
- Args:
81
- n_states: Number of hidden states in the generative model
82
- n_observations: Number of observation categories
83
- n_actions: Number of possible actions
84
- sensory_dims: Dimensionality of raw sensory input
85
- sensory_bits: Bits per dimension for Morton encoding
86
- context_dim: Dimensionality of episodic context vectors
87
- associative_dim: Dimensionality of associative memory patterns
88
- planning_horizon: How far ahead to plan
89
- precision: Inverse temperature for policy selection
90
- zipf_exponent: Controls power-law memory access
91
- unified_obs_dim: Observation layer width for UnifiedField (default matches prior hardcoded wiring)
92
- unified_hidden_dims: NGC hidden layer sizes; defaults to ``[128, 32]`` when None
93
- unified_fhrr_dim: FHRR encoder dimensionality
94
- unified_hopfield_beta: Hopfield inverse temperature in UnifiedField
95
- unified_ngc_settle_steps: NGC settling iterations
96
- unified_ngc_learning_rate: Hebbian learning rate inside UnifiedField
97
- epistemic_tension_threshold: Only run costly intervention search when causal tension exceeds this level
98
- epistemic_info_gain_threshold: Minimum estimated information gain required for epistemic actions
99
- """
100
- def _req_pos_int(name: str, v: Any) -> int:
101
- if not isinstance(v, int) or int(v) < 1:
102
- raise ValueError(f"{name} must be a positive integer")
103
- return int(v)
104
-
105
- n_states = _req_pos_int("n_states", n_states)
106
- n_observations = _req_pos_int("n_observations", n_observations)
107
- n_actions = _req_pos_int("n_actions", n_actions)
108
- sensory_dims = _req_pos_int("sensory_dims", sensory_dims)
109
- sensory_bits = _req_pos_int("sensory_bits", sensory_bits)
110
- context_dim = _req_pos_int("context_dim", context_dim)
111
- associative_dim = _req_pos_int("associative_dim", associative_dim)
112
- if not isinstance(planning_horizon, int) or planning_horizon < 1:
113
- raise ValueError("planning_horizon must be a positive integer")
114
- if precision < 0.0:
115
- raise ValueError("precision must be non-negative")
116
- if zipf_exponent < 0.0:
117
- raise ValueError("zipf_exponent must be non-negative")
118
- unified_obs_dim = _req_pos_int("unified_obs_dim", unified_obs_dim)
119
- if unified_hidden_dims is not None:
120
- if not isinstance(unified_hidden_dims, list) or any(
121
- not isinstance(x, int) or x < 1 for x in unified_hidden_dims
122
- ):
123
- raise ValueError("unified_hidden_dims must be a list of positive integers")
124
- unified_fhrr_dim = _req_pos_int("unified_fhrr_dim", unified_fhrr_dim)
125
- if unified_hopfield_beta < 0.0:
126
- raise ValueError("unified_hopfield_beta must be non-negative")
127
- unified_ngc_settle_steps = _req_pos_int("unified_ngc_settle_steps", unified_ngc_settle_steps)
128
- if unified_ngc_learning_rate < 0.0:
129
- raise ValueError("unified_ngc_learning_rate must be non-negative")
130
- if not (0.0 <= float(epistemic_tension_threshold) <= 1.0):
131
- raise ValueError("epistemic_tension_threshold must be in [0, 1]")
132
- if not (0.0 <= float(epistemic_info_gain_threshold) <= 1.0):
133
- raise ValueError("epistemic_info_gain_threshold must be in [0, 1]")
134
-
135
- self.n_states = n_states
136
- self.n_obs = n_observations
137
- self.n_actions = n_actions
138
-
139
- # === SENSORY INTERFACE (Markov Blanket) ===
140
- self.encoder = MortonEncoder(n_dims=sensory_dims, bits_per_dim=sensory_bits)
141
- self.blanket = MarkovBlanket(
142
- encoder=self.encoder,
143
- n_sensory_channels=1,
144
- n_active_channels=1,
145
- observation_buffer_size=256
146
- )
147
-
148
- # === MEMORY SYSTEMS ===
149
- self.epistemic = EpistemicMemory(
150
- n_states=n_states,
151
- n_observations=n_observations,
152
- n_actions=n_actions,
153
- zipf_exponent=zipf_exponent
154
- )
155
-
156
- self.episodic = EpisodicMemory(
157
- context_dim=context_dim,
158
- capacity=10000,
159
- drift_rate=0.95,
160
- encoding_strength=0.3,
161
- zipf_exponent=zipf_exponent
162
- )
163
-
164
- self.associative = AssociativeMemory(
165
- pattern_dim=associative_dim,
166
- beta=precision,
167
- max_patterns=5000,
168
- zipf_exponent=zipf_exponent
169
- )
170
-
171
- # === INFERENCE ENGINE ===
172
- self.engine = FreeEnergyEngine(
173
- n_states=n_states,
174
- n_observations=n_observations,
175
- n_actions=n_actions,
176
- planning_horizon=planning_horizon,
177
- precision=precision,
178
- policy_depth=min(planning_horizon, 3)
179
- )
180
-
181
- # === CAUSAL ARENA ===
182
- self.arena = CausalArena(
183
- prior_concentration=1.0,
184
- falsification_threshold=-100.0,
185
- min_models=2
186
- )
187
-
188
- # === AGENT STATE ===
189
- self._step_count = 0
190
- self._total_surprise = 0.0
191
- self._total_free_energy = 0.0
192
- self._prev_belief_for_transition: Optional[np.ndarray] = None
193
- self._pending_action: Optional[int] = None
194
- self._pending_action_confidence: float = 0.0
195
- self._last_action_distribution: Optional[np.ndarray] = None
196
- self.epistemic_tension_threshold = float(epistemic_tension_threshold)
197
- self.epistemic_info_gain_threshold = float(epistemic_info_gain_threshold)
198
-
199
- # Initialize with default competing models
200
- self._init_default_models()
201
-
202
- u_hidden = unified_hidden_dims if unified_hidden_dims is not None else [128, 32]
203
- # Single perceptual substrate: FHRR → NGC → Hopfield (replaces parallel Morton-sense path).
204
- self.field = UnifiedField(
205
- obs_dim=unified_obs_dim,
206
- hidden_dims=u_hidden,
207
- fhrr_dim=unified_fhrr_dim,
208
- hopfield_beta=unified_hopfield_beta,
209
- ngc_settle_steps=unified_ngc_settle_steps,
210
- ngc_learning_rate=unified_ngc_learning_rate,
211
- )
212
-
213
- def _init_default_models(self):
214
- """
215
- Initialize the causal arena with default competing models.
216
-
217
- We start with two models that represent competing hypotheses
218
- about the causal structure of observations:
219
- Model A: "States cause observations directly" (simple)
220
- Model B: "States mediate between hidden causes and observations" (complex)
221
- """
222
- # Model A: Simple — direct state-observation link
223
- model_a = StructuralCausalModel(name="direct_causal")
224
- model_a.add_variable("state", n_values=self.n_states)
225
- model_a.add_variable("observation", n_values=self.n_obs,
226
- parents=["state"])
227
-
228
- # Model B: Mediated — hidden cause → state → observation
229
- model_b = StructuralCausalModel(name=DEFAULT_MEDIATED_SCM_NAME)
230
- model_b.add_variable("cause", n_values=self.n_states)
231
- model_b.add_variable("state", n_values=self.n_states,
232
- parents=["cause"])
233
- model_b.add_variable("observation", n_values=self.n_obs,
234
- parents=["state"])
235
-
236
- self.arena.register_model(model_a)
237
- self.arena.register_model(model_b)
238
-
239
- def _morton_to_obs_index(self, morton_codes: np.ndarray) -> int:
240
- """Map Morton codes to a discrete observation index (legacy hashing).
241
-
242
- The main ``perceive`` path fingerprints the unified observation vector
243
- with SHA-256 modulo ``n_obs``; use this routine only where an explicit
244
- Morton-code → observation-bin mapping is intentional.
245
- """
246
- if self.n_obs <= 0:
247
- raise ValueError(
248
- "n_observations must be a positive integer for _morton_to_obs_index mapping"
249
- )
250
- if isinstance(morton_codes, (int, np.integer)):
251
- return int(morton_codes) % self.n_obs
252
- # For multiple codes, hash the combination
253
- combined = 0
254
- for code in morton_codes:
255
- combined ^= int(code)
256
- return combined % self.n_obs
257
-
258
- def _obs_to_associative_pattern(self, observation: int,
259
- belief_state: np.ndarray) -> np.ndarray:
260
- """Project observation + belief into associative memory space."""
261
- rng = np.random.RandomState(observation)
262
-
263
- # Combine observation (one-hot) and belief state
264
- obs_vec = np.zeros(self.n_obs)
265
- obs_vec[observation] = 1.0
266
- combined = np.concatenate([obs_vec, belief_state])
267
-
268
- # Random projection to associative_dim
269
- W = rng.randn(self.associative.dim, len(combined)) / np.sqrt(len(combined))
270
- pattern = W @ combined
271
- norm = np.linalg.norm(pattern)
272
- if norm > 0:
273
- pattern /= norm
274
- return pattern
275
-
276
- def perceive(self, raw_observation: np.ndarray) -> Dict[str, Any]:
277
- """
278
- One perception path: numeric vector → UnifiedField (FHRR / NGC / Hopfield)
279
- → discrete observation index → active inference engine → causal arena.
280
-
281
- Episodic and classical Hopfield associative traces are not written here;
282
- memory consolidation for this path lives inside UnifiedField.
283
- """
284
- self._step_count += 1
285
- raw = np.asarray(raw_observation, dtype=np.float64).ravel()
286
-
287
- cycle = self.field.observe(raw, input_type="numeric")
288
- obs_vec = cycle["observation"]
289
- decomp = cycle["energy"]
290
- surprise = float(decomp.surprise)
291
-
292
- # Integer-safe deterministic index from observation vector (avoid float dot overflow)
293
- h = hashlib.sha256(obs_vec.astype(np.float64, copy=False).tobytes()).digest()
294
- obs_idx = int.from_bytes(h[:8], byteorder="big", signed=False) % max(self.n_obs, 1)
295
-
296
- A = self.epistemic.A
297
- B = self.epistemic.B
298
- C = self.epistemic.C
299
- D = self.epistemic.D
300
- log_A = self.epistemic.log_A
301
-
302
- # Capture the action that actually led into this transition before
303
- # ``engine.step`` samples the next action for the current state.
304
- previous_action = self.engine.prev_action
305
- inference_result = self.engine.step(obs_idx, A, B, C, D, log_A)
306
- q_states = inference_result["belief_state"]
307
- F = float(inference_result["free_energy"])
308
-
309
- self._pending_action = int(inference_result["action"])
310
- self._pending_action_confidence = float(inference_result["action_confidence"])
311
-
312
- self.epistemic.update_likelihood(obs_idx, q_states)
313
- if (previous_action is not None
314
- and self._prev_belief_for_transition is not None):
315
- self.epistemic.update_transition(
316
- self._prev_belief_for_transition, q_states,
317
- previous_action)
318
- self._prev_belief_for_transition = q_states.copy()
319
-
320
- causal_obs = {
321
- "state": int(np.argmax(q_states)),
322
- "observation": obs_idx,
323
- }
324
- if DEFAULT_MEDIATED_SCM_NAME in self.arena.models:
325
- causal_obs["cause"] = int(np.argmax(q_states))
326
-
327
- arena_result = self.arena.compete(causal_obs)
328
-
329
- obs_codes = np.array([obs_idx], dtype=np.int64)
330
- self.blanket.surprise = surprise
331
-
332
- # Keep all memory systems live on the unified perception path. Earlier
333
- # versions updated only the UnifiedField's internal Hopfield bank, which
334
- # left experience replay and agent introspection effectively empty.
335
- assoc_pattern = self._obs_to_associative_pattern(obs_idx, q_states)
336
- self.associative.store(
337
- assoc_pattern,
338
- metadata={"step": self._step_count, "obs_idx": obs_idx, "free_energy": F},
339
- )
340
- self.episodic.encode(
341
- observation=raw,
342
- morton_code=obs_codes,
343
- belief_state=q_states,
344
- action=self._pending_action,
345
- surprise=surprise,
346
- free_energy=F,
347
- metadata={
348
- "obs_idx": obs_idx,
349
- "field_energy": float(decomp.total),
350
- "memory_similarity": float(cycle.get("memory_similarity", 0.0)),
351
- },
352
- )
353
-
354
- self._total_surprise += surprise
355
- self._total_free_energy += F
356
-
357
- return {
358
- "step": self._step_count,
359
- "obs_codes": obs_codes,
360
- "observation_index": obs_idx,
361
- "belief_state": q_states,
362
- "free_energy": F,
363
- "surprise": surprise,
364
- "action": inference_result["action"],
365
- "action_confidence": inference_result["action_confidence"],
366
- "arena": arena_result,
367
- "associative_energy": float(decomp.memory),
368
- "epistemic_value": self.engine.epistemic_value,
369
- "pragmatic_value": self.engine.pragmatic_value,
370
- "field_cycle": cycle,
371
- }
372
-
373
- def act(self) -> Dict[str, Any]:
374
- """
375
- Select and emit an action through the active boundary.
376
-
377
- Uses the policy posterior from the last perception step.
378
- Also checks if an epistemic action (experiment) would be more valuable.
379
- """
380
- # Check if an experiment would help resolve causal tension. Intervention
381
- # search is intentionally gated because it performs model rollouts; when
382
- # the model posterior is already sharp, this was the dominant runtime cost.
383
- current_tension = self.arena.current_tension
384
- experiment = None
385
- if current_tension >= self.epistemic_tension_threshold:
386
- experiment = self.arena.suggest_experiment()
387
-
388
- # Compare epistemic value of experiment vs pragmatic action
389
- if (experiment is not None and
390
- experiment["expected_info_gain"] > self.epistemic_info_gain_threshold):
391
- # Epistemic action: run an experiment to resolve tension
392
- return {
393
- 'type': 'epistemic',
394
- 'experiment': experiment,
395
- 'reason': 'High causal tension — exploring to resolve',
396
- 'tension': current_tension,
397
- }
398
-
399
- # Pragmatic action: act to achieve preferences
400
- action_dist = np.zeros(self.n_actions)
401
- for pi_idx, policy in enumerate(self.engine.policies):
402
- if len(policy) > 0:
403
- action_dist[policy[0]] += self.engine.q_policies[pi_idx]
404
- if action_dist.sum() > 0:
405
- action_dist /= action_dist.sum()
406
- else:
407
- action_dist[:] = 1.0 / self.n_actions
408
- self._last_action_distribution = action_dist.copy()
409
-
410
- if self._pending_action is None:
411
- # Allows act() to be called before the first perceive().
412
- action, confidence = self.engine.select_action()
413
- self._pending_action = int(action)
414
- self._pending_action_confidence = float(confidence)
415
- selected = int(self._pending_action)
416
- confidence = float(self._pending_action_confidence)
417
- self.blanket.active_state = np.array([selected])
418
- self._pending_action = None
419
- self._pending_action_confidence = 0.0
420
-
421
- return {
422
- 'type': 'pragmatic',
423
- 'action': selected,
424
- 'confidence': confidence,
425
- 'action_distribution': action_dist,
426
- 'free_energy': self.engine.F_history[-1] if self.engine.F_history else None,
427
- }
428
-
429
- def experience_replay(self, n_episodes: int = 10) -> Dict[str, Any]:
430
- """
431
- Replay past episodes to strengthen beliefs.
432
-
433
- This is the offline learning loop: re-process past observations
434
- through the epistemic memory to update Dirichlet parameters.
435
- Weighted by surprise — surprising experiences teach more.
436
- """
437
- episodes = self.episodic.replay(n_episodes)
438
-
439
- for ep in episodes:
440
- obs_idx = ep.metadata.get('obs_idx', 0)
441
- self.epistemic.update_likelihood(obs_idx, ep.belief_state)
442
-
443
- return {
444
- 'episodes_replayed': len(episodes),
445
- 'mean_surprise': np.mean([ep.surprise for ep in episodes]) if episodes else 0,
446
- 'epistemic_entropy': self.epistemic.entropy(),
447
- }
448
-
449
- def introspect(self) -> Dict[str, Any]:
450
- """
451
- Full introspection: report on all system components.
452
- """
453
- return {
454
- 'step': self._step_count,
455
- 'average_surprise': self._total_surprise / max(self._step_count, 1),
456
- 'average_free_energy': self._total_free_energy / max(self._step_count, 1),
457
- 'inference': self.engine.statistics,
458
- 'arena': self.arena.statistics,
459
- 'epistemic_memory': {
460
- 'entropy': self.epistemic.entropy(),
461
- 'access_distribution': self.epistemic.get_access_distribution(),
462
- },
463
- 'episodic_memory': self.episodic.statistics,
464
- 'associative_memory': self.associative.statistics,
465
- 'blanket': self.blanket.state,
466
- 'tension_trajectory': self.arena.tension_history[-20:],
467
- 'free_energy_trajectory': self.engine.F_history[-20:],
468
- }
469
-
470
- def add_causal_model(self, model: StructuralCausalModel):
471
- """Add a new competing causal model to the arena."""
472
- self.arena.register_model(model)
473
-
474
- def counterfactual(self, evidence: Dict[str, int],
475
- intervention: Dict[str, int],
476
- query: List[str]) -> Dict[str, Any]:
477
- """
478
- Ask: "What would have happened if we had done X instead?"
479
-
480
- Each competing model gives its own answer. Disagreement = tension.
481
- """
482
- return self.arena.counterfactual_comparison(evidence, intervention, query)
483
-
484
- @classmethod
485
- def from_config(cls, config: Dict[str, Any]) -> 'TensegrityAgent':
486
- """Create an agent from a configuration dictionary (unknown keys ignored)."""
487
- sig = inspect.signature(cls.__init__)
488
- allowed = {k for k in sig.parameters if k != "self"}
489
- kwargs = {k: v for k, v in config.items() if k in allowed}
490
- return cls(**kwargs)
491
-
492
- def __repr__(self):
493
- return (f"TensegrityAgent(states={self.n_states}, obs={self.n_obs}, "
494
- f"actions={self.n_actions}, step={self._step_count}, "
495
- f"tension={self.arena.current_tension:.3f})")
496
-
497
-