Spaces:
Paused
Paused
File size: 3,785 Bytes
31226fd | 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 | """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"]
|