Skip to main content

sc_neurocore_engine/bindings/
jansen_rit.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 — Jansen–Rit PyO3 scalar and batch binding
8
9//! Python boundary for the equation-(6) explicit-Euler contract.
10
11use numpy::{IntoPyArray, PyReadonlyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::neurons::JansenRitUnit;
17
18/// Register the scalar class and batch function without growing `lib.rs`.
19pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20    module.add_class::<PyJansenRitUnit>()?;
21    module.add_function(wrap_pyfunction!(py_jansen_rit_simulate, module)?)?;
22    Ok(())
23}
24
25#[pyclass(
26    name = "JansenRitUnit",
27    module = "sc_neurocore_engine.sc_neurocore_engine"
28)]
29#[derive(Clone)]
30pub struct PyJansenRitUnit {
31    inner: JansenRitUnit,
32}
33
34#[pymethods]
35impl PyJansenRitUnit {
36    #[new]
37    #[pyo3(signature = (
38        y0=0.0, y3=0.0, y1=0.0, y4=0.0, y2=0.0, y5=0.0,
39        a_exc=3.25, b_exc=22.0, a_rate=100.0, b_rate=50.0,
40        c=135.0, e0=2.5, v0=6.0, r=0.56, dt=0.0001,
41    ))]
42    #[allow(clippy::too_many_arguments)]
43    fn new(
44        y0: f64,
45        y3: f64,
46        y1: f64,
47        y4: f64,
48        y2: f64,
49        y5: f64,
50        a_exc: f64,
51        b_exc: f64,
52        a_rate: f64,
53        b_rate: f64,
54        c: f64,
55        e0: f64,
56        v0: f64,
57        r: f64,
58        dt: f64,
59    ) -> PyResult<Self> {
60        let inner = JansenRitUnit::with_parameters(
61            y0, y3, y1, y4, y2, y5, a_exc, b_exc, a_rate, b_rate, c, e0, v0, r, dt,
62        )
63        .map_err(PyValueError::new_err)?;
64        Ok(Self { inner })
65    }
66
67    #[pyo3(signature = (p_ext=220.0))]
68    fn step(&mut self, p_ext: f64) -> PyResult<f64> {
69        self.inner.step(p_ext).map_err(PyValueError::new_err)
70    }
71
72    fn reset(&mut self) {
73        self.inner.reset();
74    }
75
76    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
77        let mapping = PyDict::new(py);
78        mapping.set_item("y0", self.inner.y[0])?;
79        mapping.set_item("y3", self.inner.y[3])?;
80        mapping.set_item("y1", self.inner.y[1])?;
81        mapping.set_item("y4", self.inner.y[4])?;
82        mapping.set_item("y2", self.inner.y[2])?;
83        mapping.set_item("y5", self.inner.y[5])?;
84        mapping.set_item("y", self.inner.y.to_vec())?;
85        Ok(mapping.into_any().unbind())
86    }
87}
88
89/// Simulate a complete Jansen–Rit batch from caller-owned external drive.
90#[pyfunction]
91#[pyo3(signature = (
92    y0_init, y3_init, y1_init, y4_init, y2_init, y5_init,
93    a_exc, b_exc, a_rate, b_rate, c, e0, v0, r, dt, p_ext,
94))]
95#[allow(clippy::too_many_arguments)]
96fn py_jansen_rit_simulate<'py>(
97    py: Python<'py>,
98    y0_init: f64,
99    y3_init: f64,
100    y1_init: f64,
101    y4_init: f64,
102    y2_init: f64,
103    y5_init: f64,
104    a_exc: f64,
105    b_exc: f64,
106    a_rate: f64,
107    b_rate: f64,
108    c: f64,
109    e0: f64,
110    v0: f64,
111    r: f64,
112    dt: f64,
113    p_ext: PyReadonlyArray1<'py, f64>,
114) -> PyResult<Py<PyAny>> {
115    let result = crate::neurons::jansen_rit::simulate(
116        y0_init,
117        y3_init,
118        y1_init,
119        y4_init,
120        y2_init,
121        y5_init,
122        a_exc,
123        b_exc,
124        a_rate,
125        b_rate,
126        c,
127        e0,
128        v0,
129        r,
130        dt,
131        p_ext.as_slice()?,
132    )
133    .map_err(PyValueError::new_err)?;
134    let mapping = PyDict::new(py);
135    mapping.set_item("y0", result.y0.into_pyarray(py))?;
136    mapping.set_item("y3", result.y3.into_pyarray(py))?;
137    mapping.set_item("y1", result.y1.into_pyarray(py))?;
138    mapping.set_item("y4", result.y4.into_pyarray(py))?;
139    mapping.set_item("y2", result.y2.into_pyarray(py))?;
140    mapping.set_item("y5", result.y5.into_pyarray(py))?;
141    mapping.set_item("eeg", result.eeg.into_pyarray(py))?;
142    mapping.set_item("y0_final", result.final_state[0])?;
143    mapping.set_item("y3_final", result.final_state[3])?;
144    mapping.set_item("y1_final", result.final_state[1])?;
145    mapping.set_item("y4_final", result.final_state[4])?;
146    mapping.set_item("y2_final", result.final_state[2])?;
147    mapping.set_item("y5_final", result.final_state[5])?;
148    Ok(mapping.into_any().unbind())
149}
150
151#[cfg(test)]
152mod tests {
153    #[test]
154    fn engine_batch_rejects_invalid_drive_without_partial_result() {
155        let result = crate::neurons::jansen_rit::simulate(
156            0.0,
157            0.0,
158            0.0,
159            0.0,
160            0.0,
161            0.0,
162            3.25,
163            22.0,
164            100.0,
165            50.0,
166            135.0,
167            2.5,
168            6.0,
169            0.56,
170            0.0001,
171            &[120.0, f64::NAN],
172        );
173        assert!(result.is_err());
174    }
175}