sc_neurocore_engine/bindings/
alpha.rs1use 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
26pub(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#[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#[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}