Skip to main content

sc_neurocore_engine/bindings/
adaptive_threshold_if.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 — Adaptive-threshold PyO3 exact-relaxation 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::trivial::adaptive_threshold_if::{self, AdaptiveThresholdIFError};
16
17py_neuron_default!("AdaptiveThresholdIFNeuron", PyAdaptiveThresholdIFNeuron, neurons::AdaptiveThresholdIFNeuron, state v, state theta);
18
19fn map_adaptive_threshold_if_error(error: AdaptiveThresholdIFError) -> PyErr {
20    match error {
21        AdaptiveThresholdIFError::NonFiniteCandidate => {
22            PyFloatingPointError::new_err(error.to_string())
23        }
24        _ => PyValueError::new_err(error.to_string()),
25    }
26}
27
28/// Register the adaptive-threshold class and configured exact-relaxation batch function.
29pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
30    module.add_class::<PyAdaptiveThresholdIFNeuron>()?;
31    module.add_function(wrap_pyfunction!(py_adaptive_threshold_if_simulate, module)?)?;
32    Ok(())
33}
34
35/// Simulate one complete piecewise-constant current batch.
36#[pyfunction]
37#[pyo3(signature = (v, theta, v_rest, v_reset, theta_rest, delta_theta, tau_m, tau_theta, dt, current))]
38#[allow(clippy::too_many_arguments)]
39fn py_adaptive_threshold_if_simulate<'py>(
40    py: Python<'py>,
41    v: f64,
42    theta: f64,
43    v_rest: f64,
44    v_reset: f64,
45    theta_rest: f64,
46    delta_theta: f64,
47    tau_m: f64,
48    tau_theta: f64,
49    dt: f64,
50    current: PyReadonlyArray1<'py, f64>,
51) -> PyResult<Py<PyAny>> {
52    let drive = current.as_slice()?;
53    let result = adaptive_threshold_if::simulate(
54        v,
55        theta,
56        v_rest,
57        v_reset,
58        theta_rest,
59        delta_theta,
60        tau_m,
61        tau_theta,
62        dt,
63        drive,
64    )
65    .map_err(map_adaptive_threshold_if_error)?;
66    let mapping = PyDict::new(py);
67    mapping.set_item("v", result.v.into_pyarray(py))?;
68    mapping.set_item("theta", result.theta.into_pyarray(py))?;
69    mapping.set_item("spikes", result.spikes.into_pyarray(py))?;
70    mapping.set_item("v_final", result.final_state[0])?;
71    mapping.set_item("theta_final", result.final_state[1])?;
72    mapping.set_item("spike_count", result.spike_count)?;
73    Ok(mapping.into_any().unbind())
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn configured_batch_rejects_nonfinite_drive_before_result_construction() {
82        let result = adaptive_threshold_if::simulate(
83            -65.0,
84            -50.0,
85            -65.0,
86            -65.0,
87            -50.0,
88            5.0,
89            10.0,
90            50.0,
91            0.1,
92            &[0.25, f64::NAN, 0.5],
93        );
94        assert!(matches!(
95            result,
96            Err(AdaptiveThresholdIFError::NonFiniteInput)
97        ));
98    }
99}