Skip to main content

sc_neurocore_engine/neurons/
hardware.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 — Hardware neuromorphic chip emulators
8
9//! Hardware neuromorphic chip emulators.
10
11/// Loihi CUBA LIF — Intel Loihi 1 fixed-point neuron. Davies et al. 2018.
12#[derive(Clone, Debug)]
13pub struct LoihiCUBANeuron {
14    pub v: i32,
15    pub u: i32,
16    pub tau_v: i32,
17    pub tau_u: i32,
18    pub v_threshold: i32,
19    pub v_reset: i32,
20}
21
22impl LoihiCUBANeuron {
23    pub fn new() -> Self {
24        Self {
25            v: 0,
26            u: 0,
27            tau_v: 10,
28            tau_u: 5,
29            v_threshold: 1000,
30            v_reset: 0,
31        }
32    }
33    pub fn step(&mut self, weighted_input: i32) -> i32 {
34        self.u = self.u - self.u / self.tau_u + weighted_input;
35        self.v = self.v - self.v / self.tau_v + self.u;
36        if self.v >= self.v_threshold {
37            self.v = self.v_reset;
38            1
39        } else {
40            0
41        }
42    }
43    pub fn reset(&mut self) {
44        self.v = 0;
45        self.u = 0;
46    }
47}
48impl Default for LoihiCUBANeuron {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54/// Loihi 2 — Intel Loihi 2 three-state integer neuron.
55#[derive(Clone, Debug)]
56pub struct Loihi2Neuron {
57    pub s1: i32,
58    pub s2: i32,
59    pub s3: i32,
60    pub tau1: i32,
61    pub tau2: i32,
62    pub tau3: i32,
63    pub w12: i32,
64    pub w13: i32,
65    pub w23: i32,
66    pub s1_threshold: i32,
67    pub s1_reset: i32,
68    pub s3_incr: i32,
69}
70
71impl Loihi2Neuron {
72    pub fn new() -> Self {
73        Self {
74            s1: 0,
75            s2: 0,
76            s3: 0,
77            tau1: 10,
78            tau2: 5,
79            tau3: 50,
80            w12: 1,
81            w13: 0,
82            w23: 0,
83            s1_threshold: 1000,
84            s1_reset: 0,
85            s3_incr: 10,
86        }
87    }
88    pub fn step(&mut self, weighted_input: i32) -> i32 {
89        self.s3 -= self.s3 / self.tau3;
90        self.s2 = self.s2 - self.s2 / self.tau2 + weighted_input + self.w23 * self.s3;
91        self.s1 = self.s1 - self.s1 / self.tau1 + self.w12 * self.s2 + self.w13 * self.s3;
92        if self.s1 >= self.s1_threshold {
93            self.s1 = self.s1_reset;
94            self.s3 += self.s3_incr;
95            1
96        } else {
97            0
98        }
99    }
100    pub fn reset(&mut self) {
101        self.s1 = 0;
102        self.s2 = 0;
103        self.s3 = 0;
104    }
105}
106impl Default for Loihi2Neuron {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112/// TrueNorth — IBM TrueNorth digital crossbar neuron. Merolla et al. 2014.
113#[derive(Clone, Debug)]
114pub struct TrueNorthNeuron {
115    pub v: i32,
116    pub leak: i32,
117    pub threshold: i32,
118    pub v_reset: i32,
119}
120
121impl TrueNorthNeuron {
122    pub fn new(threshold: i32) -> Self {
123        Self {
124            v: 0,
125            leak: 0,
126            threshold,
127            v_reset: 0,
128        }
129    }
130    pub fn step(&mut self, weighted_input: i32) -> i32 {
131        self.v += weighted_input - self.leak;
132        if self.v >= self.threshold {
133            self.v = self.v_reset;
134            1
135        } else {
136            0
137        }
138    }
139    pub fn reset(&mut self) {
140        self.v = 0;
141    }
142}
143impl Default for TrueNorthNeuron {
144    fn default() -> Self {
145        Self::new(100)
146    }
147}
148
149/// BrainScaleS AdEx — Heidelberg analog wafer-scale. Schemmel et al. 2010.
150#[derive(Clone, Debug)]
151pub struct BrainScaleSAdExNeuron {
152    pub v: f64,
153    pub w: f64,
154    pub v_rest: f64,
155    pub v_reset: f64,
156    pub v_threshold: f64,
157    pub delta_t: f64,
158    pub v_rh: f64,
159    pub tau: f64,
160    pub tau_w: f64,
161    pub a: f64,
162    pub b: f64,
163    pub hw_speedup: f64,
164    pub dt: f64,
165}
166
167impl BrainScaleSAdExNeuron {
168    pub fn new() -> Self {
169        Self {
170            v: -65.0,
171            w: 0.0,
172            v_rest: -65.0,
173            v_reset: -68.0,
174            v_threshold: -50.0,
175            delta_t: 2.0,
176            v_rh: -55.0,
177            tau: 20.0,
178            tau_w: 100.0,
179            a: 0.5,
180            b: 7.0,
181            hw_speedup: 1000.0,
182            dt: 0.1,
183        }
184    }
185    pub fn step(&mut self, current: f64) -> i32 {
186        let exp_arg = ((self.v - self.v_rh) / self.delta_t).clamp(-20.0, 20.0);
187        let exp_term = self.delta_t * exp_arg.exp();
188        let dv = (-(self.v - self.v_rest) + exp_term - self.w + current) / self.tau * self.dt;
189        let dw = (self.a * (self.v - self.v_rest) - self.w) / self.tau_w * self.dt;
190        self.v += dv;
191        self.w += dw;
192        if self.v >= self.v_threshold {
193            self.v = self.v_reset;
194            self.w += self.b;
195            1
196        } else {
197            0
198        }
199    }
200    pub fn reset(&mut self) {
201        self.v = self.v_rest;
202        self.w = 0.0;
203    }
204}
205impl Default for BrainScaleSAdExNeuron {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211/// SpiNNaker LIF — ARM Cortex-M4 digital LIF with refractory. Furber et al. 2014.
212#[derive(Clone, Debug)]
213pub struct SpiNNakerLIFNeuron {
214    pub v: f64,
215    pub v_rest: f64,
216    pub v_reset: f64,
217    pub v_threshold: f64,
218    pub tau_m: f64,
219    pub i_offset: f64,
220    pub tau_refrac: f64,
221    pub refrac_count: f64,
222    pub dt: f64,
223}
224
225impl SpiNNakerLIFNeuron {
226    pub fn new() -> Self {
227        Self {
228            v: -70.0,
229            v_rest: -70.0,
230            v_reset: -70.0,
231            v_threshold: -50.0,
232            tau_m: 20.0,
233            i_offset: 0.0,
234            tau_refrac: 2.0,
235            refrac_count: 0.0,
236            dt: 1.0,
237        }
238    }
239    pub fn step(&mut self, current: f64) -> i32 {
240        if self.refrac_count > 0.0 {
241            self.refrac_count -= self.dt;
242            return 0;
243        }
244        self.v += (-(self.v - self.v_rest) + current + self.i_offset) / self.tau_m * self.dt;
245        if self.v >= self.v_threshold {
246            self.v = self.v_reset;
247            self.refrac_count = self.tau_refrac;
248            1
249        } else {
250            0
251        }
252    }
253    pub fn reset(&mut self) {
254        self.v = self.v_rest;
255        self.refrac_count = 0.0;
256    }
257}
258impl Default for SpiNNakerLIFNeuron {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264/// SpiNNaker2 — TU Dresden ARM Cortex-M4F fixed-point LIF.
265#[derive(Clone, Debug)]
266pub struct SpiNNaker2Neuron {
267    pub v: i32,
268    pub v_rest: i32,
269    pub v_reset: i32,
270    pub v_threshold: i32,
271    pub decay_mult: i32,
272    pub decay_shift: i32,
273    pub refrac_steps: i32,
274    pub refrac_count: i32,
275}
276
277impl SpiNNaker2Neuron {
278    pub fn new() -> Self {
279        Self {
280            v: 0,
281            v_rest: 0,
282            v_reset: 0,
283            v_threshold: 1024,
284            decay_mult: 243,
285            decay_shift: 8,
286            refrac_steps: 2,
287            refrac_count: 0,
288        }
289    }
290    pub fn step(&mut self, current: i32) -> i32 {
291        if self.refrac_count > 0 {
292            self.refrac_count -= 1;
293            return 0;
294        }
295        self.v = ((self.v - self.v_rest).wrapping_mul(self.decay_mult) >> self.decay_shift)
296            + self.v_rest
297            + current;
298        if self.v >= self.v_threshold {
299            self.v = self.v_reset;
300            self.refrac_count = self.refrac_steps;
301            1
302        } else {
303            0
304        }
305    }
306    pub fn reset(&mut self) {
307        self.v = self.v_rest;
308        self.refrac_count = 0;
309    }
310}
311impl Default for SpiNNaker2Neuron {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317/// DPI — Differential Pair Integrator (log-domain analog). Bartolozzi & Indiveri 2007.
318#[derive(Clone, Debug)]
319pub struct DPINeuron {
320    pub i_mem: f64,
321    pub i_threshold: f64,
322    pub i_reset: f64,
323    pub i_leak: f64,
324    pub tau: f64,
325    pub gain: f64,
326    pub dt: f64,
327}
328
329impl DPINeuron {
330    pub fn new() -> Self {
331        Self {
332            i_mem: 0.0,
333            i_threshold: 1.0,
334            i_reset: 0.0,
335            i_leak: 0.01,
336            tau: 20.0,
337            gain: 1.0,
338            dt: 1.0,
339        }
340    }
341    pub fn step(&mut self, i_syn: f64) -> i32 {
342        self.i_mem += (-self.i_mem + self.gain * i_syn + self.i_leak) / self.tau * self.dt;
343        if self.i_mem >= self.i_threshold {
344            self.i_mem = self.i_reset;
345            1
346        } else {
347            0
348        }
349    }
350    pub fn reset(&mut self) {
351        self.i_mem = 0.0;
352    }
353}
354impl Default for DPINeuron {
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360/// Akida — BrainChip event-domain rank-order neuron.
361#[derive(Clone, Debug)]
362pub struct AkidaNeuron {
363    pub v: i32,
364    pub threshold: i32,
365    pub modulation: f64,
366    pub rank: i32,
367    pub spiked: bool,
368}
369
370impl AkidaNeuron {
371    pub fn new(threshold: i32) -> Self {
372        Self {
373            v: 0,
374            threshold,
375            modulation: 0.75,
376            rank: 0,
377            spiked: false,
378        }
379    }
380    pub fn step(&mut self, weight: f64) -> i32 {
381        if self.spiked {
382            return 0;
383        }
384        self.v += (weight * self.modulation.powi(self.rank)) as i32;
385        self.rank += 1;
386        if self.v >= self.threshold {
387            self.spiked = true;
388            1
389        } else {
390            0
391        }
392    }
393    pub fn reset(&mut self) {
394        self.v = 0;
395        self.rank = 0;
396        self.spiked = false;
397    }
398}
399impl Default for AkidaNeuron {
400    fn default() -> Self {
401        Self::new(100)
402    }
403}
404
405/// NeuroGrid — Boahen 2014 subthreshold analog 2-compartment.
406#[derive(Clone, Debug)]
407pub struct NeuroGridNeuron {
408    pub v_s: f64,
409    pub v_d: f64,
410    pub tau_s: f64,
411    pub tau_d: f64,
412    pub g_c: f64,
413    pub delta_t: f64,
414    pub v_rest: f64,
415    pub v_threshold: f64,
416    pub v_peak: f64,
417    pub v_reset: f64,
418    pub dt: f64,
419}
420
421impl NeuroGridNeuron {
422    pub fn new() -> Self {
423        Self {
424            v_s: -65.0,
425            v_d: -65.0,
426            tau_s: 20.0,
427            tau_d: 50.0,
428            g_c: 0.5,
429            delta_t: 2.0,
430            v_rest: -65.0,
431            v_threshold: -50.0,
432            v_peak: 20.0,
433            v_reset: -65.0,
434            dt: 0.1,
435        }
436    }
437    pub fn step(&mut self, current: f64) -> i32 {
438        let dv_d =
439            (-(self.v_d - self.v_rest) + current - self.g_c * (self.v_d - self.v_s)) / self.tau_d;
440        self.v_d += dv_d * self.dt;
441        let exp_arg = ((self.v_s - self.v_threshold) / self.delta_t).min(20.0);
442        let exp_term = self.delta_t * exp_arg.exp();
443        let dv_s =
444            (-(self.v_s - self.v_rest) + exp_term + self.g_c * (self.v_d - self.v_s)) / self.tau_s;
445        self.v_s += dv_s * self.dt;
446        if self.v_s >= self.v_peak {
447            self.v_s = self.v_reset;
448            1
449        } else {
450            0
451        }
452    }
453    pub fn reset(&mut self) {
454        self.v_s = -65.0;
455        self.v_d = -65.0;
456    }
457}
458impl Default for NeuroGridNeuron {
459    fn default() -> Self {
460        Self::new()
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn loihi_cuba_fires() {
470        let mut n = LoihiCUBANeuron::new();
471        let t: i32 = (0..200).map(|_| n.step(100)).sum();
472        assert!(t > 0);
473    }
474    #[test]
475    fn loihi2_fires() {
476        let mut n = Loihi2Neuron {
477            tau3: 8,
478            ..Loihi2Neuron::new()
479        };
480        let t: i32 = (0..500).map(|_| n.step(200)).sum();
481        assert!(t > 0);
482    }
483    #[test]
484    fn truenorth_fires() {
485        let mut n = TrueNorthNeuron::default();
486        let t: i32 = (0..10).map(|_| n.step(50)).sum();
487        assert!(t > 0);
488    }
489    #[test]
490    fn brainscales_fires() {
491        let mut n = BrainScaleSAdExNeuron::new();
492        let t: i32 = (0..2000).map(|_| n.step(500.0)).sum();
493        assert!(t > 0);
494    }
495    #[test]
496    fn spinnaker_fires() {
497        let mut n = SpiNNakerLIFNeuron::new();
498        let t: i32 = (0..200).map(|_| n.step(30.0)).sum();
499        assert!(t > 0);
500    }
501    #[test]
502    fn spinnaker2_fires() {
503        let mut n = SpiNNaker2Neuron::new();
504        let t: i32 = (0..200).map(|_| n.step(100)).sum();
505        assert!(t > 0);
506    }
507    #[test]
508    fn dpi_fires() {
509        let mut n = DPINeuron::new();
510        let t: i32 = (0..100).map(|_| n.step(1.0)).sum();
511        assert!(t > 0);
512    }
513    #[test]
514    fn akida_fires() {
515        let mut n = AkidaNeuron::default();
516        let t: i32 = (0..10).map(|_| n.step(50.0)).sum();
517        assert!(t > 0);
518    }
519    #[test]
520    fn neurogrid_fires() {
521        let mut n = NeuroGridNeuron::new();
522        let t: i32 = (0..2000).map(|_| n.step(500.0)).sum();
523        assert!(t > 0);
524    }
525
526    // ── Multi-angle tests for hardware models ──
527
528    // -- LoihiCUBA --
529    #[test]
530    fn loihi_cuba_silent() {
531        let mut n = LoihiCUBANeuron::new();
532        let t: i32 = (0..200).map(|_| n.step(0)).sum();
533        assert_eq!(t, 0);
534    }
535    #[test]
536    fn loihi_cuba_reset() {
537        let mut n = LoihiCUBANeuron::new();
538        for _ in 0..50 {
539            n.step(100);
540        }
541        n.reset();
542        assert_eq!(n.v, 0);
543        assert_eq!(n.u, 0);
544    }
545    #[test]
546    fn loihi_cuba_bounded() {
547        let mut n = LoihiCUBANeuron::new();
548        for _ in 0..1000 {
549            n.step(10000);
550        }
551    }
552
553    // -- Loihi2 --
554    #[test]
555    fn loihi2_silent() {
556        let mut n = Loihi2Neuron::new();
557        let t: i32 = (0..200).map(|_| n.step(0)).sum();
558        assert_eq!(t, 0);
559    }
560    #[test]
561    fn loihi2_reset() {
562        let mut n = Loihi2Neuron::new();
563        for _ in 0..50 {
564            n.step(200);
565        }
566        n.reset();
567        assert_eq!(n.s1, 0);
568    }
569    #[test]
570    fn loihi2_bounded() {
571        let mut n = Loihi2Neuron {
572            tau3: 8,
573            ..Loihi2Neuron::new()
574        };
575        for _ in 0..1000 {
576            n.step(10000);
577        }
578    }
579
580    // -- TrueNorth --
581    #[test]
582    fn truenorth_silent() {
583        let mut n = TrueNorthNeuron::default();
584        let t: i32 = (0..100).map(|_| n.step(0)).sum();
585        assert_eq!(t, 0);
586    }
587    #[test]
588    fn truenorth_reset() {
589        let mut n = TrueNorthNeuron::default();
590        for _ in 0..10 {
591            n.step(50);
592        }
593        n.reset();
594        assert_eq!(n.v, 0);
595    }
596
597    // -- BrainScaleSAdEx --
598    #[test]
599    fn brainscales_silent() {
600        let mut n = BrainScaleSAdExNeuron::new();
601        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
602        assert_eq!(t, 0);
603    }
604    #[test]
605    fn brainscales_reset() {
606        let mut n = BrainScaleSAdExNeuron::new();
607        for _ in 0..100 {
608            n.step(500.0);
609        }
610        n.reset();
611        assert!((n.v - n.v_rest).abs() < 1e-10);
612    }
613    #[test]
614    fn brainscales_bounded() {
615        let mut n = BrainScaleSAdExNeuron::new();
616        for _ in 0..2000 {
617            n.step(1e4);
618        }
619        assert!(n.v.is_finite());
620    }
621    #[test]
622    fn brainscales_nan_no_panic() {
623        BrainScaleSAdExNeuron::new().step(f64::NAN);
624    }
625
626    // -- SpiNNakerLIF --
627    #[test]
628    fn spinnaker_silent() {
629        let mut n = SpiNNakerLIFNeuron::new();
630        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
631        assert_eq!(t, 0);
632    }
633    #[test]
634    fn spinnaker_reset() {
635        let mut n = SpiNNakerLIFNeuron::new();
636        for _ in 0..50 {
637            n.step(30.0);
638        }
639        n.reset();
640        assert!((n.v - n.v_rest).abs() < 1e-10);
641    }
642    #[test]
643    fn spinnaker_bounded() {
644        let mut n = SpiNNakerLIFNeuron::new();
645        for _ in 0..1000 {
646            n.step(1e4);
647        }
648        assert!(n.v.is_finite());
649    }
650    #[test]
651    fn spinnaker_nan_no_panic() {
652        SpiNNakerLIFNeuron::new().step(f64::NAN);
653    }
654
655    // -- SpiNNaker2 --
656    #[test]
657    fn spinnaker2_silent() {
658        let mut n = SpiNNaker2Neuron::new();
659        let t: i32 = (0..200).map(|_| n.step(0)).sum();
660        assert_eq!(t, 0);
661    }
662    #[test]
663    fn spinnaker2_reset() {
664        let mut n = SpiNNaker2Neuron::new();
665        for _ in 0..50 {
666            n.step(100);
667        }
668        n.reset();
669    }
670    #[test]
671    fn spinnaker2_bounded() {
672        let mut n = SpiNNaker2Neuron::new();
673        for _ in 0..1000 {
674            n.step(10000);
675        }
676    }
677
678    // -- DPI --
679    #[test]
680    fn dpi_silent() {
681        let mut n = DPINeuron::new();
682        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
683        assert_eq!(t, 0);
684    }
685    #[test]
686    fn dpi_reset() {
687        let mut n = DPINeuron::new();
688        for _ in 0..50 {
689            n.step(1.0);
690        }
691        n.reset();
692    }
693    #[test]
694    fn dpi_nan_no_panic() {
695        DPINeuron::new().step(f64::NAN);
696    }
697
698    // -- Akida --
699    #[test]
700    fn akida_silent() {
701        let mut n = AkidaNeuron::default();
702        let t: i32 = (0..100).map(|_| n.step(0.0)).sum();
703        assert_eq!(t, 0);
704    }
705    #[test]
706    fn akida_reset() {
707        let mut n = AkidaNeuron::default();
708        for _ in 0..10 {
709            n.step(50.0);
710        }
711        n.reset();
712    }
713    #[test]
714    fn akida_nan_no_panic() {
715        AkidaNeuron::default().step(f64::NAN);
716    }
717
718    // -- NeuroGrid --
719    #[test]
720    fn neurogrid_silent() {
721        let mut n = NeuroGridNeuron::new();
722        let t: i32 = (0..200).map(|_| n.step(0.0)).sum();
723        assert_eq!(t, 0);
724    }
725    #[test]
726    fn neurogrid_reset() {
727        let mut n = NeuroGridNeuron::new();
728        for _ in 0..100 {
729            n.step(500.0);
730        }
731        n.reset();
732        assert!((n.v_s - (-65.0)).abs() < 1e-10);
733    }
734    #[test]
735    fn neurogrid_bounded() {
736        let mut n = NeuroGridNeuron::new();
737        for _ in 0..2000 {
738            n.step(1e4);
739        }
740        assert!(n.v_s.is_finite());
741    }
742    #[test]
743    fn neurogrid_nan_no_panic() {
744        NeuroGridNeuron::new().step(f64::NAN);
745    }
746}