Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,413 @@
//! 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);
}
}

View File

@@ -0,0 +1,316 @@
//! Leaky Integrate-and-Fire (LIF) neuron model.
//!
//! The LIF model is the workhorse of neuromorphic computing:
//! - Simple dynamics: membrane voltage leaks toward rest
//! - Spikes when threshold crossed
//! - Resets and enters refractory period
//!
//! ## ASIC Benefits
//!
//! - Single multiply-accumulate per timestep
//! - No division (pre-computed decay factor)
//! - 2-3 comparisons per update
//! - ~100 gates in digital implementation
use super::{NeuronParams, NeuronState, SpikingNeuron, EnergyModel};
use serde::{Deserialize, Serialize};
/// Parameters for LIF neuron.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct LIFParams {
/// Membrane time constant (ms) - controls leak rate
pub tau_m: f32,
/// Spike threshold (mV)
pub threshold: f32,
/// Reset potential after spike (mV)
pub reset: f32,
/// Resting membrane potential (mV)
pub resting: f32,
/// Refractory period (ms)
pub refractory: f32,
/// Membrane resistance (MOhm) - scales input current
pub resistance: f32,
}
impl Default for LIFParams {
fn default() -> Self {
Self {
tau_m: 20.0,
threshold: -50.0,
reset: -70.0,
resting: -65.0,
refractory: 2.0,
resistance: 10.0, // 10 MOhm typical for cortical neurons
}
}
}
impl NeuronParams for LIFParams {
fn threshold(&self) -> f32 {
self.threshold
}
fn reset_potential(&self) -> f32 {
self.reset
}
fn resting_potential(&self) -> f32 {
self.resting
}
fn refractory_period(&self) -> f32 {
self.refractory
}
fn validate(&self) -> Option<String> {
if self.tau_m <= 0.0 {
return Some("tau_m must be positive".into());
}
if self.threshold <= self.reset {
return Some("threshold must be greater than reset".into());
}
if self.refractory < 0.0 {
return Some("refractory period cannot be negative".into());
}
if self.resistance <= 0.0 {
return Some("resistance must be positive".into());
}
None
}
}
/// Leaky Integrate-and-Fire neuron.
///
/// Implements the differential equation:
/// ```text
/// τ_m * dV/dt = -(V - V_rest) + R * I
/// ```
///
/// With spike condition: V ≥ V_threshold
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LIFNeuron {
/// Neuron parameters
params: LIFParams,
/// Current membrane potential (mV)
membrane_potential: f32,
/// Time remaining in refractory period (ms)
refractory_remaining: f32,
/// Accumulated input current for this timestep
input_current: f32,
/// Time since last spike (ms)
time_since_spike: Option<f32>,
/// Pre-computed decay factor for efficiency
decay_factor: f32,
}
impl LIFNeuron {
/// Create LIF neuron with default parameters.
pub fn with_defaults() -> Self {
Self::new(LIFParams::default())
}
/// Pre-compute decay factor for given timestep.
///
/// This avoids division in the hot path.
/// decay = exp(-dt / tau_m) ≈ 1 - dt/tau_m for small dt
fn compute_decay(&self, dt: f32) -> f32 {
// Use linear approximation for ASIC compatibility
// Error < 1% for dt < 2ms with tau_m = 20ms
1.0 - dt / self.params.tau_m
}
/// Get the pre-computed decay factor.
pub fn decay_factor(&self) -> f32 {
self.decay_factor
}
}
impl SpikingNeuron for LIFNeuron {
type Params = LIFParams;
fn new(params: LIFParams) -> Self {
let decay_factor = 1.0 - 1.0 / params.tau_m; // For dt=1ms default
Self {
params,
membrane_potential: params.resting,
refractory_remaining: 0.0,
input_current: 0.0,
time_since_spike: None,
decay_factor,
}
}
fn state(&self) -> NeuronState {
NeuronState {
membrane_potential: self.membrane_potential,
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) {
// Accumulate input - this is the sparse event
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 refractory period
if self.refractory_remaining > 0.0 {
self.refractory_remaining -= dt;
self.input_current = 0.0; // Clear accumulated input
return false;
}
// Compute decay factor for this timestep
let decay = self.compute_decay(dt);
// LIF dynamics: V = decay * V + (1-decay) * V_rest + R * I * dt / tau_m
// Simplified: V = decay * (V - V_rest) + V_rest + R * I * dt / tau_m
let v_diff = self.membrane_potential - self.params.resting;
let input_term = self.params.resistance * self.input_current * dt / self.params.tau_m;
self.membrane_potential = decay * v_diff + self.params.resting + input_term;
// Clear input for next timestep
self.input_current = 0.0;
// Check for spike
if self.membrane_potential >= self.params.threshold {
// Spike!
self.membrane_potential = self.params.reset;
self.refractory_remaining = self.params.refractory;
self.time_since_spike = Some(0.0);
true
} else {
false
}
}
fn reset(&mut self) {
self.membrane_potential = self.params.resting;
self.refractory_remaining = 0.0;
self.input_current = 0.0;
self.time_since_spike = None;
}
fn is_refractory(&self) -> bool {
self.refractory_remaining > 0.0
}
fn membrane_potential(&self) -> f32 {
self.membrane_potential
}
fn time_since_spike(&self) -> Option<f32> {
self.time_since_spike
}
}
impl EnergyModel for LIFNeuron {
fn update_energy(&self) -> f32 {
// Estimate: 1 multiply, 3 adds, 2 comparisons
// At 28nm: ~0.5 pJ per operation
3.0 // picojoules
}
fn spike_energy(&self) -> f32 {
// Spike packet generation and routing
10.0 // picojoules
}
fn silicon_area(&self) -> f32 {
// ~100 gates at 28nm ≈ 0.1 μm² per gate
10.0 // square micrometers
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lif_default_creation() {
let neuron = LIFNeuron::with_defaults();
assert_eq!(neuron.membrane_potential(), -65.0);
assert!(!neuron.is_refractory());
}
#[test]
fn test_lif_spike_generation() {
let mut neuron = LIFNeuron::with_defaults();
// Inject strong current
for _ in 0..100 {
neuron.receive_input(5.0); // Strong input
if neuron.update(1.0) {
// Spiked!
assert!(neuron.is_refractory());
assert_eq!(neuron.membrane_potential(), neuron.params.reset);
return;
}
}
panic!("Neuron should have spiked with strong input");
}
#[test]
fn test_lif_refractory_period() {
let params = LIFParams {
refractory: 5.0,
..Default::default()
};
let mut neuron = LIFNeuron::new(params);
// Force a spike
neuron.membrane_potential = params.threshold + 1.0;
neuron.update(1.0);
// Should be refractory
assert!(neuron.is_refractory());
// Should not spike during refractory
neuron.receive_input(100.0);
assert!(!neuron.update(1.0));
// After refractory period
for _ in 0..5 {
neuron.update(1.0);
}
assert!(!neuron.is_refractory());
}
#[test]
fn test_lif_leak_to_rest() {
let mut neuron = LIFNeuron::with_defaults();
neuron.membrane_potential = -55.0; // Above resting
// Without input, should decay toward resting
for _ in 0..100 {
neuron.update(1.0);
}
// Should be close to resting potential
assert!((neuron.membrane_potential() - (-65.0)).abs() < 1.0);
}
#[test]
fn test_params_validation() {
let invalid = LIFParams {
tau_m: -1.0,
..Default::default()
};
assert!(invalid.validate().is_some());
let valid = LIFParams::default();
assert!(valid.validate().is_none());
}
}

View File

@@ -0,0 +1,41 @@
//! Spiking neuron models.
//!
//! This module provides biologically-inspired neuron models optimized for
//! event-driven computation. Neurons stay silent until a threshold is crossed,
//! eliminating wasted cycles on unchanged state.
//!
//! ## Available Models
//!
//! - **LIF (Leaky Integrate-and-Fire)**: Simple, efficient, ASIC-friendly
//! - **Izhikevich**: Rich dynamics, biologically plausible spiking patterns
//!
//! ## ASIC Considerations
//!
//! These models are designed for minimal silicon cost:
//! - Fixed-point compatible arithmetic
//! - No division operations in hot paths
//! - Predictable memory access patterns
//! - Branch-friendly state machines
mod lif;
mod izhikevich;
mod traits;
pub use lif::{LIFNeuron, LIFParams};
pub use izhikevich::{IzhikevichNeuron, IzhikevichParams, IzhikevichType};
pub use traits::{NeuronParams, SpikingNeuron, NeuronState};
/// Default membrane time constant (ms)
pub const DEFAULT_TAU_M: f32 = 20.0;
/// Default spike threshold (mV)
pub const DEFAULT_THRESHOLD: f32 = -50.0;
/// Default resting potential (mV)
pub const DEFAULT_RESTING: f32 = -65.0;
/// Default reset potential (mV)
pub const DEFAULT_RESET: f32 = -70.0;
/// Default refractory period (ms)
pub const DEFAULT_REFRACTORY: f32 = 2.0;

View File

@@ -0,0 +1,108 @@
//! Trait definitions for spiking neurons.
use serde::{Deserialize, Serialize};
/// State of a spiking neuron.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct NeuronState {
/// Membrane potential (mV)
pub membrane_potential: f32,
/// Time since last spike (ms), None if never spiked
pub time_since_spike: Option<f32>,
/// Whether the neuron is currently in refractory period
pub is_refractory: bool,
/// Accumulated input current for this timestep
pub input_current: f32,
}
impl Default for NeuronState {
fn default() -> Self {
Self {
membrane_potential: -65.0, // Resting potential
time_since_spike: None,
is_refractory: false,
input_current: 0.0,
}
}
}
/// Parameters that define a spiking neuron's behavior.
pub trait NeuronParams: Clone + Send + Sync {
/// Get the spike threshold voltage
fn threshold(&self) -> f32;
/// Get the reset voltage after spike
fn reset_potential(&self) -> f32;
/// Get the resting membrane potential
fn resting_potential(&self) -> f32;
/// Get the refractory period in milliseconds
fn refractory_period(&self) -> f32;
/// Validate parameters, returning error message if invalid
fn validate(&self) -> Option<String>;
}
/// Core trait for spiking neuron models.
///
/// Implementing types should be efficient for ASIC deployment:
/// - Avoid floating-point division in `update()`
/// - Use predictable branching
/// - Minimize memory footprint
pub trait SpikingNeuron: Clone + Send + Sync {
/// Associated parameter type
type Params: NeuronParams;
/// Create a new neuron with given parameters
fn new(params: Self::Params) -> Self;
/// Get current neuron state
fn state(&self) -> NeuronState;
/// Get neuron parameters
fn params(&self) -> &Self::Params;
/// Add input current (from incoming spikes or external input)
///
/// This is a sparse operation - only called when input arrives.
fn receive_input(&mut self, current: f32);
/// Update neuron state for one timestep.
///
/// Returns `true` if the neuron fires a spike.
///
/// # Arguments
/// * `dt` - Time step in milliseconds
///
/// # ASIC Optimization
/// This is the hot path. Implementations should:
/// - Use only additions and multiplications
/// - Avoid conditional branches where possible
/// - Use fixed-point compatible operations
fn update(&mut self, dt: f32) -> bool;
/// Reset neuron to initial state
fn reset(&mut self);
/// Check if neuron is in refractory period
fn is_refractory(&self) -> bool;
/// Get membrane potential
fn membrane_potential(&self) -> f32;
/// Get time since last spike (if any)
fn time_since_spike(&self) -> Option<f32>;
}
/// Energy estimation for ASIC cost analysis.
pub trait EnergyModel {
/// Estimate energy cost for a single update step (picojoules)
fn update_energy(&self) -> f32;
/// Estimate energy cost for spike emission (picojoules)
fn spike_energy(&self) -> f32;
/// Estimate silicon area (square micrometers)
fn silicon_area(&self) -> f32;
}