sc_neurocore_engine/bindings/
threshold_linear_rate.rs1use numpy::{IntoPyArray, PyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::neurons;
17
18pub(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#[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#[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}