MHRN-Space / src /core /synapse_stochastic.py
ThomasHeisig's picture
Sync GitHub main 4645f4137666463023fa501984a14336dd2fc666 (part 11)
31226fd verified
Raw History Blame Contribute Delete
3.79 kB
"""Seeded quantal-release synapse variant built on one coherent STP model."""
from __future__ import annotations
import random
from dataclasses import asdict, dataclass, field
from typing import Any
from .biophysical_contracts import ModelProvenance, ModelTimescale
@dataclass(frozen=True, slots=True)
class QuantalSynapseConfig:
enabled: bool = False
seed: int = 0
release_sites: int = 4
release_probability: float = 0.25
quantal_size: float = 0.05
facilitation_u: float = 0.2
tau_recovery_ticks: float = 800.0
tau_facilitation_ticks: float = 50.0
def __post_init__(self) -> None:
if self.release_sites < 1:
raise ValueError("release_sites must be >= 1")
if not 0.0 <= self.release_probability <= 1.0:
raise ValueError("release_probability must be in [0, 1]")
if self.quantal_size < 0.0:
raise ValueError("quantal_size must be >= 0")
if self.tau_recovery_ticks <= 0.0 or self.tau_facilitation_ticks <= 0.0:
raise ValueError("time constants must be > 0")
@dataclass(slots=True)
class QuantalSTPSynapse:
target_id: int
weight: float
delay: int
config: QuantalSynapseConfig
available_fraction: float = 1.0
facilitation: float = 0.0
last_tick: int = 0
release_count: int = 0
_rng: random.Random = field(init=False, repr=False)
def __post_init__(self) -> None:
if self.delay < 1:
raise ValueError("delay must be >= 1")
self._rng = random.Random(self.config.seed)
@property
def provenance_tag(self) -> ModelProvenance:
return ModelProvenance(
model_id="quantal-tsodyks-markram-v1",
version="mhrn-experimental-1",
timescale=ModelTimescale.TICK,
enabled=self.config.enabled,
parameters=asdict(self.config),
seed=self.config.seed,
)
def _recover(self, tick: int) -> None:
elapsed = max(0, tick - self.last_tick)
if elapsed == 0:
return
self.available_fraction += (1.0 - self.available_fraction) * min(
1.0, elapsed / self.config.tau_recovery_ticks
)
self.facilitation *= max(
0.0, 1.0 - elapsed / self.config.tau_facilitation_ticks
)
self.available_fraction = min(1.0, max(0.0, self.available_fraction))
self.facilitation = min(1.0, max(0.0, self.facilitation))
def release(self, tick: int) -> float:
if not self.config.enabled:
return 0.0
self._recover(tick)
effective_p = min(
1.0,
self.config.release_probability
* (1.0 + self.facilitation)
* self.available_fraction,
)
quanta = sum(
1
for _ in range(self.config.release_sites)
if self._rng.random() < effective_p
)
used_fraction = quanta / self.config.release_sites
self.available_fraction = max(0.0, self.available_fraction - used_fraction)
self.facilitation = min(1.0, self.facilitation + self.config.facilitation_u)
self.last_tick = tick
self.release_count += quanta
return self.weight * self.config.quantal_size * quanta
def to_dict(self) -> dict[str, Any]:
return {
"schema_version": 1,
"target_id": self.target_id,
"weight": self.weight,
"delay": self.delay,
"available_fraction": self.available_fraction,
"facilitation": self.facilitation,
"last_tick": self.last_tick,
"release_count": self.release_count,
"provenance": self.provenance_tag.to_dict(),
}
__all__ = ["QuantalSTPSynapse", "QuantalSynapseConfig"]