Skip to main content

sc_neurocore_engine/neurons/hardware/
dpi_neuron.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — DPI Neuron Circuit Emulator
8
9/// DPI current-mode adaptive integrate-and-fire circuit.
10///
11/// Implements Indiveri, Stefanini & Chicca (2010), Eqs. (2)–(3). The
12/// normalised operating point, explicit-Euler macro-step, constant resting
13/// input, threshold event, and digital refractory scheduling are maintained
14/// numerical choices around the source circuit equations.
15pub 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    /// Advance one compatibility scalar step.
171    ///
172    /// Invalid state or arithmetic leaves the instance unchanged and returns
173    /// zero. Use [`Self::simulate_complete`] when rejection must be observable.
174    pub fn step(&mut self, current: f64) -> i32 {
175        i32::from(self.step_checked(current).unwrap_or(0))
176    }
177
178    /// Return aligned state and event traces and commit only a valid full run.
179    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;