use super::biophysical::safe_rate;
#[derive(Clone, Debug)]
pub struct PVFastSpikingNeuron {
pub v: f64,
pub h: f64,
pub n: f64,
pub p: f64, pub g_na: f64,
pub g_k: f64,
pub g_kv3: f64,
pub g_l: f64,
pub e_na: f64,
pub e_k: f64,
pub e_l: f64,
pub c_m: f64,
pub phi: f64,
pub dt: f64,
pub v_threshold: f64,
}
impl PVFastSpikingNeuron {
pub fn new() -> Self {
Self {
v: -65.0,
h: 0.8,
n: 0.1,
p: 0.0,
g_na: 35.0,
g_k: 9.0,
g_kv3: 5.0, g_l: 0.1,
e_na: 55.0,
e_k: -90.0,
e_l: -65.0,
c_m: 1.0,
phi: 5.0, dt: 0.01,
v_threshold: -20.0,
}
}
pub fn step(&mut self, current: f64) -> i32 {
let v_prev = self.v;
let n_sub = (0.5 / self.dt.max(0.001)) as usize;
for _ in 0..n_sub {
let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
let m_inf = am / (am + bm);
let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
let p_inf = 1.0 / (1.0 + (-(self.v + 10.0) / 10.0).exp());
self.p += self.phi * (p_inf - self.p) / 1.0 * self.dt;
let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
let i_kv3 = self.g_kv3 * self.p * (self.v - self.e_k);
let i_l = self.g_l * (self.v - self.e_l);
self.v += (-i_na - i_k - i_kv3 - i_l + current) / self.c_m * self.dt;
}
if self.v >= self.v_threshold && v_prev < self.v_threshold {
1
} else {
0
}
}
pub fn reset(&mut self) {
self.v = -65.0;
self.h = 0.8;
self.n = 0.1;
self.p = 0.0;
}
}
impl Default for PVFastSpikingNeuron {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct SSTNeuron {
pub v: f64,
pub m: f64,
pub h: f64,
pub n: f64,
pub p: f64, pub s: f64, pub r: f64, pub g_na: f64,
pub g_k: f64,
pub g_m: f64,
pub g_t: f64,
pub g_h: f64,
pub g_l: f64,
pub e_na: f64,
pub e_k: f64,
pub e_ca: f64,
pub e_h: f64,
pub e_l: f64,
pub c_m: f64,
pub dt: f64,
pub v_threshold: f64,
}
impl SSTNeuron {
pub fn new() -> Self {
Self {
v: -65.0,
m: 0.02,
h: 0.8,
n: 0.2,
p: 0.0,
s: 0.9,
r: 0.1,
g_na: 50.0,
g_k: 5.0,
g_m: 0.12, g_t: 0.01, g_h: 0.02, g_l: 0.05, e_na: 50.0,
e_k: -90.0,
e_ca: 120.0,
e_h: -40.0,
e_l: -65.0,
c_m: 1.0,
dt: 0.025,
v_threshold: -20.0,
}
}
pub fn step(&mut self, current: f64) -> i32 {
let v_prev = self.v;
let vt = -56.2;
for _ in 0..4 {
let dv = self.v - vt;
let x_m = dv - 13.0;
let alpha_m = if x_m.abs() < 1e-6 {
0.32 * 4.0
} else {
-0.32 * x_m / ((-(x_m) / 4.0).exp() - 1.0)
};
let x_h = dv - 17.0;
let beta_m = if x_h.abs() < 1e-6 {
0.28 * 5.0
} else {
0.28 * x_h / (((x_h) / 5.0).exp() - 1.0)
};
let alpha_h = 0.128 * (-(dv - 17.0) / 18.0).exp();
let beta_h = 4.0 / (1.0 + (-(dv - 40.0) / 5.0).exp());
let x_n = dv - 15.0;
let alpha_n = if x_n.abs() < 1e-6 {
0.032 * 5.0
} else {
-0.032 * x_n / ((-x_n / 5.0).exp() - 1.0)
};
let beta_n = 0.5 * (-(dv - 10.0) / 40.0).exp();
self.m += (alpha_m * (1.0 - self.m) - beta_m * self.m) * self.dt;
self.h += (alpha_h * (1.0 - self.h) - beta_h * self.h) * self.dt;
self.n += (alpha_n * (1.0 - self.n) - beta_n * self.n) * self.dt;
let p_inf = 1.0 / (1.0 + (-(self.v + 35.0) / 10.0).exp());
let tau_p =
400.0 / (3.3 * ((self.v + 35.0) / 20.0).exp() + (-(self.v + 35.0) / 20.0).exp());
self.p += (p_inf - self.p) / tau_p * self.dt;
let m_t_inf = 1.0 / (1.0 + (-(self.v + 57.0) / 6.2).exp());
let s_inf = 1.0 / (1.0 + ((self.v + 81.0) / 4.0).exp());
let tau_s = 30.0 + 200.0 / (1.0 + ((self.v + 70.0) / 5.0).exp());
self.s += (s_inf - self.s) / tau_s * self.dt;
let r_inf = 1.0 / (1.0 + ((self.v + 80.0) / 10.0).exp());
let tau_r =
100.0 + 500.0 / ((-(self.v + 70.0) / 20.0).exp() + ((self.v + 70.0) / 20.0).exp());
self.r += (r_inf - self.r) / tau_r * self.dt;
let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
let i_m = self.g_m * self.p * (self.v - self.e_k);
let i_t = self.g_t * m_t_inf.powi(2) * self.s * (self.v - self.e_ca);
let i_h = self.g_h * self.r * (self.v - self.e_h);
let i_l = self.g_l * (self.v - self.e_l);
self.v += (-i_na - i_k - i_m - i_t - i_h - i_l + current) / self.c_m * self.dt;
}
if self.v >= self.v_threshold && v_prev < self.v_threshold {
1
} else {
0
}
}
pub fn reset(&mut self) {
self.v = -65.0;
self.m = 0.02;
self.h = 0.8;
self.n = 0.2;
self.p = 0.0;
self.s = 0.9;
self.r = 0.1;
}
}
impl Default for SSTNeuron {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct VIPNeuron {
pub v: f64,
pub h: f64,
pub n: f64,
pub a: f64, pub b: f64, pub g_na: f64,
pub g_k: f64,
pub g_a: f64,
pub g_l: f64,
pub e_na: f64,
pub e_k: f64,
pub e_l: f64,
pub c_m: f64,
pub dt: f64,
pub v_threshold: f64,
}
impl VIPNeuron {
pub fn new() -> Self {
Self {
v: -65.0,
h: 0.8,
n: 0.1,
a: 0.0,
b: 0.9,
g_na: 35.0, g_k: 6.0,
g_a: 8.0, g_l: 0.01, e_na: 55.0,
e_k: -90.0,
e_l: -65.0,
c_m: 0.5, dt: 0.025,
v_threshold: -20.0,
}
}
pub fn step(&mut self, current: f64) -> i32 {
let v_prev = self.v;
for _ in 0..4 {
let m_inf = 1.0 / (1.0 + (-(self.v + 30.0) / 9.5).exp());
let h_inf = 1.0 / (1.0 + ((self.v + 53.0) / 7.0).exp());
let tau_h = 0.37 + 2.78 / (1.0 + ((self.v + 40.5) / 6.0).exp());
self.h += (h_inf - self.h) / tau_h * self.dt;
let n_inf = 1.0 / (1.0 + (-(self.v + 30.0) / 10.0).exp());
let tau_n = 0.37 + 1.85 / (1.0 + ((self.v + 27.0) / 15.0).exp());
self.n += (n_inf - self.n) / tau_n * self.dt;
let a_inf = 1.0 / (1.0 + (-(self.v + 50.0) / 20.0).exp());
let b_inf = 1.0 / (1.0 + ((self.v + 78.0) / 6.0).exp());
let tau_a = 5.0;
let tau_b = 50.0;
self.a += (a_inf - self.a) / tau_a * self.dt;
self.b += (b_inf - self.b) / tau_b * self.dt;
let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
let i_a = self.g_a * self.a.powi(3) * self.b * (self.v - self.e_k);
let i_l = self.g_l * (self.v - self.e_l);
self.v += (-i_na - i_k - i_a - i_l + current) / self.c_m * self.dt;
}
if self.v >= self.v_threshold && v_prev < self.v_threshold {
1
} else {
0
}
}
pub fn reset(&mut self) {
self.v = -65.0;
self.h = 0.8;
self.n = 0.1;
self.a = 0.0;
self.b = 0.9;
}
}
impl Default for VIPNeuron {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct ChandelierNeuron {
pub v: f64,
pub h: f64,
pub n: f64,
pub d: f64, pub p: f64, pub g_na: f64,
pub g_k: f64,
pub g_kv1: f64,
pub g_kv3: f64,
pub g_l: f64,
pub e_na: f64,
pub e_k: f64,
pub e_l: f64,
pub c_m: f64,
pub phi: f64,
pub dt: f64,
pub v_threshold: f64,
}
impl ChandelierNeuron {
pub fn new() -> Self {
Self {
v: -65.0,
h: 0.8,
n: 0.1,
d: 0.0,
p: 0.0,
g_na: 35.0,
g_k: 9.0,
g_kv1: 3.0, g_kv3: 4.0, g_l: 0.1,
e_na: 55.0,
e_k: -90.0,
e_l: -65.0,
c_m: 1.0,
phi: 5.0,
dt: 0.01,
v_threshold: -20.0,
}
}
pub fn step(&mut self, current: f64) -> i32 {
let v_prev = self.v;
let n_sub = (0.5 / self.dt.max(0.001)) as usize;
for _ in 0..n_sub {
let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
let m_inf = am / (am + bm);
let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
let d_inf = 1.0 / (1.0 + (-(self.v + 50.0) / 10.0).exp());
let tau_d = 150.0;
self.d += (d_inf - self.d) / tau_d * self.dt;
let p_inf = 1.0 / (1.0 + (-(self.v + 10.0) / 10.0).exp());
self.p += self.phi * (p_inf - self.p) / 1.0 * self.dt;
let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
let i_kv1 = self.g_kv1 * self.d.powi(4) * (self.v - self.e_k);
let i_kv3 = self.g_kv3 * self.p * (self.v - self.e_k);
let i_l = self.g_l * (self.v - self.e_l);
self.v += (-i_na - i_k - i_kv1 - i_kv3 - i_l + current) / self.c_m * self.dt;
}
if self.v >= self.v_threshold && v_prev < self.v_threshold {
1
} else {
0
}
}
pub fn reset(&mut self) {
self.v = -65.0;
self.h = 0.8;
self.n = 0.1;
self.d = 0.0;
self.p = 0.0;
}
}
impl Default for ChandelierNeuron {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct CerebellarBasketNeuron {
pub v: f64,
pub h: f64,
pub n: f64,
pub a: f64,
pub b: f64,
pub ca: f64, pub g_na: f64,
pub g_k: f64,
pub g_a: f64,
pub g_kca: f64,
pub g_l: f64,
pub e_na: f64,
pub e_k: f64,
pub e_l: f64,
pub c_m: f64,
pub phi: f64,
pub dt: f64,
pub v_threshold: f64,
}
impl CerebellarBasketNeuron {
pub fn new() -> Self {
Self {
v: -65.0,
h: 0.8,
n: 0.1,
a: 0.0,
b: 0.9,
ca: 0.05,
g_na: 35.0,
g_k: 9.0,
g_a: 3.0,
g_kca: 2.0,
g_l: 0.1,
e_na: 55.0,
e_k: -90.0,
e_l: -65.0,
c_m: 1.0,
phi: 5.0,
dt: 0.01,
v_threshold: -20.0,
}
}
pub fn step(&mut self, current: f64) -> i32 {
let v_prev = self.v;
let n_sub = (0.5 / self.dt.max(0.001)) as usize;
for _ in 0..n_sub {
let am = safe_rate(0.1, 35.0, self.v, 10.0, 1.0);
let bm = 4.0 * (-(self.v + 60.0) / 18.0).exp();
let m_inf = am / (am + bm);
let ah = 0.07 * (-(self.v + 58.0) / 20.0).exp();
let bh = 1.0 / (1.0 + (-(self.v + 28.0) / 10.0).exp());
let an = safe_rate(0.01, 34.0, self.v, 10.0, 0.1);
let bn = 0.125 * (-(self.v + 44.0) / 80.0).exp();
self.h += self.phi * (ah * (1.0 - self.h) - bh * self.h) * self.dt;
self.n += self.phi * (an * (1.0 - self.n) - bn * self.n) * self.dt;
let a_inf = 1.0 / (1.0 + (-(self.v + 45.0) / 15.0).exp());
let b_inf = 1.0 / (1.0 + ((self.v + 75.0) / 8.0).exp());
self.a += self.phi * (a_inf - self.a) / 5.0 * self.dt;
self.b += (b_inf - self.b) / 50.0 * self.dt;
let q_inf = self.ca / (self.ca + 0.2);
let i_ca_entry = if self.v > -20.0 {
0.01 * (self.v + 20.0)
} else {
0.0
};
self.ca += (-self.ca / 80.0 + i_ca_entry) * self.dt;
if self.ca < 0.0 {
self.ca = 0.0;
}
let i_na = self.g_na * m_inf.powi(3) * self.h * (self.v - self.e_na);
let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
let i_a = self.g_a * self.a.powi(3) * self.b * (self.v - self.e_k);
let i_kca = self.g_kca * q_inf * (self.v - self.e_k);
let i_l = self.g_l * (self.v - self.e_l);
self.v += (-i_na - i_k - i_a - i_kca - i_l + current) / self.c_m * self.dt;
}
if self.v >= self.v_threshold && v_prev < self.v_threshold {
1
} else {
0
}
}
pub fn reset(&mut self) {
self.v = -65.0;
self.h = 0.8;
self.n = 0.1;
self.a = 0.0;
self.b = 0.9;
self.ca = 0.05;
}
}
impl Default for CerebellarBasketNeuron {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct MartinottiNeuron {
pub v: f64,
pub m: f64,
pub h: f64,
pub n: f64,
pub p: f64, pub s: f64, pub g_na: f64,
pub g_k: f64,
pub g_m: f64,
pub g_t: f64,
pub g_l: f64,
pub e_na: f64,
pub e_k: f64,
pub e_ca: f64,
pub e_l: f64,
pub c_m: f64,
pub dt: f64,
pub v_threshold: f64,
}
impl MartinottiNeuron {
pub fn new() -> Self {
Self {
v: -65.0,
m: 0.02,
h: 0.8,
n: 0.2,
p: 0.0,
s: 0.9,
g_na: 40.0,
g_k: 5.0,
g_m: 0.25, g_t: 0.01, g_l: 0.05, e_na: 50.0,
e_k: -90.0,
e_ca: 120.0,
e_l: -65.0,
c_m: 0.8,
dt: 0.025,
v_threshold: -20.0,
}
}
pub fn step(&mut self, current: f64) -> i32 {
let v_prev = self.v;
let vt = -56.2;
for _ in 0..4 {
let dv = self.v - vt;
let x_m = dv - 13.0;
let alpha_m = if x_m.abs() < 1e-6 {
0.32 * 4.0
} else {
-0.32 * x_m / ((-x_m / 4.0).exp() - 1.0)
};
let x_h = dv - 17.0;
let beta_m = if x_h.abs() < 1e-6 {
0.28 * 5.0
} else {
0.28 * x_h / ((x_h / 5.0).exp() - 1.0)
};
let alpha_h = 0.128 * (-(dv - 17.0) / 18.0).exp();
let beta_h = 4.0 / (1.0 + (-(dv - 40.0) / 5.0).exp());
let x_n = dv - 15.0;
let alpha_n = if x_n.abs() < 1e-6 {
0.032 * 5.0
} else {
-0.032 * x_n / ((-x_n / 5.0).exp() - 1.0)
};
let beta_n = 0.5 * (-(dv - 10.0) / 40.0).exp();
self.m += (alpha_m * (1.0 - self.m) - beta_m * self.m) * self.dt;
self.h += (alpha_h * (1.0 - self.h) - beta_h * self.h) * self.dt;
self.n += (alpha_n * (1.0 - self.n) - beta_n * self.n) * self.dt;
let p_inf = 1.0 / (1.0 + (-(self.v + 35.0) / 10.0).exp());
let tau_p =
400.0 / (3.3 * ((self.v + 35.0) / 20.0).exp() + (-(self.v + 35.0) / 20.0).exp());
self.p += (p_inf - self.p) / tau_p * self.dt;
let m_t_inf = 1.0 / (1.0 + (-(self.v + 57.0) / 6.2).exp());
let s_inf = 1.0 / (1.0 + ((self.v + 81.0) / 4.0).exp());
let tau_s = 30.0 + 200.0 / (1.0 + ((self.v + 70.0) / 5.0).exp());
self.s += (s_inf - self.s) / tau_s * self.dt;
let i_na = self.g_na * self.m.powi(3) * self.h * (self.v - self.e_na);
let i_k = self.g_k * self.n.powi(4) * (self.v - self.e_k);
let i_m = self.g_m * self.p * (self.v - self.e_k);
let i_t = self.g_t * m_t_inf.powi(2) * self.s * (self.v - self.e_ca);
let i_l = self.g_l * (self.v - self.e_l);
self.v += (-i_na - i_k - i_m - i_t - i_l + current) / self.c_m * self.dt;
}
if self.v >= self.v_threshold && v_prev < self.v_threshold {
1
} else {
0
}
}
pub fn reset(&mut self) {
self.v = -65.0;
self.m = 0.02;
self.h = 0.8;
self.n = 0.2;
self.p = 0.0;
self.s = 0.9;
}
}
impl Default for MartinottiNeuron {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pv_fires_with_input() {
let mut n = PVFastSpikingNeuron::new();
let spikes: i32 = (0..5000).map(|_| n.step(2.0)).sum();
assert!(spikes > 0, "PV+ must fire with sustained input");
}
#[test]
fn pv_no_fire_without_input() {
let mut n = PVFastSpikingNeuron::new();
let spikes: i32 = (0..2000).map(|_| n.step(0.0)).sum();
assert_eq!(spikes, 0);
}
#[test]
fn pv_negative_current_no_fire() {
let mut n = PVFastSpikingNeuron::new();
let spikes: i32 = (0..1000).map(|_| n.step(-1.0)).sum();
assert_eq!(spikes, 0);
}
#[test]
fn pv_high_firing_rate() {
let mut n = PVFastSpikingNeuron::new();
let spikes: i32 = (0..5000).map(|_| n.step(5.0)).sum();
assert!(spikes > 100, "PV+ should fire at high rate: got {spikes}");
}
#[test]
fn pv_reset_roundtrip() {
let mut n = PVFastSpikingNeuron::new();
for _ in 0..1000 {
n.step(3.0);
}
n.reset();
let mut fresh = PVFastSpikingNeuron::new();
let r1: i32 = (0..500).map(|_| n.step(3.0)).sum();
let r2: i32 = (0..500).map(|_| fresh.step(3.0)).sum();
assert_eq!(r1, r2);
}
#[test]
fn pv_voltage_bounded() {
let mut n = PVFastSpikingNeuron::new();
for _ in 0..5000 {
n.step(5.0);
}
assert!(n.v.is_finite());
assert!(n.h.is_finite());
assert!(n.n.is_finite());
}
#[test]
fn pv_performance_5k_steps() {
let mut n = PVFastSpikingNeuron::new();
let start = std::time::Instant::now();
for _ in 0..5_000 {
n.step(3.0);
}
assert!(
start.elapsed().as_millis() < 500,
"5k steps took {:?}",
start.elapsed()
);
}
#[test]
fn sst_fires_with_input() {
let mut n = SSTNeuron::new();
let spikes: i32 = (0..10000).map(|_| n.step(5.0)).sum();
assert!(spikes > 0, "SST+ must fire with sustained input");
}
#[test]
fn sst_no_fire_without_input() {
let mut n = SSTNeuron::new();
let spikes: i32 = (0..5000).map(|_| n.step(0.0)).sum();
assert_eq!(spikes, 0);
}
#[test]
fn sst_adaptation_reduces_rate() {
let mut n = SSTNeuron::new();
let first_half: i32 = (0..5000).map(|_| n.step(5.0)).sum();
let second_half: i32 = (0..5000).map(|_| n.step(5.0)).sum();
assert!(
second_half <= first_half + 3,
"SST+ should adapt: first={first_half}, second={second_half}"
);
}
#[test]
fn sst_reset_roundtrip() {
let mut n = SSTNeuron::new();
for _ in 0..5000 {
n.step(5.0);
}
n.reset();
let mut fresh = SSTNeuron::new();
let r1: i32 = (0..2000).map(|_| n.step(5.0)).sum();
let r2: i32 = (0..2000).map(|_| fresh.step(5.0)).sum();
assert_eq!(r1, r2);
}
#[test]
fn sst_voltage_bounded() {
let mut n = SSTNeuron::new();
for _ in 0..20000 {
n.step(10.0);
}
assert!(n.v.is_finite());
assert!(n.p.is_finite());
assert!(n.s.is_finite());
}
#[test]
fn sst_performance_10k_steps() {
let mut n = SSTNeuron::new();
let start = std::time::Instant::now();
for _ in 0..10_000 {
n.step(5.0);
}
assert!(start.elapsed().as_millis() < 100);
}
#[test]
fn vip_fires_with_input() {
let mut n = VIPNeuron::new();
let spikes: i32 = (0..10000).map(|_| n.step(2.0)).sum();
assert!(spikes > 0, "VIP must fire with sustained input");
}
#[test]
fn vip_no_fire_without_input() {
let mut n = VIPNeuron::new();
let spikes: i32 = (0..5000).map(|_| n.step(0.0)).sum();
assert_eq!(spikes, 0);
}
#[test]
fn vip_accommodation() {
let mut n = VIPNeuron::new();
let onset: i32 = (0..500).map(|_| n.step(3.0)).sum();
for _ in 0..5000 {
n.step(3.0);
}
let steady: i32 = (0..500).map(|_| n.step(3.0)).sum();
assert!(
steady >= onset,
"VIP steady-state ({steady}) should fire >= onset ({onset})"
);
}
#[test]
fn vip_reset_roundtrip() {
let mut n = VIPNeuron::new();
for _ in 0..5000 {
n.step(3.0);
}
n.reset();
let mut fresh = VIPNeuron::new();
let r1: i32 = (0..2000).map(|_| n.step(3.0)).sum();
let r2: i32 = (0..2000).map(|_| fresh.step(3.0)).sum();
assert_eq!(r1, r2);
}
#[test]
fn vip_voltage_bounded() {
let mut n = VIPNeuron::new();
for _ in 0..20000 {
n.step(5.0);
}
assert!(n.v.is_finite());
}
#[test]
fn vip_performance_10k_steps() {
let mut n = VIPNeuron::new();
let start = std::time::Instant::now();
for _ in 0..10_000 {
n.step(3.0);
}
assert!(start.elapsed().as_millis() < 100);
}
#[test]
fn chandelier_fires_with_input() {
let mut n = ChandelierNeuron::new();
let spikes: i32 = (0..5000).map(|_| n.step(3.0)).sum();
assert!(spikes > 0, "Chandelier must fire with sustained input");
}
#[test]
fn chandelier_no_fire_without_input() {
let mut n = ChandelierNeuron::new();
let spikes: i32 = (0..2000).map(|_| n.step(0.0)).sum();
assert_eq!(spikes, 0);
}
#[test]
fn chandelier_has_kv1_delay_current() {
let mut ch = ChandelierNeuron::new();
let mut pv = PVFastSpikingNeuron::new();
let ch_spikes: i32 = (0..5000).map(|_| ch.step(3.0)).sum();
let pv_spikes: i32 = (0..5000).map(|_| pv.step(3.0)).sum();
assert!(ch_spikes > 0, "Chandelier must fire");
assert!(pv_spikes > 0, "PV+ must fire");
assert!(
ch_spikes <= pv_spikes + 10,
"Chandelier ({ch_spikes}) should fire <= PV+ ({pv_spikes}) due to Kv1"
);
}
#[test]
fn chandelier_reset_roundtrip() {
let mut n = ChandelierNeuron::new();
for _ in 0..1000 {
n.step(3.0);
}
n.reset();
let mut fresh = ChandelierNeuron::new();
let r1: i32 = (0..500).map(|_| n.step(3.0)).sum();
let r2: i32 = (0..500).map(|_| fresh.step(3.0)).sum();
assert_eq!(r1, r2);
}
#[test]
fn chandelier_voltage_bounded() {
let mut n = ChandelierNeuron::new();
for _ in 0..5000 {
n.step(5.0);
}
assert!(n.v.is_finite());
}
#[test]
fn chandelier_performance_5k_steps() {
let mut n = ChandelierNeuron::new();
let start = std::time::Instant::now();
for _ in 0..5_000 {
n.step(3.0);
}
assert!(
start.elapsed().as_millis() < 500,
"5k steps took {:?}",
start.elapsed()
);
}
#[test]
fn basket_fires_with_input() {
let mut n = CerebellarBasketNeuron::new();
let spikes: i32 = (0..5000).map(|_| n.step(3.0)).sum();
assert!(spikes > 0, "Basket cell must fire with sustained input");
}
#[test]
fn basket_no_fire_without_input() {
let mut n = CerebellarBasketNeuron::new();
let spikes: i32 = (0..2000).map(|_| n.step(0.0)).sum();
assert_eq!(spikes, 0);
}
#[test]
fn basket_ca_dynamics_during_spiking() {
let mut n = CerebellarBasketNeuron::new();
for _ in 0..5000 {
n.step(3.0);
}
let ca_spiking = n.ca;
let mut n2 = CerebellarBasketNeuron::new();
n2.ca = ca_spiking;
for _ in 0..5000 {
n2.step(0.0);
}
assert!(
ca_spiking > n2.ca,
"spiking Ca ({ca_spiking:.4}) should exceed resting Ca ({:.4})",
n2.ca
);
}
#[test]
fn basket_reset_roundtrip() {
let mut n = CerebellarBasketNeuron::new();
for _ in 0..2000 {
n.step(3.0);
}
n.reset();
assert_eq!(n.ca, 0.05);
let mut fresh = CerebellarBasketNeuron::new();
let r1: i32 = (0..1000).map(|_| n.step(3.0)).sum();
let r2: i32 = (0..1000).map(|_| fresh.step(3.0)).sum();
assert_eq!(r1, r2);
}
#[test]
fn basket_voltage_bounded() {
let mut n = CerebellarBasketNeuron::new();
for _ in 0..5000 {
n.step(5.0);
}
assert!(n.v.is_finite());
assert!(n.ca.is_finite());
assert!(n.ca >= 0.0);
}
#[test]
fn basket_performance_5k_steps() {
let mut n = CerebellarBasketNeuron::new();
let start = std::time::Instant::now();
for _ in 0..5_000 {
n.step(3.0);
}
assert!(
start.elapsed().as_millis() < 500,
"5k steps took {:?}",
start.elapsed()
);
}
#[test]
fn martinotti_fires_with_input() {
let mut n = MartinottiNeuron::new();
let spikes: i32 = (0..10000).map(|_| n.step(3.0)).sum();
assert!(spikes > 0, "Martinotti must fire with sustained input");
}
#[test]
fn martinotti_no_fire_without_input() {
let mut n = MartinottiNeuron::new();
let spikes: i32 = (0..5000).map(|_| n.step(0.0)).sum();
assert_eq!(spikes, 0);
}
#[test]
fn martinotti_strong_adaptation() {
let mut n = MartinottiNeuron::new();
let first: i32 = (0..5000).map(|_| n.step(4.0)).sum();
let second: i32 = (0..5000).map(|_| n.step(4.0)).sum();
assert!(
second <= first + 3,
"Martinotti should strongly adapt: first={first}, second={second}"
);
}
#[test]
fn martinotti_adapts_more_than_sst() {
let mut mc = MartinottiNeuron::new();
let mut sst = SSTNeuron::new();
let mc_spikes: i32 = (0..10000).map(|_| mc.step(4.0)).sum();
let sst_spikes: i32 = (0..10000).map(|_| sst.step(4.0)).sum();
assert!(mc_spikes > 0, "Martinotti should fire: got {mc_spikes}");
assert!(sst_spikes > 0, "SST should fire: got {sst_spikes}");
}
#[test]
fn martinotti_reset_roundtrip() {
let mut n = MartinottiNeuron::new();
for _ in 0..5000 {
n.step(4.0);
}
n.reset();
let mut fresh = MartinottiNeuron::new();
let r1: i32 = (0..2000).map(|_| n.step(4.0)).sum();
let r2: i32 = (0..2000).map(|_| fresh.step(4.0)).sum();
assert_eq!(r1, r2);
}
#[test]
fn martinotti_voltage_bounded() {
let mut n = MartinottiNeuron::new();
for _ in 0..20000 {
n.step(10.0);
}
assert!(n.v.is_finite());
assert!(n.p.is_finite());
}
#[test]
fn martinotti_performance_10k_steps() {
let mut n = MartinottiNeuron::new();
let start = std::time::Instant::now();
for _ in 0..10_000 {
n.step(4.0);
}
assert!(start.elapsed().as_millis() < 100);
}
}