Skip to main content

axon_encoder/encoders/
delta.rs

1use crate::prelude::*;
2
3/// A simple delta-based encoder.
4///
5/// Fires a spike when the absolute difference between the current input and the last
6/// encoded value exceeds a threshold. This is useful for event-based encoding where
7/// only changes in the input signal are relevant.
8///
9/// # Mathematical Model
10///
11/// ```text
12/// delta = |current_value - last_value|
13/// spike if delta > threshold
14/// ```
15///
16/// # When to Use
17///
18/// - Event-based encoding where changes are more important than absolute values
19/// - Sensor data where baseline can drift but changes are meaningful
20/// - Reducing power consumption by only encoding when changes occur
21///
22/// # Parameters
23///
24/// - `threshold`: Minimum change required to trigger a spike
25/// - `num_channels`: Number of input channels to track
26///
27/// # Examples
28///
29/// ```rust
30/// use axon_encoder::prelude::*;
31/// # fn main() -> Result<(), EncoderError> {
32/// let mut enc = DeltaEncoder::try_new(0.1, 2)?;
33/// // Baseline starts at zeros; last_values update only when a spike fires.
34/// // A jump of 0.5 from 0 exceeds threshold 0.1 on channel 0.
35/// let out = enc.encode(&[0.5, 0.0]);
36/// assert!(!out.spikes.is_empty());
37/// # Ok(())
38/// # }
39/// ```
40#[derive(Clone, Debug, PartialEq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42pub struct DeltaEncoder {
43    last_values: Vec<f32>,
44    threshold: f32,
45}
46
47impl DeltaEncoder {
48    /// Creates a new `DeltaEncoder`, panicking if configuration is invalid.
49    ///
50    /// Prefer [`try_new`](Self::try_new) for typed validation errors.
51    pub fn new(threshold: f32, num_channels: usize) -> Self {
52        Self::try_new(threshold, num_channels).expect("invalid DeltaEncoder configuration")
53    }
54
55    /// Creates a new `DeltaEncoder`, returning an [`EncoderError`] for invalid configuration.
56    ///
57    /// `threshold == 0.0` is valid and means any nonzero change fires a spike
58    /// (`delta > 0`).
59    pub fn try_new(threshold: f32, num_channels: usize) -> Result<Self, EncoderError> {
60        crate::error::validate_non_negative_finite("threshold", threshold)?;
61        crate::error::validate_channel_count(num_channels)?;
62        Ok(Self {
63            last_values: vec![0.0; num_channels],
64            threshold,
65        })
66    }
67
68    fn encode_with_threshold_scale(
69        &mut self,
70        input: &[f32],
71        threshold_scale: f32,
72    ) -> EncodedOutput {
73        let mut output = EncodedOutput::new();
74        let effective_threshold = (self.threshold * threshold_scale).max(0.0);
75
76        for (i, &value) in input.iter().enumerate() {
77            if i >= self.last_values.len() {
78                break;
79            }
80            let Ok(channel) = u16::try_from(i) else {
81                // Remaining channels exceed u16::MAX; stop rather than wrap.
82                break;
83            };
84            let delta = (value - self.last_values[i]).abs();
85            if delta > effective_threshold {
86                output.spikes.push(SpikeEvent {
87                    channel,
88                    timestamp: 0,
89                    polarity: value > self.last_values[i],
90                });
91                self.last_values[i] = value;
92            }
93        }
94        output
95    }
96
97    /// Encodes input using neuromodulator-driven gain curves.
98    ///
99    /// Inherent wrapper so callers need not import [`ModulatedEncoder`].
100    pub fn encode_with_modulators(
101        &mut self,
102        input: &[f32],
103        modulators: &NeuroModulators,
104        gain_curves: &NeuromodulatorGainCurves,
105    ) -> EncodedOutput {
106        <Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
107    }
108
109    /// Step-wise variant of [`encode_with_modulators`](Self::encode_with_modulators).
110    pub fn encode_step_with_modulators(
111        &mut self,
112        input: &[f32],
113        modulators: &NeuroModulators,
114        gain_curves: &NeuromodulatorGainCurves,
115    ) -> EncodedOutput {
116        <Self as ModulatedEncoder>::encode_step_with_modulators(
117            self,
118            input,
119            modulators,
120            gain_curves,
121        )
122    }
123}
124
125#[cfg(feature = "serde")]
126impl<'de> serde::Deserialize<'de> for DeltaEncoder {
127    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128    where
129        D: serde::Deserializer<'de>,
130    {
131        #[derive(serde::Deserialize)]
132        struct Helper {
133            last_values: Vec<f32>,
134            threshold: f32,
135        }
136        let helper = Helper::deserialize(deserializer)?;
137        let mut encoder = Self::try_new(helper.threshold, helper.last_values.len())
138            .map_err(serde::de::Error::custom)?;
139        if helper.last_values.iter().any(|value| !value.is_finite()) {
140            return Err(serde::de::Error::custom("last_values must be finite"));
141        }
142        encoder.last_values = helper.last_values;
143        Ok(encoder)
144    }
145}
146
147impl Encoder for DeltaEncoder {
148    fn encode(&mut self, input: &[f32]) -> EncodedOutput {
149        self.encode_with_threshold_scale(input, 1.0)
150    }
151
152    fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
153        let safe_input = if input.len() > self.last_values.len() {
154            &input[..self.last_values.len()]
155        } else {
156            input
157        };
158        self.encode(safe_input)
159    }
160
161    fn reset(&mut self) {
162        for val in self.last_values.iter_mut() {
163            *val = 0.0;
164        }
165    }
166}
167
168impl ModulatedEncoder for DeltaEncoder {
169    fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
170        self.encode_with_threshold_scale(input, gains.sanitize().threshold_scale)
171    }
172}
173
174/// Simplified: delta-based spike generation (per feature).
175///
176/// This is a utility function that takes a slice of deltas and returns a boolean spike train.
177/// It can be used to feed the resulting binary/event sequences into LIF/RSNN layers.
178pub fn encode_deltas_to_spikes(deltas: &[f32], threshold: f32) -> Vec<bool> {
179    deltas.iter().map(|&d| d.abs() > threshold).collect()
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_delta_encoder() {
188        let mut encoder = DeltaEncoder::new(2.0, 1);
189        let output = encoder.encode(&[1.0]); // 1.0 - 0.0 = 1.0 < 2.0 -> no spike
190        assert!(output.spikes.is_empty());
191
192        let output = encoder.encode(&[3.5]); // 3.5 - 0.0 = 3.5 > 2.0 -> spike
193        assert!(!output.spikes.is_empty());
194        assert!(output.spikes[0].polarity);
195
196        let output = encoder.encode(&[4.0]); // 4.0 - 3.5 = 0.5 < 2.0 -> no spike
197        assert!(output.spikes.is_empty());
198
199        let output = encoder.encode(&[1.0]); // 1.0 - 3.5 = -2.5.abs() = 2.5 > 2.0 -> spike
200        assert!(!output.spikes.is_empty());
201        assert!(!output.spikes[0].polarity);
202    }
203
204    #[test]
205    fn test_delta_encoder_encode_step() {
206        let mut encoder = DeltaEncoder::new(2.0, 2);
207        let output = encoder.encode_step(&[3.0, 3.0, 3.0]); // 3rd channel ignored
208        assert_eq!(output.spikes.len(), 2);
209    }
210
211    #[test]
212    fn test_delta_encoder_multi_channel_reset() {
213        let mut encoder = DeltaEncoder::new(1.0, 2);
214        encoder.encode(&[2.0, 2.0]);
215        assert_eq!(encoder.last_values, vec![2.0, 2.0]);
216        encoder.reset();
217        assert_eq!(encoder.last_values, vec![0.0, 0.0]);
218    }
219
220    #[test]
221    fn test_delta_encoder_empty_input() {
222        let mut encoder = DeltaEncoder::new(1.0, 5);
223        let output = encoder.encode(&[]);
224        assert!(output.spikes.is_empty());
225    }
226
227    #[test]
228    fn test_encode_deltas_to_spikes() {
229        let deltas = [0.1, 0.5, -0.8, 1.2];
230        let threshold = 0.7;
231        let spikes = encode_deltas_to_spikes(&deltas, threshold);
232        assert_eq!(spikes, vec![false, false, true, true]);
233    }
234
235    #[test]
236    fn test_delta_encoder_modulators_reduce_threshold() {
237        let mut encoder = DeltaEncoder::new(1.0, 1);
238        let modulators = NeuroModulators {
239            dopamine: 1.0,
240            ..Default::default()
241        };
242        let gain_curves = NeuromodulatorGainCurves {
243            dopamine: ModulatorGainCurves {
244                threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
245                ..Default::default()
246            },
247            ..Default::default()
248        };
249
250        assert!(encoder.encode(&[0.75]).spikes.is_empty());
251        encoder.reset();
252
253        let modulated = encoder.encode_with_modulators(&[0.75], &modulators, &gain_curves);
254        assert_eq!(modulated.spikes.len(), 1);
255    }
256
257    #[test]
258    fn test_delta_encoder_encode_step_with_modulators() {
259        let mut encoder = DeltaEncoder::new(1.0, 1);
260        let modulators = NeuroModulators {
261            dopamine: 1.0,
262            ..Default::default()
263        };
264        let gain_curves = NeuromodulatorGainCurves {
265            dopamine: ModulatorGainCurves {
266                threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
267                ..Default::default()
268            },
269            ..Default::default()
270        };
271
272        assert!(
273            encoder
274                .encode_step_with_modulators(&[0.0], &modulators, &gain_curves)
275                .spikes
276                .is_empty()
277        );
278        let modulated = encoder.encode_step_with_modulators(&[0.75], &modulators, &gain_curves);
279        assert_eq!(modulated.spikes.len(), 1);
280    }
281
282    #[test]
283    fn test_delta_encoder_step_shorter_input() {
284        let mut encoder = DeltaEncoder::new(1.0, 2);
285        let output = encoder.encode_step(&[2.0]);
286        assert_eq!(output.spikes.len(), 1);
287    }
288
289    #[test]
290    fn test_delta_encoder_truncates_excess_channels() {
291        let mut encoder = DeltaEncoder::new(1.0, 1);
292        let output = encoder.encode(&[2.0, 3.0]);
293        assert_eq!(output.spikes.len(), 1);
294    }
295
296    #[test]
297    fn test_delta_encoder_zero_threshold_scale_spikes_on_any_change() {
298        let mut encoder = DeltaEncoder::new(1.0, 1);
299        encoder.encode(&[0.0]);
300        let output = encoder.encode_with_threshold_scale(&[0.01], 0.0);
301        assert_eq!(output.spikes.len(), 1);
302    }
303    #[test]
304    fn test_delta_encoder_zero_threshold_spikes_on_any_change() {
305        let mut encoder = DeltaEncoder::new(0.0, 1);
306        encoder.encode(&[0.0]);
307        let output = encoder.encode(&[0.01]);
308        assert_eq!(output.spikes.len(), 1);
309        let quiet = encoder.encode(&[0.01]);
310        assert!(quiet.spikes.is_empty());
311    }
312
313    #[test]
314    fn test_delta_encoder_try_new_validation() {
315        assert!(DeltaEncoder::try_new(0.0, 1).is_ok());
316        assert_eq!(
317            DeltaEncoder::try_new(-1.0, 1).err(),
318            Some(EncoderError::NonNegativeFinite {
319                parameter: "threshold"
320            })
321        );
322        assert_eq!(
323            DeltaEncoder::try_new(f32::NAN, 1).err(),
324            Some(EncoderError::NonNegativeFinite {
325                parameter: "threshold"
326            })
327        );
328        assert_eq!(
329            DeltaEncoder::try_new(1.0, u16::MAX as usize + 2).err(),
330            Some(EncoderError::NumChannelsTooLarge)
331        );
332    }
333}