sc_neurocore_engine/neuron/
homeostatic_lif.rs1#[derive(Clone, Debug)]
14pub struct HomeostaticLif {
15 pub v: f64,
16 pub v_threshold: f64,
17 pub v_rest: f64,
18 pub v_reset: f64,
19 pub rate_trace: f64,
20 pub target_rate: f64,
21 pub adaptation_rate: f64,
22 pub trace_decay: f64,
23 initial_threshold: f64,
24}
25
26impl HomeostaticLif {
27 pub fn new(target_rate: f64, adaptation_rate: f64, trace_decay: f64) -> Self {
28 Self {
29 v: 0.0,
30 v_threshold: 1.0,
31 v_rest: 0.0,
32 v_reset: 0.0,
33 rate_trace: 0.0,
34 target_rate,
35 adaptation_rate,
36 trace_decay,
37 initial_threshold: 1.0,
38 }
39 }
40
41 pub fn with_defaults() -> Self {
42 Self::new(0.1, 0.01, 0.95)
43 }
44
45 pub fn step(&mut self, current: f64) -> i32 {
47 let tau = 20.0;
48 self.v += (-(self.v - self.v_rest) + current) / tau;
49
50 let spike = if self.v >= self.v_threshold {
51 self.v = self.v_reset;
52 1
53 } else {
54 0
55 };
56
57 self.rate_trace =
58 self.rate_trace * self.trace_decay + spike as f64 * (1.0 - self.trace_decay);
59 let error = self.rate_trace - self.target_rate;
60 self.v_threshold += self.adaptation_rate * error;
61 self.v_threshold = self.v_threshold.clamp(0.1, self.initial_threshold * 10.0);
62
63 spike
64 }
65
66 pub fn reset(&mut self) {
67 self.v = self.v_rest;
68 self.rate_trace = 0.0;
69 self.v_threshold = self.initial_threshold;
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::HomeostaticLif;
76
77 #[test]
78 fn strong_input_produces_spikes() {
79 let mut neuron = HomeostaticLif::with_defaults();
80 let spikes: i32 = (0..200).map(|_| neuron.step(25.0)).sum();
81 assert!(spikes > 0, "must fire with strong input");
82 }
83
84 #[test]
85 fn repeated_spiking_adapts_threshold() {
86 let mut neuron = HomeostaticLif::with_defaults();
87 let initial = neuron.v_threshold;
88 for _ in 0..500 {
89 neuron.step(25.0);
90 }
91 assert!(
92 (neuron.v_threshold - initial).abs() > 1e-6,
93 "threshold must adapt"
94 );
95 }
96
97 #[test]
98 fn zero_input_remains_silent() {
99 let mut neuron = HomeostaticLif::with_defaults();
100 let spikes: i32 = (0..100).map(|_| neuron.step(0.0)).sum();
101 assert_eq!(spikes, 0);
102 }
103
104 #[test]
105 fn adaptive_threshold_remains_bounded() {
106 let mut neuron = HomeostaticLif::with_defaults();
107 for _ in 0..10_000 {
108 neuron.step(50.0);
109 }
110 assert!((0.1..=10.0).contains(&neuron.v_threshold));
111 }
112}