Skip to main content

sc_neurocore_engine/bindings/
resonate_and_fire.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 — Resonate-and-fire PyO3 exact-flow batch binding
8
9use numpy::{IntoPyArray, PyReadonlyArray1};
10use pyo3::exceptions::{PyFloatingPointError, PyValueError};
11use pyo3::prelude::*;
12use pyo3::types::PyDict;
13
14use crate::neurons;
15use crate::neurons::simple_spiking::resonate_and_fire::{self, ResonateAndFireError};
16
17py_neuron_default!("ResonateAndFireNeuron", PyResonateAndFireNeuron, neurons::ResonateAndFireNeuron, state x, state y);
18
19fn map_resonate_and_fire_error(error: ResonateAndFireError) -> PyErr {
20    match error {
21        ResonateAndFireError::NonFiniteCandidate => {
22            PyFloatingPointError::new_err(error.to_string())
23        }
24        _ => PyValueError::new_err(error.to_string()),
25    }
26}
27
28/// Register the resonate-and-fire class and exact-flow batch function.
29pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
30    module.add_class::<PyResonateAndFireNeuron>()?;
31    module.add_function(wrap_pyfunction!(py_resonate_and_fire_simulate, module)?)?;
32    Ok(())
33}
34
35/// Simulate one complete piecewise-constant current batch.
36#[pyfunction]
37#[pyo3(signature = (x, y, b, omega, threshold, dt, current))]
38fn py_resonate_and_fire_simulate<'py>(
39    py: Python<'py>,
40    x: f64,
41    y: f64,
42    b: f64,
43    omega: f64,
44    threshold: f64,
45    dt: f64,
46    current: PyReadonlyArray1<'py, f64>,
47) -> PyResult<Py<PyAny>> {
48    let drive = current.as_slice()?;
49    let result = resonate_and_fire::simulate(x, y, b, omega, threshold, dt, drive)
50        .map_err(map_resonate_and_fire_error)?;
51    let mapping = PyDict::new(py);
52    mapping.set_item("x", result.x.into_pyarray(py))?;
53    mapping.set_item("y", result.y.into_pyarray(py))?;
54    mapping.set_item("spikes", result.spikes.into_pyarray(py))?;
55    mapping.set_item("x_final", result.final_state[0])?;
56    mapping.set_item("y_final", result.final_state[1])?;
57    mapping.set_item("spike_count", result.spike_count)?;
58    Ok(mapping.into_any().unbind())
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn configured_batch_rejects_nonfinite_drive_before_result_construction() {
67        let result =
68            resonate_and_fire::simulate(0.0, 0.0, -1.0, 10.0, 1.0, 0.01, &[0.25, f64::NAN, 0.5]);
69        assert!(matches!(result, Err(ResonateAndFireError::NonFiniteInput)));
70    }
71}