Skip to main content

sc_neurocore_engine/bindings/trivial/
mat.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 — source MAT* PyO3 binding
8
9use numpy::{IntoPyArray, PyReadonlyArray1};
10use pyo3::exceptions::PyValueError;
11use pyo3::prelude::*;
12use pyo3::types::PyDict;
13
14use crate::neurons;
15
16/// Python-owned complete source MAT* neuron.
17#[pyclass(name = "MATNeuron", module = "sc_neurocore_engine.sc_neurocore_engine")]
18#[derive(Clone)]
19pub struct PyMATNeuron {
20    inner: neurons::MATNeuron,
21}
22
23#[pymethods]
24impl PyMATNeuron {
25    /// Construct a configured MAT* neuron; omitted values select paper RS defaults.
26    #[new]
27    #[pyo3(signature = (
28        v=0.0, theta1=0.0, theta2=0.0, refractory_remaining=0.0,
29        omega=19.0, tau_m=5.0, tau_1=10.0, tau_2=200.0,
30        alpha_1=37.0, alpha_2=2.0, resistance=50.0,
31        refractory_period=2.0, dt=0.001
32    ))]
33    #[allow(clippy::too_many_arguments)]
34    fn new(
35        v: f64,
36        theta1: f64,
37        theta2: f64,
38        refractory_remaining: f64,
39        omega: f64,
40        tau_m: f64,
41        tau_1: f64,
42        tau_2: f64,
43        alpha_1: f64,
44        alpha_2: f64,
45        resistance: f64,
46        refractory_period: f64,
47        dt: f64,
48    ) -> PyResult<Self> {
49        let inner = neurons::MATNeuron {
50            v,
51            theta1,
52            theta2,
53            refractory_remaining,
54            omega,
55            tau_m,
56            tau_1,
57            tau_2,
58            alpha_1,
59            alpha_2,
60            resistance,
61            refractory_period,
62            dt,
63        };
64        if !inner.validate() {
65            return Err(PyValueError::new_err("invalid MAT state or configuration"));
66        }
67        Ok(Self { inner })
68    }
69
70    /// Advance one atomic source-model step.
71    fn step(&mut self, current: f64) -> PyResult<i32> {
72        self.inner.try_step(current).map_err(PyValueError::new_err)
73    }
74
75    /// Reset dynamic state while retaining the configured profile.
76    fn reset(&mut self) {
77        self.inner.reset();
78    }
79
80    /// Return all four dynamic state fields.
81    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
82        let state = PyDict::new(py);
83        state.set_item("v", self.inner.v)?;
84        state.set_item("theta1", self.inner.theta1)?;
85        state.set_item("theta2", self.inner.theta2)?;
86        state.set_item("refractory_remaining", self.inner.refractory_remaining)?;
87        Ok(state.into_any().unbind())
88    }
89}
90
91/// Simulate one configured source MAT* trace without Python per-step overhead.
92#[pyfunction]
93#[allow(clippy::too_many_arguments)]
94#[pyo3(signature = (
95    v, theta1, theta2, refractory_remaining, omega, tau_m, tau_1, tau_2,
96    alpha_1, alpha_2, resistance, refractory_period, dt, currents
97))]
98fn py_mat_simulate<'py>(
99    py: Python<'py>,
100    v: f64,
101    theta1: f64,
102    theta2: f64,
103    refractory_remaining: f64,
104    omega: f64,
105    tau_m: f64,
106    tau_1: f64,
107    tau_2: f64,
108    alpha_1: f64,
109    alpha_2: f64,
110    resistance: f64,
111    refractory_period: f64,
112    dt: f64,
113    currents: PyReadonlyArray1<'py, f64>,
114) -> PyResult<Py<PyAny>> {
115    let mut neuron = neurons::MATNeuron {
116        v,
117        theta1,
118        theta2,
119        refractory_remaining,
120        omega,
121        tau_m,
122        tau_1,
123        tau_2,
124        alpha_1,
125        alpha_2,
126        resistance,
127        refractory_period,
128        dt,
129    };
130    if !neuron.validate() {
131        return Err(PyValueError::new_err("invalid MAT state or configuration"));
132    }
133    let inputs = currents.as_slice()?;
134    let mut voltages = Vec::with_capacity(inputs.len());
135    let mut theta1_trace = Vec::with_capacity(inputs.len());
136    let mut theta2_trace = Vec::with_capacity(inputs.len());
137    let mut refractory_trace = Vec::with_capacity(inputs.len());
138    let mut events = Vec::with_capacity(inputs.len());
139    for &current in inputs {
140        events.push(neuron.try_step(current).map_err(PyValueError::new_err)?);
141        voltages.push(neuron.v);
142        theta1_trace.push(neuron.theta1);
143        theta2_trace.push(neuron.theta2);
144        refractory_trace.push(neuron.refractory_remaining);
145    }
146    let result = PyDict::new(py);
147    result.set_item("voltages", voltages.into_pyarray(py))?;
148    result.set_item("theta1", theta1_trace.into_pyarray(py))?;
149    result.set_item("theta2", theta2_trace.into_pyarray(py))?;
150    result.set_item("refractory", refractory_trace.into_pyarray(py))?;
151    result.set_item("events", events.into_pyarray(py))?;
152    result.set_item("v_final", neuron.v)?;
153    result.set_item("theta1_final", neuron.theta1)?;
154    result.set_item("theta2_final", neuron.theta2)?;
155    result.set_item("refractory_final", neuron.refractory_remaining)?;
156    Ok(result.into_any().unbind())
157}
158
159/// Register the source MAT* class and native batch simulator.
160pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
161    module.add_class::<PyMATNeuron>()?;
162    module.add_function(wrap_pyfunction!(py_mat_simulate, module)?)?;
163    Ok(())
164}