Skip to main content

sc_neurocore_engine/neurons/misc/
graded_synapse.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Graded Synapse Neuron Model
8
9//! Non-spiking interneuron with graded transmitter release.
10
11// ═══════════════════════════════════════════════════════════════════
12// Graded Synapse Neuron (non-spiking interneuron)
13// ═══════════════════════════════════════════════════════════════════
14
15/// Non-spiking interneuron with graded synaptic release.
16///
17/// Models interneurons that communicate via graded potential changes
18/// rather than action potentials (e.g., retinal bipolar/amacrine cells,
19/// C. elegans interneurons, crustacean stomatogastric neurons).
20///
21/// The membrane potential follows passive RC dynamics with saturation:
22///
23///   C dV/dt = -g_L(V - E_L) + g_in * I_ext
24///
25/// Transmitter release is a sigmoid function of V:
26///
27///   release = 1 / (1 + exp(-(V - V_half) / k_release))
28///
29/// A "spike" event is emitted when V crosses a threshold from below,
30/// representing a significant release event.
31///
32/// Roberts & Bush, J Comp Physiol A 185:549, 1999.
33#[derive(Clone, Debug)]
34pub struct GradedSynapseNeuron {
35    pub v: f64,           // Membrane potential (mV)
36    pub c_m: f64,         // Membrane capacitance (normalised)
37    pub g_l: f64,         // Leak conductance
38    pub e_l: f64,         // Leak reversal potential (mV)
39    pub g_in: f64,        // Input conductance scaling
40    pub v_half: f64,      // Release sigmoid half-activation (mV)
41    pub k_release: f64,   // Release sigmoid slope
42    pub v_min: f64,       // Saturation floor (mV)
43    pub v_max: f64,       // Saturation ceiling (mV)
44    pub v_threshold: f64, // "Spike" detection threshold (mV)
45    pub dt: f64,
46    pub gain: f64,
47}
48
49impl Default for GradedSynapseNeuron {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl GradedSynapseNeuron {
56    pub fn new() -> Self {
57        Self {
58            v: -60.0,
59            c_m: 1.0,
60            g_l: 0.05, // Moderate leak
61            e_l: -60.0,
62            g_in: 0.1,      // Input scaling
63            v_half: -40.0,  // Release kicks in at depolarised potential
64            k_release: 5.0, // Sigmoid slope
65            v_min: -80.0,
66            v_max: -10.0,
67            v_threshold: -35.0, // "Spike" threshold for pipeline
68            dt: 0.1,
69            gain: 1.0,
70        }
71    }
72
73    /// Returns the graded transmitter release level [0, 1].
74    pub fn release(&self) -> f64 {
75        1.0 / (1.0 + (-(self.v - self.v_half) / self.k_release).exp())
76    }
77
78    pub fn step(&mut self, current: f64) -> i32 {
79        let input = self.gain * current;
80        let v_prev = self.v;
81
82        let dv = (-self.g_l * (self.v - self.e_l) + self.g_in * input) / self.c_m;
83        self.v += self.dt * dv;
84
85        // Saturation bounds
86        self.v = self.v.clamp(self.v_min, self.v_max);
87        if !self.v.is_finite() {
88            self.v = self.e_l;
89        }
90
91        // Threshold crossing = significant release event
92        if self.v >= self.v_threshold && v_prev < self.v_threshold {
93            1
94        } else {
95            0
96        }
97    }
98
99    pub fn reset(&mut self) {
100        *self = Self::new();
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    // -- Graded Synapse Neuron tests --
109
110    #[test]
111    fn graded_depolarises_with_input() {
112        let mut n = GradedSynapseNeuron::new();
113        let v0 = n.v;
114        for _ in 0..10_000 {
115            n.step(200.0);
116        }
117        assert!(
118            n.v > v0,
119            "Must depolarise with positive input: v0={v0}, v={}",
120            n.v
121        );
122    }
123
124    #[test]
125    fn graded_hyperpolarises_with_negative_input() {
126        let mut n = GradedSynapseNeuron::new();
127        let v0 = n.v;
128        for _ in 0..10_000 {
129            n.step(-200.0);
130        }
131        assert!(
132            n.v < v0,
133            "Must hyperpolarise with negative input: v0={v0}, v={}",
134            n.v
135        );
136    }
137
138    #[test]
139    fn graded_fires_with_strong_input() {
140        let mut n = GradedSynapseNeuron::new();
141        let mut spikes = 0;
142        for _ in 0..50_000 {
143            spikes += n.step(500.0);
144        }
145        assert!(
146            spikes > 0,
147            "Must cross threshold with strong input, got {spikes}"
148        );
149    }
150
151    #[test]
152    fn graded_silent_without_input() {
153        let mut n = GradedSynapseNeuron::new();
154        let mut spikes = 0;
155        for _ in 0..50_000 {
156            spikes += n.step(0.0);
157        }
158        assert_eq!(
159            spikes, 0,
160            "Must be silent without input (V starts at E_L), got {spikes}"
161        );
162    }
163
164    #[test]
165    fn graded_release_monotonic() {
166        // Release should increase with depolarisation
167        let mut n = GradedSynapseNeuron::new();
168        n.v = -70.0;
169        let r_low = n.release();
170        n.v = -40.0;
171        let r_mid = n.release();
172        n.v = -10.0;
173        let r_high = n.release();
174        assert!(
175            r_low < r_mid && r_mid < r_high,
176            "Release must be monotonic: r_low={r_low:.3}, r_mid={r_mid:.3}, r_high={r_high:.3}"
177        );
178    }
179
180    #[test]
181    fn graded_release_bounded() {
182        let mut n = GradedSynapseNeuron::new();
183        n.v = -100.0;
184        assert!(n.release() >= 0.0 && n.release() <= 1.0);
185        n.v = 50.0;
186        assert!(n.release() >= 0.0 && n.release() <= 1.0);
187    }
188
189    #[test]
190    fn graded_v_saturates() {
191        let mut n = GradedSynapseNeuron::new();
192        for _ in 0..50_000 {
193            n.step(1e6);
194        }
195        assert!(
196            n.v <= n.v_max,
197            "V must not exceed v_max={}, got {}",
198            n.v_max,
199            n.v
200        );
201        n.reset();
202        for _ in 0..50_000 {
203            n.step(-1e6);
204        }
205        assert!(
206            n.v >= n.v_min,
207            "V must not go below v_min={}, got {}",
208            n.v_min,
209            n.v
210        );
211    }
212
213    #[test]
214    fn graded_nan_input_stays_finite() {
215        let mut n = GradedSynapseNeuron::new();
216        n.step(f64::NAN);
217        assert!(n.v.is_finite());
218    }
219
220    #[test]
221    fn graded_reset_clears_state() {
222        let mut n = GradedSynapseNeuron::new();
223        for _ in 0..10_000 {
224            n.step(500.0);
225        }
226        n.reset();
227        assert_eq!(n.v, -60.0);
228    }
229
230    #[test]
231    fn graded_performance_100k_steps() {
232        let start = std::time::Instant::now();
233        let mut n = GradedSynapseNeuron::new();
234        for _ in 0..100_000 {
235            std::hint::black_box(n.step(100.0));
236        }
237        let elapsed = start.elapsed();
238        assert!(
239            elapsed.as_millis() < 50,
240            "100k steps must complete in <50ms"
241        );
242    }
243
244    #[test]
245    fn graded_default_matches_constructor() {
246        let default = GradedSynapseNeuron::default();
247        let constructed = GradedSynapseNeuron::new();
248        assert_eq!(default.v, constructed.v);
249        assert_eq!(default.v_threshold, constructed.v_threshold);
250        assert_eq!(default.dt, constructed.dt);
251    }
252}