Skip to main content

sc_neurocore_engine/
adc_to_spike.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// Copyright (C) 2020-2026 Miroslav Sotek. All rights reserved.
4// ORCID: 0009-0009-3560-0851
5// Contact: www.anulum.li | protoscience@anulum.li
6// SC-NeuroCore - ADC-to-spike decimating rate-code reference (per-window)
7
8//! Bit-true integer reference for the ADC-to-spike window rate-code encoder.
9
10use std::error::Error;
11use std::fmt;
12
13/// Per-window outputs of the ADC-to-spike encoder.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct AdcSpikeWindowResult {
16    /// Sign-aware averaged Q-format window codes.
17    pub window_values_q: Vec<i32>,
18    /// Deterministic per-window spike counts (`|window| / threshold`).
19    pub spike_counts: Vec<i32>,
20    /// `true` where the window code is negative.
21    pub polarities: Vec<bool>,
22}
23
24/// ADC-to-spike contract errors.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum AdcSpikeError {
27    InvalidAdcWidth(u32),
28    InvalidQFormat { q_int: u32, q_frac: u32 },
29    InvalidDecimation(u32),
30    InvalidThreshold(i64),
31    TooFewSamples { samples: usize, decimation: u32 },
32}
33
34impl fmt::Display for AdcSpikeError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::InvalidAdcWidth(width) => {
38                write!(f, "adc_width must be greater than one, got {width}")
39            }
40            Self::InvalidQFormat { q_int, q_frac } => write!(
41                f,
42                "Q-format needs positive integer bits and non-negative fraction bits, got q_int={q_int}, q_frac={q_frac}"
43            ),
44            Self::InvalidDecimation(decimation) => {
45                write!(f, "decimation must be positive, got {decimation}")
46            }
47            Self::InvalidThreshold(threshold) => {
48                write!(f, "threshold_q must be positive, got {threshold}")
49            }
50            Self::TooFewSamples {
51                samples,
52                decimation,
53            } => write!(
54                f,
55                "need at least decimation={decimation} samples, got {samples}"
56            ),
57        }
58    }
59}
60
61impl Error for AdcSpikeError {}
62
63/// Q-format code bounds for a `q_int`/`q_frac` signed fixed-point format.
64fn q_bounds(q_int: u32, q_frac: u32) -> (i64, i64) {
65    let q_total = q_int + q_frac;
66    let half = 1_i64 << (q_total - 1);
67    (-half, half - 1)
68}
69
70/// Centre and quantise one raw ADC sample to a Q-format code.
71fn quantise_adc(sample: i64, adc_width: u32, q_int: u32, q_frac: u32, signed_input: bool) -> i64 {
72    let q_total = q_int + q_frac;
73    let (q_min, q_max) = q_bounds(q_int, q_frac);
74    let centred = if signed_input {
75        let sign_bit = 1_i64 << (adc_width - 1);
76        let mask = (1_i64 << adc_width) - 1;
77        let masked = sample & mask;
78        if masked & sign_bit != 0 {
79            masked - (1_i64 << adc_width)
80        } else {
81            masked
82        }
83    } else {
84        sample - (1_i64 << (adc_width - 1))
85    };
86
87    let rounded = if q_total > adc_width {
88        centred << (q_total - adc_width)
89    } else if adc_width > q_total {
90        let shift = adc_width - q_total;
91        let half = 1_i64 << (shift - 1);
92        if centred >= 0 {
93            (centred + half) >> shift
94        } else {
95            (centred - half) >> shift
96        }
97    } else {
98        centred
99    };
100    rounded.clamp(q_min, q_max)
101}
102
103/// Sign-aware round-then-truncate window average (truncation toward zero).
104fn average_window(total_q: i64, decimation: u32, q_min: i64, q_max: i64) -> i64 {
105    let half = i64::from(decimation / 2);
106    let adjusted = if total_q >= 0 {
107        total_q + half
108    } else {
109        total_q - half
110    };
111    // Integer division truncates toward zero, matching `int(adjusted / decimation)`.
112    let averaged = adjusted / i64::from(decimation);
113    averaged.clamp(q_min, q_max)
114}
115
116/// Encode raw ADC samples into per-window spike rate codes.
117///
118/// Consumes the first `samples.len() / decimation` complete windows. Each window
119/// is quantised sample-by-sample, sign-aware averaged, and converted into a spike
120/// count of `|window| / threshold` with the window sign as polarity.
121#[allow(clippy::too_many_arguments)]
122pub fn adc_to_spike_windows(
123    samples: &[i64],
124    adc_width: u32,
125    q_int: u32,
126    q_frac: u32,
127    decimation: u32,
128    signed_input: bool,
129    threshold_q: i64,
130) -> Result<AdcSpikeWindowResult, AdcSpikeError> {
131    if adc_width <= 1 {
132        return Err(AdcSpikeError::InvalidAdcWidth(adc_width));
133    }
134    if q_int == 0 {
135        return Err(AdcSpikeError::InvalidQFormat { q_int, q_frac });
136    }
137    if decimation == 0 {
138        return Err(AdcSpikeError::InvalidDecimation(decimation));
139    }
140    if threshold_q <= 0 {
141        return Err(AdcSpikeError::InvalidThreshold(threshold_q));
142    }
143    let decim = decimation as usize;
144    let n_windows = samples.len() / decim;
145    if n_windows == 0 {
146        return Err(AdcSpikeError::TooFewSamples {
147            samples: samples.len(),
148            decimation,
149        });
150    }
151
152    let (q_min, q_max) = q_bounds(q_int, q_frac);
153    let mut window_values_q = Vec::with_capacity(n_windows);
154    let mut spike_counts = Vec::with_capacity(n_windows);
155    let mut polarities = Vec::with_capacity(n_windows);
156    for window in 0..n_windows {
157        let base = window * decim;
158        let mut total: i64 = 0;
159        for offset in 0..decim {
160            total += quantise_adc(
161                samples[base + offset],
162                adc_width,
163                q_int,
164                q_frac,
165                signed_input,
166            );
167        }
168        let window_q = average_window(total, decimation, q_min, q_max);
169        window_values_q.push(window_q as i32);
170        spike_counts.push((window_q.abs() / threshold_q) as i32);
171        polarities.push(window_q < 0);
172    }
173    Ok(AdcSpikeWindowResult {
174        window_values_q,
175        spike_counts,
176        polarities,
177    })
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn quantise_equal_width_is_identity_after_centring() {
186        // adc_width == q_total (16): signed centring then no rescale.
187        assert_eq!(quantise_adc(0, 16, 8, 8, true), 0);
188        assert_eq!(quantise_adc(1, 16, 8, 8, true), 1);
189        assert_eq!(quantise_adc((1 << 16) - 1, 16, 8, 8, true), -1);
190    }
191
192    #[test]
193    fn average_truncates_toward_zero() {
194        // total -7, decimation 8 -> adjusted -7-4 = -11 -> -11/8 = -1 (toward zero).
195        assert_eq!(average_window(-7, 8, -32768, 32767), -1);
196        assert_eq!(average_window(7, 8, -32768, 32767), 1);
197    }
198
199    #[test]
200    fn windows_emit_rate_code_and_polarity() {
201        // Eight mid-scale-negative samples -> negative window -> polarity set.
202        let samples = vec![0_i64; 8];
203        let result = adc_to_spike_windows(&samples, 16, 8, 8, 8, false, 256).unwrap();
204        assert_eq!(result.window_values_q.len(), 1);
205        // offset-binary 0 -> centred -(1<<15) = -32768 -> averaged -32768 -> 128 spikes.
206        assert_eq!(result.window_values_q[0], -32768);
207        assert_eq!(result.spike_counts[0], 128);
208        assert!(result.polarities[0]);
209    }
210
211    #[test]
212    fn rejects_bad_config_and_short_streams() {
213        assert_eq!(
214            adc_to_spike_windows(&[0; 8], 1, 8, 8, 8, true, 256).unwrap_err(),
215            AdcSpikeError::InvalidAdcWidth(1)
216        );
217        assert_eq!(
218            adc_to_spike_windows(&[0; 8], 16, 0, 8, 8, true, 256).unwrap_err(),
219            AdcSpikeError::InvalidQFormat {
220                q_int: 0,
221                q_frac: 8
222            }
223        );
224        assert_eq!(
225            adc_to_spike_windows(&[0; 8], 16, 8, 8, 0, true, 256).unwrap_err(),
226            AdcSpikeError::InvalidDecimation(0)
227        );
228        assert_eq!(
229            adc_to_spike_windows(&[0; 8], 16, 8, 8, 8, true, 0).unwrap_err(),
230            AdcSpikeError::InvalidThreshold(0)
231        );
232        assert_eq!(
233            adc_to_spike_windows(&[0; 3], 16, 8, 8, 8, true, 256).unwrap_err(),
234            AdcSpikeError::TooFewSamples {
235                samples: 3,
236                decimation: 8
237            }
238        );
239    }
240}