Skip to main content

sc_neurocore_engine/bindings/
threshold_linear_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 — Threshold-linear rate PyO3 simulation binding
8
9//! Python binding for the configurable memoryless threshold-linear contract.
10
11use numpy::{IntoPyArray, PyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::neurons;
17
18/// Register this binding through the neuron registry rather than the crate root.
19pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20    module.add_class::<PyThresholdLinearRateNeuron>()?;
21    module.add_function(wrap_pyfunction!(py_threshold_linear_rate_simulate, module)?)?;
22    Ok(())
23}
24
25// ThresholdLinearRateNeuron: step returns f64
26#[pyclass(
27    name = "ThresholdLinearRateNeuron",
28    module = "sc_neurocore_engine.sc_neurocore_engine"
29)]
30#[derive(Clone)]
31pub struct PyThresholdLinearRateNeuron {
32    inner: neurons::ThresholdLinearRateNeuron,
33}
34
35#[pymethods]
36impl PyThresholdLinearRateNeuron {
37    #[new]
38    #[pyo3(signature = (r=0.0, theta=0.0, gain=1.0))]
39    fn new(r: f64, theta: f64, gain: f64) -> PyResult<Self> {
40        Ok(Self {
41            inner: neurons::ThresholdLinearRateNeuron::with_parameters(r, theta, gain)
42                .map_err(PyValueError::new_err)?,
43        })
44    }
45    fn step(&mut self, current: f64) -> PyResult<f64> {
46        self.inner.try_step(current).map_err(PyValueError::new_err)
47    }
48    fn reset(&mut self) {
49        self.inner.reset();
50    }
51    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
52        let d = PyDict::new(py);
53        d.set_item("r", self.inner.r)?;
54        d.set_item("theta", self.inner.theta)?;
55        d.set_item("gain", self.inner.gain)?;
56        Ok(d.into_any().unbind())
57    }
58}
59
60fn simulate_threshold_linear_rate(
61    r: f64,
62    theta: f64,
63    gain: f64,
64    n_steps: usize,
65    current: f64,
66) -> Result<(Vec<f64>, f64), String> {
67    let mut neuron = crate::neurons::ThresholdLinearRateNeuron::with_parameters(r, theta, gain)?;
68    let mut trace = Vec::with_capacity(n_steps);
69    for _ in 0..n_steps {
70        trace.push(neuron.try_step(current)?);
71    }
72    Ok((trace, neuron.r))
73}
74
75/// Evaluate a constant-input threshold-linear rate trace.
76#[pyfunction]
77#[pyo3(signature = (r, theta, gain, n_steps, current))]
78fn py_threshold_linear_rate_simulate<'py>(
79    py: Python<'py>,
80    r: f64,
81    theta: f64,
82    gain: f64,
83    n_steps: usize,
84    current: f64,
85) -> PyResult<(Bound<'py, PyArray1<f64>>, f64)> {
86    let (trace, final_rate) = simulate_threshold_linear_rate(r, theta, gain, n_steps, current)
87        .map_err(PyValueError::new_err)?;
88    Ok((trace.into_pyarray(py), final_rate))
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn configured_batch_matches_piecewise_linear_golden() {
97        let (trace, final_rate) = simulate_threshold_linear_rate(0.25, 1.5, 2.0, 6, 3.0).unwrap();
98        assert_eq!(trace, vec![3.0; 6]);
99        assert_eq!(final_rate, 3.0);
100    }
101
102    #[test]
103    fn empty_batch_preserves_initial_rate() {
104        let (trace, final_rate) = simulate_threshold_linear_rate(0.25, 1.5, 2.0, 0, 3.0).unwrap();
105        assert!(trace.is_empty());
106        assert_eq!(final_rate, 0.25);
107    }
108
109    #[test]
110    fn batch_rejects_invalid_contracts() {
111        assert!(simulate_threshold_linear_rate(-0.1, 1.5, 2.0, 1, 3.0).is_err());
112        assert!(simulate_threshold_linear_rate(0.25, f64::NAN, 2.0, 1, 3.0).is_err());
113        assert!(simulate_threshold_linear_rate(0.25, 1.5, -2.0, 1, 3.0).is_err());
114        assert!(simulate_threshold_linear_rate(0.25, 1.5, 2.0, 1, f64::NAN).is_err());
115    }
116
117    #[test]
118    fn overflow_rejection_is_atomic() {
119        assert!(simulate_threshold_linear_rate(0.25, 0.0, 1.0e308, 1, 1.0e308).is_err());
120    }
121}