Skip to main content

sc_neurocore_engine/bindings/
mcculloch_pitts.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 — McCulloch-Pitts PyO3 batch binding
8
9//! Python binding for the source-faithful McCulloch-Pitts batch contract.
10
11use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::neurons;
17
18/// Register this binding without adding implementation code to the crate root.
19pub(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/// Evaluate one fully validated varying-input batch.
74#[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}