Skip to main content

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