Skip to main content

sc_neurocore_engine/neurons/
hardware.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later | Commercial license available
2// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
3// © Code 2020–2026 Miroslav Šotek. All rights reserved.
4// ORCID: 0009-0009-3560-0851
5// Contact: www.anulum.li | protoscience@anulum.li
6// SC-NeuroCore — Hardware neuromorphic chip emulators
7
8//! Hardware neuromorphic chip emulators.
9
10/// Loihi CUBA LIF — Intel Loihi 1 fixed-point neuron. Davies et al. 2018.
11#[derive(Clone, Debug)]
12pub struct LoihiCUBANeuron {
13    pub v: i32,
14    pub u: i32,
15    pub tau_v: i32,
16    pub tau_u: i32,
17    pub v_threshold: i32,
18    pub v_reset: i32,
19}
20
21impl LoihiCUBANeuron {
22    pub fn new() -> Self {
23        Self {
24            v: 0,
25            u: 0,
26            tau_v: 10,
27            tau_u: 5,
28            v_threshold: 1000,
29            v_reset: 0,
30        }
31    }
32    pub fn step(&mut self, weighted_input: i32) -> i32 {
33        self.u = self.u - self.u / self.tau_u + weighted_input;
34        self.v = self.v - self.v / self.tau_v + self.u;
35        if self.v >= self.v_threshold {
36            self.v = self.v_reset;
37            1
38        } else {
39            0
40        }
41    }
42    pub fn reset(&mut self) {
43        self.v = 0;
44        self.u = 0;
45    }
46}
47impl Default for LoihiCUBANeuron {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53/// Loihi 2 — Intel Loihi 2 three-state integer neuron.
54#[derive(Clone, Debug)]
55pub struct Loihi2Neuron {
56    pub s1: i32,
57    pub s2: i32,
58    pub s3: i32,
59    pub tau1: i32,
60    pub tau2: i32,
61    pub tau3: i32,
62    pub w12: i32,
63    pub w13: i32,
64    pub w23: i32,
65    pub s1_threshold: i32,
66    pub s1_reset: i32,
67    pub s3_incr: i32,
68}
69
70impl Loihi2Neuron {
71    pub fn new() -> Self {
72        Self {
73            s1: 0,
74            s2: 0,
75            s3: 0,
76            tau1: 10,
77            tau2: 5,
78            tau3: 50,
79            w12: 1,
80            w13: 0,
81            w23: 0,
82            s1_threshold: 1000,
83            s1_reset: 0,
84            s3_incr: 10,
85        }
86    }
87    pub fn step(&mut self, weighted_input: i32) -> i32 {
88        self.s2 = self.s2 - (self.s2 >> self.tau2.max(1)) + self.w12 * self.s1;
89        self.s3 = self.s3 - (self.s3 >> self.tau3.max(1)) + self.w13 * self.s1 + self.w23 * self.s2;
90        self.s1 = self.s1 - (self.s1 >> self.tau1.max(1)) + self.s2 + weighted_input;
91        if self.s1 >= self.s1_threshold {
92            self.s1 = self.s1_reset;
93            self.s3 += self.s3_incr;
94            1
95        } else {
96            0
97        }
98    }
99    pub fn reset(&mut self) {
100        self.s1 = 0;
101        self.s2 = 0;
102        self.s3 = 0;
103    }
104}
105impl Default for Loihi2Neuron {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111/// TrueNorth — IBM TrueNorth digital crossbar neuron. Merolla et al. 2014.
112#[derive(Clone, Debug)]
113pub struct TrueNorthNeuron {
114    pub v: i32,
115    pub leak: i32,
116    pub threshold: i32,
117    pub v_reset: i32,
118}
119
120impl TrueNorthNeuron {
121    pub fn new(threshold: i32) -> Self {
122        Self {
123            v: 0,
124            leak: 0,
125            threshold,
126            v_reset: 0,
127        }
128    }
129    pub fn step(&mut self, weighted_input: i32) -> i32 {
130        self.v += weighted_input - self.leak;
131        if self.v >= self.threshold {
132            self.v = self.v_reset;
133            1
134        } else {
135            0
136        }
137    }
138    pub fn reset(&mut self) {
139        self.v = 0;
140    }
141}
142impl Default for TrueNorthNeuron {
143    fn default() -> Self {
144        Self::new(100)
145    }
146}
147
148/// BrainScaleS AdEx — Heidelberg analog wafer-scale. Schemmel et al. 2010.
149#[derive(Clone, Debug)]
150pub struct BrainScaleSAdExNeuron {
151    pub v: f64,
152    pub w: f64,
153    pub v_rest: f64,
154    pub v_reset: f64,
155    pub v_threshold: f64,
156    pub delta_t: f64,
157    pub v_rh: f64,
158    pub tau: f64,
159    pub tau_w: f64,
160    pub a: f64,
161    pub b: f64,
162    pub hw_speedup: f64,
163    pub dt: f64,
164}
165
166impl BrainScaleSAdExNeuron {
167    pub fn new() -> Self {
168        Self {
169            v: -65.0,
170            w: 0.0,
171            v_rest: -65.0,
172            v_reset: -68.0,
173            v_threshold: -50.0,
174            delta_t: 2.0,
175            v_rh: -55.0,
176            tau: 20.0,
177            tau_w: 100.0,
178            a: 0.5,
179            b: 7.0,
180            hw_speedup: 1000.0,
181            dt: 0.1,
182        }
183    }
184    pub fn step(&mut self, current: f64) -> i32 {
185        let exp_arg = ((self.v - self.v_rh) / self.delta_t).clamp(-20.0, 20.0);
186        let exp_term = self.delta_t * exp_arg.exp();
187        let dv = (-(self.v - self.v_rest) + exp_term - self.w + current) / self.tau * self.dt;
188        let dw = (self.a * (self.v - self.v_rest) - self.w) / self.tau_w * self.dt;
189        self.v += dv;
190        self.w += dw;
191        if self.v >= self.v_threshold {
192            self.v = self.v_reset;
193            self.w += self.b;
194            1
195        } else {
196            0
197        }
198    }
199    pub fn reset(&mut self) {
200        self.v = self.v_rest;
201        self.w = 0.0;
202    }
203}
204impl Default for BrainScaleSAdExNeuron {
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210/// SpiNNaker LIF — ARM Cortex-M4 digital LIF with refractory. Furber et al. 2014.
211#[derive(Clone, Debug)]
212pub struct SpiNNakerLIFNeuron {
213    pub v: f64,
214    pub v_rest: f64,
215    pub v_reset: f64,
216    pub v_threshold: f64,
217    pub tau_m: f64,
218    pub i_offset: f64,
219    pub tau_refrac: f64,
220    pub refrac_count: f64,
221    pub dt: f64,
222}
223
224impl SpiNNakerLIFNeuron {
225    pub fn new() -> Self {
226        Self {
227            v: -70.0,
228            v_rest: -70.0,
229            v_reset: -70.0,
230            v_threshold: -50.0,
231            tau_m: 20.0,
232            i_offset: 0.0,
233            tau_refrac: 2.0,
234            refrac_count: 0.0,
235            dt: 1.0,
236        }
237    }
238    pub fn step(&mut self, current: f64) -> i32 {
239        if self.refrac_count > 0.0 {
240            self.refrac_count -= self.dt;
241            return 0;
242        }
243        self.v += (-(self.v - self.v_rest) + current + self.i_offset) / self.tau_m * self.dt;
244        if self.v >= self.v_threshold {
245            self.v = self.v_reset;
246            self.refrac_count = self.tau_refrac;
247            1
248        } else {
249            0
250        }
251    }
252    pub fn reset(&mut self) {
253        self.v = self.v_rest;
254        self.refrac_count = 0.0;
255    }
256}
257impl Default for SpiNNakerLIFNeuron {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263/// SpiNNaker2 — TU Dresden ARM Cortex-M4F fixed-point LIF.
264#[derive(Clone, Debug)]
265pub struct SpiNNaker2Neuron {
266    pub v: i32,
267    pub v_rest: i32,
268    pub v_reset: i32,
269    pub v_threshold: i32,
270    pub decay_mult: i32,
271    pub decay_shift: i32,
272    pub refrac_steps: i32,
273    pub refrac_count: i32,
274}
275
276impl SpiNNaker2Neuron {
277    pub fn new() -> Self {
278        Self {
279            v: 0,
280            v_rest: 0,
281            v_reset: 0,
282            v_threshold: 1024,
283            decay_mult: 243,
284            decay_shift: 8,
285            refrac_steps: 2,
286            refrac_count: 0,
287        }
288    }
289    pub fn step(&mut self, current: i32) -> i32 {
290        if self.refrac_count > 0 {
291            self.refrac_count -= 1;
292            return 0;
293        }
294        self.v = ((self.v - self.v_rest).wrapping_mul(self.decay_mult) >> self.decay_shift)
295            + self.v_rest
296            + current;
297        if self.v >= self.v_threshold {
298            self.v = self.v_reset;
299            self.refrac_count = self.refrac_steps;
300            1
301        } else {
302            0
303        }
304    }
305    pub fn reset(&mut self) {
306        self.v = self.v_rest;
307        self.refrac_count = 0;
308    }
309}
310impl Default for SpiNNaker2Neuron {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316/// DPI — Differential Pair Integrator (log-domain analog). Bartolozzi & Indiveri 2007.
317#[derive(Clone, Debug)]
318pub struct DPINeuron {
319    pub i_mem: f64,
320    pub i_threshold: f64,
321    pub i_reset: f64,
322    pub i_leak: f64,
323    pub tau: f64,
324    pub gain: f64,
325    pub dt: f64,
326}
327
328impl DPINeuron {
329    pub fn new() -> Self {
330        Self {
331            i_mem: 0.0,
332            i_threshold: 1.0,
333            i_reset: 0.0,
334            i_leak: 0.01,
335            tau: 20.0,
336            gain: 1.0,
337            dt: 1.0,
338        }
339    }
340    pub fn step(&mut self, i_syn: f64) -> i32 {
341        self.i_mem += (-self.i_mem + self.gain * i_syn + self.i_leak) / self.tau * self.dt;
342        if self.i_mem >= self.i_threshold {
343            self.i_mem = self.i_reset;
344            1
345        } else {
346            0
347        }
348    }
349    pub fn reset(&mut self) {
350        self.i_mem = 0.0;
351    }
352}
353impl Default for DPINeuron {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359/// Akida — BrainChip event-domain rank-order neuron.
360#[derive(Clone, Debug)]
361pub struct AkidaNeuron {
362    pub v: i32,
363    pub threshold: i32,
364    pub modulation: f64,
365    pub rank: i32,
366    pub spiked: bool,
367}
368
369impl AkidaNeuron {
370    pub fn new(threshold: i32) -> Self {
371        Self {
372            v: 0,
373            threshold,
374            modulation: 0.75,
375            rank: 0,
376            spiked: false,
377        }
378    }
379    pub fn step(&mut self, weight: i32) -> i32 {
380        if self.spiked {
381            return 0;
382        }
383        self.v += (weight as f64 * self.modulation.powi(self.rank)) as i32;
384        self.rank += 1;
385        if self.v >= self.threshold {
386            self.spiked = true;
387            1
388        } else {
389            0
390        }
391    }
392    pub fn reset(&mut self) {
393        self.v = 0;
394        self.rank = 0;
395        self.spiked = false;
396    }
397}
398impl Default for AkidaNeuron {
399    fn default() -> Self {
400        Self::new(100)
401    }
402}
403
404/// NeuroGrid — Boahen 2014 subthreshold analog 2-compartment.
405#[derive(Clone, Debug)]
406pub struct NeuroGridNeuron {
407    pub v_s: f64,
408    pub v_d: f64,
409    pub tau_s: f64,
410    pub tau_d: f64,
411    pub g_c: f64,
412    pub delta_t: f64,
413    pub v_rest: f64,
414    pub v_threshold: f64,
415    pub v_peak: f64,
416    pub v_reset: f64,
417    pub dt: f64,
418}
419
420impl NeuroGridNeuron {
421    pub fn new() -> Self {
422        Self {
423            v_s: -65.0,
424            v_d: -65.0,
425            tau_s: 20.0,
426            tau_d: 50.0,
427            g_c: 0.5,
428            delta_t: 2.0,
429            v_rest: -65.0,
430            v_threshold: -50.0,
431            v_peak: 20.0,
432            v_reset: -65.0,
433            dt: 0.1,
434        }
435    }
436    pub fn step(&mut self, current: f64) -> i32 {
437        let dv_d =
438            (-(self.v_d - self.v_rest) + current - self.g_c * (self.v_d - self.v_s)) / self.tau_d;
439        self.v_d += dv_d * self.dt;
440        let exp_arg = ((self.v_s - self.v_threshold) / self.delta_t).min(20.0);
441        let exp_term = self.delta_t * exp_arg.exp();
442        let dv_s =
443            (-(self.v_s - self.v_rest) + exp_term + self.g_c * (self.v_d - self.v_s)) / self.tau_s;
444        self.v_s += dv_s * self.dt;
445        if self.v_s >= self.v_peak {
446            self.v_s = self.v_reset;
447            1
448        } else {
449            0
450        }
451    }
452    pub fn reset(&mut self) {
453        self.v_s = -65.0;
454        self.v_d = -65.0;
455    }
456}
457impl Default for NeuroGridNeuron {
458    fn default() -> Self {
459        Self::new()
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn loihi_cuba_fires() {
469        let mut n = LoihiCUBANeuron::new();
470        let t: i32 = (0..200).map(|_| n.step(100)).sum();
471        assert!(t > 0);
472    }
473    #[test]
474    fn loihi2_fires() {
475        let mut n = Loihi2Neuron {
476            tau3: 8,
477            ..Loihi2Neuron::new()
478        };
479        let t: i32 = (0..500).map(|_| n.step(200)).sum();
480        assert!(t > 0);
481    }
482    #[test]
483    fn truenorth_fires() {
484        let mut n = TrueNorthNeuron::default();
485        let t: i32 = (0..10).map(|_| n.step(50)).sum();
486        assert!(t > 0);
487    }
488    #[test]
489    fn brainscales_fires() {
490        let mut n = BrainScaleSAdExNeuron::new();
491        let t: i32 = (0..2000).map(|_| n.step(500.0)).sum();
492        assert!(t > 0);
493    }
494    #[test]
495    fn spinnaker_fires() {
496        let mut n = SpiNNakerLIFNeuron::new();
497        let t: i32 = (0..200).map(|_| n.step(30.0)).sum();
498        assert!(t > 0);
499    }
500    #[test]
501    fn spinnaker2_fires() {
502        let mut n = SpiNNaker2Neuron::new();
503        let t: i32 = (0..200).map(|_| n.step(100)).sum();
504        assert!(t > 0);
505    }
506    #[test]
507    fn dpi_fires() {
508        let mut n = DPINeuron::new();
509        let t: i32 = (0..100).map(|_| n.step(1.0)).sum();
510        assert!(t > 0);
511    }
512    #[test]
513    fn akida_fires() {
514        let mut n = AkidaNeuron::default();
515        let t: i32 = (0..10).map(|_| n.step(50)).sum();
516        assert!(t > 0);
517    }
518    #[test]
519    fn neurogrid_fires() {
520        let mut n = NeuroGridNeuron::new();
521        let t: i32 = (0..2000).map(|_| n.step(500.0)).sum();
522        assert!(t > 0);
523    }
524}