Skip to main content

sc_neurocore_engine/bindings/
exp_if.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 — Exponential integrate-and-fire PyO3 binding
8
9//! Python binding for the exponential integrate-and-fire neuron.
10
11use numpy::{IntoPyArray, PyArray1};
12use pyo3::exceptions::PyFloatingPointError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::neuron;
17
18/// Register the exponential integrate-and-fire neuron with the extension module.
19pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20    module.add_class::<PyExpIFNeuron>()?;
21    module.add_function(wrap_pyfunction!(expif_simulate_complete, module)?)?;
22    Ok(())
23}
24
25type CompleteTracePacket<'py> = (
26    Bound<'py, PyArray1<f64>>,
27    Bound<'py, PyArray1<f64>>,
28    Bound<'py, PyArray1<u8>>,
29    f64,
30    f64,
31);
32
33/// Run the full-parameter checked ExpIF recurrence across one Rust boundary.
34#[pyfunction]
35#[pyo3(signature = (
36    v, v_rest, v_reset, v_threshold, v_rh, delta_t, tau, dt,
37    refractory_period, refractory_remaining, source_profile, n_steps, current
38))]
39#[allow(clippy::too_many_arguments)]
40fn expif_simulate_complete<'py>(
41    py: Python<'py>,
42    v: f64,
43    v_rest: f64,
44    v_reset: f64,
45    v_threshold: f64,
46    v_rh: f64,
47    delta_t: f64,
48    tau: f64,
49    dt: f64,
50    refractory_period: f64,
51    refractory_remaining: f64,
52    source_profile: bool,
53    n_steps: usize,
54    current: f64,
55) -> PyResult<CompleteTracePacket<'py>> {
56    let mut model = neuron::ExpIfNeuron {
57        v,
58        v_rest,
59        v_reset,
60        v_threshold,
61        v_rh,
62        delta_t,
63        tau,
64        dt,
65        refractory_period,
66        refractory_remaining,
67        source_profile,
68        inv_delta_t: 1.0 / delta_t,
69        dt_div_tau: dt / tau,
70    };
71    let (voltage, refractory, events) =
72        model.simulate_complete(n_steps, current).map_err(|error| {
73            PyFloatingPointError::new_err(format!("ExpIF batch rejected: {error:?}"))
74        })?;
75    Ok((
76        voltage.into_pyarray(py),
77        refractory.into_pyarray(py),
78        events.into_pyarray(py),
79        model.v,
80        model.refractory_remaining,
81    ))
82}
83
84/// Register the historical mixed-case class alias.
85pub(crate) fn register_legacy_alias(module: &Bound<'_, PyModule>) -> PyResult<()> {
86    module.add_class::<PyExpIfNeuron>()?;
87    Ok(())
88}
89
90#[pyclass(
91    name = "ExpIFNeuron",
92    module = "sc_neurocore_engine.sc_neurocore_engine"
93)]
94#[derive(Clone)]
95pub struct PyExpIFNeuron {
96    inner: neuron::ExpIfNeuron,
97}
98
99#[pymethods]
100impl PyExpIFNeuron {
101    #[new]
102    fn new() -> Self {
103        Self {
104            inner: neuron::ExpIfNeuron::new(),
105        }
106    }
107
108    fn step(&mut self, current: f64) -> i32 {
109        self.inner.step(current)
110    }
111
112    fn reset(&mut self) {
113        self.inner.reset();
114    }
115
116    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
117        let d = PyDict::new(py);
118        d.set_item("v", self.inner.v)?;
119        d.set_item("refractory_remaining", self.inner.refractory_remaining)?;
120        Ok(d.into_any().unbind())
121    }
122}
123
124#[pyclass(
125    name = "ExpIfNeuron",
126    module = "sc_neurocore_engine.sc_neurocore_engine"
127)]
128#[derive(Clone)]
129pub struct PyExpIfNeuron {
130    inner: neuron::ExpIfNeuron,
131}
132
133#[pymethods]
134impl PyExpIfNeuron {
135    #[new]
136    fn new() -> Self {
137        Self {
138            inner: neuron::ExpIfNeuron::new(),
139        }
140    }
141    fn step(&mut self, current: f64) -> i32 {
142        self.inner.step(current)
143    }
144    fn reset(&mut self) {
145        self.inner.reset();
146    }
147    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
148        let d = PyDict::new(py);
149        d.set_item("v", self.inner.v)?;
150        d.set_item("refractory_remaining", self.inner.refractory_remaining)?;
151        Ok(d.into_any().unbind())
152    }
153}