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"]