use crate::prelude::*;
use std::collections::VecDeque;
use std::fmt;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PredictiveEncoderError {
HistoryDepthTooSmall,
NumChannelsTooLarge,
InvalidDeviationThreshold,
}
impl fmt::Display for PredictiveEncoderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::HistoryDepthTooSmall => write!(f, "history_depth must be at least 5"),
Self::NumChannelsTooLarge => write!(
f,
"num_channels exceeds u16::MAX as usize + 1 (max addressable spike channels)"
),
Self::InvalidDeviationThreshold => {
write!(f, "deviation_threshold must be finite and non-negative")
}
}
}
}
impl std::error::Error for PredictiveEncoderError {}
impl From<PredictiveEncoderError> for EncoderError {
fn from(error: PredictiveEncoderError) -> Self {
match error {
PredictiveEncoderError::HistoryDepthTooSmall => {
EncoderError::HistoryDepthTooSmall { minimum: 5 }
}
PredictiveEncoderError::NumChannelsTooLarge => EncoderError::NumChannelsTooLarge,
PredictiveEncoderError::InvalidDeviationThreshold => EncoderError::NonNegativeFinite {
parameter: "deviation_threshold",
},
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PredictiveEncoder {
history: Vec<VecDeque<f32>>,
thresholds: Vec<f32>,
history_depth: usize,
deviation_thresholds: Vec<(f32, u16)>,
}
impl PredictiveEncoder {
pub fn new(
history_depth: usize,
deviation_thresholds: Vec<(f32, u16)>,
num_channels: usize,
) -> Result<Self, PredictiveEncoderError> {
Self::try_new(history_depth, deviation_thresholds, num_channels).map_err(
|error| match error {
EncoderError::HistoryDepthTooSmall { .. } => {
PredictiveEncoderError::HistoryDepthTooSmall
}
EncoderError::NumChannelsTooLarge => PredictiveEncoderError::NumChannelsTooLarge,
EncoderError::NonNegativeFinite {
parameter: "deviation_threshold",
} => PredictiveEncoderError::InvalidDeviationThreshold,
other => panic!("unexpected EncoderError from PredictiveEncoder::try_new: {other}"),
},
)
}
pub fn try_new(
history_depth: usize,
deviation_thresholds: Vec<(f32, u16)>,
num_channels: usize,
) -> Result<Self, EncoderError> {
if history_depth < 5 {
return Err(EncoderError::HistoryDepthTooSmall { minimum: 5 });
}
for &(threshold, _) in &deviation_thresholds {
crate::error::validate_non_negative_finite("deviation_threshold", threshold)?;
}
crate::error::validate_channel_count(num_channels)?;
Ok(Self {
history: vec![VecDeque::with_capacity(history_depth); num_channels],
thresholds: vec![0.0; num_channels],
history_depth,
deviation_thresholds,
})
}
fn encode_with_threshold_scale(
&mut self,
input: &[f32],
threshold_scale: f32,
) -> EncodedOutput {
let mut output = EncodedOutput::new();
for (i, &value) in input.iter().enumerate() {
if i >= self.history.len() {
break;
}
let channel_history = &mut self.history[i];
if channel_history.len() < 5 {
channel_history.push_back(value);
if channel_history.len() == 5 {
self.thresholds[i] = channel_history.iter().rev().take(5).sum::<f32>() / 5.0;
}
continue;
}
let prediction = self.thresholds[i];
let error = value - prediction;
let deviation = error.abs();
for &(threshold, _spike_val) in self.deviation_thresholds.iter().rev() {
if deviation > (threshold * threshold_scale).max(0.0) {
let Ok(channel) = u16::try_from(i) else {
break;
};
output.spikes.push(SpikeEvent {
channel,
timestamp: 0,
polarity: error >= 0.0,
});
break;
}
}
if channel_history.len() == self.history_depth {
channel_history.pop_front();
}
channel_history.push_back(value);
let recent_avg = channel_history.iter().rev().take(5).sum::<f32>() / 5.0;
self.thresholds[i] = 0.9 * self.thresholds[i] + 0.1 * recent_avg;
}
output
}
pub fn encode_with_modulators(
&mut self,
input: &[f32],
modulators: &NeuroModulators,
gain_curves: &NeuromodulatorGainCurves,
) -> EncodedOutput {
<Self as ModulatedEncoder>::encode_with_modulators(self, input, modulators, gain_curves)
}
pub fn encode_step_with_modulators(
&mut self,
input: &[f32],
modulators: &NeuroModulators,
gain_curves: &NeuromodulatorGainCurves,
) -> EncodedOutput {
<Self as ModulatedEncoder>::encode_step_with_modulators(
self,
input,
modulators,
gain_curves,
)
}
}
impl Encoder for PredictiveEncoder {
fn encode(&mut self, input: &[f32]) -> EncodedOutput {
self.encode_with_threshold_scale(input, 1.0)
}
fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
let safe_input = if input.len() > self.history.len() {
&input[..self.history.len()]
} else {
input
};
self.encode_with_threshold_scale(safe_input, 1.0)
}
fn reset(&mut self) {
for history in self.history.iter_mut() {
history.clear();
}
for threshold in self.thresholds.iter_mut() {
*threshold = 0.0;
}
}
}
impl ModulatedEncoder for PredictiveEncoder {
fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
let safe_input = if input.len() > self.history.len() {
&input[..self.history.len()]
} else {
input
};
self.encode_with_threshold_scale(safe_input, gains.sanitize().threshold_scale)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PredictiveEncoder {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use std::collections::VecDeque;
#[derive(serde::Deserialize)]
struct Helper {
history: Vec<VecDeque<f32>>,
thresholds: Vec<f32>,
history_depth: usize,
deviation_thresholds: Vec<(f32, u16)>,
}
let helper = Helper::deserialize(deserializer)?;
if helper.history.len() != helper.thresholds.len() {
return Err(serde::de::Error::custom(format!(
"mismatched history length ({}) and thresholds length ({})",
helper.history.len(),
helper.thresholds.len()
)));
}
if helper.history.len() > u16::MAX as usize + 1 {
return Err(serde::de::Error::custom(
"num_channels exceeds u16::MAX as usize + 1 (max addressable spike channels)",
));
}
if helper.history_depth < 5 {
return Err(serde::de::Error::custom("history_depth must be at least 5"));
}
for &(threshold, _) in &helper.deviation_thresholds {
crate::error::validate_non_negative_finite("deviation_threshold", threshold)
.map_err(serde::de::Error::custom)?;
}
for (i, deque) in helper.history.iter().enumerate() {
if deque.len() > helper.history_depth {
return Err(serde::de::Error::custom(format!(
"history channel {} length ({}) exceeds history_depth ({})",
i,
deque.len(),
helper.history_depth
)));
}
}
Ok(Self {
history: helper.history,
thresholds: helper.thresholds,
history_depth: helper.history_depth,
deviation_thresholds: helper.deviation_thresholds,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_predictive_encoder_rejects_small_history_depth() {
let err = PredictiveEncoder::new(4, vec![(2.0, 1)], 1).err();
assert_eq!(err, Some(PredictiveEncoderError::HistoryDepthTooSmall));
assert_eq!(
PredictiveEncoderError::HistoryDepthTooSmall.to_string(),
"history_depth must be at least 5"
);
assert!(PredictiveEncoder::new(5, vec![(2.0, 1)], 1).is_ok());
assert!(PredictiveEncoder::new(0, vec![(2.0, 1)], 1).is_err());
assert_eq!(
PredictiveEncoder::try_new(4, vec![(2.0, 1)], 1).err(),
Some(EncoderError::HistoryDepthTooSmall { minimum: 5 })
);
assert_eq!(
PredictiveEncoder::try_new(5, vec![(2.0, 1)], u16::MAX as usize + 2).err(),
Some(EncoderError::NumChannelsTooLarge)
);
assert_eq!(
PredictiveEncoder::try_new(5, vec![(f32::NAN, 1)], 1).err(),
Some(EncoderError::NonNegativeFinite {
parameter: "deviation_threshold"
})
);
assert_eq!(
PredictiveEncoder::try_new(5, vec![(-1.0, 1)], 1).err(),
Some(EncoderError::NonNegativeFinite {
parameter: "deviation_threshold"
})
);
assert_eq!(
PredictiveEncoder::new(5, vec![(-1.0, 1)], 1).err(),
Some(PredictiveEncoderError::InvalidDeviationThreshold)
);
assert!(PredictiveEncoder::try_new(5, vec![(0.0, 1)], 1).is_ok());
}
#[test]
fn test_predictive_encoder_num_channels_u16_range() {
let max_ok = u16::MAX as usize + 1;
let first_bad = max_ok + 1;
assert_eq!(
PredictiveEncoder::new(5, vec![(0.2, 1)], first_bad).err(),
Some(PredictiveEncoderError::NumChannelsTooLarge)
);
assert!(
PredictiveEncoderError::NumChannelsTooLarge
.to_string()
.contains("u16::MAX")
);
let encoder =
PredictiveEncoder::new(5, vec![(0.2, 1)], max_ok).expect("max u16 channel count");
assert_eq!(encoder.history.len(), max_ok);
assert_eq!(encoder.thresholds.len(), max_ok);
}
#[test]
fn test_predictive_encoder() {
let mut encoder =
PredictiveEncoder::new(5, vec![(2.0, 1)], 1).expect("valid PredictiveEncoder");
let _output = encoder.encode(&[1.0]);
let _output = encoder.encode(&[1.0]);
let _output = encoder.encode(&[1.0]);
let _output = encoder.encode(&[1.0]);
let _output = encoder.encode(&[1.0]);
let output = encoder.encode(&[10.0]);
assert!(!output.spikes.is_empty());
}
#[test]
fn test_predictive_encoder_constant_signal_has_no_cold_start_burst() {
let mut encoder =
PredictiveEncoder::new(5, vec![(0.5, 1)], 1).expect("valid PredictiveEncoder");
for _ in 0..16 {
let output = encoder.encode(&[42.0]);
assert!(output.spikes.is_empty());
}
}
#[test]
fn test_predictive_encoder_short_history_warms_up_without_spikes() {
let mut encoder =
PredictiveEncoder::new(5, vec![(0.5, 1)], 1).expect("valid PredictiveEncoder");
for _ in 0..5 {
let output = encoder.encode(&[10.0]);
assert!(output.spikes.is_empty());
}
assert_eq!(encoder.history[0].len(), 5);
assert_eq!(encoder.thresholds[0], 10.0);
}
#[test]
fn test_predictive_encoder_positive_step_preserves_positive_polarity() {
let mut encoder =
PredictiveEncoder::new(5, vec![(1.0, 1)], 1).expect("valid PredictiveEncoder");
for _ in 0..5 {
assert!(encoder.encode(&[1.0]).spikes.is_empty());
}
let output = encoder.encode(&[4.0]);
assert_eq!(output.spikes.len(), 1);
assert!(output.spikes[0].polarity);
}
#[test]
fn test_predictive_encoder_negative_step_preserves_negative_polarity() {
let mut encoder =
PredictiveEncoder::new(5, vec![(1.0, 1)], 1).expect("valid PredictiveEncoder");
for _ in 0..5 {
assert!(encoder.encode(&[4.0]).spikes.is_empty());
}
let output = encoder.encode(&[1.0]);
assert_eq!(output.spikes.len(), 1);
assert!(!output.spikes[0].polarity);
}
#[test]
fn test_predictive_encoder_trend_uses_prior_prediction_before_update() {
let mut encoder =
PredictiveEncoder::new(5, vec![(0.75, 1)], 1).expect("valid PredictiveEncoder");
for value in [1.0, 2.0, 3.0, 4.0, 5.0] {
assert!(encoder.encode(&[value]).spikes.is_empty());
}
assert_eq!(encoder.thresholds[0], 3.0);
let output = encoder.encode(&[6.0]);
assert_eq!(output.spikes.len(), 1);
assert!(output.spikes[0].polarity);
assert_eq!(encoder.thresholds[0], 3.1);
}
#[test]
fn test_predictive_encoder_reset() {
let mut encoder =
PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
for _ in 0..6 {
encoder.encode(&[1.0, 2.0]);
}
encoder.reset();
assert!(encoder.history.iter().all(|h| h.is_empty()));
assert!(encoder.thresholds.iter().all(|&t| t == 0.0));
}
#[test]
fn test_predictive_encoder_reset_restarts_warmup_without_spikes() {
let mut encoder =
PredictiveEncoder::new(5, vec![(0.5, 1)], 1).expect("valid PredictiveEncoder");
for _ in 0..5 {
assert!(encoder.encode(&[1.0]).spikes.is_empty());
}
assert_eq!(encoder.encode(&[10.0]).spikes.len(), 1);
encoder.reset();
for _ in 0..5 {
assert!(encoder.encode(&[10.0]).spikes.is_empty());
}
assert_eq!(encoder.thresholds[0], 10.0);
}
#[test]
fn test_predictive_encoder_multi_channel() {
let mut encoder =
PredictiveEncoder::new(5, vec![(2.0, 1)], 3).expect("valid PredictiveEncoder");
for _ in 0..6 {
encoder.encode(&[1.0, 2.0, 3.0]);
}
let output = encoder.encode(&[10.0, 20.0, 30.0]);
assert!(!output.spikes.is_empty());
}
#[test]
fn test_predictive_encoder_input_truncation() {
let mut encoder =
PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
for _ in 0..6 {
encoder.encode(&[1.0, 2.0, 3.0, 4.0]);
}
let output = encoder.encode(&[10.0, 20.0, 30.0, 40.0]);
assert!(output.spikes.len() <= 2);
}
#[test]
fn test_predictive_encoder_step_input_truncation() {
let mut encoder =
PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
for _ in 0..6 {
encoder.encode_step(&[1.0, 2.0, 3.0]);
}
let output = encoder.encode_step(&[10.0, 20.0, 30.0]);
assert!(output.spikes.len() <= 2);
}
#[test]
fn test_predictive_encoder_encode_with_modulators() {
let mut encoder =
PredictiveEncoder::new(5, vec![(5.0, 1)], 1).expect("valid PredictiveEncoder");
let mods = NeuroModulators {
acetylcholine: 1.0,
..Default::default()
};
let curves = NeuromodulatorGainCurves {
acetylcholine: ModulatorGainCurves {
threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
..Default::default()
},
..Default::default()
};
for _ in 0..5 {
encoder.encode_with_modulators(&[1.0], &mods, &curves);
}
let output = encoder.encode_with_modulators(&[5.0], &mods, &curves);
assert_eq!(output.spikes.len(), 1);
}
#[test]
fn test_predictive_encoder_modulators_reduce_threshold() {
let mut encoder =
PredictiveEncoder::new(5, vec![(5.0, 1)], 1).expect("valid PredictiveEncoder");
let modulators = NeuroModulators {
acetylcholine: 1.0,
..Default::default()
};
let gain_curves = NeuromodulatorGainCurves {
acetylcholine: ModulatorGainCurves {
threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
..Default::default()
},
..Default::default()
};
for _ in 0..5 {
encoder.encode(&[1.0]);
}
assert!(encoder.encode(&[5.0]).spikes.is_empty());
encoder.reset();
for _ in 0..5 {
encoder.encode_step_with_modulators(&[1.0], &modulators, &gain_curves);
}
let output = encoder.encode_step_with_modulators(&[5.0], &modulators, &gain_curves);
assert_eq!(output.spikes.len(), 1);
}
#[test]
fn test_predictive_encoder_encode_with_modulators_truncate() {
let mut encoder =
PredictiveEncoder::new(5, vec![(5.0, 1)], 1).expect("valid PredictiveEncoder");
let mods = NeuroModulators {
acetylcholine: 1.0,
..Default::default()
};
let curves = NeuromodulatorGainCurves {
acetylcholine: ModulatorGainCurves {
threshold: Some(GainCurve::new((0.0, 1.0), (1.0, 0.5))),
..Default::default()
},
..Default::default()
};
for _ in 0..5 {
encoder.encode_with_modulators(&[1.0, 2.0], &mods, &curves);
}
let output = encoder.encode_with_modulators(&[5.0, 6.0], &mods, &curves);
assert_eq!(output.spikes.len(), 1);
}
#[test]
fn test_predictive_encoder_step_shorter_input() {
let mut encoder =
PredictiveEncoder::new(5, vec![(2.0, 1)], 2).expect("valid PredictiveEncoder");
for _ in 0..6 {
encoder.encode_step(&[1.0]);
}
let output = encoder.encode_step(&[10.0]);
assert!(!output.spikes.is_empty());
}
#[cfg(feature = "serde")]
#[test]
fn test_predictive_serde_history_channel_too_long() {
let json = r#"{
"history": [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]],
"thresholds": [0.0],
"history_depth": 5,
"deviation_thresholds": []
}"#;
let res: Result<PredictiveEncoder, _> = serde_json::from_str(json);
assert!(res.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn test_predictive_serde_rejects_too_many_channels() {
let max_ok = u16::MAX as usize + 1;
let first_bad = max_ok + 1;
let history: Vec<Vec<f32>> = vec![vec![]; first_bad];
let thresholds = vec![0.0f32; first_bad];
let value = serde_json::json!({
"history": history,
"thresholds": thresholds,
"history_depth": 5,
"deviation_thresholds": [[0.2, 1]],
});
let res: Result<PredictiveEncoder, _> = serde_json::from_value(value);
assert!(res.is_err());
let err = res.err().unwrap().to_string();
assert!(
err.contains("u16::MAX") || err.contains("num_channels"),
"unexpected error: {err}"
);
let history_ok: Vec<Vec<f32>> = vec![vec![]; max_ok];
let thresholds_ok = vec![0.0f32; max_ok];
let value_ok = serde_json::json!({
"history": history_ok,
"thresholds": thresholds_ok,
"history_depth": 5,
"deviation_thresholds": [[0.2, 1]],
});
let enc: PredictiveEncoder =
serde_json::from_value(value_ok).expect("max channel count deserializes");
assert_eq!(enc.history.len(), max_ok);
assert_eq!(enc.thresholds.len(), max_ok);
}
}