git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
414 lines
11 KiB
Rust
414 lines
11 KiB
Rust
//! Izhikevich neuron model.
|
|
//!
|
|
//! The Izhikevich model captures rich spiking dynamics with just two variables:
|
|
//! - Membrane potential (fast)
|
|
//! - Recovery variable (slow)
|
|
//!
|
|
//! This allows simulation of 20+ different firing patterns observed in cortical neurons,
|
|
//! while remaining computationally efficient.
|
|
//!
|
|
//! ## Firing Patterns
|
|
//!
|
|
//! - Regular spiking (RS) - most common excitatory
|
|
//! - Intrinsically bursting (IB) - burst then regular
|
|
//! - Chattering (CH) - fast rhythmic bursting
|
|
//! - Fast spiking (FS) - inhibitory interneurons
|
|
//! - Low-threshold spiking (LTS) - inhibitory
|
|
//!
|
|
//! ## ASIC Considerations
|
|
//!
|
|
//! - 2 multiply-accumulates per timestep
|
|
//! - 1 multiplication for recovery
|
|
//! - ~150-200 gates in digital implementation
|
|
|
|
use super::{NeuronParams, NeuronState, SpikingNeuron, EnergyModel};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Pre-defined Izhikevich neuron types with biological parameters.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum IzhikevichType {
|
|
/// Regular spiking - most common excitatory cortical neuron
|
|
RegularSpiking,
|
|
/// Intrinsically bursting - initial burst then regular spikes
|
|
IntrinsicallyBursting,
|
|
/// Chattering - fast rhythmic bursting
|
|
Chattering,
|
|
/// Fast spiking - typical inhibitory interneuron
|
|
FastSpiking,
|
|
/// Low-threshold spiking - inhibitory with rebound
|
|
LowThresholdSpiking,
|
|
/// Thalamo-cortical - two firing modes
|
|
ThalamoCortical,
|
|
/// Resonator - subthreshold oscillations
|
|
Resonator,
|
|
}
|
|
|
|
impl IzhikevichType {
|
|
/// Get parameters for this neuron type.
|
|
pub fn params(self) -> IzhikevichParams {
|
|
match self {
|
|
Self::RegularSpiking => IzhikevichParams {
|
|
a: 0.02,
|
|
b: 0.2,
|
|
c: -65.0,
|
|
d: 8.0,
|
|
threshold: 30.0,
|
|
refractory: 0.0, // Implicit in dynamics
|
|
},
|
|
Self::IntrinsicallyBursting => IzhikevichParams {
|
|
a: 0.02,
|
|
b: 0.2,
|
|
c: -55.0,
|
|
d: 4.0,
|
|
threshold: 30.0,
|
|
refractory: 0.0,
|
|
},
|
|
Self::Chattering => IzhikevichParams {
|
|
a: 0.02,
|
|
b: 0.2,
|
|
c: -50.0,
|
|
d: 2.0,
|
|
threshold: 30.0,
|
|
refractory: 0.0,
|
|
},
|
|
Self::FastSpiking => IzhikevichParams {
|
|
a: 0.1,
|
|
b: 0.2,
|
|
c: -65.0,
|
|
d: 2.0,
|
|
threshold: 30.0,
|
|
refractory: 0.0,
|
|
},
|
|
Self::LowThresholdSpiking => IzhikevichParams {
|
|
a: 0.02,
|
|
b: 0.25,
|
|
c: -65.0,
|
|
d: 2.0,
|
|
threshold: 30.0,
|
|
refractory: 0.0,
|
|
},
|
|
Self::ThalamoCortical => IzhikevichParams {
|
|
a: 0.02,
|
|
b: 0.25,
|
|
c: -65.0,
|
|
d: 0.05,
|
|
threshold: 30.0,
|
|
refractory: 0.0,
|
|
},
|
|
Self::Resonator => IzhikevichParams {
|
|
a: 0.1,
|
|
b: 0.26,
|
|
c: -65.0,
|
|
d: 2.0,
|
|
threshold: 30.0,
|
|
refractory: 0.0,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parameters for Izhikevich neuron model.
|
|
///
|
|
/// The model equations are:
|
|
/// ```text
|
|
/// dv/dt = 0.04*v² + 5*v + 140 - u + I
|
|
/// du/dt = a*(b*v - u)
|
|
/// if v >= 30 mV: v = c, u = u + d
|
|
/// ```
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct IzhikevichParams {
|
|
/// Time scale of recovery variable (smaller = slower recovery)
|
|
pub a: f32,
|
|
/// Sensitivity of recovery to subthreshold membrane potential
|
|
pub b: f32,
|
|
/// After-spike reset value of membrane potential (mV)
|
|
pub c: f32,
|
|
/// After-spike reset increment of recovery variable
|
|
pub d: f32,
|
|
/// Spike threshold (mV) - typically 30
|
|
pub threshold: f32,
|
|
/// Explicit refractory period (ms) - usually 0 for Izhikevich
|
|
pub refractory: f32,
|
|
}
|
|
|
|
impl Default for IzhikevichParams {
|
|
fn default() -> Self {
|
|
IzhikevichType::RegularSpiking.params()
|
|
}
|
|
}
|
|
|
|
impl NeuronParams for IzhikevichParams {
|
|
fn threshold(&self) -> f32 {
|
|
self.threshold
|
|
}
|
|
|
|
fn reset_potential(&self) -> f32 {
|
|
self.c
|
|
}
|
|
|
|
fn resting_potential(&self) -> f32 {
|
|
// Resting potential is approximately -65 to -70 mV
|
|
-65.0
|
|
}
|
|
|
|
fn refractory_period(&self) -> f32 {
|
|
self.refractory
|
|
}
|
|
|
|
fn validate(&self) -> Option<String> {
|
|
if self.a <= 0.0 || self.a > 1.0 {
|
|
return Some("a should be in (0, 1]".into());
|
|
}
|
|
if self.threshold < 0.0 {
|
|
return Some("threshold should be positive".into());
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Izhikevich neuron model.
|
|
///
|
|
/// Provides rich spiking dynamics while remaining computationally efficient.
|
|
/// The two-variable model captures most qualitative behaviors of biological neurons.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct IzhikevichNeuron {
|
|
/// Model parameters
|
|
params: IzhikevichParams,
|
|
/// Membrane potential (mV)
|
|
v: f32,
|
|
/// Recovery variable (dimensionless)
|
|
u: f32,
|
|
/// Accumulated input current
|
|
input_current: f32,
|
|
/// Time since last spike
|
|
time_since_spike: Option<f32>,
|
|
/// Refractory countdown (if explicit refractory used)
|
|
refractory_remaining: f32,
|
|
}
|
|
|
|
impl IzhikevichNeuron {
|
|
/// Create neuron from predefined type.
|
|
pub fn from_type(neuron_type: IzhikevichType) -> Self {
|
|
Self::new(neuron_type.params())
|
|
}
|
|
|
|
/// Create regular spiking neuron (most common).
|
|
pub fn regular_spiking() -> Self {
|
|
Self::from_type(IzhikevichType::RegularSpiking)
|
|
}
|
|
|
|
/// Create fast spiking neuron (inhibitory).
|
|
pub fn fast_spiking() -> Self {
|
|
Self::from_type(IzhikevichType::FastSpiking)
|
|
}
|
|
|
|
/// Get recovery variable.
|
|
pub fn recovery(&self) -> f32 {
|
|
self.u
|
|
}
|
|
}
|
|
|
|
impl SpikingNeuron for IzhikevichNeuron {
|
|
type Params = IzhikevichParams;
|
|
|
|
fn new(params: IzhikevichParams) -> Self {
|
|
// Initialize at resting state
|
|
let v = params.c;
|
|
let u = params.b * v;
|
|
Self {
|
|
params,
|
|
v,
|
|
u,
|
|
input_current: 0.0,
|
|
time_since_spike: None,
|
|
refractory_remaining: 0.0,
|
|
}
|
|
}
|
|
|
|
fn state(&self) -> NeuronState {
|
|
NeuronState {
|
|
membrane_potential: self.v,
|
|
time_since_spike: self.time_since_spike,
|
|
is_refractory: self.refractory_remaining > 0.0,
|
|
input_current: self.input_current,
|
|
}
|
|
}
|
|
|
|
fn params(&self) -> &Self::Params {
|
|
&self.params
|
|
}
|
|
|
|
fn receive_input(&mut self, current: f32) {
|
|
self.input_current += current;
|
|
}
|
|
|
|
fn update(&mut self, dt: f32) -> bool {
|
|
// Update time since spike
|
|
if let Some(ref mut t) = self.time_since_spike {
|
|
*t += dt;
|
|
}
|
|
|
|
// Handle explicit refractory if set
|
|
if self.refractory_remaining > 0.0 {
|
|
self.refractory_remaining -= dt;
|
|
self.input_current = 0.0;
|
|
return false;
|
|
}
|
|
|
|
// Izhikevich dynamics with Euler integration
|
|
// For numerical stability, use two half-steps for v
|
|
let i = self.input_current;
|
|
|
|
// Half step 1
|
|
let dv1 = 0.04 * self.v * self.v + 5.0 * self.v + 140.0 - self.u + i;
|
|
self.v += dv1 * dt * 0.5;
|
|
|
|
// Half step 2
|
|
let dv2 = 0.04 * self.v * self.v + 5.0 * self.v + 140.0 - self.u + i;
|
|
self.v += dv2 * dt * 0.5;
|
|
|
|
// Recovery variable
|
|
let du = self.params.a * (self.params.b * self.v - self.u);
|
|
self.u += du * dt;
|
|
|
|
// Clear input
|
|
self.input_current = 0.0;
|
|
|
|
// Spike check
|
|
if self.v >= self.params.threshold {
|
|
// Spike!
|
|
self.v = self.params.c;
|
|
self.u += self.params.d;
|
|
self.time_since_spike = Some(0.0);
|
|
self.refractory_remaining = self.params.refractory;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.v = self.params.c;
|
|
self.u = self.params.b * self.v;
|
|
self.input_current = 0.0;
|
|
self.time_since_spike = None;
|
|
self.refractory_remaining = 0.0;
|
|
}
|
|
|
|
fn is_refractory(&self) -> bool {
|
|
self.refractory_remaining > 0.0
|
|
}
|
|
|
|
fn membrane_potential(&self) -> f32 {
|
|
self.v
|
|
}
|
|
|
|
fn time_since_spike(&self) -> Option<f32> {
|
|
self.time_since_spike
|
|
}
|
|
}
|
|
|
|
impl EnergyModel for IzhikevichNeuron {
|
|
fn update_energy(&self) -> f32 {
|
|
// Estimate: 3 multiplies, 6 adds, 1 comparison
|
|
// More complex than LIF
|
|
5.0 // picojoules
|
|
}
|
|
|
|
fn spike_energy(&self) -> f32 {
|
|
10.0 // picojoules
|
|
}
|
|
|
|
fn silicon_area(&self) -> f32 {
|
|
// ~150-200 gates at 28nm
|
|
17.5 // square micrometers
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_izhikevich_types() {
|
|
// Test all predefined types can be created
|
|
let types = [
|
|
IzhikevichType::RegularSpiking,
|
|
IzhikevichType::IntrinsicallyBursting,
|
|
IzhikevichType::Chattering,
|
|
IzhikevichType::FastSpiking,
|
|
IzhikevichType::LowThresholdSpiking,
|
|
IzhikevichType::ThalamoCortical,
|
|
IzhikevichType::Resonator,
|
|
];
|
|
|
|
for neuron_type in types {
|
|
let neuron = IzhikevichNeuron::from_type(neuron_type);
|
|
assert!(neuron.params().validate().is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_regular_spiking_behavior() {
|
|
let mut neuron = IzhikevichNeuron::regular_spiking();
|
|
let mut spike_count = 0;
|
|
|
|
// Inject constant current and count spikes
|
|
for _ in 0..1000 {
|
|
neuron.receive_input(10.0);
|
|
if neuron.update(1.0) {
|
|
spike_count += 1;
|
|
}
|
|
}
|
|
|
|
// Should spike regularly
|
|
assert!(spike_count > 10, "Regular spiking neuron should fire regularly");
|
|
assert!(spike_count < 200, "Should not fire too fast");
|
|
}
|
|
|
|
#[test]
|
|
fn test_fast_spiking_behavior() {
|
|
let mut fs = IzhikevichNeuron::fast_spiking();
|
|
let mut rs = IzhikevichNeuron::regular_spiking();
|
|
|
|
let mut fs_spikes = 0;
|
|
let mut rs_spikes = 0;
|
|
|
|
// Same input to both
|
|
for _ in 0..1000 {
|
|
fs.receive_input(14.0);
|
|
rs.receive_input(14.0);
|
|
|
|
if fs.update(1.0) { fs_spikes += 1; }
|
|
if rs.update(1.0) { rs_spikes += 1; }
|
|
}
|
|
|
|
// Fast spiking should fire more often
|
|
assert!(fs_spikes > rs_spikes, "Fast spiking should fire more than regular");
|
|
}
|
|
|
|
#[test]
|
|
fn test_recovery_dynamics() {
|
|
let mut neuron = IzhikevichNeuron::regular_spiking();
|
|
let initial_u = neuron.recovery();
|
|
|
|
// After spike, recovery should increase
|
|
neuron.v = 35.0; // Above threshold
|
|
neuron.update(1.0);
|
|
|
|
assert!(neuron.recovery() > initial_u, "Recovery should increase after spike");
|
|
}
|
|
|
|
#[test]
|
|
fn test_subthreshold_dynamics() {
|
|
let mut neuron = IzhikevichNeuron::regular_spiking();
|
|
|
|
// Weak input should not cause immediate spike
|
|
neuron.receive_input(2.0);
|
|
assert!(!neuron.update(1.0));
|
|
|
|
// Voltage should rise but not spike
|
|
assert!(neuron.membrane_potential() > neuron.params.c);
|
|
}
|
|
}
|