Spaces:
Paused
Paused
File size: 16,885 Bytes
5e0b58b | 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 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 | """Restore a real Brain-5D core from snapshot, journal, and runtime sidecar."""
from __future__ import annotations
import random
from pathlib import Path
from typing import Any, Protocol, assert_never, cast
from src.config.loader import ConfigDict
from src.core.network import NeuralNetwork, SpikeEvent
from src.core.spatial_index import unpack_coords
from .b5d import B5DReader
from .checkpoint import RuntimeCheckpoint, read_runtime_checkpoint
from .recovery import RecoveryManager
from .structural_journal import StructuralChangeKind, StructuralChangeRecord
from .structural_recovery import StructuralRecoveryManager
class StructuralRestoreManipulator(Protocol):
def create_neuron(self, coord: tuple[int, int, int, int, int]) -> int: ...
def delete_neuron(self, neuron_id: int) -> None: ...
def create_synapse(
self, source_id: int, target_id: int, weight: float, delay: int
) -> None: ...
def delete_synapse(self, source_id: int, target_id: int) -> None: ...
class _StructuralReplayTarget:
def __init__(self, manipulator: StructuralRestoreManipulator) -> None:
self._manipulator = manipulator
def apply_structural_record(self, record: StructuralChangeRecord) -> bool:
if record.kind is StructuralChangeKind.NEURON_ADD:
if record.coord is None:
return False
neuron_id = self._manipulator.create_neuron(record.coord)
return record.neuron_id is None or neuron_id == record.neuron_id
if record.kind is StructuralChangeKind.NEURON_REMOVE:
if record.neuron_id is None:
return False
self._manipulator.delete_neuron(record.neuron_id)
return True
if record.kind is StructuralChangeKind.SYNAPSE_ADD:
if (
record.source_id is None
or record.target_id is None
or record.weight is None
or record.delay is None
):
return False
self._manipulator.create_synapse(
record.source_id,
record.target_id,
record.weight,
record.delay,
)
return True
if record.kind is StructuralChangeKind.SYNAPSE_REMOVE:
if record.source_id is None or record.target_id is None:
return False
self._manipulator.delete_synapse(record.source_id, record.target_id)
return True
assert_never(record.kind)
class RestoredNeuralNetwork(NeuralNetwork):
"""NeuralNetwork subclass exposing a narrow restoration hook."""
def restore_queued_count(self, value: int) -> None:
"""Restore queue counter after event slots have been reconstructed."""
self._queued_event_count = value
def restore_network(
snapshot_path: Path,
journal_path: Path,
checkpoint_path: Path,
config: ConfigDict,
recovered_path: Path,
*,
structural_journal_path: Path | None = None,
) -> RestoredNeuralNetwork:
"""Recover persisted state and return a runnable deterministic network.
Args:
snapshot_path: Path to the base `.b5d` snapshot.
journal_path: Path to the `.b5d.journal` delta journal.
checkpoint_path: Path to the runtime checkpoint JSON.
config: Network configuration (dict).
recovered_path: Path to write the recovered snapshot (if needed).
structural_journal_path: Optional structural journal path.
Returns:
A restored RestoredNeuralNetwork instance.
Raises:
RuntimeError: If recovery fails.
ValueError: If the snapshot is not restart-capable or required fields are missing.
"""
result = RecoveryManager(snapshot_path, journal_path).recover(recovered_path)
if not result.success:
raise RuntimeError(result.error or "snapshot recovery failed")
checkpoint = read_runtime_checkpoint(checkpoint_path)
rng = _restore_rng(checkpoint)
# Cast ConfigDict to dict[str, Any] for the NeuralNetwork constructor
network = RestoredNeuralNetwork(cast(dict[str, Any], config), rng)
with B5DReader(recovered_path) as reader:
for neuron_record in reader.iter_neurons():
neuron_id = network.add_neuron(unpack_coords(neuron_record.neuron_id))
neuron = network.neurons[neuron_id]
# Ensure restart-capable snapshot was used
if neuron_record.a is None or neuron_record.b is None:
raise ValueError("restart-capable snapshot required")
if neuron_record.c is None or neuron_record.d is None:
raise ValueError("restart-capable snapshot required")
if neuron_record.spike_cost is None:
raise ValueError("restart-capable snapshot required")
if neuron_record.spike_counter is None:
raise ValueError("restart-capable snapshot required")
if neuron_record.last_spike_tick is None:
raise ValueError("restart-capable snapshot required")
# Restore core neuron parameters
neuron.a = neuron_record.a
neuron.b = neuron_record.b
neuron.c = neuron_record.c
neuron.d = neuron_record.d
neuron.spike_cost = neuron_record.spike_cost
neuron.spike_counter = neuron_record.spike_counter
neuron.last_spike_tick = neuron_record.last_spike_tick
# Restore dynamic state from optical record
neuron.v = neuron_record.optical.membrane_v
neuron.u = neuron_record.optical.recovery_u
neuron.energy = neuron_record.optical.energy
neuron.threshold_adaptation = neuron_record.optical.threshold_adaptation
for synapse_record in reader.iter_synapses():
network.connect(
synapse_record.source_id,
synapse_record.target_id,
synapse_record.weight,
synapse_record.delay,
)
synapse = network.synapses[synapse_record.source_id][-1]
synapse.eligibility = synapse_record.eligibility
synapse.last_pre_spike = synapse_record.last_pre_spike
# Replay structural plasticity journal if provided
if structural_journal_path is not None:
from src.manipulation.manipulator import Brain5DManipulator
structural_target = _StructuralReplayTarget(Brain5DManipulator(network))
StructuralRecoveryManager().replay(
structural_target,
structural_journal_path,
)
# Restore runtime state and exact neuron/synapse values from checkpoint
_restore_runtime(network, checkpoint)
_restore_exact_neuron_state(network, checkpoint)
_restore_exact_synapse_state(network, checkpoint)
return network
def _restore_rng(checkpoint: RuntimeCheckpoint) -> random.Random:
"""Restore the random number generator from checkpoint state."""
rng = random.Random()
rng.setstate(
(
checkpoint.rng.version,
tuple(checkpoint.rng.state),
checkpoint.rng.gauss_next,
)
)
return rng
def _restore_runtime(
network: RestoredNeuralNetwork,
checkpoint: RuntimeCheckpoint,
) -> None:
"""Restore runtime state from checkpoint."""
network.current_tick = checkpoint.current_tick
network.total_spikes = checkpoint.total_spikes
network.total_events_processed = checkpoint.total_events_processed
network.pending_currents = dict(checkpoint.pending_currents)
network.input_cells = set(checkpoint.input_cells)
network.output_cells = set(checkpoint.output_cells)
# Rebuild event queue
network.event_slots = [[] for _ in range(network.max_delay + 1)]
queued = 0
for event in checkpoint.queued_events:
slot = event.delivery_tick % len(network.event_slots)
network.event_slots[slot].append(
SpikeEvent(
source_id=event.source_id,
target_id=event.target_id,
weight=event.weight,
delivery_tick=event.delivery_tick,
)
)
queued += 1
network.restore_queued_count(queued)
def _restore_exact_neuron_state(
network: RestoredNeuralNetwork,
checkpoint: RuntimeCheckpoint,
) -> None:
"""Overlay exact values lost by the compact snapshot representation."""
for state in checkpoint.neuron_states:
neuron = network.neurons.get(state.neuron_id)
if neuron is None:
raise ValueError(
f"runtime checkpoint references missing neuron {state.neuron_id}"
)
neuron.a = state.a
neuron.b = state.b
neuron.c = state.c
neuron.d = state.d
neuron.v = state.membrane_v
neuron.u = state.recovery_u
neuron.energy = state.energy
neuron.spike_cost = state.spike_cost
neuron.threshold_adaptation = state.threshold_adaptation
neuron.spike_counter = state.spike_counter
neuron.last_spike_tick = state.last_spike_tick
neuron.last_external_current = state.last_external_current
neuron.last_synaptic_current = state.last_synaptic_current
neuron.firing_rate_estimate = state.firing_rate_estimate
neuron._spike_count_window = ( # pyright: ignore[reportPrivateUsage]
state.spike_count_window
)
neuron._last_update_tick = ( # pyright: ignore[reportPrivateUsage]
state.last_update_tick
)
neuron.pre_trace = state.pre_trace
neuron.post_trace = state.post_trace
def _restore_exact_synapse_state(
network: RestoredNeuralNetwork,
checkpoint: RuntimeCheckpoint,
) -> None:
"""Overlay exact synaptic float state lost by V1 float32 fields."""
if not checkpoint.synapse_states:
return
for state in checkpoint.synapse_states:
candidates = network.synapses.get(state.source_id)
if candidates is None:
raise ValueError(
f"runtime checkpoint references missing source {state.source_id}"
)
synapse = next(
(item for item in candidates if item.target_id == state.target_id),
None,
)
if synapse is None:
raise ValueError(
"runtime checkpoint references missing synapse "
f"{state.source_id}->{state.target_id}"
)
synapse.weight = state.weight
synapse.delay = state.delay
synapse.eligibility = state.eligibility
synapse.last_pre_spike = state.last_pre_spike
def restore_homeostasis_state(
homeostasis_engine: Any,
checkpoint: RuntimeCheckpoint,
) -> None:
"""Restore homeostasis engine state from checkpoint v4.
Args:
homeostasis_engine: A HomeostasisEngine instance (must have _rates_hz dict).
checkpoint: The RuntimeCheckpoint with homeostasis_state.
Raises:
ValueError: If a neuron referenced in the checkpoint no longer exists.
"""
if not checkpoint.homeostasis_state:
return
rates = {}
for record in checkpoint.homeostasis_state:
rates[int(record.neuron_id)] = float(record.rate_hz)
# Restore the internal rates dict
if hasattr(homeostasis_engine, "_rates_hz"):
homeostasis_engine._rates_hz = rates
def restore_learning_state(
learning_engine: Any,
checkpoint: RuntimeCheckpoint,
) -> None:
"""Restore learning engine state from checkpoint v4.
Restores per-synapse traces (last_pre_tick, last_post_tick, eligibility)
and pending reward signals.
Args:
learning_engine: A LearningEngine instance (must have _states dict
and _pending_rewards list).
checkpoint: The RuntimeCheckpoint with learning_state and pending_rewards.
Raises:
ValueError: If a synapse referenced in the checkpoint no longer exists.
"""
if not hasattr(learning_engine, "_states"):
return
# Restore per-synapse traces
# Uses stable (pre_id, target_id) key matching LearningEngine's
# deterministic topology indexing.
if checkpoint.learning_state:
states = learning_engine._states
for record in checkpoint.learning_state:
pre_id = int(record.pre_id)
target_id = int(record.target_id)
key = (pre_id, target_id)
if key in states:
state = states[key]
state.last_pre_tick = record.last_pre_tick
state.last_post_tick = record.last_post_tick
# EligibilityTrace uses .value, not ._trace
state.eligibility.value = record.eligibility_value
# Restore per-trace last_tick so decay is accurate
state.eligibility.last_tick = record.eligibility_last_tick
# Restore pending rewards (independent of learning_state presence)
if checkpoint.pending_rewards and hasattr(learning_engine, "_pending_rewards"):
from src.learning.reward import RewardSignal
learning_engine._pending_rewards = [
RewardSignal(value=float(r.value), tick=int(r.tick))
for r in checkpoint.pending_rewards
]
# ============================================================================
# Production Restore Bundle
# ============================================================================
class RestoredBundle:
"""Complete production restore result: network + optional engines.
This bundles the restored neural network together with its optional
homeostasis and learning engines, providing a single restore-and-continue
entry point for production use.
Attributes:
network: The restored RestoredNeuralNetwork instance.
homeostasis_engine: Optional restored HomeostasisEngine, or None.
learning_engine: Optional restored LearningEngine, or None.
"""
def __init__(
self,
network: RestoredNeuralNetwork,
homeostasis_engine: Any | None = None,
learning_engine: Any | None = None,
) -> None:
self.network = network
self.homeostasis_engine = homeostasis_engine
self.learning_engine = learning_engine
def restore_full(
snapshot_path: Path,
journal_path: Path,
checkpoint_path: Path,
config: ConfigDict,
recovered_path: Path,
*,
structural_journal_path: Path | None = None,
create_homeostasis_engine: bool = False,
create_learning_engine: bool = False,
) -> RestoredBundle:
"""Restore network and optionally create and restore engines.
This is the production entry point that replaces calling
``restore_network()`` followed by manual engine creation and restore.
It ensures the full restore chain is executed atomically:
1. Recover snapshot and restore network (same as restore_network())
2. Optionally create HomeostasisEngine and restore its state
3. Optionally create LearningEngine and restore its state
Args:
snapshot_path: Path to the base ``.b5d`` snapshot.
journal_path: Path to the ``.b5d.journal`` delta journal.
checkpoint_path: Path to the runtime checkpoint JSON.
config: Network configuration (dict).
recovered_path: Path to write the recovered snapshot.
structural_journal_path: Optional structural journal path.
create_homeostasis_engine: If True, create and restore
HomeostasisEngine from checkpoint.
create_learning_engine: If True, create and restore
LearningEngine from checkpoint.
Returns:
A RestoredBundle containing the network and optionally the engines.
Raises:
RuntimeError: If recovery fails.
ValueError: If the snapshot is not restart-capable.
"""
network = restore_network(
snapshot_path=snapshot_path,
journal_path=journal_path,
checkpoint_path=checkpoint_path,
config=config,
recovered_path=recovered_path,
structural_journal_path=structural_journal_path,
)
checkpoint = read_runtime_checkpoint(checkpoint_path)
homeostasis_engine: Any = None
learning_engine: Any = None
if create_homeostasis_engine:
from src.homeostasis.engine import HomeostasisEngine
homeostasis_engine = HomeostasisEngine(network, dict(config))
restore_homeostasis_state(homeostasis_engine, checkpoint)
homeostasis_engine.attach()
if create_learning_engine:
from src.learning.learning_engine import LearningEngine
learning_engine = LearningEngine(network, dict(config))
restore_learning_state(learning_engine, checkpoint)
learning_engine.attach()
return RestoredBundle(
network=network,
homeostasis_engine=homeostasis_engine,
learning_engine=learning_engine,
)
|