Skip to main content

sc_neurocore_engine/bindings/
lapicque_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 — Lapicque neuron PyO3 binding
8
9use numpy::{IntoPyArray, PyArray1};
10use pyo3::exceptions::{PyFloatingPointError, PyValueError};
11use pyo3::prelude::*;
12use pyo3::types::PyDict;
13
14use crate::neuron;
15
16#[pyclass(
17    name = "LapicqueNeuron",
18    module = "sc_neurocore_engine.sc_neurocore_engine"
19)]
20#[derive(Clone)]
21pub struct PyLapicqueNeuron {
22    inner: neuron::LapicqueNeuron,
23}
24
25#[pymethods]
26impl PyLapicqueNeuron {
27    #[new]
28    #[pyo3(signature = (tau=20.0, resistance=1.0, threshold=1.0, dt=1.0))]
29    fn new(tau: f64, resistance: f64, threshold: f64, dt: f64) -> PyResult<Self> {
30        let inner = neuron::LapicqueNeuron::new(tau, resistance, threshold, dt);
31        if !inner.valid() {
32            return Err(PyValueError::new_err(
33                "Lapicque SC compatibility parameters violate the finite positive-RC contract",
34            ));
35        }
36        Ok(Self { inner })
37    }
38    fn step(&mut self, current: f64) -> PyResult<i32> {
39        self.inner
40            .try_step(current)
41            .map_err(|error| PyFloatingPointError::new_err(error.to_string()))
42    }
43    fn reset(&mut self) {
44        self.inner.reset();
45    }
46    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
47        let d = PyDict::new(py);
48        d.set_item("v", self.inner.v)?;
49        Ok(d.into_any().unbind())
50    }
51}
52
53/// Register the Lapicque neuron class.
54pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
55    module.add_class::<PyLapicqueNeuron>()?;
56    module.add_function(wrap_pyfunction!(lapicque_simulate_complete, module)?)?;
57    Ok(())
58}
59
60type LapicqueCompletePacket<'py> = (
61    Bound<'py, PyArray1<f64>>,
62    Bound<'py, PyArray1<u8>>,
63    f64,
64    bool,
65);
66
67/// Execute either the source or preserved SC profile in one checked boundary.
68#[pyfunction]
69#[pyo3(signature = (
70    v, v_rest, v_reset, v_threshold, tau, resistance, dt, capacitance,
71    series_resistance, polarization_resistance, excited, source_profile,
72    n_steps, drive
73))]
74#[allow(clippy::too_many_arguments)]
75fn lapicque_simulate_complete<'py>(
76    py: Python<'py>,
77    v: f64,
78    v_rest: f64,
79    v_reset: f64,
80    v_threshold: f64,
81    tau: f64,
82    resistance: f64,
83    dt: f64,
84    capacitance: f64,
85    series_resistance: f64,
86    polarization_resistance: f64,
87    excited: bool,
88    source_profile: bool,
89    n_steps: usize,
90    drive: f64,
91) -> PyResult<LapicqueCompletePacket<'py>> {
92    let model = neuron::LapicqueNeuron {
93        v,
94        v_rest,
95        v_reset,
96        v_threshold,
97        tau,
98        resistance,
99        dt,
100        capacitance,
101        series_resistance,
102        polarization_resistance,
103        excited,
104        source_profile,
105    };
106    let (voltage, events, final_v, final_excited) =
107        model.simulate_complete(n_steps, drive).map_err(|error| {
108            PyFloatingPointError::new_err(format!("Lapicque batch rejected: {error}"))
109        })?;
110    Ok((
111        voltage.into_pyarray(py),
112        events.into_pyarray(py),
113        final_v,
114        final_excited,
115    ))
116}