ThomasHeisig's picture
Sync GitHub main af692537c27175c8410bf57a747be0caf23718e2 (part 7)
07fb183 verified
Raw History Blame Contribute Delete
6.27 kB
"""Ordered scalar synapse oracle for the canonical bounded CUDA reference."""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import TYPE_CHECKING
from src.runtime.determinism import (
release_uniform as release_uniform,
)
from src.runtime.determinism import (
ring_slot as ring_slot,
)
if TYPE_CHECKING:
from ..recurrent.backend import RecurrentInputs
@dataclass(frozen=True)
class SynapseConfig:
stdp: bool = True
stp: bool = True
reward_modulated: bool = True
seed: int = 12345
learning_rate: float = 0.2
weight_decay: float = 0.001
weight_max: float = 20.0
eligibility_tau: float = 100.0
credit_window: int = 64
td_lambda: float = 0.9
gamma: float = 0.99
def parameters(self) -> list[float]:
return [
float(self.stdp),
float(self.stp),
float(self.reward_modulated),
float(self.seed),
self.learning_rate,
self.weight_decay,
self.weight_max,
self.eligibility_tau,
float(self.credit_window),
self.td_lambda,
self.gamma,
]
def validate(self) -> None:
if any(
type(value) is not bool
for value in (self.stdp, self.stp, self.reward_modulated)
):
raise ValueError("synapse mode flags must be boolean")
if type(self.seed) is not int or not 0 <= self.seed <= 0xFFFFFFFF:
raise ValueError("synapse seed must be uint32")
if type(self.credit_window) is not int or not 0 <= self.credit_window <= 2000:
raise ValueError("credit_window must be in [0,2000]")
for name, value, low, high in (
("learning_rate", self.learning_rate, 0.0, 1.0),
("weight_decay", self.weight_decay, 0.0, 1.0),
("weight_max", self.weight_max, 0.001, 100.0),
("eligibility_tau", self.eligibility_tau, 0.001, 10000.0),
("td_lambda", self.td_lambda, 0.0, 1.0),
("gamma", self.gamma, 0.0, 1.0),
):
if (
isinstance(value, bool)
or not math.isfinite(value)
or not low <= value <= high
):
raise ValueError(f"invalid synapse {name}")
class SynapseState:
def __init__(self, inputs: RecurrentInputs, config: SynapseConfig) -> None:
self.inputs = inputs
self.config = config
self.weights = list(inputs.weights)
self.eligibility = [0.0] * len(inputs.sources)
self.available = [1.0] * len(inputs.sources)
self.last_event = [-10_000_000] * len(inputs.sources)
self.last_spike = [-10_000_000] * inputs.n_neurons
self.ring_size = max(inputs.delays, default=1) + 1
self.emitted = [[0.0] * len(inputs.sources) for _ in range(self.ring_size)]
def incoming(self, tick: int, neuron: int) -> float:
# Kept for external callers; the membrane oracle sums in edge order.
return sum(
self.edge_current(tick, edge)
for edge in range(
self.inputs.offsets[neuron], self.inputs.offsets[neuron + 1]
)
)
def edge_current(self, tick: int, edge: int) -> float:
previous = tick - self.inputs.delays[edge]
return (
self.emitted[ring_slot(previous, self.ring_size)][edge]
if previous >= 0
else 0.0
)
def update(self, tick: int, spikes: list[int], reward: float) -> None:
c, inputs = self.config, self.inputs
for target in range(inputs.n_neurons):
for edge in range(inputs.offsets[target], inputs.offsets[target + 1]):
source = inputs.sources[edge]
pre, post = spikes[source], spikes[target]
weight = self.weights[edge]
eligibility = self.eligibility[edge] * math.exp(
-inputs.dt_ms / c.eligibility_tau
)
# Match ascending-neuron pair-rule ordering, including self edges.
operations = ("pre", "post") if source <= target else ("post", "pre")
for operation in operations:
if operation == "pre" and pre:
if c.stdp and 0 < tick - self.last_spike[target] <= 20:
weight = max(0.0, weight - 0.08)
eligibility -= 0.5
if operation == "post" and post:
if c.stdp and 0 < tick - self.last_spike[source] <= 20:
weight = min(100.0, weight + 0.1)
eligibility += 1.0
if pre or post:
self.last_event[edge] = tick
if (
c.reward_modulated
and tick - self.last_event[edge] <= c.credit_window
):
weight = min(
100.0,
max(
0.0,
weight
+ c.learning_rate
* c.td_lambda
* c.gamma
* eligibility
* reward,
),
)
amplitude = weight if pre else 0.0
available = self.available[edge]
if c.stp:
if pre:
amplitude *= float(
release_uniform(c.seed, tick, edge)
< min(0.95, 0.25 + 0.7 * available)
)
available = max(0.1, available * 0.72)
available = min(1.0, available + 0.025)
self.available[edge] = available
self.emitted[ring_slot(tick, self.ring_size)][edge] = amplitude
self.weights[edge] = min(
c.weight_max, max(0.0, weight * (1.0 - c.weight_decay))
)
self.eligibility[edge] = eligibility
for neuron, spike in enumerate(spikes):
if spike:
self.last_spike[neuron] = tick