feat: eliminate canvas warping, add 3D observatory & teleoperation, and ground experience-driven adaptation
2244bf1 verified Download synapse/adaptation_study.py from Kai9987kai/Sonapse-LAB: direct link, hf CLI and curl.
- Browser
- Download file 20.2 kB
-
https://huggingface.co/Kai9987kai/Sonapse-LAB/resolve/main/synapse/adaptation_study.py
- Command line
-
hf download hf://Kai9987kai/Sonapse-LAB/synapse/adaptation_study.py
-
curl -L -o adaptation_study.py https://huggingface.co/Kai9987kai/Sonapse-LAB/resolve/main/synapse/adaptation_study.py
20.2 kB
| """Measured paired native-world pilot with support-only learning and frozen queries. | |
| Replay verifies raw-record arithmetic and hashes. It does not re-execute the | |
| environment or authenticate a receipt rewritten consistently by an adversary. | |
| """ | |
| from __future__ import annotations | |
| from copy import deepcopy | |
| import hashlib | |
| import json | |
| from pathlib import Path | |
| import time | |
| import numpy as np | |
| from .evidence_engine import bootstrap_confidence_interval | |
| from .stateio import digest, save_state, load_state | |
| ARMS = ('off', 'model', 'memory', 'shuffled') | |
| TASKS = ('forage', 'maze', 'push', 'grasp_place', 'peg_insert', 'delayed_match', 'go_nogo', 'button_sequence') | |
| DEFAULTS = {'seeds': [7, 11, 19], 'tasks': ['forage', 'push'], 'support_steps': 64, 'query_steps': 64, 'nodes_per_source': 16} | |
| def validate_config(config=None): | |
| if config is None: | |
| config = {} | |
| if not isinstance(config, dict) or set(config)-set(DEFAULTS): | |
| raise ValueError('unknown adaptation-study configuration') | |
| c = deepcopy({**DEFAULTS, **config}) | |
| if not isinstance(c['seeds'], list) or not 1 <= len(c['seeds']) <= 8 or any(type(s) is not int or not 0 <= s < 2**31 for s in c['seeds']) or len(set(c['seeds'])) != len(c['seeds']): | |
| raise ValueError('seeds must contain 1..8 unique nonnegative 31-bit integers') | |
| if not isinstance(c['tasks'], list) or not 1 <= len(c['tasks']) <= len(TASKS) or any(t not in TASKS for t in c['tasks']) or len(set(c['tasks'])) != len(c['tasks']): | |
| raise ValueError('tasks must be unique supported native task names') | |
| for name, lo, hi in [('support_steps', 8, 512), ('query_steps', 8, 512), ('nodes_per_source', 16, 200)]: | |
| if type(c[name]) is not int or not lo <= c[name] <= hi: | |
| raise ValueError(f'{name} must be an integer in [{lo},{hi}]') | |
| if len(c['seeds'])*len(c['tasks'])*(2*c['support_steps']+4*c['query_steps']) > 16384: | |
| raise ValueError('total pilot inference budget exceeds 16384 steps') | |
| return c | |
| def _seed(namespace, seed, task): | |
| return int(hashlib.sha256(f'{namespace}:{seed}:{task}'.encode()).hexdigest()[:8], 16) % (2**31) | |
| def _json_hash(value): | |
| return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()).hexdigest() | |
| def _file_hash(path): | |
| return hashlib.sha256(Path(path).read_bytes()).hexdigest() | |
| def _parameters(agent): | |
| tensors = {f'{name}.{key}': value.detach().clone() for name, module in agent.modules.items() for key, value in module.named_parameters()} | |
| tensors['mb.w_mbon'] = agent.mb.w_mbon.detach().clone() | |
| return tensors | |
| def _learned_hash(agent): | |
| return digest({'parameters': _parameters(agent), 'experience': agent.experience_model.export_state(), 'memory': agent.surprise_memory.export_state()}) | |
| def _source_fingerprints(): | |
| from .config import ARTIFACTS | |
| root = Path(__file__).resolve().parent | |
| names = sorted(p.name for p in root.glob('*.py')) | |
| names.extend(f'_vendor/graftbrain/graftbrain/{name}.py' for name in ('brain', 'plasticity', 'neuro_modules', 'world', 'connectomes', 'graft')) | |
| names.append('_vendor/poseidon/poseidon/core.py') | |
| values = {name: _file_hash(root/name) for name in names} | |
| values['artifact:tidal_core.pt'] = _file_hash(ARTIFACTS/'tidal_core.pt') | |
| return values | |
| def _environment(): | |
| import platform | |
| import scipy | |
| import torch | |
| return {'python': platform.python_version(), 'platform': platform.platform(), | |
| 'numpy': str(np.__version__), 'scipy': str(scipy.__version__), 'torch': str(torch.__version__)} | |
| def _summarize(records, config): | |
| indexed = {(r['seed'], r['task'], r['arm']): r for r in records} | |
| expected = {(seed, task, arm) for seed in config['seeds'] for task in config['tasks'] for arm in ARMS} | |
| if len(records) != len(expected) or set(indexed) != expected: | |
| raise ValueError('missing or duplicate paired query records') | |
| units = [(seed, task) for seed in config['seeds'] for task in config['tasks']] | |
| arms = {} | |
| for arm in ARMS: | |
| rows = [indexed[(seed, task, arm)] for seed, task in units] | |
| values = [r['return'] for r in rows] | |
| arms[arm] = {'mean_return': float(np.mean(values)), 'returns': values, | |
| 'mean_reward_mse': float(np.mean([r['reward_mse'] for r in rows])), | |
| 'planner_use_fraction': float(np.mean([r['planner_used_steps']/r['steps'] for r in rows])), | |
| 'memory_use_fraction': float(np.mean([r['memory_used_steps']/r['steps'] for r in rows]))} | |
| comparisons = {} | |
| for first, second in [('model', 'off'), ('memory', 'off'), ('memory', 'model'), ('memory', 'shuffled')]: | |
| diffs = np.asarray(arms[first]['returns'])-np.asarray(arms[second]['returns']) | |
| comparisons[f'{first}_minus_{second}'] = {'paired_differences': diffs.tolist(), 'mean_difference': float(diffs.mean()), | |
| 'paired_bootstrap_95': list(bootstrap_confidence_interval(diffs)) if len(diffs) > 1 else None} | |
| return {'paired_units': [{'seed': seed, 'task': task} for seed, task in units], 'arms': arms, 'comparisons': comparisons, | |
| 'interpretation': 'development pilot; no outcome-based model promotion; small finite native worlds and shared task types limit generalization', | |
| 'uncertainty': 'percentile bootstrap over independent seed/task actor-world units; time steps are not independent replicates; one unit has no interval'} | |
| def format_adaptation_report(receipt): | |
| lines = ['# SYNAPSE measured adaptation development pilot', '', receipt['scope'], '', | |
| 'Support trajectories train prediction and outcome memory. All query arms share paired initial worlds and post-support actor parameters; query learning is disabled.', '', | |
| '| Frozen query arm | Mean return | Reward prediction MSE | Planner use | Memory use |', '|---|---:|---:|---:|---:|'] | |
| for arm, row in receipt['summary']['arms'].items(): | |
| lines.append(f"| {arm} | {row['mean_return']:.6f} | {row['mean_reward_mse']:.6f} | {row['planner_use_fraction']:.3f} | {row['memory_use_fraction']:.3f} |") | |
| lines.extend(['', '| Paired comparison | Mean return difference | Descriptive 95% interval |', '|---|---:|---|']) | |
| for name, row in receipt['summary']['comparisons'].items(): | |
| ci = row['paired_bootstrap_95'] | |
| interval = f'[{ci[0]:+.6f}, {ci[1]:+.6f}]' if ci else 'not estimated (one unit)' | |
| lines.append(f"| {name} | {row['mean_difference']:+.6f} | {interval} |") | |
| lines.extend(['', receipt['summary']['uncertainty'], '', receipt['summary']['interpretation']+'.', '', | |
| 'Returns are fixed-step cumulative rewards, including any episode resets. The shuffled arm permutes actual support rewards and keeps observed transitions fixed; constant rewards can make this control ineffective.', '', | |
| 'Freeze checks compare learned parameters, ensemble state, and memory before/after queries. Raw traces and source hashes are in receipt.json. Verification recomputes arithmetic and hashes; it does not reproduce environmental execution or prove external authenticity.', '', | |
| f"Configuration: `{json.dumps(receipt['config'], sort_keys=True)}`", '']) | |
| return '\n'.join(lines) | |
| def run_adaptation_study(output_dir=None, config=None): | |
| """Run actual action-dependent worlds; never select a policy using query scores.""" | |
| from .agent import IntegratedAgent | |
| from .config import SynapseConfig | |
| from .experience import encode_experience_state | |
| from graftbrain.world import World | |
| c = validate_config(config) | |
| out = Path(output_dir) if output_dir is not None else None | |
| if out is not None: | |
| out.mkdir(parents=True, exist_ok=False) | |
| started = time.perf_counter() | |
| supports, records, files = [], [], {} | |
| for seed in c['seeds']: | |
| for task in c['tasks']: | |
| agent_seed = _seed('actor', seed, task) | |
| support_seed, query_seed = _seed('support', seed, task), _seed('query', seed, task) | |
| agent = IntegratedAgent(SynapseConfig(seed=agent_seed, task=task, nodes_per_source=c['nodes_per_source'], experience_planning=False, surprise_memory=True)) | |
| initial = agent.export_state() | |
| parameter_hash = digest(_parameters(agent)) | |
| world = World(seed=support_seed, curriculum=task) | |
| obs = world.observe() | |
| rng = np.random.default_rng(_seed('exploration', seed, task)) | |
| trace = [] | |
| for step in range(c['support_steps']): | |
| result = agent.act(obs, learn=False, planner_mode='off') | |
| action = rng.uniform(-.8, .8, 5).astype(np.float32) | |
| next_obs, reward, done, _ = world.step(action) | |
| agent.observe_transition(next_obs, float(reward), bool(done), action=action) | |
| trace.append({'step': step, 'obs': obs.tolist(), 'action': action.tolist(), 'next_obs': next_obs.tolist(), 'reward': float(reward), 'done': bool(done)}) | |
| obs = next_obs | |
| if done: | |
| agent.reset_episode() | |
| obs = world.reset() | |
| trained = agent.export_state() | |
| if digest(_parameters(agent)) != parameter_hash: | |
| raise RuntimeError('support changed backbone parameters in a prediction-only study') | |
| # Retrain only prediction/memory on identical recorded inputs and a | |
| # permutation of observed rewards. No fabricated reward distribution. | |
| permutation = rng.permutation(len(trace)).tolist() | |
| rewards = [trace[i]['reward'] for i in permutation] | |
| agent.import_state(initial) | |
| for row, shuffled_reward in zip(trace, rewards): | |
| agent.act(np.asarray(row['obs'], np.float32), learn=False, planner_mode='off') | |
| agent.observe_transition(np.asarray(row['next_obs'], np.float32), shuffled_reward, row['done'], action=np.asarray(row['action'], np.float32)) | |
| if row['done']: | |
| agent.reset_episode() | |
| shuffled_model, shuffled_memory = agent.experience_model.export_state(), agent.surprise_memory.export_state() | |
| shuffled_summary = {'model': agent.experience_model.summary(), 'memory': agent.surprise_memory.summary()} | |
| agent.import_state(trained) | |
| roundtrip_ok = digest(agent.export_state()) == digest(trained) | |
| if not roundtrip_ok: | |
| raise RuntimeError('post-support checkpoint did not restore exactly') | |
| unit = {'seed': seed, 'task': task, 'agent_seed': agent_seed, 'support_seed': support_seed, 'query_seed': query_seed, | |
| 'trace': trace, 'support_digest': _json_hash(trace), 'reward_permutation': permutation, | |
| 'reward_shuffle_changed': rewards != [r['reward'] for r in trace], | |
| 'post_support_parameters': parameter_hash, 'post_support_state': digest(trained), 'checkpoint_roundtrip_ok': roundtrip_ok, | |
| 'learned_support': {'model': agent.experience_model.summary(), 'memory': agent.surprise_memory.summary()}, 'shuffled_support': shuffled_summary} | |
| if out is not None: | |
| name = f'support_{seed}_{task}.pt' | |
| save_state(out/name, trained) | |
| agent.import_state(load_state(out/name)) | |
| if digest(agent.export_state()) != digest(trained): | |
| raise RuntimeError('persisted post-support checkpoint did not restore exactly') | |
| for artifact in [name, name+'.json']: | |
| files[artifact] = _file_hash(out/artifact) | |
| unit['checkpoint'] = name | |
| supports.append(unit) | |
| for arm in ARMS: | |
| agent.import_state(trained) | |
| if arm == 'shuffled': | |
| agent.experience_model.import_state(shuffled_model) | |
| agent.surprise_memory.import_state(shuffled_memory) | |
| agent.reset_episode() | |
| before = _learned_hash(agent) | |
| world = World(seed=query_seed, curriculum=task) | |
| obs = world.observe() | |
| query_trace = [] | |
| mode = 'memory' if arm == 'shuffled' else arm | |
| for step in range(c['query_steps']): | |
| result = agent.act(obs, learn=False, planner_mode=mode) | |
| action = np.asarray(result['action'], np.float32) | |
| state = encode_experience_state(obs, result['features']) | |
| prediction = float(np.asarray(agent.experience_model.predict(state, action[None])['reward']).reshape(-1)[0]) | |
| next_obs, reward, done, _ = world.step(action) | |
| planner = result['telemetry'].get('experience_planner', {}) | |
| query_trace.append({'step': step, 'reward': float(reward), 'predicted_reward': prediction, 'action': action.tolist(), 'done': bool(done), | |
| 'planner_used': bool(planner.get('used', False)), 'memory_used': bool(planner.get('memory_used', False)), | |
| 'planner_reason': str(planner.get('reason', 'unreported'))}) | |
| obs = next_obs | |
| if done: | |
| agent.reset_episode() | |
| obs = world.reset() | |
| after = _learned_hash(agent) | |
| if before != after: | |
| raise RuntimeError('query execution mutated learned state') | |
| records.append({'seed': seed, 'task': task, 'arm': arm, 'query_seed': query_seed, 'support_digest': unit['support_digest'], | |
| 'steps': len(query_trace), 'return': float(sum(t['reward'] for t in query_trace)), | |
| 'reward_mse': float(np.mean([(t['reward']-t['predicted_reward'])**2 for t in query_trace])), | |
| 'planner_used_steps': sum(t['planner_used'] for t in query_trace), 'memory_used_steps': sum(t['memory_used'] for t in query_trace), | |
| 'learned_before': before, 'learned_after': after, 'trace': query_trace, 'trace_digest': _json_hash(query_trace)}) | |
| n = len(supports) | |
| receipt = {'schema': 'synapse-adaptation-study-v1', 'status': 'complete', 'config': c, | |
| 'scope': 'measured CPU native-world development pilot; frozen untrained actor bridges; one-step heuristic planning; no confirmed general advantage', | |
| 'supports': supports, 'records': records, 'summary': _summarize(records, c), | |
| 'costs': {'support_world_steps': n*c['support_steps'], 'shuffled_support_replay_steps': n*c['support_steps'], | |
| 'query_world_steps': n*len(ARMS)*c['query_steps'], 'model_update_calls': 2*n*c['support_steps'], | |
| 'query_update_calls': 0, 'actor_calls': n*(2*c['support_steps']+len(ARMS)*c['query_steps'])}, | |
| 'source_fingerprints': _source_fingerprints(), 'environment': _environment(), 'files': files, | |
| 'wall_seconds': time.perf_counter()-started} | |
| if out is not None: | |
| (out/'report.md').write_text(format_adaptation_report(receipt), encoding='utf-8') | |
| receipt['files']['report.md'] = _file_hash(out/'report.md') | |
| receipt['integrity'] = _json_hash(receipt) | |
| if out is not None: | |
| (out/'receipt.json').write_text(json.dumps(receipt, indent=2, allow_nan=False), encoding='utf-8') | |
| return receipt | |
| def verify_adaptation_study(path_or_receipt): | |
| """Recompute hashes, trace metrics, paired summaries and recorded freeze checks.""" | |
| errors, warnings, recomputed = [], [], 0 | |
| try: | |
| if isinstance(path_or_receipt, dict): | |
| receipt, out = deepcopy(path_or_receipt), None | |
| else: | |
| path = Path(path_or_receipt) | |
| path = path/'receipt.json' if path.is_dir() else path | |
| receipt, out = json.loads(path.read_text(encoding='utf-8')), path.parent | |
| expected = receipt.pop('integrity', None) | |
| if expected != _json_hash(receipt): | |
| errors.append('receipt integrity') | |
| if receipt['schema'] != 'synapse-adaptation-study-v1' or receipt['status'] != 'complete': | |
| errors.append('study is not complete or schema is unsupported') | |
| saved_sources = receipt['source_fingerprints'] | |
| if not isinstance(saved_sources, dict) or not saved_sources or any(not isinstance(v, str) or len(v) != 64 for v in saved_sources.values()): | |
| errors.append('invalid source fingerprints') | |
| else: | |
| try: | |
| current_sources = _source_fingerprints() | |
| for name in sorted(set(current_sources) | set(saved_sources)): | |
| if current_sources.get(name) != saved_sources.get(name): | |
| warnings.append('current source differs: '+name) | |
| current_environment = _environment() | |
| for name, version in receipt['environment'].items(): | |
| if current_environment.get(name) != version: | |
| warnings.append('current environment differs: '+name) | |
| except OSError as exc: | |
| warnings.append('current source comparison unavailable: '+str(exc)) | |
| c = validate_config(receipt['config']) | |
| if out is not None: | |
| for name, expected_file in receipt['files'].items(): | |
| if Path(name).name != name or not (out/name).is_file() or _file_hash(out/name) != expected_file: | |
| errors.append('file integrity:'+name) | |
| supports = {(s['seed'], s['task']): s for s in receipt['supports']} | |
| for unit in supports.values(): | |
| if len(unit['trace']) != c['support_steps'] or unit['support_digest'] != _json_hash(unit['trace']) or not unit['checkpoint_roundtrip_ok']: | |
| errors.append('support integrity') | |
| if sorted(unit['reward_permutation']) != list(range(c['support_steps'])): | |
| errors.append('reward permutation') | |
| for row in receipt['records']: | |
| unit = supports[(row['seed'], row['task'])] | |
| trace = row['trace'] | |
| if len(trace) != c['query_steps'] or row['steps'] != len(trace) or row['trace_digest'] != _json_hash(trace): | |
| errors.append('query trace integrity') | |
| if row['query_seed'] != unit['query_seed'] or row['query_seed'] == unit['support_seed'] or row['support_digest'] != unit['support_digest']: | |
| errors.append('support/query pairing') | |
| metrics = {'return': float(sum(t['reward'] for t in trace)), 'reward_mse': float(np.mean([(t['reward']-t['predicted_reward'])**2 for t in trace])), | |
| 'planner_used_steps': sum(t['planner_used'] for t in trace), 'memory_used_steps': sum(t['memory_used'] for t in trace)} | |
| if any(row[key] != value for key, value in metrics.items()): | |
| errors.append('query metric') | |
| if row['learned_before'] != row['learned_after']: | |
| errors.append('query learned-state mutation') | |
| recomputed += 1 | |
| if _summarize(receipt['records'], c) != receipt['summary']: | |
| errors.append('summary recomputation') | |
| n = len(c['seeds'])*len(c['tasks']) | |
| if len(supports) != n or receipt['costs'] != {'support_world_steps': n*c['support_steps'], 'shuffled_support_replay_steps': n*c['support_steps'], 'query_world_steps': n*len(ARMS)*c['query_steps'], | |
| 'model_update_calls': 2*n*c['support_steps'], 'query_update_calls': 0, 'actor_calls': n*(2*c['support_steps']+len(ARMS)*c['query_steps'])}: | |
| errors.append('cost accounting') | |
| except (KeyError, TypeError, ValueError, OSError, IndexError, OverflowError) as exc: | |
| errors.append(f'invalid receipt: {exc}') | |
| return {'ok': not errors, 'errors': errors, 'warnings': warnings, 'recomputed': recomputed, | |
| 'scope': 'raw-record aggregate and hash replay; recorded freeze assertions checked; environment execution is not rerun or independently authenticated'} | |