Skip to main content

sc_neurocore_engine/bindings/
ermentrout_kopell_pop.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 — Montbrió population PyO3 scalar and batch binding
8
9use 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
27/// Register the scalar class and batch function outside the crate root.
28pub(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    /// Construct and validate a complete scalar MPR configuration.
46    #[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    /// Apply one atomic simultaneous Euler step and return the new rate.
67    #[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    /// Restore both dynamic states while preserving all parameters.
73    fn reset(&mut self) {
74        self.inner.reset();
75    }
76
77    /// Return the two current dynamic states as a Python mapping.
78    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/// Simulate one complete MPR external-drive batch.
87#[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}