Skip to main content

sc_neurocore_engine/neuron/
fixed_point_lif.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 — Fixed-point leaky-integrate-and-fire neuron
8
9/// Mask and sign-interpret an integer to `width` bits (branchless).
10#[inline]
11pub fn mask(value: i32, width: u32) -> i16 {
12    assert!(
13        width > 0 && width <= 32,
14        "mask width must be 1..=32, got {width}"
15    );
16    let mask = (1_i64 << width) - 1;
17    let value = (value as i64) & mask;
18    let shift = 64 - width;
19    ((value << shift) >> shift) as i16
20}
21
22/// Fixed-point leaky-integrate-and-fire neuron state and parameters.
23#[derive(Clone, Debug)]
24pub struct FixedPointLif {
25    pub v: i16,
26    pub refractory_counter: i32,
27    pub data_width: u32,
28    pub fraction: u32,
29    pub v_rest: i16,
30    pub v_reset: i16,
31    pub v_threshold: i16,
32    pub refractory_period: i32,
33}
34
35impl FixedPointLif {
36    pub fn new(
37        data_width: u32,
38        fraction: u32,
39        v_rest: i16,
40        v_reset: i16,
41        v_threshold: i16,
42        refractory_period: i32,
43    ) -> Self {
44        Self {
45            v: v_rest,
46            refractory_counter: 0,
47            data_width,
48            fraction,
49            v_rest,
50            v_reset,
51            v_threshold,
52            refractory_period,
53        }
54    }
55
56    #[allow(non_snake_case)]
57    pub fn step(&mut self, leak_k: i16, gain_k: i16, i_t: i16, noise_in: i16) -> (i32, i16) {
58        let width = self.data_width;
59        if self.refractory_counter > 0 {
60            self.refractory_counter -= 1;
61            self.v = self.v_rest;
62            return (0, mask(self.v_rest as i32, width));
63        }
64
65        let diff = mask((self.v_rest as i32) - (self.v as i32), 2 * width) as i32;
66        let dv_leak = mask((diff * (leak_k as i32)) >> self.fraction, self.data_width);
67        let dv_in = mask(
68            ((i_t as i32) * (gain_k as i32)) >> self.fraction,
69            self.data_width,
70        );
71        let v_next = mask(
72            (self.v as i32) + (dv_leak as i32) + (dv_in as i32) + (noise_in as i32),
73            self.data_width,
74        );
75
76        if v_next >= self.v_threshold {
77            self.v = self.v_reset;
78            self.refractory_counter = self.refractory_period;
79            (1, mask(self.v_reset as i32, width))
80        } else {
81            self.v = v_next;
82            (0, mask(v_next as i32, width))
83        }
84    }
85
86    pub fn reset(&mut self) {
87        self.v = self.v_rest;
88        self.refractory_counter = 0;
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::{mask, FixedPointLif};
95
96    #[test]
97    fn branchless_mask_matches_signed_reference() {
98        for &width in &[16_u32, 32] {
99            for value in [
100                -32768_i32,
101                -1,
102                0,
103                1,
104                32767,
105                65535,
106                -65536,
107                i16::MAX as i32,
108                i16::MIN as i32,
109            ] {
110                let result = mask(value, width);
111                let bit_mask = (1_i64 << width) - 1;
112                let mut expected = (value as i64) & bit_mask;
113                if expected >= (1_i64 << (width - 1)) {
114                    expected -= 1_i64 << width;
115                }
116                let expected = if width >= 32 {
117                    expected as i32 as i16
118                } else {
119                    expected as i16
120                };
121                assert_eq!(result, expected, "value={value}, width={width}");
122            }
123        }
124    }
125
126    #[test]
127    fn refractory_period_enforces_two_silent_steps() {
128        let mut neuron = FixedPointLif::new(16, 8, 0, 0, 256, 2);
129        let spikes: Vec<_> = (0..30).map(|_| neuron.step(1, 256, 50, 0).0).collect();
130        assert!(spikes.iter().sum::<i32>() > 0);
131        for (index, &spike) in spikes.iter().enumerate() {
132            if spike == 1 && index + 2 < spikes.len() {
133                assert_eq!(spikes[index + 1], 0);
134                assert_eq!(spikes[index + 2], 0);
135            }
136        }
137    }
138
139    #[test]
140    fn zero_refractory_period_allows_repeated_firing() {
141        let mut neuron = FixedPointLif::new(16, 8, 0, 0, 256, 0);
142        let spikes: i32 = (0..20).map(|_| neuron.step(1, 256, 50, 0).0).sum();
143        assert!(spikes > 0);
144    }
145}