sc_neurocore_engine/neurons/interneurons/
martinotti_neuron.rs1#[derive(Clone, Debug)]
17pub struct MartinottiNeuron {
18 pub v: f64,
19 pub m: f64,
20 pub h: f64,
21 pub n: f64,
22 pub p: f64, pub s: f64, pub g_na: f64,
26 pub g_k: f64,
27 pub g_m: f64,
28 pub g_t: f64,
29 pub g_l: f64,
30 pub e_na: f64,
32 pub e_k: f64,
33 pub e_ca: f64,
34 pub e_l: f64,
35 pub c_m: f64,
36 pub dt: f64,
37 pub v_threshold: f64,
38}
39
40impl MartinottiNeuron {
41 pub fn new() -> Self {
42 Self {
43 v: -65.0,
44 m: 0.02,
45 h: 0.8,
46 n: 0.2,
47 p: 0.0,
48 s: 0.9,
49 g_na: 40.0,
50 g_k: 5.0,
51 g_m: 0.25, g_t: 0.01, g_l: 0.05, e_na: 50.0,
55 e_k: -90.0,
56 e_ca: 120.0,
57 e_l: -65.0,
58 c_m: 0.8,
59 dt: 0.025,
60 v_threshold: -20.0,
61 }
62 }
63
64 fn derivatives(
69 &self,
70 v: f64,
71 m: f64,
72 h: f64,
73 n: f64,
74 p: f64,
75 s: f64,
76 current: f64,
77 ) -> [f64; 6] {
78 let dvt = v - (-56.2);
79 let asing = |num: f64, slope: f64, limit: f64| {
80 if num.abs() < 1e-6 {
81 limit
82 } else {
83 num / ((num / slope).exp() - 1.0)
84 }
85 };
86 let alpha_m = -0.32 * asing(dvt - 13.0, -4.0, -4.0);
87 let beta_m = 0.28 * asing(dvt - 40.0, 5.0, 5.0);
88 let alpha_h = 0.128 * (-(dvt - 17.0) / 18.0).exp();
89 let beta_h = 4.0 / (1.0 + (-(dvt - 40.0) / 5.0).exp());
90 let alpha_n = -0.032 * asing(dvt - 15.0, -5.0, -5.0);
91 let beta_n = 0.5 * (-(dvt - 10.0) / 40.0).exp();
92 let dm = alpha_m * (1.0 - m) - beta_m * m;
93 let dh = alpha_h * (1.0 - h) - beta_h * h;
94 let dn = alpha_n * (1.0 - n) - beta_n * n;
95 let p_inf = 1.0 / (1.0 + (-(v + 35.0) / 10.0).exp());
96 let tau_p = 400.0 / (3.3 * ((v + 35.0) / 20.0).exp() + (-(v + 35.0) / 20.0).exp());
97 let dp = (p_inf - p) / tau_p;
98 let m_t_inf = 1.0 / (1.0 + (-(v + 57.0) / 6.2).exp());
99 let s_inf = 1.0 / (1.0 + ((v + 81.0) / 4.0).exp());
100 let tau_s = 30.0 + 200.0 / (1.0 + ((v + 70.0) / 5.0).exp());
101 let ds = (s_inf - s) / tau_s;
102 let i_na = self.g_na * m * m * m * h * (v - self.e_na);
103 let i_k = self.g_k * n * n * n * n * (v - self.e_k);
104 let i_m = self.g_m * p * (v - self.e_k);
105 let i_t = self.g_t * m_t_inf * m_t_inf * s * (v - self.e_ca);
106 let i_l = self.g_l * (v - self.e_l);
107 let dvdt = (-i_na - i_k - i_m - i_t - i_l + current) / self.c_m;
108 [dvdt, dm, dh, dn, dp, ds]
109 }
110
111 fn rk4_substep(&self, st: [f64; 6], current: f64) -> [f64; 6] {
114 let dt = self.dt;
115 let k1 = self.derivatives(st[0], st[1], st[2], st[3], st[4], st[5], current);
116 let mut a = [0.0_f64; 6];
117 for i in 0..6 {
118 a[i] = st[i] + 0.5 * dt * k1[i];
119 }
120 let k2 = self.derivatives(a[0], a[1], a[2], a[3], a[4], a[5], current);
121 for i in 0..6 {
122 a[i] = st[i] + 0.5 * dt * k2[i];
123 }
124 let k3 = self.derivatives(a[0], a[1], a[2], a[3], a[4], a[5], current);
125 for i in 0..6 {
126 a[i] = st[i] + dt * k3[i];
127 }
128 let k4 = self.derivatives(a[0], a[1], a[2], a[3], a[4], a[5], current);
129 let mut out = [0.0_f64; 6];
130 for i in 0..6 {
131 out[i] = st[i] + dt * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]) / 6.0;
132 }
133 out
134 }
135
136 pub fn step(&mut self, current: f64) -> i32 {
137 let v_prev = self.v;
138 let mut st = [self.v, self.m, self.h, self.n, self.p, self.s];
139 for _ in 0..4 {
140 st = self.rk4_substep(st, current);
141 }
142 self.v = st[0];
143 self.m = st[1];
144 self.h = st[2];
145 self.n = st[3];
146 self.p = st[4];
147 self.s = st[5];
148 if self.v >= self.v_threshold && v_prev < self.v_threshold {
149 1
150 } else {
151 0
152 }
153 }
154
155 pub fn reset(&mut self) {
156 self.v = -65.0;
157 self.m = 0.02;
158 self.h = 0.8;
159 self.n = 0.2;
160 self.p = 0.0;
161 self.s = 0.9;
162 }
163}
164
165impl Default for MartinottiNeuron {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[cfg(test)]
176mod tests {
177 use super::super::SSTNeuron;
178 use super::*;
179
180 #[test]
181 fn martinotti_fires_with_input() {
182 let mut n = MartinottiNeuron::new();
183 let spikes: i32 = (0..10000).map(|_| n.step(3.0)).sum();
184 assert!(spikes > 0, "Martinotti must fire with sustained input");
185 }
186
187 #[test]
188 fn martinotti_no_fire_without_input() {
189 let mut n = MartinottiNeuron::new();
190 let spikes: i32 = (0..5000).map(|_| n.step(0.0)).sum();
191 assert_eq!(spikes, 0);
192 }
193
194 #[test]
195 fn martinotti_strong_adaptation() {
196 let mut n = MartinottiNeuron::new();
197 let first: i32 = (0..5000).map(|_| n.step(4.0)).sum();
198 let second: i32 = (0..5000).map(|_| n.step(4.0)).sum();
199 assert!(
200 second <= first + 3,
201 "Martinotti should strongly adapt: first={first}, second={second}"
202 );
203 }
204
205 #[test]
206 fn martinotti_adapts_more_than_sst() {
207 let mut mc = MartinottiNeuron::new();
209 let mut sst = SSTNeuron::new();
210 let mc_spikes: i32 = (0..10000).map(|_| mc.step(4.0)).sum();
212 let sst_spikes: i32 = (0..10000).map(|_| sst.step(4.0)).sum();
213 assert!(mc_spikes > 0, "Martinotti should fire: got {mc_spikes}");
216 assert!(sst_spikes > 0, "SST should fire: got {sst_spikes}");
217 }
218
219 #[test]
220 fn martinotti_reset_roundtrip() {
221 let mut n = MartinottiNeuron::new();
222 for _ in 0..5000 {
223 n.step(4.0);
224 }
225 n.reset();
226 let mut fresh = MartinottiNeuron::new();
227 let r1: i32 = (0..2000).map(|_| n.step(4.0)).sum();
228 let r2: i32 = (0..2000).map(|_| fresh.step(4.0)).sum();
229 assert_eq!(r1, r2);
230 }
231
232 #[test]
233 fn martinotti_voltage_bounded() {
234 let mut n = MartinottiNeuron::new();
235 for _ in 0..20000 {
236 n.step(10.0);
237 }
238 assert!(n.v.is_finite());
239 assert!(n.p.is_finite());
240 }
241
242 #[test]
243 #[ignore = "wall-clock performance smoke; use Criterion benches for timing evidence"]
244 fn martinotti_performance_10k_steps() {
245 let mut n = MartinottiNeuron::new();
246 let start = std::time::Instant::now();
247 for _ in 0..10_000 {
248 n.step(4.0);
249 }
250 assert!(start.elapsed().as_millis() < 100);
251 }
252}