use super::{Axon, Dendrite, Nuclei, Soma};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SpatialNeuron {
pub soma: Soma,
pub dendrite: Dendrite,
pub axon: Axon,
pub nuclei: Nuclei,
pub membrane: i16,
pub threshold: i16,
pub trace: i8,
pub stamina: u8,
pub last_update_us: u64,
pub last_spike_us: u64,
pub last_arrival_us: u64,
}
impl SpatialNeuron {
pub const RESTING_POTENTIAL: i16 = -7000;
pub const RESET_POTENTIAL: i16 = -8000;
pub const DEFAULT_THRESHOLD: i16 = -5500;
pub const STAMINA_RECOVERY_US: u64 = 5_000;
#[inline]
pub const fn new(soma: Soma, dendrite: Dendrite, axon: Axon, nuclei: Nuclei) -> Self {
Self {
soma,
dendrite,
axon,
nuclei,
membrane: Self::RESTING_POTENTIAL,
threshold: Self::DEFAULT_THRESHOLD,
trace: 0,
stamina: 255,
last_update_us: 0,
last_spike_us: 0,
last_arrival_us: 0,
}
}
#[inline]
pub fn at(position: [f32; 3], nuclei: Nuclei) -> Self {
Self::new(
Soma::at(position),
Dendrite::default(),
Axon::toward(position), nuclei,
)
}
#[inline]
pub fn pyramidal_at(position: [f32; 3]) -> Self {
Self::at(position, Nuclei::pyramidal())
}
#[inline]
pub fn interneuron_at(position: [f32; 3]) -> Self {
Self::at(position, Nuclei::interneuron())
}
#[inline]
pub fn sensory_at(position: [f32; 3], channel: u16, modality: u8) -> Self {
Self::at(position, Nuclei::sensory(channel, modality))
}
#[inline]
pub fn motor_at(position: [f32; 3], channel: u16, modality: u8) -> Self {
Self::at(position, Nuclei::motor(channel, modality))
}
#[inline]
pub const fn above_threshold(&self) -> bool {
self.membrane >= self.threshold
}
#[inline]
pub const fn in_refractory(&self, current_time_us: u64) -> bool {
if self.last_spike_us == 0 {
return false;
}
current_time_us < self.last_spike_us + self.nuclei.refractory as u64
}
#[inline]
pub const fn can_fire(&self, current_time_us: u64) -> bool {
self.above_threshold() && !self.in_refractory(current_time_us) && self.stamina > 0
}
pub fn apply_leak(&mut self, current_time_us: u64) {
if current_time_us <= self.last_update_us {
return;
}
let dt_us = current_time_us - self.last_update_us;
let tau = self.nuclei.leak_tau_us() as f32;
let decay = (-(dt_us as f32) / tau).exp();
let delta_from_rest = self.membrane as i32 - Self::RESTING_POTENTIAL as i32;
let decayed = (delta_from_rest as f32 * decay) as i32;
self.membrane = (Self::RESTING_POTENTIAL as i32 + decayed).clamp(i16::MIN as i32, i16::MAX as i32) as i16;
self.last_update_us = current_time_us;
}
#[inline]
pub fn integrate(&mut self, current: i16) {
self.membrane = self.membrane.saturating_add(current);
}
pub fn fire(&mut self, current_time_us: u64) {
self.membrane = Self::RESET_POTENTIAL;
self.last_spike_us = current_time_us;
self.trace = self.trace.saturating_add(30); self.axon.boost(10);
let cost = (self.nuclei.metabolic_rate / 10).max(5);
self.stamina = self.stamina.saturating_sub(cost);
}
#[inline]
pub fn decay_trace(&mut self, retention: f32) {
self.trace = (self.trace as f32 * retention) as i8;
}
pub fn oscillator_should_fire(&self, current_time_us: u64) -> bool {
if !self.nuclei.is_oscillator() {
return false;
}
let period = self.nuclei.oscillation_period as u64;
current_time_us >= self.last_spike_us + period
}
pub fn oscillator_ramp(&mut self, current_time_us: u64) {
if !self.nuclei.is_oscillator() {
return;
}
let dt_us = current_time_us.saturating_sub(self.last_update_us);
let period = self.nuclei.oscillation_period;
if period == 0 {
return;
}
let ramp_total = Self::DEFAULT_THRESHOLD - Self::RESET_POTENTIAL;
let ramp_per_us = ramp_total as f32 / period as f32;
let ramp = (dt_us as f32 * ramp_per_us) as i16;
self.membrane = self.membrane.saturating_add(ramp);
}
#[inline]
pub fn distance_to(&self, pos: [f32; 3]) -> f32 {
let dx = self.soma.position[0] - pos[0];
let dy = self.soma.position[1] - pos[1];
let dz = self.soma.position[2] - pos[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[inline]
pub fn axon_length(&self) -> f32 {
self.axon.length(self.soma.position)
}
#[inline]
pub const fn axon_alive(&self) -> bool {
self.axon.is_alive()
}
pub fn migrate(&mut self, delta: [f32; 3]) {
self.soma.translate(delta);
self.axon.terminal[0] += delta[0] * 0.8;
self.axon.terminal[1] += delta[1] * 0.8;
self.axon.terminal[2] += delta[2] * 0.8;
}
}
impl Default for SpatialNeuron {
fn default() -> Self {
Self::at([0.0, 0.0, 0.0], Nuclei::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spatial::Interface;
#[test]
fn test_neuron_creation() {
let n = SpatialNeuron::pyramidal_at([1.0, 2.0, 3.0]);
assert_eq!(n.soma.position, [1.0, 2.0, 3.0]);
assert!(n.nuclei.is_excitatory());
}
#[test]
fn test_firing() {
let mut n = SpatialNeuron::default();
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
assert!(n.can_fire(0));
n.fire(100);
assert!(!n.can_fire(100)); assert!(!n.in_refractory(100 + n.nuclei.refractory as u64 + 1));
}
#[test]
fn test_leak() {
let mut n = SpatialNeuron::default();
n.membrane = -4000; n.last_update_us = 0;
n.apply_leak(50_000); assert!(n.membrane < -4000); assert!(n.membrane > SpatialNeuron::RESTING_POTENTIAL);
}
#[test]
fn test_oscillator() {
let mut osc = SpatialNeuron::at([0.0, 0.0, 0.0], Nuclei::oscillator(10_000));
osc.last_spike_us = 0;
assert!(!osc.oscillator_should_fire(5_000));
assert!(osc.oscillator_should_fire(10_000));
}
#[test]
fn test_factory_methods() {
let inter = SpatialNeuron::interneuron_at([0.0, 0.0, 0.0]);
assert!(inter.nuclei.is_inhibitory());
let sens = SpatialNeuron::sensory_at([0.0, 0.0, 0.0], 5, Interface::MODALITY_AUDITORY);
assert!(sens.nuclei.is_sensory());
let mot = SpatialNeuron::motor_at([0.0, 0.0, 0.0], 10, Interface::MODALITY_AUDITORY);
assert!(mot.nuclei.is_motor());
}
#[test]
fn test_integration() {
let mut n = SpatialNeuron::default();
let initial = n.membrane;
n.integrate(500);
assert_eq!(n.membrane, initial + 500);
n.membrane = i16::MAX - 100;
n.integrate(200);
assert_eq!(n.membrane, i16::MAX);
}
#[test]
fn test_trace_decay() {
let mut n = SpatialNeuron::default();
n.trace = 100;
n.decay_trace(0.5);
assert_eq!(n.trace, 50);
n.decay_trace(0.5);
assert_eq!(n.trace, 25);
}
#[test]
fn test_trace_boost_on_fire() {
let mut n = SpatialNeuron::default();
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
let initial_trace = n.trace;
n.fire(1000);
assert!(n.trace > initial_trace); }
#[test]
fn test_axon_health_boost_on_fire() {
let mut n = SpatialNeuron::default();
n.axon.health = 100;
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
n.fire(1000);
assert!(n.axon.health > 100); }
#[test]
fn test_distance_to() {
let n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
assert!((n.distance_to([3.0, 4.0, 0.0]) - 5.0).abs() < 0.001);
}
#[test]
fn test_axon_length() {
let mut n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
n.axon = Axon::toward([3.0, 4.0, 0.0]);
assert!((n.axon_length() - 5.0).abs() < 0.001);
}
#[test]
fn test_axon_alive() {
let mut n = SpatialNeuron::default();
assert!(n.axon_alive());
n.axon.health = 0;
assert!(!n.axon_alive());
}
#[test]
fn test_migrate() {
let mut n = SpatialNeuron::pyramidal_at([5.0, 5.0, 5.0]);
n.axon = Axon::toward([7.0, 5.0, 5.0]);
n.migrate([1.0, 0.0, 0.0]);
assert_eq!(n.soma.position[0], 6.0);
assert!((n.axon.terminal[0] - 7.8).abs() < 0.001);
}
#[test]
fn test_refractory_never_spiked() {
let n = SpatialNeuron::default();
assert!(!n.in_refractory(0));
assert!(!n.in_refractory(100));
}
#[test]
fn test_refractory_after_spike() {
let mut n = SpatialNeuron::default();
n.last_spike_us = 1000;
assert!(n.in_refractory(1000));
assert!(n.in_refractory(1000 + n.nuclei.refractory as u64 - 1));
assert!(!n.in_refractory(1000 + n.nuclei.refractory as u64));
}
#[test]
fn test_oscillator_ramp() {
let mut osc = SpatialNeuron::at([0.0, 0.0, 0.0], Nuclei::oscillator(10_000));
osc.membrane = SpatialNeuron::RESET_POTENTIAL;
osc.last_update_us = 0;
osc.oscillator_ramp(5_000);
assert!(osc.membrane > SpatialNeuron::RESET_POTENTIAL);
assert!(osc.membrane < SpatialNeuron::DEFAULT_THRESHOLD);
}
#[test]
fn test_non_oscillator_no_ramp() {
let mut n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
n.membrane = SpatialNeuron::RESET_POTENTIAL;
let initial = n.membrane;
n.oscillator_ramp(5_000);
assert_eq!(n.membrane, initial); }
#[test]
fn test_leak_no_time_change() {
let mut n = SpatialNeuron::default();
n.membrane = -4000;
n.last_update_us = 100;
n.apply_leak(100);
assert_eq!(n.membrane, -4000);
n.apply_leak(50);
assert_eq!(n.membrane, -4000);
}
#[test]
fn test_above_threshold() {
let mut n = SpatialNeuron::default();
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD - 1;
assert!(!n.above_threshold());
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD;
assert!(n.above_threshold());
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 1;
assert!(n.above_threshold());
}
#[test]
fn test_new_with_anatomy() {
let soma = Soma::at([1.0, 2.0, 3.0]);
let dendrite = Dendrite::new(2.0, 200);
let axon = Axon::myelinated([5.0, 5.0, 5.0], 200);
let nuclei = Nuclei::pyramidal();
let n = SpatialNeuron::new(soma, dendrite, axon, nuclei);
assert_eq!(n.soma.position, [1.0, 2.0, 3.0]);
assert_eq!(n.dendrite.radius, 2.0);
assert_eq!(n.dendrite.spine_count, 200);
assert_eq!(n.axon.myelin, 200);
assert!(n.nuclei.is_excitatory());
}
#[test]
fn test_stamina_starts_full() {
let n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
assert_eq!(n.stamina, 255);
}
#[test]
fn test_stamina_drains_on_fire() {
let mut n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
assert_eq!(n.stamina, 255);
n.fire(1000);
assert_eq!(n.stamina, 245);
}
#[test]
fn test_stamina_depletion_blocks_firing() {
let mut n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
n.stamina = 0;
assert!(n.above_threshold());
assert!(!n.can_fire(0));
}
#[test]
fn test_stamina_no_recovery_in_leak() {
let mut n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
n.stamina = 100;
n.last_update_us = 0;
n.apply_leak(10_000);
assert_eq!(n.stamina, 100);
}
#[test]
fn test_rapid_firing_depletes_stamina() {
let mut n = SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0]);
for i in 0..25 {
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
n.fire(i * 100);
}
assert_eq!(n.stamina, 5);
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
n.fire(2600);
assert_eq!(n.stamina, 0);
n.membrane = SpatialNeuron::DEFAULT_THRESHOLD + 100;
assert!(!n.can_fire(3000));
}
}