sc_neurocore_engine/bindings/
mcculloch_pitts.rs1use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
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::<PyMcCullochPittsNeuron>()?;
21 module.add_function(wrap_pyfunction!(py_mcculloch_pitts_evaluate_batch, module)?)?;
22 Ok(())
23}
24
25#[pyclass(
26 name = "McCullochPittsNeuron",
27 module = "sc_neurocore_engine.sc_neurocore_engine"
28)]
29#[derive(Clone)]
30pub struct PyMcCullochPittsNeuron {
31 inner: neurons::McCullochPittsNeuron,
32}
33
34fn mcculloch_pitts_count(value: f64, name: &str, minimum: i32) -> PyResult<i32> {
35 if !value.is_finite()
36 || value.fract() != 0.0
37 || value < f64::from(minimum)
38 || value > f64::from(i32::MAX)
39 {
40 return Err(PyValueError::new_err(format!(
41 "{name} must be an integer in [{minimum}, {}]",
42 i32::MAX
43 )));
44 }
45 Ok(value as i32)
46}
47
48#[pymethods]
49impl PyMcCullochPittsNeuron {
50 #[new]
51 #[pyo3(signature = (theta=1.0))]
52 fn new(theta: f64) -> PyResult<Self> {
53 let theta = mcculloch_pitts_count(theta, "theta", 1)?;
54 Ok(Self {
55 inner: neurons::McCullochPittsNeuron::new(theta).map_err(PyValueError::new_err)?,
56 })
57 }
58 #[pyo3(signature = (excitatory_count, inhibitory_active=false))]
59 fn step(&self, excitatory_count: f64, inhibitory_active: bool) -> PyResult<i32> {
60 let excitatory_count = mcculloch_pitts_count(excitatory_count, "excitatory_count", 0)?;
61 self.inner
62 .try_step(excitatory_count, inhibitory_active)
63 .map_err(PyValueError::new_err)
64 }
65 fn reset(&self) -> PyResult<()> {
66 self.inner.validate().map_err(PyValueError::new_err)
67 }
68 fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
69 Ok(PyDict::new(py).into_any().unbind())
70 }
71}
72
73#[pyfunction]
75fn py_mcculloch_pitts_evaluate_batch<'py>(
76 py: Python<'py>,
77 theta: i64,
78 excitatory_counts: PyReadonlyArray1<'py, i64>,
79 inhibitory_flags: PyReadonlyArray1<'py, u8>,
80) -> PyResult<(Bound<'py, PyArray1<u8>>, i64)> {
81 let theta = i32::try_from(theta)
82 .map_err(|_| PyValueError::new_err("theta must be in signed 32-bit range"))?;
83 let neuron = crate::neurons::McCullochPittsNeuron::new(theta).map_err(PyValueError::new_err)?;
84 let counts = excitatory_counts.as_slice()?;
85 let flags = inhibitory_flags.as_slice()?;
86 if counts.len() != flags.len() {
87 return Err(PyValueError::new_err(
88 "inhibitory_flags must match excitatory_counts length",
89 ));
90 }
91
92 let mut validated = Vec::with_capacity(counts.len());
93 for (&count, &flag) in counts.iter().zip(flags) {
94 let count = i32::try_from(count).map_err(|_| {
95 PyValueError::new_err("excitatory counts must be non-negative signed 32-bit integers")
96 })?;
97 if count < 0 {
98 return Err(PyValueError::new_err(
99 "excitatory counts must be non-negative signed 32-bit integers",
100 ));
101 }
102 if flag > 1 {
103 return Err(PyValueError::new_err(
104 "inhibitory flags must contain only zero or one",
105 ));
106 }
107 validated.push((count, flag != 0));
108 }
109
110 let mut event_count = 0_i64;
111 let events: Vec<u8> = validated
112 .into_iter()
113 .map(|(count, inhibited)| {
114 let event = neuron
115 .try_step(count, inhibited)
116 .expect("the complete batch was validated before evaluation");
117 event_count += i64::from(event);
118 event as u8
119 })
120 .collect();
121 Ok((events.into_pyarray(py), event_count))
122}