Skip to main content

axon_encoder/encoders/
predictive.rs

1use crate::prelude::*;
2use std::collections::VecDeque;
3use std::fmt;
4
5/// Errors that can occur when initializing a [`PredictiveEncoder`].
6#[non_exhaustive]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum PredictiveEncoderError {
9    /// `history_depth` was less than 5 (the minimum window used by the predictor).
10    HistoryDepthTooSmall,
11    /// `num_channels` exceeds the `u16` channel-ID range used when emitting spikes.
12    ///
13    /// Valid channel indices are `0..=u16::MAX`, so at most `u16::MAX as usize + 1` channels.
14    NumChannelsTooLarge,
15    /// A deviation threshold was non-finite or negative.
16    InvalidDeviationThreshold,
17}
18
19impl fmt::Display for PredictiveEncoderError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Self::HistoryDepthTooSmall => write!(f, "history_depth must be at least 5"),
23            Self::NumChannelsTooLarge => write!(
24                f,
25                "num_channels exceeds u16::MAX as usize + 1 (max addressable spike channels)"
26            ),
27            Self::InvalidDeviationThreshold => {
28                write!(f, "deviation_threshold must be finite and non-negative")
29            }
30        }
31    }
32}
33
34impl std::error::Error for PredictiveEncoderError {}
35
36impl From<PredictiveEncoderError> for EncoderError {
37    fn from(error: PredictiveEncoderError) -> Self {
38        match error {
39            PredictiveEncoderError::HistoryDepthTooSmall => {
40                EncoderError::HistoryDepthTooSmall { minimum: 5 }
41            }
42            PredictiveEncoderError::NumChannelsTooLarge => EncoderError::NumChannelsTooLarge,
43            PredictiveEncoderError::InvalidDeviationThreshold => EncoderError::NonNegativeFinite {
44                parameter: "deviation_threshold",
45            },
46        }
47    }
48}
49
50/// Encodes based on causal predictive error from expected values.
51///
52/// This encoder is best understood as an adaptive EWMA anomaly detector with
53/// predictive-coding-style signed error spikes. It keeps per-channel history,
54/// predicts the next sample from prior samples only, and fires a spike when the
55/// signed prediction error is large enough. Positive errors emit
56/// `polarity: true`; negative errors emit `polarity: false`.
57///
58/// # Mathematical Model
59///
60/// Tracks an exponentially weighted moving average of recent history means per
61/// channel. The first five samples are a warm-up period: they update history and
62/// initialize the prediction baseline but never emit spikes. After warm-up, each
63/// input is evaluated against the predictor state formed before that input is
64/// inserted, so the current observation cannot leak into its own prediction.
65///
66/// ```text
67/// if history.len() < 5:
68///     push value; initialize prediction when five samples are available; no spike
69/// else:
70///     prediction = threshold[i]
71///     error = value - prediction
72///     spike if |error| > threshold, with polarity = error >= 0
73///     push value
74///     threshold[i] = 0.9 * threshold[i] + 0.1 * mean(history[-5:])
75/// ```
76///
77/// # When to Use
78///
79/// - Anomaly detection in sensor streams
80/// - Learning patterns and detecting deviations
81/// - Adaptive encoding that adjusts to baseline activity
82///
83/// # Parameters
84///
85/// - `history_depth`: Number of past values to track per channel
86/// - `deviation_thresholds`: Vec of (threshold, spike_value) pairs
87/// - `num_channels`: Number of input channels
88///
89/// # Examples
90///
91/// ```rust
92/// use axon_encoder::prelude::*;
93/// # fn main() -> Result<(), EncoderError> {
94/// let mut enc = PredictiveEncoder::try_new(8, vec![(0.5, 1)], 1)?;
95/// // First five samples warm up without spikes.
96/// for v in [1.0, 1.0, 1.0, 1.0, 1.0] {
97///     assert!(enc.encode_step(&[v]).spikes.is_empty());
98/// }
99/// // A large jump after warm-up can emit a prediction-error spike.
100/// let _ = enc.encode_step(&[3.0]);
101/// # Ok(())
102/// # }
103/// ```
104#[derive(Clone, Debug, PartialEq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize))]
106pub struct PredictiveEncoder {
107    history: Vec<VecDeque<f32>>,
108    thresholds: Vec<f32>,
109    history_depth: usize,
110    deviation_thresholds: Vec<(f32, u16)>,
111}
112
113impl PredictiveEncoder {
114    /// Creates a new `PredictiveEncoder`.
115    ///
116    /// Retains the historical `PredictiveEncoderError` surface for source
117    /// compatibility. Prefer [`try_new`](Self::try_new) for the unified
118    /// [`EncoderError`] type used by other fallible constructors.
119    ///
120    /// # Errors
121    ///
122    /// - [`PredictiveEncoderError::HistoryDepthTooSmall`] if `history_depth < 5`
123    /// - [`PredictiveEncoderError::NumChannelsTooLarge`] if `num_channels > u16::MAX as usize + 1`
124    ///   (spike `channel` IDs are `u16`, so indices must stay in `0..=u16::MAX`)
125    /// - [`PredictiveEncoderError::InvalidDeviationThreshold`] if any threshold is
126    ///   non-finite or negative
127    pub fn new(
128        history_depth: usize,
129        deviation_thresholds: Vec<(f32, u16)>,
130        num_channels: usize,
131    ) -> Result<Self, PredictiveEncoderError> {
132        Self::try_new(history_depth, deviation_thresholds, num_channels).map_err(
133            |error| match error {
134                EncoderError::HistoryDepthTooSmall { .. } => {
135                    PredictiveEncoderError::HistoryDepthTooSmall
136                }
137                EncoderError::NumChannelsTooLarge => PredictiveEncoderError::NumChannelsTooLarge,
138                EncoderError::NonNegativeFinite {
139                    parameter: "deviation_threshold",
140                } => PredictiveEncoderError::InvalidDeviationThreshold,
141                other => panic!("unexpected EncoderError from PredictiveEncoder::try_new: {other}"),
142            },
143        )
144    }
145
146    /// Creates a new `PredictiveEncoder`, returning the unified [`EncoderError`].
147    ///
148    /// Prefer this over [`new`](Self::new) when propagating constructor failures
149    /// alongside other encoders via `EncoderError`. Each `deviation_threshold`
150    /// must be finite and non-negative (same rule as
151    /// [`TemporalEncoder::try_new`](crate::encoders::TemporalEncoder::try_new)).
152    pub fn try_new(
153        history_depth: usize,
154        deviation_thresholds: Vec<(f32, u16)>,
155        num_channels: usize,
156    ) -> Result<Self, EncoderError> {
157        if history_depth < 5 {
158            return Err(EncoderError::HistoryDepthTooSmall { minimum: 5 });
159        }
160        for &(threshold, _) in &deviation_thresholds {
161            crate::error::validate_non_negative_finite("deviation_threshold", threshold)?;
162        }
163        // encode_with_threshold_scale maps channel index → u16 via try_from.
164        crate::error::validate_channel_count(num_channels)?;
165        Ok(Self {
166            history: vec![VecDeque::with_capacity(history_depth); num_channels],
167            thresholds: vec![0.0; num_channels],
168            history_depth,
169            deviation_thresholds,
170        })
171    }
172
173    fn encode_with_threshold_scale(
174        &mut self,
175        input: &[f32],
176        threshold_scale: f32,
177    ) -> EncodedOutput {
178        let mut output = EncodedOutput::new();
179        for (i, &value) in input.iter().enumerate() {
180            if i >= self.history.len() {
181                break;
182            }
183            let channel_history = &mut self.history[i];
184
185            // Warm-up: history_depth is always >= 5, so no eviction can fire here.
186            if channel_history.len() < 5 {
187                channel_history.push_back(value);
188
189                if channel_history.len() == 5 {
190                    self.thresholds[i] = channel_history.iter().rev().take(5).sum::<f32>() / 5.0;
191                }
192
193                continue;
194            }
195
196            let prediction = self.thresholds[i];
197            let error = value - prediction;
198            let deviation = error.abs();
199
200            for &(threshold, _spike_val) in self.deviation_thresholds.iter().rev() {
201                if deviation > (threshold * threshold_scale).max(0.0) {
202                    let Ok(channel) = u16::try_from(i) else {
203                        break;
204                    };
205                    output.spikes.push(SpikeEvent {
206                        channel,
207                        timestamp: 0,
208                        polarity: error >= 0.0,
209                    });
210                    break;
211                }
212            }
213
214            if channel_history.len() == self.history_depth {
215                channel_history.pop_front();
216            }
217            channel_history.push_back(value);
218
219            let recent_avg = channel_history.iter().rev().take(5).sum::<f32>() / 5.0;
220            self.thresholds[i] = 0.9 * self.thresholds[i] + 0.1 * recent_avg;
221        }
222        output
223    }
224
225    /// Encodes input using neuromodulator-driven gain curves.
226    ///
227    /// Inherent wrapper so callers need not import [`ModulatedEncoder`].
228    pub fn encode_with_modulators(
229        &mut self,
230        input: &[f32],
231        modulators: &NeuroModulators,
232        gain_curves: &NeuromodulatorGainCurves,
233    ) -> EncodedOutput {
234        <Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
235    }
236
237    /// Step-wise variant of [`encode_with_modulators`](Self::encode_with_modulators).
238    pub fn encode_step_with_modulators(
239        &mut self,
240        input: &[f32],
241        modulators: &NeuroModulators,
242        gain_curves: &NeuromodulatorGainCurves,
243    ) -> EncodedOutput {
244        <Self as ModulatedEncoder>::encode_step_with_modulators(
245            self,
246            input,
247            modulators,
248            gain_curves,
249        )
250    }
251}
252
253impl Encoder for PredictiveEncoder {
254    fn encode(&mut self, input: &[f32]) -> EncodedOutput {
255        self.encode_with_threshold_scale(input, 1.0)
256    }
257
258    fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
259        let safe_input = if input.len() > self.history.len() {
260            &input[..self.history.len()]
261        } else {
262            input
263        };
264        self.encode_with_threshold_scale(safe_input, 1.0)
265    }
266
267    fn reset(&mut self) {
268        for history in self.history.iter_mut() {
269            history.clear();
270        }
271        for threshold in self.thresholds.iter_mut() {
272            *threshold = 0.0;
273        }
274    }
275}
276
277impl ModulatedEncoder for PredictiveEncoder {
278    fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
279        let safe_input = if input.len() > self.history.len() {
280            &input[..self.history.len()]
281        } else {
282            input
283        };
284        self.encode_with_threshold_scale(safe_input, gains.sanitize().threshold_scale)
285    }
286}
287
288#[cfg(feature = "serde")]
289impl<'de> serde::Deserialize<'de> for PredictiveEncoder {
290    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
291    where
292        D: serde::Deserializer<'de>,
293    {
294        use std::collections::VecDeque;
295
296        #[derive(serde::Deserialize)]
297        struct Helper {
298            history: Vec<VecDeque<f32>>,
299            thresholds: Vec<f32>,
300            history_depth: usize,
301            deviation_thresholds: Vec<(f32, u16)>,
302        }
303
304        let helper = Helper::deserialize(deserializer)?;
305
306        if helper.history.len() != helper.thresholds.len() {
307            return Err(serde::de::Error::custom(format!(
308                "mismatched history length ({}) and thresholds length ({})",
309                helper.history.len(),
310                helper.thresholds.len()
311            )));
312        }
313
314        // Mirror `new()`: spike channel IDs are u16 (indices 0..=u16::MAX).
315        if helper.history.len() > u16::MAX as usize + 1 {
316            return Err(serde::de::Error::custom(
317                "num_channels exceeds u16::MAX as usize + 1 (max addressable spike channels)",
318            ));
319        }
320
321        if helper.history_depth < 5 {
322            return Err(serde::de::Error::custom("history_depth must be at least 5"));
323        }
324
325        // Match try_new: reject non-finite / negative deviation thresholds on load.
326        for &(threshold, _) in &helper.deviation_thresholds {
327            crate::error::validate_non_negative_finite("deviation_threshold", threshold)
328                .map_err(serde::de::Error::custom)?;
329        }
330
331        for (i, deque) in helper.history.iter().enumerate() {
332            if deque.len() > helper.history_depth {
333                return Err(serde::de::Error::custom(format!(
334                    "history channel {} length ({}) exceeds history_depth ({})",
335                    i,
336                    deque.len(),
337                    helper.history_depth
338                )));
339            }
340        }
341
342        Ok(Self {
343            history: helper.history,
344            thresholds: helper.thresholds,
345            history_depth: helper.history_depth,
346            deviation_thresholds: helper.deviation_thresholds,
347        })
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_predictive_encoder_rejects_small_history_depth() {
357        let err = PredictiveEncoder::new(4, vec![(2.0, 1)], 1).err();
358        assert_eq!(err, Some(PredictiveEncoderError::HistoryDepthTooSmall));
359        assert_eq!(
360            PredictiveEncoderError::HistoryDepthTooSmall.to_string(),
361            "history_depth must be at least 5"
362        );
363        assert!(PredictiveEncoder::new(5, vec![(2.0, 1)], 1).is_ok());
364        assert!(PredictiveEncoder::new(0, vec![(2.0, 1)], 1).is_err());
365
366        // try_new uses the unified EncoderError surface.
367        assert_eq!(
368            PredictiveEncoder::try_new(4, vec![(2.0, 1)], 1).err(),
369            Some(EncoderError::HistoryDepthTooSmall { minimum: 5 })
370        );
371        assert_eq!(
372            PredictiveEncoder::try_new(5, vec![(2.0, 1)], u16::MAX as usize + 2).err(),
373            Some(EncoderError::NumChannelsTooLarge)
374        );
375        assert_eq!(
376            PredictiveEncoder::try_new(5, vec![(f32::NAN, 1)], 1).err(),
377            Some(EncoderError::NonNegativeFinite {
378                parameter: "deviation_threshold"
379            })
380        );
381        assert_eq!(
382            PredictiveEncoder::try_new(5, vec![(-1.0, 1)], 1).err(),
383            Some(EncoderError::NonNegativeFinite {
384                parameter: "deviation_threshold"
385            })
386        );
387        assert_eq!(
388            PredictiveEncoder::new(5, vec![(-1.0, 1)], 1).err(),
389            Some(PredictiveEncoderError::InvalidDeviationThreshold)
390        );
391        assert!(PredictiveEncoder::try_new(5, vec![(0.0, 1)], 1).is_ok());
392    }
393
394    #[test]
395    fn test_predictive_encoder_num_channels_u16_range() {
396        let max_ok = u16::MAX as usize + 1;
397        let first_bad = max_ok + 1;
398
399        // First rejected: fails before allocation / without panicking.
400        assert_eq!(
401            PredictiveEncoder::new(5, vec![(0.2, 1)], first_bad).err(),
402            Some(PredictiveEncoderError::NumChannelsTooLarge)
403        );
404        assert!(
405            PredictiveEncoderError::NumChannelsTooLarge
406                .to_string()
407                .contains("u16::MAX")
408        );
409
410        // Accepted maximum: every channel index is representable as u16.
411        let encoder =
412            PredictiveEncoder::new(5, vec![(0.2, 1)], max_ok).expect("max u16 channel count");
413        assert_eq!(encoder.history.len(), max_ok);
414        assert_eq!(encoder.thresholds.len(), max_ok);
415    }
416
417    #[test]
418    fn test_predictive_encoder() {
419        let mut encoder =
420            PredictiveEncoder::new(5, vec![(2.0, 1)], 1).expect("valid PredictiveEncoder");
421        let _output = encoder.encode(&[1.0]);
422        let _output = encoder.encode(&[1.0]);
423        let _output = encoder.encode(&[1.0]);
424        let _output = encoder.encode(&[1.0]);
425        let _output = encoder.encode(&[1.0]);
426        let output = encoder.encode(&[10.0]);
427        assert!(!output.spikes.is_empty());
428    }
429
430    #[test]
431    fn test_predictive_encoder_constant_signal_has_no_cold_start_burst() {
432        let mut encoder =
433            PredictiveEncoder::new(5, vec![(0.5, 1)], 1).expect("valid PredictiveEncoder");
434
435        for _ in 0..16 {
436            let output = encoder.encode(&[42.0]);
437            assert!(output.spikes.is_empty());
438        }
439    }
440
441    #[test]
442    fn test_predictive_encoder_short_history_warms_up_without_spikes() {
443        let mut encoder =
444            PredictiveEncoder::new(5, vec![(0.5, 1)], 1).expect("valid PredictiveEncoder");
445
446        for _ in 0..5 {
447            let output = encoder.encode(&[10.0]);
448            assert!(output.spikes.is_empty());
449        }
450
451        assert_eq!(encoder.history[0].len(), 5);
452        assert_eq!(encoder.thresholds[0], 10.0);
453    }
454
455    #[test]
456    fn test_predictive_encoder_positive_step_preserves_positive_polarity() {
457        let mut encoder =
458            PredictiveEncoder::new(5, vec![(1.0, 1)], 1).expect("valid PredictiveEncoder");
459        for _ in 0..5 {
460            assert!(encoder.encode(&[1.0]).spikes.is_empty());
461        }
462
463        let output = encoder.encode(&[4.0]);
464        assert_eq!(output.spikes.len(), 1);
465        assert!(output.spikes[0].polarity);
466    }
467
468    #[test]
469    fn test_predictive_encoder_negative_step_preserves_negative_polarity() {
470        let mut encoder =
471            PredictiveEncoder::new(5, vec![(1.0, 1)], 1).expect("valid PredictiveEncoder");
472        for _ in 0..5 {
473            assert!(encoder.encode(&[4.0]).spikes.is_empty());
474        }
475
476        let output = encoder.encode(&[1.0]);
477        assert_eq!(output.spikes.len(), 1);
478        assert!(!output.spikes[0].polarity);
479    }
480
481    #[test]
482    fn test_predictive_encoder_trend_uses_prior_prediction_before_update() {
483        let mut encoder =
484            PredictiveEncoder::new(5, vec![(0.75, 1)], 1).expect("valid PredictiveEncoder");
485        for value in [1.0, 2.0, 3.0, 4.0, 5.0] {
486            assert!(encoder.encode(&[value]).spikes.is_empty());
487        }
488        assert_eq!(encoder.thresholds[0], 3.0);
489
490        let output = encoder.encode(&[6.0]);
491        assert_eq!(output.spikes.len(), 1);
492        assert!(output.spikes[0].polarity);
493        assert_eq!(encoder.thresholds[0], 3.1);
494    }
495
496    #[test]
497    fn test_predictive_encoder_reset() {
498        let mut encoder =
499            PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
500        for _ in 0..6 {
501            encoder.encode(&[1.0, 2.0]);
502        }
503        encoder.reset();
504        assert!(encoder.history.iter().all(|h| h.is_empty()));
505        assert!(encoder.thresholds.iter().all(|&t| t == 0.0));
506    }
507
508    #[test]
509    fn test_predictive_encoder_reset_restarts_warmup_without_spikes() {
510        let mut encoder =
511            PredictiveEncoder::new(5, vec![(0.5, 1)], 1).expect("valid PredictiveEncoder");
512        for _ in 0..5 {
513            assert!(encoder.encode(&[1.0]).spikes.is_empty());
514        }
515        assert_eq!(encoder.encode(&[10.0]).spikes.len(), 1);
516
517        encoder.reset();
518
519        for _ in 0..5 {
520            assert!(encoder.encode(&[10.0]).spikes.is_empty());
521        }
522        assert_eq!(encoder.thresholds[0], 10.0);
523    }
524
525    #[test]
526    fn test_predictive_encoder_multi_channel() {
527        let mut encoder =
528            PredictiveEncoder::new(5, vec![(2.0, 1)], 3).expect("valid PredictiveEncoder");
529        for _ in 0..6 {
530            encoder.encode(&[1.0, 2.0, 3.0]);
531        }
532        let output = encoder.encode(&[10.0, 20.0, 30.0]);
533        // All channels should spike on large deviation
534        assert!(!output.spikes.is_empty());
535    }
536
537    #[test]
538    fn test_predictive_encoder_input_truncation() {
539        let mut encoder =
540            PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
541        for _ in 0..6 {
542            // 4 values but only 2 channels tracked — should truncate
543            encoder.encode(&[1.0, 2.0, 3.0, 4.0]);
544        }
545        // Should not panic; only first 2 channels processed
546        let output = encoder.encode(&[10.0, 20.0, 30.0, 40.0]);
547        assert!(output.spikes.len() <= 2);
548    }
549
550    #[test]
551    fn test_predictive_encoder_step_input_truncation() {
552        let mut encoder =
553            PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
554        for _ in 0..6 {
555            encoder.encode_step(&[1.0, 2.0, 3.0]);
556        }
557        let output = encoder.encode_step(&[10.0, 20.0, 30.0]);
558        assert!(output.spikes.len() <= 2);
559    }
560
561    #[test]
562    fn test_predictive_encoder_encode_with_modulators() {
563        let mut encoder =
564            PredictiveEncoder::new(5, vec![(5.0, 1)], 1).expect("valid PredictiveEncoder");
565        let mods = NeuroModulators {
566            acetylcholine: 1.0,
567            ..Default::default()
568        };
569        let curves = NeuromodulatorGainCurves {
570            acetylcholine: ModulatorGainCurves {
571                threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
572                ..Default::default()
573            },
574            ..Default::default()
575        };
576        for _ in 0..5 {
577            encoder.encode_with_modulators(&[1.0], &mods, &curves);
578        }
579        let output = encoder.encode_with_modulators(&[5.0], &mods, &curves);
580        assert_eq!(output.spikes.len(), 1);
581    }
582
583    #[test]
584    fn test_predictive_encoder_modulators_reduce_threshold() {
585        let mut encoder =
586            PredictiveEncoder::new(5, vec![(5.0, 1)], 1).expect("valid PredictiveEncoder");
587        let modulators = NeuroModulators {
588            acetylcholine: 1.0,
589            ..Default::default()
590        };
591        let gain_curves = NeuromodulatorGainCurves {
592            acetylcholine: ModulatorGainCurves {
593                threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
594                ..Default::default()
595            },
596            ..Default::default()
597        };
598
599        for _ in 0..5 {
600            encoder.encode(&[1.0]);
601        }
602        assert!(encoder.encode(&[5.0]).spikes.is_empty());
603
604        encoder.reset();
605
606        for _ in 0..5 {
607            encoder.encode_step_with_modulators(&[1.0], &modulators, &gain_curves);
608        }
609        let output = encoder.encode_step_with_modulators(&[5.0], &modulators, &gain_curves);
610        assert_eq!(output.spikes.len(), 1);
611    }
612
613    #[test]
614    fn test_predictive_encoder_encode_with_modulators_truncate() {
615        let mut encoder =
616            PredictiveEncoder::new(5, vec![(5.0, 1)], 1).expect("valid PredictiveEncoder");
617        let mods = NeuroModulators {
618            acetylcholine: 1.0,
619            ..Default::default()
620        };
621        let curves = NeuromodulatorGainCurves {
622            acetylcholine: ModulatorGainCurves {
623                threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
624                ..Default::default()
625            },
626            ..Default::default()
627        };
628        for _ in 0..5 {
629            encoder.encode_with_modulators(&[1.0, 2.0], &mods, &curves);
630        }
631        let output = encoder.encode_with_modulators(&[5.0, 6.0], &mods, &curves);
632        assert_eq!(output.spikes.len(), 1);
633    }
634
635    #[test]
636    fn test_predictive_encoder_step_shorter_input() {
637        let mut encoder =
638            PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
639        for _ in 0..6 {
640            encoder.encode_step(&[1.0]);
641        }
642        let output = encoder.encode_step(&[10.0]);
643        assert!(!output.spikes.is_empty());
644    }
645
646    #[cfg(feature = "serde")]
647    #[test]
648    fn test_predictive_serde_history_channel_too_long() {
649        let json = r#"{
650            "history": [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]],
651            "thresholds": [0.0],
652            "history_depth": 5,
653            "deviation_thresholds": []
654        }"#;
655        let res: Result<PredictiveEncoder, _> = serde_json::from_str(json);
656        assert!(res.is_err());
657    }
658
659    #[cfg(feature = "serde")]
660    #[test]
661    fn test_predictive_serde_rejects_too_many_channels() {
662        let max_ok = u16::MAX as usize + 1;
663        let first_bad = max_ok + 1;
664
665        // First rejected: same ceiling as `new()` (untrusted saved state cannot bypass).
666        let history: Vec<Vec<f32>> = vec![vec![]; first_bad];
667        let thresholds = vec![0.0f32; first_bad];
668        let value = serde_json::json!({
669            "history": history,
670            "thresholds": thresholds,
671            "history_depth": 5,
672            "deviation_thresholds": [[0.2, 1]],
673        });
674        let res: Result<PredictiveEncoder, _> = serde_json::from_value(value);
675        assert!(res.is_err());
676        let err = res.err().unwrap().to_string();
677        assert!(
678            err.contains("u16::MAX") || err.contains("num_channels"),
679            "unexpected error: {err}"
680        );
681
682        // Accepted maximum boundary still deserializes.
683        let history_ok: Vec<Vec<f32>> = vec![vec![]; max_ok];
684        let thresholds_ok = vec![0.0f32; max_ok];
685        let value_ok = serde_json::json!({
686            "history": history_ok,
687            "thresholds": thresholds_ok,
688            "history_depth": 5,
689            "deviation_thresholds": [[0.2, 1]],
690        });
691        let enc: PredictiveEncoder =
692            serde_json::from_value(value_ok).expect("max channel count deserializes");
693        assert_eq!(enc.history.len(), max_ok);
694        assert_eq!(enc.thresholds.len(), max_ok);
695    }
696}