Skip to main content

axon_encoder/encoders/
latency.rs

1use crate::prelude::*;
2
3/// Encodes analog values into latency-coded spike times
4///
5/// Each input channel produces exactly one positive spike whose timestamp is
6/// determined by the input strength within the configured range. Stronger
7/// inputs fire earlier. Values below the range minimum map to the latest
8/// possible spike at `max_latency`, and values above the range maximum map to
9/// timestamp `0`.
10///
11/// # Examples
12///
13/// ```rust
14/// use axon_encoder::prelude::*;
15/// # fn main() -> Result<(), EncoderError> {
16/// let mut enc = LatencyEncoder::try_new(10, (0.0, 1.0))?;
17/// let out = enc.encode(&[1.0, 0.0]); // strong → early, weak → late
18/// assert_eq!(out.spikes.len(), 2);
19/// assert!(out.spikes[0].timestamp <= out.spikes[1].timestamp);
20/// # Ok(())
21/// # }
22/// ```
23#[derive(Clone, Debug, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25pub struct LatencyEncoder {
26    max_latency: u64,
27    range: (f32, f32),
28}
29
30impl LatencyEncoder {
31    /// Creates a new `LatencyEncoder`, panicking if configuration is invalid.
32    ///
33    /// Prefer [`try_new`](Self::try_new) for typed validation errors.
34    ///
35    /// # Panics
36    ///
37    /// Panics if `range.0 >= range.1` or either bound is non-finite.
38    ///
39    /// `max_latency == 0` is valid and emits every spike at timestamp `0`
40    /// (instantaneous response).
41    pub fn new(max_latency: u64, range: (f32, f32)) -> Self {
42        Self::try_new(max_latency, range).unwrap_or_else(|error| panic!("{error}"))
43    }
44
45    /// Creates a new `LatencyEncoder`, returning an [`EncoderError`] for invalid configuration.
46    ///
47    /// `max_latency == 0` is accepted and maps every input to timestamp `0`.
48    pub fn try_new(max_latency: u64, range: (f32, f32)) -> Result<Self, EncoderError> {
49        crate::error::validate_range("range", range)?;
50        Ok(Self { max_latency, range })
51    }
52
53    fn normalize(&self, value: f32) -> f64 {
54        // Use f64 to prevent overflow for valid f32 ranges (e.g., f32::MIN..f32::MAX).
55        let clamped = value.clamp(self.range.0, self.range.1) as f64;
56        let lo = self.range.0 as f64;
57        let hi = self.range.1 as f64;
58        (clamped - lo) / (hi - lo)
59    }
60
61    fn timestamp_for(&self, value: f32) -> u64 {
62        if self.max_latency == 0 {
63            return 0;
64        }
65        if value.is_nan() {
66            return self.max_latency;
67        }
68
69        let normalized = self.normalize(value);
70        ((1.0 - normalized) * self.max_latency as f64).round() as u64
71    }
72
73    fn timestamp_for_with_latency_scale(&self, value: f32, latency_scale: f32) -> u64 {
74        let scaled_latency = ((self.max_latency as f64) * (latency_scale as f64)).round() as u64;
75        if scaled_latency == 0 {
76            return 0;
77        }
78        if value.is_nan() {
79            return scaled_latency;
80        }
81
82        let normalized = self.normalize(value);
83        ((1.0 - normalized) * scaled_latency as f64).round() as u64
84    }
85
86    fn encode_with_latency_scale(&mut self, input: &[f32], latency_scale: f32) -> EncodedOutput {
87        let mut output = EncodedOutput::new();
88        output.spikes.reserve(input.len());
89
90        for (channel, &value) in input.iter().enumerate() {
91            let Ok(channel) = u16::try_from(channel) else {
92                // Remaining channels exceed u16::MAX; stop rather than wrap.
93                break;
94            };
95            output.spikes.push(SpikeEvent {
96                channel,
97                timestamp: self.timestamp_for_with_latency_scale(value, latency_scale),
98                polarity: true,
99            });
100        }
101
102        output
103    }
104
105    /// Encodes input using neuromodulator-driven gain curves.
106    ///
107    /// Inherent wrapper so callers need not import [`ModulatedEncoder`].
108    pub fn encode_with_modulators(
109        &mut self,
110        input: &[f32],
111        modulators: &NeuroModulators,
112        gain_curves: &NeuromodulatorGainCurves,
113    ) -> EncodedOutput {
114        <Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
115    }
116
117    /// Step-wise variant of [`encode_with_modulators`](Self::encode_with_modulators).
118    pub fn encode_step_with_modulators(
119        &mut self,
120        input: &[f32],
121        modulators: &NeuroModulators,
122        gain_curves: &NeuromodulatorGainCurves,
123    ) -> EncodedOutput {
124        <Self as ModulatedEncoder>::encode_step_with_modulators(
125            self,
126            input,
127            modulators,
128            gain_curves,
129        )
130    }
131}
132
133impl Encoder for LatencyEncoder {
134    fn encode(&mut self, input: &[f32]) -> EncodedOutput {
135        let mut output = EncodedOutput::new();
136        output.spikes.reserve(input.len());
137
138        for (channel, &value) in input.iter().enumerate() {
139            let Ok(channel) = u16::try_from(channel) else {
140                // Remaining channels exceed u16::MAX; stop rather than wrap.
141                break;
142            };
143            output.spikes.push(SpikeEvent {
144                channel,
145                timestamp: self.timestamp_for(value),
146                polarity: true,
147            });
148        }
149
150        output
151    }
152
153    fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
154        self.encode(input)
155    }
156
157    fn reset(&mut self) {
158        // Stateless encoder.
159    }
160}
161
162impl ModulatedEncoder for LatencyEncoder {
163    fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
164        self.encode_with_latency_scale(input, gains.sanitize().latency_scale)
165    }
166}
167
168#[cfg(feature = "serde")]
169impl<'de> serde::Deserialize<'de> for LatencyEncoder {
170    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171    where
172        D: serde::Deserializer<'de>,
173    {
174        #[derive(serde::Deserialize)]
175        struct Helper {
176            max_latency: u64,
177            range: (f32, f32),
178        }
179
180        let helper = Helper::deserialize(deserializer)?;
181
182        Self::try_new(helper.max_latency, helper.range).map_err(serde::de::Error::custom)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn latency_encoder_emits_one_positive_spike_per_channel() {
192        let mut encoder = LatencyEncoder::new(10, (0.0, 1.0));
193
194        let output = encoder.encode(&[0.0, 0.5, 1.0]);
195
196        assert_eq!(output.spikes.len(), 3);
197        assert_eq!(
198            output.spikes,
199            vec![
200                SpikeEvent {
201                    channel: 0,
202                    timestamp: 10,
203                    polarity: true,
204                },
205                SpikeEvent {
206                    channel: 1,
207                    timestamp: 5,
208                    polarity: true,
209                },
210                SpikeEvent {
211                    channel: 2,
212                    timestamp: 0,
213                    polarity: true,
214                },
215            ]
216        );
217    }
218
219    #[test]
220    fn test_latency_encoder_nan() {
221        let mut encoder = LatencyEncoder::new(10, (0.0, 1.0));
222        let output = encoder.encode(&[f32::NAN]);
223        assert_eq!(output.spikes[0].timestamp, 10);
224    }
225
226    #[test]
227    fn test_latency_encoder_reset() {
228        let mut encoder = LatencyEncoder::new(10, (0.0, 1.0));
229        encoder.reset(); // Should do nothing
230    }
231
232    #[test]
233    fn latency_encoder_stronger_inputs_fire_earlier() {
234        let mut encoder = LatencyEncoder::new(12, (0.0, 3.0));
235
236        let output = encoder.encode(&[0.5, 1.5, 2.5]);
237
238        assert_eq!(output.spikes.len(), 3);
239        assert!(output.spikes[0].timestamp > output.spikes[1].timestamp);
240        assert!(output.spikes[1].timestamp > output.spikes[2].timestamp);
241    }
242
243    #[test]
244    fn latency_encoder_clamps_inputs_to_range() {
245        let mut encoder = LatencyEncoder::new(8, (2.0, 6.0));
246
247        let output = encoder.encode(&[0.0, 2.0, 4.0, 6.0, 9.0]);
248
249        assert_eq!(
250            output
251                .spikes
252                .iter()
253                .map(|spike| spike.timestamp)
254                .collect::<Vec<_>>(),
255            vec![8, 8, 4, 0, 0]
256        );
257    }
258
259    #[test]
260    fn latency_encoder_encode_step_matches_encode() {
261        let mut encoder = LatencyEncoder::new(20, (-1.0, 1.0));
262        let input = [-1.0, -0.25, 0.75, 1.5];
263
264        let batch = encoder.encode(&input);
265        let step = encoder.encode_step(&input);
266
267        assert_eq!(batch, step);
268    }
269
270    #[test]
271    fn latency_encoder_handles_empty_input() {
272        let mut encoder = LatencyEncoder::new(5, (0.0, 1.0));
273
274        let output = encoder.encode(&[]);
275
276        assert!(output.spikes.is_empty());
277    }
278
279    #[test]
280    fn latency_encoder_nan_maps_to_max_latency() {
281        let mut encoder = LatencyEncoder::new(7, (0.0, 1.0));
282
283        let output = encoder.encode(&[f32::NAN, 1.0]);
284
285        assert_eq!(output.spikes[0].timestamp, 7);
286        assert_eq!(output.spikes[1].timestamp, 0);
287    }
288
289    #[test]
290    #[should_panic(expected = "range must be finite and min must be less than max")]
291    fn latency_encoder_rejects_invalid_range() {
292        let _ = LatencyEncoder::new(5, (1.0, 1.0));
293    }
294
295    #[test]
296    #[should_panic(expected = "range must be finite and min must be less than max")]
297    fn latency_encoder_rejects_infinite_range() {
298        let _ = LatencyEncoder::new(10, (f32::NEG_INFINITY, f32::INFINITY));
299    }
300
301    #[test]
302    fn latency_encoder_truncates_channel_overflow() {
303        let mut encoder = LatencyEncoder::new(1, (0.0, 1.0));
304        let input = vec![0.0f32; (u16::MAX as usize) + 2];
305        let output = encoder.encode(&input);
306        assert_eq!(output.spikes.len(), u16::MAX as usize + 1);
307    }
308
309    #[test]
310    fn latency_encoder_encode_with_modulators_identity() {
311        let mut encoder = LatencyEncoder::new(10, (0.0, 1.0));
312        let curves = NeuromodulatorGainCurves::default();
313        let mods = NeuroModulators::default();
314
315        let plain = encoder.encode(&[0.5]);
316        let modulated = encoder.encode_with_modulators(&[0.5], &mods, &curves);
317
318        assert_eq!(plain.spikes[0].timestamp, modulated.spikes[0].timestamp);
319    }
320
321    #[test]
322    fn latency_encoder_encode_with_modulators_latency_scale() {
323        let mut encoder = LatencyEncoder::new(10, (0.0, 1.0));
324        let curves = NeuromodulatorGainCurves {
325            dopamine: ModulatorGainCurves {
326                latency: Some(GainCurve::new((0.0, 1.0), (0.5, 0.5))),
327                ..Default::default()
328            },
329            ..Default::default()
330        };
331        let mods = NeuroModulators {
332            dopamine: 1.0,
333            ..Default::default()
334        };
335
336        let output = encoder.encode_with_modulators(&[0.5], &mods, &curves);
337        // latency_scale = 0.5, so max_latency = 10 * 0.5 = 5
338        // normalized(0.5) = 0.5, timestamp = (1.0 - 0.5) * 5 = 2.5 → 3
339        assert_eq!(output.spikes[0].timestamp, 3);
340    }
341
342    #[test]
343    fn latency_encoder_encode_step_with_modulators_matches_encode() {
344        let mut encoder = LatencyEncoder::new(10, (0.0, 1.0));
345        let curves = NeuromodulatorGainCurves::default();
346        let mods = NeuroModulators::default();
347
348        let batch = encoder.encode_with_modulators(&[0.5], &mods, &curves);
349        let step = encoder.encode_step_with_modulators(&[0.5], &mods, &curves);
350
351        assert_eq!(batch, step);
352    }
353
354    #[test]
355    fn latency_encoder_modulators_zero_scale_maps_to_zero() {
356        let mut encoder = LatencyEncoder::new(10, (0.0, 1.0));
357        let curves = NeuromodulatorGainCurves {
358            dopamine: ModulatorGainCurves {
359                latency: Some(GainCurve::new((0.0, 1.0), (1.0, 0.0))),
360                ..Default::default()
361            },
362            ..Default::default()
363        };
364        let mods = NeuroModulators {
365            dopamine: 1.0,
366            ..Default::default()
367        };
368
369        let output = encoder.encode_with_modulators(&[0.5, f32::NAN], &mods, &curves);
370        assert_eq!(output.spikes.len(), 2);
371        assert!(output.spikes.iter().all(|s| s.timestamp == 0));
372    }
373    #[test]
374    fn latency_encoder_supports_zero_max_latency() {
375        let mut encoder = LatencyEncoder::new(0, (0.0, 1.0));
376        let output = encoder.encode(&[0.0, 0.5, 1.0]);
377        assert_eq!(output.spikes.len(), 3);
378        assert!(output.spikes.iter().all(|s| s.timestamp == 0));
379    }
380
381    #[test]
382    fn latency_encoder_try_new_rejects_invalid_configuration() {
383        assert!(LatencyEncoder::try_new(0, (0.0, 1.0)).is_ok());
384        assert_eq!(
385            LatencyEncoder::try_new(1, (1.0, 1.0)).err(),
386            Some(EncoderError::InvalidRange { parameter: "range" })
387        );
388    }
389}