pub struct ReadoutNeuron {
pub membrane: i32,
pub kappa: i16,
}
impl ReadoutNeuron {
pub fn new(kappa: i16) -> Self {
Self { membrane: 0, kappa }
}
#[inline]
pub fn step(&mut self, weighted_input: i32) {
let decayed = (self.membrane as i64 * self.kappa as i64) >> 14;
self.membrane = (decayed as i32).saturating_add(weighted_input);
}
#[inline]
pub fn output_i32(&self) -> i32 {
self.membrane
}
#[inline]
pub fn output_f64(&self, scale: f64) -> f64 {
self.membrane as f64 * scale
}
pub fn reset(&mut self) {
self.membrane = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snn::lif::{f64_to_q14, Q14_HALF, Q14_ONE};
#[test]
fn new_readout_has_zero_membrane() {
let r = ReadoutNeuron::new(Q14_HALF);
assert_eq!(r.membrane, 0);
assert_eq!(r.output_i32(), 0);
}
#[test]
fn step_accumulates_input() {
let mut r = ReadoutNeuron::new(Q14_ONE); r.step(1000);
assert_eq!(r.output_i32(), 1000);
r.step(500);
assert_eq!(r.output_i32(), 1500, "should accumulate with kappa=1.0");
}
#[test]
fn step_decays_membrane() {
let mut r = ReadoutNeuron::new(Q14_HALF); r.step(1000);
assert_eq!(r.output_i32(), 1000);
r.step(0);
assert_eq!(r.output_i32(), 500, "membrane should decay by kappa");
r.step(0);
assert_eq!(r.output_i32(), 250, "membrane should continue decaying");
}
#[test]
fn output_f64_dequantizes_correctly() {
let mut r = ReadoutNeuron::new(Q14_ONE);
r.membrane = Q14_ONE as i32; let scale = 1.0 / Q14_ONE as f64;
let out = r.output_f64(scale);
assert!(
(out - 1.0).abs() < 0.001,
"output_f64 should dequantize 16384 to ~1.0, got {}",
out
);
}
#[test]
fn reset_clears_membrane() {
let mut r = ReadoutNeuron::new(Q14_HALF);
r.step(5000);
assert!(r.output_i32() != 0);
r.reset();
assert_eq!(r.output_i32(), 0, "reset should zero the membrane");
}
#[test]
fn no_overflow_with_large_accumulation() {
let mut r = ReadoutNeuron::new(f64_to_q14(0.99));
for _ in 0..1000 {
r.step(10000);
}
assert!(
r.output_i32() > 0,
"membrane should be positive after positive inputs"
);
}
#[test]
fn decay_with_negative_membrane() {
let mut r = ReadoutNeuron::new(Q14_HALF);
r.step(-2000);
assert_eq!(r.output_i32(), -2000);
r.step(0);
assert_eq!(
r.output_i32(),
-1000,
"negative membrane should decay toward 0"
);
}
#[test]
fn zero_kappa_forgets_immediately() {
let mut r = ReadoutNeuron::new(0); r.step(5000);
assert_eq!(r.output_i32(), 5000);
r.step(0);
assert_eq!(
r.output_i32(),
0,
"zero kappa should forget membrane immediately"
);
}
}