Skip to main content

sc_neurocore_engine/bindings/
escape_rate.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 — EscapeRate PyO3 binding
8
9//! Python binding for the Gerstner 2000 stochastic-threshold cell.
10
11use numpy::{IntoPyArray, PyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::neurons;
17
18/// Register the EscapeRate simulator with the extension module.
19pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20    module.add_class::<PyEscapeRateNeuron>()?;
21    module.add_function(wrap_pyfunction!(py_escape_rate_simulate, module)?)?;
22    Ok(())
23}
24
25// EscapeRateNeuron needs seed
26#[pyclass(
27    name = "EscapeRateNeuron",
28    module = "sc_neurocore_engine.sc_neurocore_engine"
29)]
30#[derive(Clone)]
31pub struct PyEscapeRateNeuron {
32    inner: neurons::EscapeRateNeuron,
33}
34
35#[pymethods]
36impl PyEscapeRateNeuron {
37    #[new]
38    #[pyo3(signature = (seed=0xACE1))]
39    fn new(seed: u16) -> Self {
40        Self {
41            inner: neurons::EscapeRateNeuron::new(u64::from(seed)),
42        }
43    }
44    fn step(&mut self, current: f64) -> i32 {
45        self.inner.step(current)
46    }
47    fn reset(&mut self) {
48        self.inner.reset();
49    }
50    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
51        let d = PyDict::new(py);
52        d.set_item("v", self.inner.v)?;
53        d.set_item("rng_state", self.inner.rng_state)?;
54        d.set_item("initial_seed", self.inner.initial_seed)?;
55        Ok(d.into_any().unbind())
56    }
57}
58
59/// Full-contract seeded EscapeRate batch with the canonical LFSR16 trial.
60#[pyfunction]
61#[pyo3(signature = (
62    v0, v_rest, v_reset, v_threshold, tau_m, rho_0, delta_u, resistance,
63    dt, rng_state, n_steps, current
64))]
65#[allow(clippy::too_many_arguments, clippy::type_complexity)]
66fn py_escape_rate_simulate<'py>(
67    py: Python<'py>,
68    v0: f64,
69    v_rest: f64,
70    v_reset: f64,
71    v_threshold: f64,
72    tau_m: f64,
73    rho_0: f64,
74    delta_u: f64,
75    resistance: f64,
76    dt: f64,
77    rng_state: u16,
78    n_steps: usize,
79    current: f64,
80) -> PyResult<(
81    Bound<'py, PyArray1<f64>>,
82    Bound<'py, PyArray1<u8>>,
83    f64,
84    u16,
85)> {
86    let mut neuron = crate::neurons::EscapeRateNeuron {
87        v: v0,
88        v_rest,
89        v_reset,
90        v_threshold,
91        tau_m,
92        rho_0,
93        delta_u,
94        resistance,
95        dt,
96        rng_state,
97        initial_seed: rng_state,
98    };
99    if !neuron.valid() || !current.is_finite() {
100        return Err(PyValueError::new_err(
101            "invalid EscapeRate simulation state or input",
102        ));
103    }
104    let mut trace = Vec::with_capacity(n_steps);
105    let mut events = Vec::with_capacity(n_steps);
106    for _ in 0..n_steps {
107        let spike = neuron.try_step(current).map_err(PyValueError::new_err)?;
108        trace.push(neuron.v);
109        events.push(spike as u8);
110    }
111    Ok((
112        trace.into_pyarray(py),
113        events.into_pyarray(py),
114        neuron.v,
115        neuron.rng_state,
116    ))
117}