Skip to main content

sc_neurocore_engine/bindings/
alpha.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 — Alpha-synapse PyO3 exact-flow batch binding
8
9use numpy::{IntoPyArray, PyReadonlyArray1};
10use pyo3::exceptions::{PyFloatingPointError, PyValueError};
11use pyo3::prelude::*;
12use pyo3::types::PyDict;
13
14use crate::neurons::{
15    self,
16    simple_spiking::alpha::{self, AlphaError},
17};
18
19fn map_alpha_error(error: AlphaError) -> PyErr {
20    match error {
21        AlphaError::NonFiniteCandidate => PyFloatingPointError::new_err(error.to_string()),
22        _ => PyValueError::new_err(error.to_string()),
23    }
24}
25
26/// Register the configured exact-flow batch function.
27pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
28    module.add_class::<PyAlphaNeuron>()?;
29    module.add_function(wrap_pyfunction!(py_alpha_simulate, module)?)?;
30    Ok(())
31}
32
33// AlphaNeuron: step(exc, inh)
34#[pyclass(
35    name = "AlphaNeuron",
36    module = "sc_neurocore_engine.sc_neurocore_engine"
37)]
38#[derive(Clone)]
39pub struct PyAlphaNeuron {
40    inner: neurons::AlphaNeuron,
41}
42
43#[pymethods]
44impl PyAlphaNeuron {
45    #[new]
46    fn new() -> Self {
47        Self {
48            inner: neurons::AlphaNeuron::new(),
49        }
50    }
51    #[pyo3(signature = (exc_current, inh_current=0.0))]
52    fn step(&mut self, exc_current: f64, inh_current: f64) -> i32 {
53        self.inner.step(exc_current, inh_current)
54    }
55    fn reset(&mut self) {
56        self.inner.reset();
57    }
58    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
59        let d = PyDict::new(py);
60        d.set_item("v", self.inner.v)?;
61        d.set_item("i_exc", self.inner.i_exc)?;
62        d.set_item("i_inh", self.inner.i_inh)?;
63        Ok(d.into_any().unbind())
64    }
65}
66
67/// Simulate one complete piecewise-constant excitatory/inhibitory drive batch.
68#[pyfunction]
69#[pyo3(signature = (v, a_exc, i_exc, a_inh, i_inh, v_rest, v_threshold, tau_v, tau_exc, tau_inh, dt, exc_current, inh_current))]
70#[allow(clippy::too_many_arguments)]
71fn py_alpha_simulate<'py>(
72    py: Python<'py>,
73    v: f64,
74    a_exc: f64,
75    i_exc: f64,
76    a_inh: f64,
77    i_inh: f64,
78    v_rest: f64,
79    v_threshold: f64,
80    tau_v: f64,
81    tau_exc: f64,
82    tau_inh: f64,
83    dt: f64,
84    exc_current: PyReadonlyArray1<'py, f64>,
85    inh_current: PyReadonlyArray1<'py, f64>,
86) -> PyResult<Py<PyAny>> {
87    let exc_drive = exc_current.as_slice()?;
88    let inh_drive = inh_current.as_slice()?;
89    let result = alpha::simulate(
90        v,
91        a_exc,
92        i_exc,
93        a_inh,
94        i_inh,
95        v_rest,
96        v_threshold,
97        tau_v,
98        tau_exc,
99        tau_inh,
100        dt,
101        exc_drive,
102        inh_drive,
103    )
104    .map_err(map_alpha_error)?;
105    let mapping = PyDict::new(py);
106    mapping.set_item("v", result.v.into_pyarray(py))?;
107    mapping.set_item("a_exc", result.a_exc.into_pyarray(py))?;
108    mapping.set_item("i_exc", result.i_exc.into_pyarray(py))?;
109    mapping.set_item("a_inh", result.a_inh.into_pyarray(py))?;
110    mapping.set_item("i_inh", result.i_inh.into_pyarray(py))?;
111    mapping.set_item("spikes", result.spikes.into_pyarray(py))?;
112    mapping.set_item("v_final", result.final_state[0])?;
113    mapping.set_item("a_exc_final", result.final_state[1])?;
114    mapping.set_item("i_exc_final", result.final_state[2])?;
115    mapping.set_item("a_inh_final", result.final_state[3])?;
116    mapping.set_item("i_inh_final", result.final_state[4])?;
117    mapping.set_item("spike_count", result.spike_count)?;
118    Ok(mapping.into_any().unbind())
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn configured_batch_rejects_nonfinite_drive_before_result_construction() {
127        let result = alpha::simulate(
128            0.0,
129            0.0,
130            0.0,
131            0.0,
132            0.0,
133            0.0,
134            1.0,
135            20.0,
136            5.0,
137            10.0,
138            1.0,
139            &[0.25, f64::NAN, 0.5],
140            &[0.1, 0.1, 0.1],
141        );
142        assert!(matches!(result, Err(AlphaError::NonFiniteInput)));
143    }
144
145    #[test]
146    fn configured_batch_rejects_mismatched_drive_lengths() {
147        let result = alpha::simulate(
148            0.0,
149            0.0,
150            0.0,
151            0.0,
152            0.0,
153            0.0,
154            1.0,
155            20.0,
156            5.0,
157            10.0,
158            1.0,
159            &[0.25, 0.5],
160            &[0.1],
161        );
162        assert!(matches!(result, Err(AlphaError::NonFiniteInput)));
163    }
164}