Skip to main content

sc_neurocore_engine/
recorder.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 — Spike recording and statistics
8
9//! Spike recording and statistics.
10
11/// Buffered spike recorder with firing rate and ISI statistics.
12#[derive(Clone, Debug)]
13pub struct SpikeRecorder {
14    pub buffer: Vec<u8>,
15    pub dt_ms: f64,
16}
17
18impl SpikeRecorder {
19    pub fn new(dt_ms: f64) -> Self {
20        Self {
21            buffer: Vec::new(),
22            dt_ms,
23        }
24    }
25
26    pub fn record(&mut self, spike: u8) {
27        debug_assert!(spike <= 1);
28        self.buffer.push(spike);
29    }
30
31    pub fn total_spikes(&self) -> u64 {
32        self.buffer.iter().map(|&s| s as u64).sum()
33    }
34
35    pub fn firing_rate_hz(&self) -> f64 {
36        let t = self.buffer.len();
37        if t == 0 {
38            return 0.0;
39        }
40        let duration_s = (t as f64 * self.dt_ms) / 1000.0;
41        if duration_s <= 0.0 {
42            return 0.0;
43        }
44        self.total_spikes() as f64 / duration_s
45    }
46
47    pub fn reset(&mut self) {
48        self.buffer.clear();
49    }
50
51    pub fn len(&self) -> usize {
52        self.buffer.len()
53    }
54
55    pub fn is_empty(&self) -> bool {
56        self.buffer.is_empty()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn all_spikes_rate() {
66        let mut r = SpikeRecorder::new(1.0);
67        for _ in 0..1000 {
68            r.record(1);
69        }
70        assert_eq!(r.total_spikes(), 1000);
71        assert!((r.firing_rate_hz() - 1000.0).abs() < 1e-6);
72    }
73
74    #[test]
75    fn no_spikes_rate() {
76        let mut r = SpikeRecorder::new(1.0);
77        for _ in 0..100 {
78            r.record(0);
79        }
80        assert_eq!(r.total_spikes(), 0);
81        assert!(r.firing_rate_hz().abs() < 1e-12);
82    }
83
84    #[test]
85    fn half_spikes() {
86        let mut r = SpikeRecorder::new(1.0);
87        for i in 0..100 {
88            r.record((i % 2) as u8);
89        }
90        assert_eq!(r.total_spikes(), 50);
91        assert!((r.firing_rate_hz() - 500.0).abs() < 1e-6);
92    }
93
94    #[test]
95    fn empty_recorder() {
96        let r = SpikeRecorder::new(1.0);
97        assert!(r.firing_rate_hz().abs() < 1e-12);
98        assert!(r.is_empty());
99    }
100
101    #[test]
102    fn reset_clears() {
103        let mut r = SpikeRecorder::new(1.0);
104        r.record(1);
105        r.reset();
106        assert!(r.is_empty());
107        assert_eq!(r.total_spikes(), 0);
108    }
109}