Skip to main content

sc_neurocore_engine/neurons/misc/
gap_junction.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 — Gap Junction Neuron Model
8
9//! Electrical-synapse neuron with voltage-dependent gap-junction coupling.
10
11// ═══════════════════════════════════════════════════════════════════
12// Gap Junction Neuron
13// ═══════════════════════════════════════════════════════════════════
14
15/// Neuron with electrical synapse (gap junction) coupling.
16///
17/// Models neurons coupled via connexin-based gap junctions that allow
18/// direct electrical current flow between cells. Found extensively in:
19/// - Inferior olive neurons (climbing fibre system)
20/// - Retinal ganglion cells (coupled networks)
21/// - Cortical interneuron networks (PV+ basket cell syncytia)
22/// - Thalamic reticular nucleus
23///
24/// Includes voltage-dependent rectification (Cx36 gating):
25///   g_eff = g_gap * g_inf(V_j)
26///   g_inf = g_min + (1 - g_min) / (1 + exp(A * (|V_j| - V_0)))
27///
28/// where V_j = V_neighbor - V is the transjunctional voltage,
29/// g_min is the residual conductance at large V_j, V_0 is the
30/// half-inactivation voltage (~30 mV for Cx36), and A is the
31/// voltage sensitivity (~0.1 mV⁻¹).
32///
33/// At small |V_j| < V_0: near-full conductance (bidirectional).
34/// At large |V_j| > V_0: conductance drops to g_min (rectification).
35///
36/// C dV/dt = -g_L(V - E_L) + g_eff * (V_neighbor - V) + I_tonic
37///
38/// Connors & Long, Annu Rev Neurosci 27:393, 2004.
39/// Vervaeke et al., Neuron 65:801, 2010 (Cx36 voltage gating).
40#[derive(Clone, Debug)]
41pub struct GapJunctionNeuron {
42    pub v: f64,       // Membrane potential (mV)
43    pub c_m: f64,     // Membrane capacitance
44    pub g_l: f64,     // Leak conductance
45    pub e_l: f64,     // Leak reversal (mV)
46    pub g_gap: f64,   // Maximal gap junction conductance
47    pub i_tonic: f64, // Tonic depolarising current
48    pub v_threshold: f64,
49    pub v_reset: f64,
50    pub refractory: f64, // Refractory period (ms)
51    pub refrac_timer: f64,
52    // Voltage-dependent rectification (Cx36)
53    pub rect_v0: f64,   // Half-inactivation voltage (mV), ~30 for Cx36
54    pub rect_a: f64,    // Voltage sensitivity (mV⁻¹), ~0.1 for Cx36
55    pub rect_gmin: f64, // Residual conductance fraction [0,1], ~0.1
56    pub dt: f64,
57    pub gain: f64,
58}
59
60impl Default for GapJunctionNeuron {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl GapJunctionNeuron {
67    pub fn new() -> Self {
68        Self {
69            v: -65.0,
70            c_m: 1.0,
71            g_l: 0.1,
72            e_l: -65.0,
73            g_gap: 0.15,  // Gap junction coupling (maximal)
74            i_tonic: 0.0, // No tonic drive by default
75            v_threshold: -50.0,
76            v_reset: -65.0,
77            refractory: 2.0, // 2 ms refractory
78            refrac_timer: 0.0,
79            rect_v0: 30.0,  // Cx36: half-inactivation at ~30 mV Vj
80            rect_a: 0.1,    // Cx36: voltage sensitivity
81            rect_gmin: 0.1, // Cx36: ~10% residual conductance
82            dt: 0.1,
83            gain: 1.0,
84        }
85    }
86
87    /// Voltage-dependent gap junction conductance (Cx36 gating).
88    ///
89    /// g_inf = g_min + (1 - g_min) / (1 + exp(A * (|V_j| - V_0)))
90    ///
91    /// Symmetric in |V_j|: rectification acts for both polarities.
92    #[inline]
93    fn rect_conductance(&self, v_j: f64) -> f64 {
94        self.rect_gmin
95            + (1.0 - self.rect_gmin) / (1.0 + (self.rect_a * (v_j.abs() - self.rect_v0)).exp())
96    }
97
98    pub fn step(&mut self, current: f64) -> i32 {
99        // current = mean neighbour voltage or external drive
100        let input = self.gain * current;
101
102        if self.refrac_timer > 0.0 {
103            self.refrac_timer -= self.dt;
104            return 0;
105        }
106
107        // Transjunctional voltage
108        let v_j = input - self.v;
109        // Voltage-dependent effective conductance
110        let g_eff = self.g_gap * self.rect_conductance(v_j);
111        let i_gap = g_eff * v_j;
112        let dv = (-self.g_l * (self.v - self.e_l) + i_gap + self.i_tonic) / self.c_m;
113        self.v += self.dt * dv;
114
115        // Safety
116        self.v = self.v.clamp(-100.0, 40.0);
117        if !self.v.is_finite() {
118            self.v = self.e_l;
119        }
120
121        // Spike
122        if self.v >= self.v_threshold {
123            self.v = self.v_reset;
124            self.refrac_timer = self.refractory;
125            return 1;
126        }
127        0
128    }
129
130    pub fn reset(&mut self) {
131        *self = Self::new();
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    // -- Gap Junction Neuron tests --
140
141    #[test]
142    fn gap_fires_with_depolarising_drive() {
143        // Input as V_neighbor = 0 mV (depolarised relative to -65 mV rest)
144        let mut n = GapJunctionNeuron::new();
145        let mut spikes = 0;
146        for _ in 0..50_000 {
147            spikes += n.step(0.0); // V_neighbor = 0 mV → depolarising
148        }
149        assert!(
150            spikes > 0,
151            "Gap junction must fire with depolarising drive, got {spikes}"
152        );
153    }
154
155    #[test]
156    fn gap_silent_at_rest() {
157        // Input = E_L → no gap junction current → silent
158        let mut n = GapJunctionNeuron::new();
159        let mut spikes = 0;
160        for _ in 0..50_000 {
161            spikes += n.step(-65.0); // V_neighbor = E_L → zero gap current
162        }
163        assert_eq!(
164            spikes, 0,
165            "Must be silent when V_neighbor = E_L, got {spikes}"
166        );
167    }
168
169    #[test]
170    fn gap_junction_pulls_toward_neighbor() {
171        // If V_neighbor > V, gap junction depolarises; if V_neighbor < V, hyperpolarises
172        let mut n = GapJunctionNeuron::new(); // V = -65
173        for _ in 0..5_000 {
174            n.step(-40.0);
175        } // V_neighbor = -40 → depolarising
176        assert!(
177            n.v > -65.0 || n.refrac_timer > 0.0,
178            "Gap junction must pull V toward neighbor: v={}",
179            n.v
180        );
181    }
182
183    #[test]
184    fn gap_stronger_coupling_more_spikes() {
185        let mut weak = GapJunctionNeuron::new();
186        weak.g_gap = 0.01;
187        let mut strong = GapJunctionNeuron::new();
188        strong.g_gap = 0.1;
189        let (mut sw, mut ss) = (0, 0);
190        for _ in 0..50_000 {
191            sw += weak.step(-20.0);
192            ss += strong.step(-20.0);
193        }
194        assert!(
195            ss >= sw,
196            "Stronger coupling → more spikes: strong={ss} vs weak={sw}"
197        );
198    }
199
200    #[test]
201    fn gap_refractory_enforced() {
202        let mut n = GapJunctionNeuron::new();
203        // Drive until first spike (V_neighbor = 0 → strong depolarising)
204        let mut first_spike_t = 0;
205        for t in 0..10_000 {
206            if n.step(0.0) == 1 {
207                first_spike_t = t;
208                break;
209            }
210        }
211        assert!(first_spike_t > 0, "Must spike");
212        // Next step should be in refractory
213        assert!(n.refrac_timer > 0.0, "Must be in refractory after spike");
214        assert_eq!(n.step(0.0), 0, "Must not spike during refractory");
215    }
216
217    #[test]
218    fn gap_hyperpolarising_drive_silent() {
219        // V_neighbor = -100 → strong hyperpolarising gap current
220        let mut n = GapJunctionNeuron::new();
221        let mut spikes = 0;
222        for _ in 0..50_000 {
223            spikes += n.step(-100.0);
224        }
225        assert_eq!(
226            spikes, 0,
227            "Hyperpolarising drive must keep silent, got {spikes}"
228        );
229    }
230
231    #[test]
232    fn gap_tonic_current_depolarises() {
233        let mut n = GapJunctionNeuron::new();
234        n.i_tonic = 5.0; // Strong tonic drive
235        let mut spikes = 0;
236        for _ in 0..50_000 {
237            spikes += n.step(-65.0); // No gap drive, but tonic current
238        }
239        assert!(
240            spikes > 0,
241            "Tonic current should produce spikes, got {spikes}"
242        );
243    }
244
245    #[test]
246    fn gap_nan_input_stays_finite() {
247        let mut n = GapJunctionNeuron::new();
248        n.step(f64::NAN);
249        assert!(n.v.is_finite());
250    }
251
252    #[test]
253    fn gap_reset_clears_state() {
254        let mut n = GapJunctionNeuron::new();
255        for _ in 0..10_000 {
256            n.step(-20.0);
257        }
258        n.reset();
259        assert_eq!(n.v, -65.0);
260        assert_eq!(n.refrac_timer, 0.0);
261    }
262
263    #[test]
264    fn gap_rectification_reduces_at_large_vj() {
265        // At large |Vj|, rectification should reduce effective conductance
266        let n = GapJunctionNeuron::new();
267        let g_small = n.rect_conductance(5.0); // |Vj|=5 mV (small)
268        let g_large = n.rect_conductance(60.0); // |Vj|=60 mV (large)
269        assert!(
270            g_small > g_large,
271            "Rectification must reduce g at large Vj: g(5)={g_small:.3} vs g(60)={g_large:.3}"
272        );
273        assert!(
274            g_large >= n.rect_gmin,
275            "Conductance must not drop below g_min={}: got {g_large:.3}",
276            n.rect_gmin
277        );
278    }
279
280    #[test]
281    fn gap_performance_100k_steps() {
282        let start = std::time::Instant::now();
283        let mut n = GapJunctionNeuron::new();
284        for _ in 0..100_000 {
285            std::hint::black_box(n.step(-20.0));
286        }
287        let elapsed = start.elapsed();
288        assert!(
289            elapsed.as_millis() < 50,
290            "100k steps must complete in <50ms"
291        );
292    }
293
294    #[test]
295    fn gap_default_matches_constructor() {
296        let default = GapJunctionNeuron::default();
297        let constructed = GapJunctionNeuron::new();
298        assert_eq!(default.v, constructed.v);
299        assert_eq!(default.g_gap, constructed.g_gap);
300        assert_eq!(default.rect_v0, constructed.rect_v0);
301    }
302}