sc_neurocore_engine/neurons/hardware/
dpi_neuron.rs1pub type DpiCompleteTrace = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<u8>);
16
17#[derive(Clone, Debug)]
18pub struct DPINeuron {
19 pub i_mem: f64,
20 pub i_ahp: f64,
21 pub refractory_time: f64,
22 pub i_threshold: f64,
23 pub i_reset: f64,
24 pub i_rest: f64,
25 pub i_tau: f64,
26 pub i_g: f64,
27 pub i_tau_ahp: f64,
28 pub i_ga: f64,
29 pub i_spike: f64,
30 pub i_0: f64,
31 pub kappa: f64,
32 pub alpha: f64,
33 pub tau: f64,
34 pub tau_ahp: f64,
35 pub refractory_period: f64,
36 pub dt: f64,
37}
38
39impl DPINeuron {
40 pub fn new() -> Self {
41 Self {
42 i_mem: 0.01,
43 i_ahp: 0.01,
44 refractory_time: 0.0,
45 i_threshold: 1.0,
46 i_reset: 0.01,
47 i_rest: 0.1,
48 i_tau: 1.0,
49 i_g: 1.0,
50 i_tau_ahp: 0.1,
51 i_ga: 1.0,
52 i_spike: 5.0,
53 i_0: 0.01,
54 kappa: 0.7,
55 alpha: 10.0,
56 tau: 20.0,
57 tau_ahp: 100.0,
58 refractory_period: 2.0,
59 dt: 0.1,
60 }
61 }
62
63 fn valid(&self) -> bool {
64 self.i_mem.is_finite()
65 && self.i_mem > 0.0
66 && self.i_ahp.is_finite()
67 && self.i_ahp >= 0.0
68 && self.refractory_time.is_finite()
69 && self.refractory_time >= 0.0
70 && self.i_threshold.is_finite()
71 && self.i_threshold > 0.0
72 && self.i_reset.is_finite()
73 && self.i_reset > 0.0
74 && self.i_reset < self.i_threshold
75 && self.i_rest.is_finite()
76 && self.i_rest >= 0.0
77 && self.i_tau.is_finite()
78 && self.i_tau > 0.0
79 && self.i_g.is_finite()
80 && self.i_g > 0.0
81 && self.i_tau_ahp.is_finite()
82 && self.i_tau_ahp > 0.0
83 && self.i_ga.is_finite()
84 && self.i_ga > 0.0
85 && self.i_spike.is_finite()
86 && self.i_spike > 0.0
87 && self.i_0.is_finite()
88 && self.i_0 > 0.0
89 && self.kappa.is_finite()
90 && self.kappa > 0.0
91 && self.alpha.is_finite()
92 && self.alpha > 0.0
93 && self.tau.is_finite()
94 && self.tau > 0.0
95 && self.tau_ahp.is_finite()
96 && self.tau_ahp > 0.0
97 && self.refractory_period.is_finite()
98 && self.refractory_period > 0.0
99 && self.dt.is_finite()
100 && self.dt > 0.0
101 && self.refractory_period >= self.dt
102 }
103
104 fn sigmoid(value: f64) -> f64 {
105 if value >= 0.0 {
106 1.0 / (1.0 + (-value).exp())
107 } else {
108 let exponential = value.exp();
109 exponential / (1.0 + exponential)
110 }
111 }
112
113 fn feedback_current(&self) -> f64 {
114 let log_current = (self.i_0.ln() + self.kappa * self.i_mem.ln()) / (self.kappa + 1.0);
115 log_current.exp() * Self::sigmoid(self.alpha * (self.i_mem - self.i_threshold))
116 }
117
118 fn step_checked(&mut self, current: f64) -> Result<u8, &'static str> {
119 if !current.is_finite() || !self.valid() {
120 return Err("DPI state, parameters, and current must be physically valid");
121 }
122 let total_input = self.i_rest + current;
123 if !total_input.is_finite() || total_input < 0.0 {
124 return Err("DPI total input current must be finite and non-negative");
125 }
126
127 let spike_active = self.refractory_time > 0.0;
128 let spike_current = if spike_active { self.i_spike } else { 0.0 };
129 let d_i_ahp = self.i_ahp / (self.tau_ahp * self.i_tau_ahp)
130 * (spike_current / (1.0 + self.i_ahp / self.i_ga) - self.i_tau_ahp);
131 let next_i_ahp = self.i_ahp + self.dt * d_i_ahp;
132
133 let (next_i_mem, next_refractory, spiked) = if spike_active {
134 (
135 self.i_reset,
136 (self.refractory_time - self.dt).max(0.0),
137 false,
138 )
139 } else {
140 let i_fb = self.feedback_current();
141 let d_i_mem = self.i_mem / (self.tau * self.i_tau)
142 * (total_input / (1.0 + self.i_mem / self.i_g) - self.i_tau + i_fb - self.i_ahp);
143 let candidate = self.i_mem + self.dt * d_i_mem;
144 if !candidate.is_finite() || candidate <= 0.0 {
145 return Err("DPI membrane Euler candidate left the physical current domain");
146 }
147 if candidate >= self.i_threshold {
148 (self.i_reset, self.refractory_period, true)
149 } else {
150 (candidate, 0.0, false)
151 }
152 };
153
154 if !next_i_mem.is_finite()
155 || !next_i_ahp.is_finite()
156 || !next_refractory.is_finite()
157 || next_i_mem <= 0.0
158 || next_i_ahp < 0.0
159 || next_refractory < 0.0
160 {
161 return Err("DPI Euler update left the physical current domain");
162 }
163
164 self.i_mem = next_i_mem;
165 self.i_ahp = next_i_ahp;
166 self.refractory_time = next_refractory;
167 Ok(u8::from(spiked))
168 }
169
170 pub fn step(&mut self, current: f64) -> i32 {
175 i32::from(self.step_checked(current).unwrap_or(0))
176 }
177
178 pub fn simulate_complete(
180 &mut self,
181 n_steps: usize,
182 current: f64,
183 ) -> Result<DpiCompleteTrace, &'static str> {
184 if !current.is_finite() || !self.valid() {
185 return Err("DPI state, parameters, and current must be physically valid");
186 }
187 let total_input = self.i_rest + current;
188 if !total_input.is_finite() || total_input < 0.0 {
189 return Err("DPI total input current must be finite and non-negative");
190 }
191
192 let mut candidate = self.clone();
193 let mut i_mem_trace = Vec::with_capacity(n_steps);
194 let mut i_ahp_trace = Vec::with_capacity(n_steps);
195 let mut refractory_trace = Vec::with_capacity(n_steps);
196 let mut events = Vec::with_capacity(n_steps);
197 for _ in 0..n_steps {
198 let event = candidate.step_checked(current)?;
199 i_mem_trace.push(candidate.i_mem);
200 i_ahp_trace.push(candidate.i_ahp);
201 refractory_trace.push(candidate.refractory_time);
202 events.push(event);
203 }
204 *self = candidate;
205 Ok((i_mem_trace, i_ahp_trace, refractory_trace, events))
206 }
207
208 pub fn reset(&mut self) {
209 self.i_mem = self.i_reset;
210 self.i_ahp = self.i_0;
211 self.refractory_time = 0.0;
212 }
213}
214impl Default for DPINeuron {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220#[cfg(test)]
221#[path = "dpi_neuron_tests.rs"]
222mod tests;