Skip to main content

axon_encoder/encoders/
temporal.rs

1use crate::prelude::*;
2use std::collections::VecDeque;
3
4/// Encodes temporal patterns by tracking history of values per channel.
5///
6/// Fires a spike when the rate of change exceeds configurable thresholds.
7/// Useful for detecting sudden changes or motion in sensor signals.
8///
9/// # Mathematical Model
10///
11/// Computes the difference between recent average (last 3 values) and older average
12/// (previous 3 values before that). A spike is generated when this change exceeds
13/// the threshold:
14///
15/// ```text
16/// change = |mean(history[-3:]) - mean(history[-6:-3])|
17/// spike if change > threshold
18/// ```
19///
20/// # When to Use
21///
22/// - Detecting sudden changes in signal (edge detection)
23/// - Motion detection in video or sensor streams
24/// - Event-based encoding where changes are more important than absolute values
25///
26/// # Parameters
27///
28/// - `history_depth`: How many past values to track per channel
29/// - `change_thresholds`: Vec of (threshold, spike_value) pairs - fires when change exceeds threshold
30/// - `num_channels`: Number of input channels
31///
32/// # Examples
33///
34/// ```rust
35/// use axon_encoder::prelude::*;
36/// # fn main() -> Result<(), EncoderError> {
37/// // history_depth must be at least 6 for the dual-window change detector.
38/// let mut enc = TemporalEncoder::try_new(6, vec![(0.5, 1)], 1)?;
39/// for v in [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0] {
40///     let _ = enc.encode_step(&[v]);
41/// }
42/// # Ok(())
43/// # }
44/// ```
45#[derive(Clone, Debug, PartialEq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize))]
47pub struct TemporalEncoder {
48    history: Vec<VecDeque<f32>>,
49    history_depth: usize,
50    change_thresholds: Vec<(f32, u16)>,
51}
52
53impl TemporalEncoder {
54    /// Creates a new `TemporalEncoder`, panicking if configuration is invalid.
55    ///
56    /// Prefer [`try_new`](Self::try_new) for typed validation errors.
57    ///
58    /// # Panics
59    ///
60    /// Panics if `history_depth < 6` or `num_channels` is unsupported.
61    pub fn new(
62        history_depth: usize,
63        change_thresholds: Vec<(f32, u16)>,
64        num_channels: usize,
65    ) -> Self {
66        Self::try_new(history_depth, change_thresholds, num_channels)
67            .expect("invalid TemporalEncoder configuration")
68    }
69
70    /// Creates a new `TemporalEncoder`, returning an [`EncoderError`] for invalid configuration.
71    ///
72    /// Each threshold in `change_thresholds` must be finite and non-negative.
73    pub fn try_new(
74        history_depth: usize,
75        change_thresholds: Vec<(f32, u16)>,
76        num_channels: usize,
77    ) -> Result<Self, EncoderError> {
78        if history_depth < 6 {
79            return Err(EncoderError::HistoryDepthTooSmall { minimum: 6 });
80        }
81        for &(threshold, _) in &change_thresholds {
82            crate::error::validate_non_negative_finite("change_threshold", threshold)?;
83        }
84        crate::error::validate_channel_count(num_channels)?;
85        Ok(Self {
86            history: vec![VecDeque::with_capacity(history_depth); num_channels],
87            history_depth,
88            change_thresholds,
89        })
90    }
91
92    fn encode_with_threshold_scale(
93        &mut self,
94        input: &[f32],
95        threshold_scale: f32,
96    ) -> EncodedOutput {
97        let mut output = EncodedOutput::new();
98        for (i, &value) in input.iter().enumerate() {
99            if i >= self.history.len() {
100                break;
101            }
102            let Ok(channel) = u16::try_from(i) else {
103                // Remaining channels exceed u16::MAX; stop rather than wrap.
104                break;
105            };
106            let channel_history = &mut self.history[i];
107            if channel_history.len() == self.history_depth {
108                channel_history.pop_front();
109            }
110            channel_history.push_back(value);
111
112            if channel_history.len() < 6 {
113                continue;
114            }
115
116            let recent_avg = channel_history.iter().rev().take(3).sum::<f32>() / 3.0;
117            let older_avg = channel_history.iter().rev().skip(3).take(3).sum::<f32>() / 3.0;
118            let change = (recent_avg - older_avg).abs();
119
120            for &(threshold, _spike_val) in self.change_thresholds.iter().rev() {
121                if change > (threshold * threshold_scale).max(0.0) {
122                    output.spikes.push(SpikeEvent {
123                        channel,
124                        timestamp: 0,   // Simplified
125                        polarity: true, // Or use spike_val to determine polarity/strength
126                    });
127                    break; // Only fire one spike per channel per step
128                }
129            }
130        }
131        output
132    }
133
134    /// Encodes input using neuromodulator-driven gain curves.
135    ///
136    /// Inherent wrapper so callers need not import [`ModulatedEncoder`].
137    pub fn encode_with_modulators(
138        &mut self,
139        input: &[f32],
140        modulators: &NeuroModulators,
141        gain_curves: &NeuromodulatorGainCurves,
142    ) -> EncodedOutput {
143        <Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
144    }
145
146    /// Step-wise variant of [`encode_with_modulators`](Self::encode_with_modulators).
147    pub fn encode_step_with_modulators(
148        &mut self,
149        input: &[f32],
150        modulators: &NeuroModulators,
151        gain_curves: &NeuromodulatorGainCurves,
152    ) -> EncodedOutput {
153        <Self as ModulatedEncoder>::encode_step_with_modulators(
154            self,
155            input,
156            modulators,
157            gain_curves,
158        )
159    }
160}
161
162impl Encoder for TemporalEncoder {
163    fn encode(&mut self, input: &[f32]) -> EncodedOutput {
164        self.encode_with_threshold_scale(input, 1.0)
165    }
166
167    fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
168        let safe_input = if input.len() > self.history.len() {
169            &input[..self.history.len()]
170        } else {
171            input
172        };
173        self.encode_with_threshold_scale(safe_input, 1.0)
174    }
175
176    fn reset(&mut self) {
177        for history in self.history.iter_mut() {
178            history.clear();
179        }
180    }
181}
182
183impl ModulatedEncoder for TemporalEncoder {
184    fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
185        let safe_input = if input.len() > self.history.len() {
186            &input[..self.history.len()]
187        } else {
188            input
189        };
190        self.encode_with_threshold_scale(safe_input, gains.sanitize().threshold_scale)
191    }
192}
193
194#[cfg(feature = "serde")]
195impl<'de> serde::Deserialize<'de> for TemporalEncoder {
196    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197    where
198        D: serde::Deserializer<'de>,
199    {
200        use std::collections::VecDeque;
201
202        #[derive(serde::Deserialize)]
203        struct Helper {
204            history: Vec<VecDeque<f32>>,
205            history_depth: usize,
206            change_thresholds: Vec<(f32, u16)>,
207        }
208
209        let helper = Helper::deserialize(deserializer)?;
210
211        if helper.history_depth < 6 {
212            return Err(serde::de::Error::custom(
213                EncoderError::HistoryDepthTooSmall { minimum: 6 },
214            ));
215        }
216        crate::error::validate_channel_count(helper.history.len())
217            .map_err(serde::de::Error::custom)?;
218
219        // Match try_new: reject non-finite / negative thresholds on load.
220        for &(threshold, _) in &helper.change_thresholds {
221            crate::error::validate_non_negative_finite("change_threshold", threshold)
222                .map_err(serde::de::Error::custom)?;
223        }
224
225        for (i, deque) in helper.history.iter().enumerate() {
226            if deque.len() > helper.history_depth {
227                return Err(serde::de::Error::custom(format!(
228                    "history channel {} length ({}) exceeds history_depth ({})",
229                    i,
230                    deque.len(),
231                    helper.history_depth
232                )));
233            }
234        }
235
236        Ok(Self {
237            history: helper.history,
238            history_depth: helper.history_depth,
239            change_thresholds: helper.change_thresholds,
240        })
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_temporal_encoder() {
250        let mut encoder = TemporalEncoder::new(6, vec![(2.0, 1), (5.0, 2)], 1);
251        let _output = encoder.encode(&[1.0]);
252        let _output = encoder.encode(&[1.0]);
253        let _output = encoder.encode(&[1.0]);
254        let _output = encoder.encode(&[8.0]);
255        let _output = encoder.encode(&[8.0]);
256        let output = encoder.encode(&[8.0]);
257        assert!(!output.spikes.is_empty());
258    }
259
260    #[test]
261    fn test_temporal_encoder_modulators_reduce_threshold() {
262        let mut encoder = TemporalEncoder::new(6, vec![(4.5, 1)], 1);
263        let modulators = NeuroModulators {
264            tempo: 1.0,
265            ..Default::default()
266        };
267        let gain_curves = NeuromodulatorGainCurves {
268            tempo: ModulatorGainCurves {
269                threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
270                ..Default::default()
271            },
272            ..Default::default()
273        };
274
275        for _ in 0..3 {
276            encoder.encode(&[1.0]);
277        }
278        for _ in 0..2 {
279            encoder.encode(&[5.0]);
280        }
281        assert!(encoder.encode(&[5.0]).spikes.is_empty());
282
283        encoder.reset();
284
285        for _ in 0..3 {
286            encoder.encode_step_with_modulators(&[1.0], &modulators, &gain_curves);
287        }
288        for _ in 0..2 {
289            encoder.encode_step_with_modulators(&[5.0], &modulators, &gain_curves);
290        }
291        let output = encoder.encode_step_with_modulators(&[5.0], &modulators, &gain_curves);
292        assert_eq!(output.spikes.len(), 1);
293    }
294
295    #[test]
296    fn test_temporal_encoder_encode_with_modulators() {
297        let mut encoder = TemporalEncoder::new(6, vec![(4.5, 1)], 1);
298        let modulators = NeuroModulators {
299            tempo: 1.0,
300            ..Default::default()
301        };
302        let gain_curves = NeuromodulatorGainCurves {
303            tempo: ModulatorGainCurves {
304                threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
305                ..Default::default()
306            },
307            ..Default::default()
308        };
309
310        for _ in 0..3 {
311            encoder.encode_with_modulators(&[1.0], &modulators, &gain_curves);
312        }
313        for _ in 0..2 {
314            encoder.encode_with_modulators(&[5.0], &modulators, &gain_curves);
315        }
316        let output = encoder.encode_with_modulators(&[5.0], &modulators, &gain_curves);
317        assert_eq!(output.spikes.len(), 1);
318    }
319
320    #[test]
321    fn test_temporal_encoder_step_longer_input() {
322        let mut encoder = TemporalEncoder::new(6, vec![(4.5, 1)], 2);
323        let output = encoder.encode_step(&[1.0, 2.0, 3.0]);
324        assert!(output.spikes.len() <= 2);
325    }
326
327    #[cfg(feature = "serde")]
328    #[test]
329    fn test_temporal_serde_history_channel_too_long() {
330        let json = r#"{
331            "history": [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]],
332            "history_depth": 6,
333            "change_thresholds": []
334        }"#;
335        let res: Result<TemporalEncoder, _> = serde_json::from_str(json);
336        assert!(res.is_err());
337    }
338
339    #[cfg(feature = "serde")]
340    #[test]
341    fn test_temporal_serde_rejects_invalid_thresholds() {
342        let negative = r#"{
343            "history": [[]],
344            "history_depth": 6,
345            "change_thresholds": [[-0.5, 1]]
346        }"#;
347        let res: Result<TemporalEncoder, _> = serde_json::from_str(negative);
348        assert!(
349            res.is_err(),
350            "negative change_threshold must fail deserialize"
351        );
352
353        let ok = r#"{
354            "history": [[]],
355            "history_depth": 6,
356            "change_thresholds": [[0.0, 1], [1.5, 2]]
357        }"#;
358        let res: Result<TemporalEncoder, _> = serde_json::from_str(ok);
359        assert!(res.is_ok());
360    }
361    #[test]
362    fn test_temporal_encoder_try_new_validation() {
363        assert_eq!(
364            TemporalEncoder::try_new(5, vec![(1.0, 1)], 1).err(),
365            Some(EncoderError::HistoryDepthTooSmall { minimum: 6 })
366        );
367        assert_eq!(
368            TemporalEncoder::try_new(6, vec![(1.0, 1)], u16::MAX as usize + 2).err(),
369            Some(EncoderError::NumChannelsTooLarge)
370        );
371        assert_eq!(
372            TemporalEncoder::try_new(6, vec![(f32::NAN, 1)], 1).err(),
373            Some(EncoderError::NonNegativeFinite {
374                parameter: "change_threshold"
375            })
376        );
377        assert_eq!(
378            TemporalEncoder::try_new(6, vec![(-0.5, 1)], 1).err(),
379            Some(EncoderError::NonNegativeFinite {
380                parameter: "change_threshold"
381            })
382        );
383        assert!(TemporalEncoder::try_new(6, vec![(0.0, 1)], 1).is_ok());
384    }
385}