use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GifNeuron {
pub membrane_potential: f32,
pub adaptation: f32,
pub leak: f32,
pub drive_scale: f32,
pub threshold: f32,
#[serde(default)]
pub base_threshold: f32,
pub adaptation_scale: f32,
pub adaptation_decay: f32,
pub adaptation_coupling: f32,
pub adaptation_increment: f32,
pub reset_ratio: f32,
pub last_spike: bool,
#[serde(default)]
pub weights: Vec<f32>,
#[serde(default)]
pub last_spike_time: i64,
}
impl Default for GifNeuron {
fn default() -> Self {
Self {
membrane_potential: 0.0,
adaptation: 0.0,
leak: 0.92,
drive_scale: 0.75,
threshold: 0.65,
base_threshold: 0.65,
adaptation_scale: 0.22,
adaptation_decay: 0.94,
adaptation_coupling: 0.05,
adaptation_increment: 1.0,
reset_ratio: 0.35,
last_spike: false,
weights: Vec::new(),
last_spike_time: -1,
}
}
}
impl GifNeuron {
pub fn new() -> Self {
Self::default()
}
pub fn integrate(&mut self, stimulus: f32) {
self.adaptation *= self.adaptation_decay;
self.membrane_potential = self.membrane_potential * self.leak + stimulus * self.drive_scale
- self.adaptation * self.adaptation_coupling;
}
pub fn check_for_spike(&mut self, current_time: i64) -> bool {
let theta = self.base_threshold + self.adaptation * self.adaptation_scale;
if self.membrane_potential >= theta {
self.membrane_potential -= theta * self.reset_ratio;
self.adaptation += self.adaptation_increment;
self.last_spike = true;
self.last_spike_time = current_time;
true
} else {
self.last_spike = false;
false
}
}
pub fn reset(&mut self) {
self.membrane_potential = 0.0;
self.adaptation = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_spike_without_input() {
let mut n = GifNeuron::new();
for t in 0..200 {
n.integrate(0.0);
assert!(
!n.check_for_spike(t),
"GIF neuron should not spike without input"
);
}
assert!(n.membrane_potential.abs() < 1e-6);
assert!(n.adaptation.abs() < 1e-6);
}
#[test]
fn test_fires_with_sufficient_input() {
let mut n = GifNeuron::new();
let mut fired = false;
for t in 0..200 {
n.integrate(0.9);
if n.check_for_spike(t) {
fired = true;
break;
}
}
assert!(
fired,
"GIF neuron should fire with sustained suprathreshold input"
);
}
#[test]
fn test_adaptation_increases_after_spike() {
let mut n = GifNeuron::new();
n.membrane_potential = 10.0; let spiked = n.check_for_spike(0);
assert!(spiked, "forced high membrane should produce a spike");
assert!(
n.adaptation > 0.0,
"adaptation should accumulate after a spike (got {})",
n.adaptation
);
}
#[test]
fn test_soft_reset_not_hard_zero() {
let mut n = GifNeuron::new();
n.membrane_potential = 10.0;
let before = n.membrane_potential;
let spiked = n.check_for_spike(0);
assert!(spiked);
assert!(
n.membrane_potential < before,
"membrane should be reduced after spike"
);
assert!(
n.membrane_potential > 0.0,
"soft reset should leave residual potential (got {}), not clamp to 0",
n.membrane_potential
);
}
#[test]
fn test_adaptation_raises_effective_threshold() {
let mut fresh = GifNeuron::new();
let mut adapted = GifNeuron::new();
adapted.adaptation = 5.0;
let mut fresh_spikes = 0usize;
let mut adapted_spikes = 0usize;
for t in 0..200 {
fresh.integrate(0.9);
if fresh.check_for_spike(t) {
fresh_spikes += 1;
}
adapted.integrate(0.9);
if adapted.check_for_spike(t) {
adapted_spikes += 1;
}
}
assert!(
fresh_spikes > adapted_spikes,
"pre-adapted neuron ({adapted_spikes} spikes) should fire less than fresh neuron ({fresh_spikes} spikes) under identical drive"
);
}
}