Skip to main content

sc_neurocore_engine/neuron/
bitstream_averager.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 — Sliding-window bitstream probability estimator
8
9/// Sliding-window bitstream probability estimator.
10///
11/// Mirrors Python's `BitstreamAverager`.
12#[derive(Clone, Debug)]
13pub struct BitstreamAverager {
14    buffer: Vec<u8>,
15    index: usize,
16    filled: bool,
17    running_sum: u64,
18}
19
20impl BitstreamAverager {
21    pub fn new(window: usize) -> Self {
22        assert!(window > 0, "window must be > 0");
23        Self {
24            buffer: vec![0; window],
25            index: 0,
26            filled: false,
27            running_sum: 0,
28        }
29    }
30
31    pub fn push(&mut self, bit: u8) {
32        debug_assert!(bit <= 1, "bit must be 0 or 1");
33        let old = self.buffer[self.index];
34        self.buffer[self.index] = bit;
35
36        if self.filled {
37            self.running_sum = self.running_sum - old as u64 + bit as u64;
38        } else {
39            self.running_sum += bit as u64;
40        }
41
42        self.index += 1;
43        if self.index == self.buffer.len() {
44            self.index = 0;
45            self.filled = true;
46        }
47    }
48
49    pub fn estimate(&self) -> f64 {
50        if !self.filled {
51            if self.index == 0 {
52                return 0.0;
53            }
54            return self.running_sum as f64 / self.index as f64;
55        }
56        self.running_sum as f64 / self.buffer.len() as f64
57    }
58
59    pub fn reset(&mut self) {
60        self.buffer.fill(0);
61        self.index = 0;
62        self.filled = false;
63        self.running_sum = 0;
64    }
65
66    pub fn window(&self) -> usize {
67        self.buffer.len()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::BitstreamAverager;
74
75    #[test]
76    fn all_ones_estimate_one() {
77        let mut avg = BitstreamAverager::new(100);
78        for _ in 0..100 {
79            avg.push(1);
80        }
81        assert!((avg.estimate() - 1.0).abs() < 1e-12);
82    }
83
84    #[test]
85    fn all_zeros_estimate_zero() {
86        let mut avg = BitstreamAverager::new(50);
87        for _ in 0..50 {
88            avg.push(0);
89        }
90        assert!(avg.estimate().abs() < 1e-12);
91    }
92
93    #[test]
94    fn alternating_bits_estimate_half() {
95        let mut avg = BitstreamAverager::new(100);
96        for i in 0..100 {
97            avg.push((i % 2) as u8);
98        }
99        assert!((avg.estimate() - 0.5).abs() < 1e-12);
100    }
101
102    #[test]
103    fn sliding_window_replaces_oldest_bits() {
104        let mut avg = BitstreamAverager::new(4);
105        for &bit in &[1_u8, 1, 0, 0] {
106            avg.push(bit);
107        }
108        assert!((avg.estimate() - 0.5).abs() < 1e-12);
109
110        avg.push(1);
111        assert!((avg.estimate() - 0.5).abs() < 1e-12);
112        avg.push(1);
113        assert!((avg.estimate() - 0.5).abs() < 1e-12);
114        avg.push(1);
115        assert!((avg.estimate() - 0.75).abs() < 1e-12);
116    }
117
118    #[test]
119    fn partial_window_uses_observed_count() {
120        let mut avg = BitstreamAverager::new(100);
121        avg.push(1);
122        avg.push(0);
123        assert!((avg.estimate() - 0.5).abs() < 1e-12);
124    }
125
126    #[test]
127    fn empty_window_estimate_is_zero() {
128        let avg = BitstreamAverager::new(10);
129        assert!(avg.estimate().abs() < 1e-12);
130    }
131
132    #[test]
133    fn reset_clears_window_state() {
134        let mut avg = BitstreamAverager::new(10);
135        for _ in 0..10 {
136            avg.push(1);
137        }
138        avg.reset();
139        assert!(avg.estimate().abs() < 1e-12);
140    }
141}