File size: 13,647 Bytes
9e36559
0d4b28f
 
 
9e36559
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e36559
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e36559
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e36559
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e36559
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e36559
 
0d4b28f
 
 
 
 
9e36559
0d4b28f
 
 
 
 
 
9e36559
 
0d4b28f
 
9e36559
 
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
 
9e36559
0d4b28f
9e36559
0d4b28f
 
 
 
 
 
9e36559
0d4b28f
 
 
 
 
9e36559
 
0d4b28f
 
 
 
9e36559
 
 
 
 
0d4b28f
 
 
9e36559
 
 
 
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
9e36559
 
0d4b28f
9e36559
 
 
 
0d4b28f
 
9e36559
0d4b28f
 
9e36559
0d4b28f
 
 
 
 
 
 
 
9e36559
0d4b28f
 
 
 
 
 
 
9e36559
 
 
0d4b28f
 
 
9e36559
 
0d4b28f
9e36559
 
 
0d4b28f
 
 
 
9e36559
 
 
 
0d4b28f
 
 
9e36559
0d4b28f
 
 
 
9e36559
 
 
 
 
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
9e36559
 
 
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
9e36559
 
 
 
 
 
 
 
0d4b28f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
"""Six-arm sequential adaptation study with matched world-transition budgets.

Grounding & Review Requirements:
Directly implements the experimental design recommended in the 6 November technical review:
Compares mechanism bundles on matched seeds; optimizer/sleep compute differs.
1. 'frozen_actor': Establish fixed baseline.
2. 'online_rl': Measure online learning capacity.
3. 'actor_replay': Measure memory and replay consolidation benefit.
4. 'continual_backprop': Measure plasticity preservation and utility replacement.
5. 'connectome_evolution': Measure structural plasticity and neurogenesis.
6. 'composed_system': Test whether accepted mechanisms compose synergistically.

Evaluates:
- Adaptation speed (steps/cumulative return per stage)
- Backward retention score (target >= 95%)
- Catastrophic forgetting index
- Inference latency per step (ms)
- Forward / negative transfer
- Real ComplexWorld environment outcomes with SHA-256 deterministic seeds.
"""
from __future__ import annotations
from copy import deepcopy
import time
import hashlib
import json
from dataclasses import dataclass, field, asdict
from typing import Dict, Any, List, Optional, Tuple
import numpy as np
import torch

from .config import SynapseConfig
from .agent import IntegratedAgent
from .complex_world import ComplexWorld
from .continual_backprop import ContinualBackpropEngine
from .sleep import SleepConsolidationEngine
from .uncertainty_calibration import TripartiteUncertaintyDecomposer
from .self_model import EfferenceCopyComparator
from .homeostasis import HomeostaticEngine


def deterministic_seed(base_seed: int, label: str) -> int:
    """Generate reproducible 31-bit seed via SHA-256."""
    h = hashlib.sha256(f"{base_seed}_{label}".encode('utf-8')).hexdigest()
    return int(h[:8], 16) % (2**31 - 1)


@dataclass
class ArmResult:
    """Results and metrics for a single experimental arm."""
    arm_name: str
    purpose: str
    stage_returns: List[float]
    final_retention_rate: float
    catastrophic_forgetting: float
    adaptation_speed: float
    negative_transfer: Optional[float]
    mean_step_latency_ms: float
    task_matrix: List[List[float]]
    auxiliary_metrics: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


class ControlledSixArmStudy:
    """Rigorous 6-arm sequential adaptation study."""

    ARMS = [
        ('frozen_actor', 'Establish fixed baseline'),
        ('online_rl', 'Measure online learning capacity'),
        ('actor_replay', 'Measure memory benefit'),
        ('continual_backprop', 'Measure plasticity preservation'),
        ('connectome_evolution', 'Measure architectural adaptation'),
        ('composed_system', 'Test whether improvements compose')
    ]

    def __init__(
        self,
        task_sequence: Optional[List[str]] = None,
        steps_per_stage: int = 150,
        eval_episodes_per_task: int = 3,
        seed: int = 42,
        nodes_per_source: int = 16
    ):
        self.task_sequence = task_sequence or ['forage', 'maze', 'push', 'ecosystem']
        self.steps_per_stage = steps_per_stage
        self.eval_episodes_per_task = eval_episodes_per_task
        self.seed = seed
        self.nodes_per_source = nodes_per_source
        self.evaluation_steps_executed = 0

    def run_study(self) -> Dict[str, Any]:
        """Execute all 6 arms under identical conditions."""
        results: Dict[str, ArmResult] = {}

        for arm_name, purpose in self.ARMS:
            res = self._run_single_arm(arm_name, purpose)
            results[arm_name] = res

        summary = self._compile_comparative_summary(results)
        return summary

    def _run_single_arm(self, arm_name: str, purpose: str) -> ArmResult:
        """Run a single arm across the full task sequence."""
        cfg = SynapseConfig(
            nodes_per_source=self.nodes_per_source,
            seed=self.seed,
            continual_backprop=(arm_name in ('continual_backprop', 'composed_system')),
            self_model=(arm_name == 'composed_system'),
            structural_learning=(arm_name in ('connectome_evolution', 'composed_system')),
        )
        agent = IntegratedAgent(cfg)

        # Auxiliary components based on arm
        sleep_engine = SleepConsolidationEngine() if arm_name in ('actor_replay', 'composed_system') else None
        cbp_engine = agent.continual_backprop
        tripartite = TripartiteUncertaintyDecomposer(state_dim=16, action_dim=5, seed=self.seed) if arm_name == 'composed_system' else None

        num_tasks = len(self.task_sequence)
        task_matrix = np.zeros((num_tasks, num_tasks), dtype=np.float32)
        stage_returns = []
        step_latencies = []
        training_seeds = {}
        eval_work_before = self.evaluation_steps_executed

        for stage_idx, current_task in enumerate(self.task_sequence):
            train_seed = deterministic_seed(self.seed, f"train_{current_task}")
            training_seeds[current_task] = train_seed
            world = ComplexWorld(seed=train_seed)
            obs = world.reset(task=current_task)
            agent.reset_episode()
            stage_cum_reward = 0.0

            for step in range(self.steps_per_stage):
                t0 = time.perf_counter()
                learn_flag = (arm_name != 'frozen_actor')

                # Act
                out = agent.act(obs, learn=learn_flag)
                act = out['action']

                # Step world
                next_obs, rew, done, info = world.step(act)
                stage_cum_reward += float(rew)
                self._advance_physiology(agent, act, rew, info)
                t_step = (time.perf_counter() - t0) * 1000.0
                step_latencies.append(t_step)

                # Arm-specific updates
                if arm_name != 'frozen_actor':
                    agent.observe_transition(next_obs, float(rew), done=bool(done), action=act)
                    agent.feedback(float(rew), done=bool(done))

                    if sleep_engine is not None:
                        sleep_engine.record_experience(obs, act, float(rew), task_id=current_task)

                    if tripartite is not None:
                        # Explicit observed channels, not an invented latent identity target.
                        z_curr, z_next = obs[:16].copy(), next_obs[:16].copy()
                        tripartite.train_batch(
                            z_curr[np.newaxis, :], act[np.newaxis, :], np.array([float(rew)]), z_next[np.newaxis, :]
                        )

                if done:
                    obs = world.reset(task=current_task)
                    agent.reset_episode()
                else:
                    obs = next_obs

            # Post-stage replay consolidation
            if sleep_engine is not None:
                seen_tasks = self.task_sequence[:stage_idx + 1]
                sleep_engine.run_sleep_cycle(agent, sleep_steps=10,
                    eval_fn=lambda candidate: float(np.mean([
                        self._evaluate_agent(candidate, task, 0) for task in seen_tasks])))

            # Evaluate on all tasks seen so far
            for prev_idx in range(stage_idx + 1):
                t_name = self.task_sequence[prev_idx]
                eval_score = self._evaluate_agent(agent, t_name, stage_idx)
                task_matrix[stage_idx, prev_idx] = eval_score

            stage_score = float(task_matrix[stage_idx, stage_idx])
            stage_returns.append(stage_score)

        retention, forgetting = self._calc_retention_and_forgetting(task_matrix)
        adaptation_speed = float(np.mean(stage_returns))
        # Different task returns are not a negative-transfer experiment.
        neg_transfer = None

        aux = {'training_seeds': training_seeds, 'training_transitions': self.steps_per_stage * num_tasks,
               'evaluation_world_steps': self.evaluation_steps_executed - eval_work_before,
               'structural_learning': cfg.structural_learning, 'negative_transfer_measured': False,
               'uncertainty_input': 'observed_channels_0_16' if tripartite else None}
        if cbp_engine:
            aux['cbp_replacements'] = cbp_engine.total_replacements
            aux['cbp_utility_updates'] = cbp_engine.step_count
        if sleep_engine:
            aux['sleep_consolidations'] = sleep_engine.sleep_cycles_completed
            aux['sleep_screens'] = deepcopy(sleep_engine.consolidation_history)

        return ArmResult(
            arm_name=arm_name,
            purpose=purpose,
            stage_returns=[round(float(s), 4) for s in stage_returns],
            final_retention_rate=round(float(retention), 4),
            catastrophic_forgetting=round(float(forgetting), 4),
            adaptation_speed=round(adaptation_speed, 4),
            negative_transfer=neg_transfer,
            mean_step_latency_ms=round(float(np.mean(step_latencies)), 3),
            task_matrix=[[round(float(v), 4) for v in row] for row in task_matrix.tolist()],
            auxiliary_metrics=aux
        )

    def _evaluate_agent(self, agent: IntegratedAgent, task_name: str, eval_id: int) -> float:
        """Evaluate agent in frozen evaluation mode over held-out episodes."""
        # eval_id is retained for API compatibility; phases use identical episodes.
        actor = deepcopy(agent)
        frozen = actor.export_state()
        episode_rewards = []

        for ep in range(self.eval_episodes_per_task):
            eval_seed = deterministic_seed(self.seed, f"eval_{task_name}_{ep}")
            world = ComplexWorld(seed=eval_seed)
            obs = world.reset(task=task_name)
            actor.import_state(frozen)
            actor.reset_episode()
            actor.homeostasis = HomeostaticEngine(seed=eval_seed)
            ep_rew = 0.0
            done = False
            steps = 0
            while not done and steps < 60:
                out = actor.act(obs, learn=False)
                next_obs, r, done, info = world.step(out['action'])
                self._advance_physiology(actor, out['action'], r, info)
                actor.pending_transition = None
                ep_rew += float(r)
                obs = next_obs
                steps += 1
                self.evaluation_steps_executed += 1
            episode_rewards.append(ep_rew)

        return float(np.mean(episode_rewards))

    @staticmethod
    def _advance_physiology(agent, action, reward, info):
        agent.homeostasis.step(action, float(reward), hazard_dist=info.get('min_hazard_dist', 10.),
            consumed_food=float(reward) > .3, in_friction_zone=info.get('in_friction_zone', False))

    @staticmethod
    def _calc_retention_and_forgetting(matrix: np.ndarray) -> Tuple[float, float]:
        """Compute backward retention and catastrophic forgetting."""
        N = matrix.shape[0]
        if N <= 1:
            return 1.0, 0.0

        retentions = []
        forgettings = []
        for j in range(N - 1):
            peak = float(np.max(matrix[j:, j]))
            final = float(matrix[-1, j])
            acquired = float(matrix[j, j])
            denom = max(abs(acquired), 1.)
            retentions.append(max(0.0, 1. - max(0., acquired - final) / denom))
            forgettings.append(max(0.0, peak - final))

        mean_ret = float(np.mean(retentions)) if retentions else 1.0
        mean_forg = float(np.mean(forgettings)) if forgettings else 0.0
        return mean_ret, mean_forg

    def _compile_comparative_summary(self, results: Dict[str, ArmResult]) -> Dict[str, Any]:
        """Compile comparative analysis table and empirical rankings."""
        summary = {
            'task_sequence': self.task_sequence,
            'steps_per_stage': self.steps_per_stage,
            'eval_episodes_per_task': self.eval_episodes_per_task,
            'base_seed': self.seed,
            'evidence_kind': 'measured_world_development_study',
            'protocol': {'matched_training_seeds': True, 'matched_query_seeds_across_stages': True,
                'evaluation_steps_per_episode': 60, 'query_learning': False, 'query_clones': True,
                'budget': 'equal training world transitions; optimizer and sleep work differ'},
            'limitations': ['Mechanism bundles retain common inherited learning and dormant mechanisms.',
                'online_rl is a legacy arm name: updates are reward-modulated Hebbian plasticity, not a new policy-gradient baseline.',
                'adaptation_speed stores mean post-stage return; no time-to-mastery or negative-transfer control is measured.',
                'Finite development tasks and seeds do not establish a general continual-learning advantage.'],
            'arms': {k: v.to_dict() for k, v in results.items()},
            'comparative_table': []
        }

        for arm_name, r in results.items():
            summary['comparative_table'].append({
                'arm': arm_name,
                'purpose': r.purpose,
                'mean_return': r.adaptation_speed,
                'retention_rate': r.final_retention_rate,
                'catastrophic_forgetting': r.catastrophic_forgetting,
                'step_latency_ms': r.mean_step_latency_ms
            })

        # Check if composed beats frozen baseline
        frozen_ret = results['frozen_actor'].adaptation_speed
        composed_ret = results['composed_system'].adaptation_speed
        summary['delta_composed_vs_frozen'] = round(composed_ret - frozen_ret, 4)
        summary['retention_target_met'] = bool(results['composed_system'].final_retention_rate >= 0.95)

        return summary