Skip to main content

sc_neurocore_engine/neuron/
lapicque.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 — Lapicque 1907 polarization + preserved SC hard-reset LIF
8
9/// Profile-explicit Lapicque state used by the production engine.
10#[derive(Clone, Debug)]
11pub struct LapicqueNeuron {
12    pub v: f64,
13    pub v_rest: f64,
14    pub v_reset: f64,
15    pub v_threshold: f64,
16    pub tau: f64,
17    pub resistance: f64,
18    pub dt: f64,
19    pub capacitance: f64,
20    pub series_resistance: f64,
21    pub polarization_resistance: f64,
22    pub excited: bool,
23    pub source_profile: bool,
24}
25
26/// Complete failure-atomic Lapicque batch result.
27pub type LapicqueCompleteTrace = (Vec<f64>, Vec<u8>, f64, bool);
28
29impl LapicqueNeuron {
30    /// Construct the preserved SC exact-flow, hard-reset LIF profile.
31    pub fn new(tau: f64, resistance: f64, threshold: f64, dt: f64) -> Self {
32        Self {
33            v: 0.0,
34            v_rest: 0.0,
35            v_reset: 0.0,
36            v_threshold: threshold,
37            tau,
38            resistance,
39            dt,
40            capacitance: 1.1,
41            series_resistance: 10.0,
42            polarization_resistance: 1.0,
43            excited: false,
44            source_profile: false,
45        }
46    }
47
48    /// Construct the normalized, one-shot Lapicque 1907 source profile.
49    pub fn lapicque_1907() -> Self {
50        let mut state = Self::new(20.0, 1.0, 1.0, 0.01);
51        state.source_profile = true;
52        state
53    }
54
55    /// Return whether all configuration and dynamic invariants hold.
56    pub fn valid(&self) -> bool {
57        if !self.v.is_finite()
58            || !self.v_threshold.is_finite()
59            || self.v_threshold <= 0.0
60            || !self.dt.is_finite()
61            || self.dt <= 0.0
62        {
63            return false;
64        }
65        if self.source_profile {
66            return (self.excited || self.v < self.v_threshold)
67                && self.capacitance.is_finite()
68                && self.capacitance > 0.0
69                && self.series_resistance.is_finite()
70                && self.series_resistance > 0.0
71                && self.polarization_resistance.is_finite()
72                && self.polarization_resistance > 0.0;
73        }
74        !self.excited
75            && self.v_rest.is_finite()
76            && self.v_reset.is_finite()
77            && self.v_threshold > self.v_rest
78            && self.v_threshold > self.v_reset
79            && self.v < self.v_threshold
80            && self.tau.is_finite()
81            && self.tau > 0.0
82            && self.resistance.is_finite()
83            && self.resistance > 0.0
84    }
85
86    /// Advance one exact constant-drive step without conflating errors with silence.
87    pub fn try_step(&mut self, drive: f64) -> Result<i32, &'static str> {
88        if !drive.is_finite() {
89            return Err("Lapicque drive must be finite");
90        }
91        if !self.valid() {
92            return Err("Lapicque state violates its profile contract");
93        }
94
95        let (v_inf, decay) = if self.source_profile {
96            let total_resistance = self.series_resistance + self.polarization_resistance;
97            let beta = self.capacitance * self.series_resistance * self.polarization_resistance
98                / total_resistance;
99            (
100                drive * self.polarization_resistance / total_resistance,
101                (-self.dt / beta).exp(),
102            )
103        } else {
104            (
105                self.v_rest + self.resistance * drive,
106                (-self.dt / self.tau).exp(),
107            )
108        };
109        let next_v = v_inf + (self.v - v_inf) * decay;
110        if !v_inf.is_finite() || !decay.is_finite() || !next_v.is_finite() {
111            return Err("Lapicque candidate must remain finite");
112        }
113
114        if self.source_profile {
115            let event = !self.excited && next_v >= self.v_threshold;
116            self.v = next_v;
117            if event {
118                self.excited = true;
119                return Ok(1);
120            }
121            return Ok(0);
122        }
123
124        if next_v >= self.v_threshold {
125            self.v = self.v_reset;
126            Ok(1)
127        } else {
128            self.v = next_v;
129            Ok(0)
130        }
131    }
132
133    /// Compatibility dispatch for NetworkRunner's uniform non-throwing trait.
134    pub fn step(&mut self, drive: f64) -> i32 {
135        self.try_step(drive).unwrap_or(0)
136    }
137
138    /// Execute a failure-atomic complete batch against a cloned candidate.
139    pub fn simulate_complete(
140        &self,
141        n_steps: usize,
142        drive: f64,
143    ) -> Result<LapicqueCompleteTrace, &'static str> {
144        if !drive.is_finite() || !self.valid() {
145            return Err("invalid Lapicque batch contract");
146        }
147        let mut candidate = self.clone();
148        let mut voltage = Vec::with_capacity(n_steps);
149        let mut events = Vec::with_capacity(n_steps);
150        for _ in 0..n_steps {
151            let event = candidate.try_step(drive)?;
152            voltage.push(candidate.v);
153            events.push(event as u8);
154        }
155        Ok((voltage, events, candidate.v, candidate.excited))
156    }
157
158    /// Re-arm a source experiment or restore the SC membrane to rest.
159    pub fn reset(&mut self) {
160        self.v = if self.source_profile {
161            0.0
162        } else {
163            self.v_rest
164        };
165        self.excited = false;
166    }
167}
168
169#[cfg(test)]
170#[path = "lapicque_tests.rs"]
171mod tests;