Skip to main content

sc_neurocore_engine/neurons/trivial/
integer_qif.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 — Integer QIF Neuron
8
9/// Wu et al. (2021) IQIF — piecewise-linear integer soma for digital hardware.
10#[derive(Clone, Debug)]
11pub struct IntegerQIFNeuron {
12    pub v: i64,
13    pub v_rest: i64,
14    pub v_threshold: i64,
15    pub v_reset: i64,
16    pub a: i64,
17    pub b: i64,
18    pub v_max: i64,
19    pub v_min: i64,
20}
21
22impl IntegerQIFNeuron {
23    #[allow(clippy::too_many_arguments)]
24    pub fn with_parameters(
25        v: i32,
26        v_rest: i32,
27        v_threshold: i32,
28        v_reset: i32,
29        a: i32,
30        b: i32,
31        v_max: i32,
32        v_min: i32,
33    ) -> Result<Self, &'static str> {
34        let neuron = Self {
35            v: i64::from(v),
36            v_rest: i64::from(v_rest),
37            v_threshold: i64::from(v_threshold),
38            v_reset: i64::from(v_reset),
39            a: i64::from(a),
40            b: i64::from(b),
41            v_max: i64::from(v_max),
42            v_min: i64::from(v_min),
43        };
44        if neuron.valid() {
45            Ok(neuron)
46        } else {
47            Err("invalid IQIF state or parameter ordering")
48        }
49    }
50
51    pub fn valid(&self) -> bool {
52        let i32_range = |value: i64| i64::from(i32::MIN) <= value && value <= i64::from(i32::MAX);
53        [
54            self.v,
55            self.v_rest,
56            self.v_threshold,
57            self.v_reset,
58            self.a,
59            self.b,
60            self.v_max,
61            self.v_min,
62        ]
63        .into_iter()
64        .all(i32_range)
65            && self.a >= 0
66            && self.b >= 0
67            && self.a + self.b > 0
68            && self.v_min < self.v_rest
69            && self.v_rest < self.v_threshold
70            && self.v_threshold < self.v_max
71            && (self.v_min..=self.v_max).contains(&self.v_reset)
72            && (self.v_min..=self.v_max).contains(&self.v)
73    }
74
75    pub fn branch_point(&self) -> i64 {
76        let numerator = self.b * self.v_threshold + self.a * self.v_rest;
77        let denominator = self.a + self.b;
78        if numerator >= 0 {
79            numerator / denominator
80        } else {
81            -((-numerator) / denominator)
82        }
83    }
84
85    pub fn try_step(&mut self, current: i32) -> Result<i32, &'static str> {
86        if !self.valid() {
87            return Err("invalid IQIF runtime state or parameter ordering");
88        }
89        let force = if self.v < self.branch_point() {
90            self.a
91                .checked_mul(self.v_rest - self.v)
92                .ok_or("IQIF restoring force overflowed")?
93        } else {
94            self.b
95                .checked_mul(self.v - self.v_threshold)
96                .ok_or("IQIF restoring force overflowed")?
97        };
98        let candidate = self
99            .v
100            .checked_add(force >> 3)
101            .and_then(|value| value.checked_add(i64::from(current)))
102            .ok_or("IQIF candidate overflowed")?;
103        if candidate > self.v_max {
104            self.v = self.v_reset;
105            Ok(1)
106        } else {
107            self.v = candidate.max(self.v_min);
108            Ok(0)
109        }
110    }
111
112    pub fn step(&mut self, current: i32) -> i32 {
113        self.try_step(current)
114            .expect("IQIF step requires a validated signed-int32 contract")
115    }
116
117    pub fn reset(&mut self) {
118        self.v = self.v_rest;
119    }
120}
121
122impl Default for IntegerQIFNeuron {
123    fn default() -> Self {
124        Self {
125            v: 128,
126            v_rest: 128,
127            v_threshold: 200,
128            v_reset: 128,
129            a: 1,
130            b: 1,
131            v_max: 255,
132            v_min: 0,
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn iqif_fires() {
143        let mut n = IntegerQIFNeuron::default();
144        let total: i32 = (0..400).map(|_| n.step(10)).sum();
145        assert_eq!(total, 26);
146    }
147    #[test]
148    fn iqif_silent_without_input() {
149        let mut n = IntegerQIFNeuron::default();
150        let t: i32 = (0..200).map(|_| n.step(0)).sum();
151        assert_eq!(t, 0);
152    }
153    #[test]
154    fn iqif_reset_clears_state() {
155        let mut n = IntegerQIFNeuron::default();
156        for _ in 0..100 {
157            n.step(10);
158        }
159        n.reset();
160        assert_eq!(n.v, 128);
161    }
162    #[test]
163    fn iqif_bounded() {
164        let mut n = IntegerQIFNeuron::default();
165        for _ in 0..1000 {
166            n.step(10000);
167        }
168        // Integer — check no overflow panic occurred
169    }
170    #[test]
171    fn iqif_negative_no_crash() {
172        let mut n = IntegerQIFNeuron::default();
173        for _ in 0..500 {
174            n.step(-50);
175        }
176        assert_eq!(n.v, 0);
177    }
178    #[test]
179    fn iqif_matches_source_tutorial_prefix_and_period() {
180        let mut n = IntegerQIFNeuron::default();
181        let mut trace = Vec::new();
182        let mut spike_steps = Vec::new();
183        for index in 0..400 {
184            if n.step(10) == 1 {
185                spike_steps.push(index);
186            }
187            trace.push(n.v);
188        }
189        assert_eq!(
190            &trace[..15],
191            &[138, 146, 153, 159, 165, 170, 176, 183, 190, 198, 207, 217, 229, 242, 128]
192        );
193        assert_eq!(spike_steps, (14..400).step_by(15).collect::<Vec<_>>());
194        assert_eq!(trace.last(), Some(&198));
195    }
196}