1use std::error::Error;
11use std::fmt;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct AdcSpikeWindowResult {
16 pub window_values_q: Vec<i32>,
18 pub spike_counts: Vec<i32>,
20 pub polarities: Vec<bool>,
22}
23
24#[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
63fn 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
70fn 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
103fn 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 let averaged = adjusted / i64::from(decimation);
113 averaged.clamp(q_min, q_max)
114}
115
116#[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 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 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 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 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}