sc_neurocore_engine/bindings/
ermentrout_kopell_pop.rs1use numpy::{IntoPyArray, PyReadonlyArray1};
10use pyo3::exceptions::{PyFloatingPointError, PyValueError};
11use pyo3::prelude::*;
12use pyo3::types::PyDict;
13
14use crate::neurons::ermentrout_kopell_pop::ErmentroutKopellPopulationError;
15use crate::neurons::ErmentroutKopellPopulation;
16
17fn map_mpr_error(error: ErmentroutKopellPopulationError) -> PyErr {
18 match error {
19 ErmentroutKopellPopulationError::NonFiniteCandidate
20 | ErmentroutKopellPopulationError::NegativeCandidateRate => {
21 PyFloatingPointError::new_err(error.to_string())
22 }
23 _ => PyValueError::new_err(error.to_string()),
24 }
25}
26
27pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
29 module.add_class::<PyErmentroutKopellPopulation>()?;
30 module.add_function(wrap_pyfunction!(py_ermentrout_kopell_pop_simulate, module)?)?;
31 Ok(())
32}
33
34#[pyclass(
35 name = "ErmentroutKopellPopulation",
36 module = "sc_neurocore_engine.sc_neurocore_engine"
37)]
38#[derive(Clone)]
39pub struct PyErmentroutKopellPopulation {
40 inner: ErmentroutKopellPopulation,
41}
42
43#[pymethods]
44impl PyErmentroutKopellPopulation {
45 #[new]
47 #[pyo3(signature = (
48 r=0.1, v=-2.0, tau=1.0, delta=1.0, eta_bar=-5.0,
49 coupling=15.0, dt=0.01,
50 ))]
51 fn new(
52 r: f64,
53 v: f64,
54 tau: f64,
55 delta: f64,
56 eta_bar: f64,
57 coupling: f64,
58 dt: f64,
59 ) -> PyResult<Self> {
60 let inner =
61 ErmentroutKopellPopulation::with_parameters(r, v, tau, delta, eta_bar, coupling, dt)
62 .map_err(map_mpr_error)?;
63 Ok(Self { inner })
64 }
65
66 #[pyo3(signature = (ext_input=0.0))]
68 fn step(&mut self, ext_input: f64) -> PyResult<f64> {
69 self.inner.try_step(ext_input).map_err(map_mpr_error)
70 }
71
72 fn reset(&mut self) {
74 self.inner.reset();
75 }
76
77 fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
79 let mapping = PyDict::new(py);
80 mapping.set_item("r", self.inner.r)?;
81 mapping.set_item("v", self.inner.v)?;
82 Ok(mapping.into_any().unbind())
83 }
84}
85
86#[pyfunction]
88#[pyo3(signature = (r, v, tau, delta, eta_bar, coupling, dt, ext_input))]
89#[expect(
90 clippy::too_many_arguments,
91 reason = "Python extension parity surface carries the complete configuration"
92)]
93fn py_ermentrout_kopell_pop_simulate<'py>(
94 py: Python<'py>,
95 r: f64,
96 v: f64,
97 tau: f64,
98 delta: f64,
99 eta_bar: f64,
100 coupling: f64,
101 dt: f64,
102 ext_input: PyReadonlyArray1<'py, f64>,
103) -> PyResult<Py<PyAny>> {
104 let result = crate::neurons::ermentrout_kopell_pop::simulate(
105 r,
106 v,
107 tau,
108 delta,
109 eta_bar,
110 coupling,
111 dt,
112 ext_input.as_slice()?,
113 )
114 .map_err(map_mpr_error)?;
115 let mapping = PyDict::new(py);
116 mapping.set_item("r", result.r.into_pyarray(py))?;
117 mapping.set_item("v", result.v.into_pyarray(py))?;
118 mapping.set_item("r_final", result.final_state[0])?;
119 mapping.set_item("v_final", result.final_state[1])?;
120 Ok(mapping.into_any().unbind())
121}
122
123#[cfg(test)]
124mod tests {
125 #[test]
126 fn engine_batch_rejects_nonfinite_drive_without_partial_result() {
127 let result = crate::neurons::ermentrout_kopell_pop::simulate(
128 0.1,
129 -2.0,
130 1.0,
131 1.0,
132 -5.0,
133 15.0,
134 0.01,
135 &[0.0, f64::NAN],
136 );
137 assert!(result.is_err());
138 }
139}