Skip to main content

sc_neurocore_engine/bindings/
dense_layer.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 — Dense-layer PyO3 binding
8
9//! Python binding for the stochastic-computing dense layer.
10
11use numpy::{
12    IntoPyArray, PyArray1, PyArray2, PyArrayMethods, PyReadonlyArray1, PyReadonlyArray2,
13    PyUntypedArrayMethods,
14};
15use pyo3::exceptions::PyValueError;
16use pyo3::prelude::*;
17
18use crate::layer;
19
20/// Register the stochastic-computing dense layer with the extension module.
21pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
22    module.add_class::<DenseLayer>()?;
23    Ok(())
24}
25
26#[pyclass(module = "sc_neurocore_engine.sc_neurocore_engine")]
27pub struct DenseLayer {
28    inner: layer::DenseLayer,
29}
30
31#[pymethods]
32impl DenseLayer {
33    #[new]
34    #[pyo3(signature = (n_inputs, n_neurons, length=1024, seed=24301))]
35    fn new(n_inputs: usize, n_neurons: usize, length: usize, seed: u64) -> Self {
36        Self {
37            inner: layer::DenseLayer::new(n_inputs, n_neurons, length, seed),
38        }
39    }
40
41    fn get_weights(&self) -> Vec<Vec<f64>> {
42        self.inner.get_weights()
43    }
44
45    fn set_weights(&mut self, weights: Vec<Vec<f64>>) -> PyResult<()> {
46        self.inner
47            .set_weights(weights)
48            .map_err(PyValueError::new_err)
49    }
50
51    fn refresh_packed_weights(&mut self) {
52        self.inner.refresh_packed_weights();
53    }
54
55    #[pyo3(signature = (input_values, seed=44257))]
56    fn forward(&self, input_values: Vec<f64>, seed: u64) -> PyResult<Vec<f64>> {
57        self.inner
58            .forward(&input_values, seed)
59            .map_err(PyValueError::new_err)
60    }
61
62    #[pyo3(signature = (input_values, seed=44257))]
63    fn forward_fast(&self, input_values: Vec<f64>, seed: u64) -> PyResult<Vec<f64>> {
64        self.inner
65            .forward_fast(&input_values, seed)
66            .map_err(PyValueError::new_err)
67    }
68
69    /// Dense forward accepting numpy input and returning numpy output.
70    ///
71    /// This performs parallel encoding + parallel compute in one FFI call.
72    #[pyo3(signature = (input_values, seed=44257))]
73    fn forward_numpy<'py>(
74        &self,
75        py: Python<'py>,
76        input_values: PyReadonlyArray1<'py, f64>,
77        seed: u64,
78    ) -> PyResult<Bound<'py, PyArray1<f64>>> {
79        let slice = input_values
80            .as_slice()
81            .map_err(|e| PyValueError::new_err(format!("Cannot read input array: {e}")))?;
82        let out = self
83            .inner
84            .forward_numpy_inner(slice, seed)
85            .map_err(PyValueError::new_err)?;
86        Ok(out.into_pyarray(py))
87    }
88
89    /// Dense forward for a batch of input samples in one FFI call.
90    ///
91    /// `inputs` must be a contiguous float64 array of shape (n_samples, n_inputs).
92    /// Returns float64 array of shape (n_samples, n_neurons).
93    #[pyo3(signature = (inputs, seed=44257))]
94    fn forward_batch_numpy<'py>(
95        &self,
96        py: Python<'py>,
97        inputs: PyReadonlyArray2<'py, f64>,
98        seed: u64,
99    ) -> PyResult<Bound<'py, PyArray2<f64>>> {
100        let shape = inputs.shape();
101        let n_samples = shape[0];
102        let n_inputs = shape[1];
103        if n_inputs != self.inner.n_inputs {
104            return Err(PyValueError::new_err(format!(
105                "Expected {} input features, got {}.",
106                self.inner.n_inputs, n_inputs
107            )));
108        }
109
110        let flat_inputs = inputs
111            .as_slice()
112            .map_err(|e| PyValueError::new_err(format!("Array not contiguous: {e}")))?;
113        let out = PyArray2::<f64>::zeros(py, [n_samples, self.inner.n_neurons], false);
114        // SAFETY: Newly allocated numpy arrays are contiguous.
115        let out_slice = unsafe {
116            out.as_slice_mut()
117                .expect("newly allocated output array must be contiguous")
118        };
119
120        self.inner
121            .forward_batch_into(flat_inputs, n_samples, seed, out_slice)
122            .map_err(PyValueError::new_err)?;
123        Ok(out)
124    }
125
126    /// Forward pass with pre-packed input bitstreams.
127    ///
128    /// Accepts either:
129    /// - 2-D numpy array of dtype uint64 with shape (n_inputs, words)
130    /// - list[list[int]]
131    fn forward_prepacked(&self, packed_inputs: &Bound<'_, PyAny>) -> PyResult<Vec<f64>> {
132        if let Ok(arr) = packed_inputs.extract::<PyReadonlyArray2<u64>>() {
133            let view = arr.as_array();
134            let rows: Vec<Vec<u64>> = (0..view.nrows()).map(|i| view.row(i).to_vec()).collect();
135            return self
136                .inner
137                .forward_prepacked(&rows)
138                .map_err(PyValueError::new_err);
139        }
140
141        let rows = packed_inputs.extract::<Vec<Vec<u64>>>().map_err(|_| {
142            PyValueError::new_err(
143                "packed_inputs must be a 2-D numpy uint64 array or list[list[int]].",
144            )
145        })?;
146        self.inner
147            .forward_prepacked(&rows)
148            .map_err(PyValueError::new_err)
149    }
150
151    /// Dense forward with pre-packed numpy 2-D input (true zero-copy).
152    ///
153    /// Accepts a contiguous numpy uint64 array of shape (n_inputs, words).
154    /// This avoids all row-copying that the `forward_prepacked` method does.
155    #[pyo3(signature = (packed_inputs,))]
156    fn forward_prepacked_numpy<'py>(
157        &self,
158        py: Python<'py>,
159        packed_inputs: PyReadonlyArray2<'py, u64>,
160    ) -> PyResult<Bound<'py, PyArray1<f64>>> {
161        let shape = packed_inputs.shape();
162        let n_inputs = shape[0];
163        let words = shape[1];
164        let flat = packed_inputs
165            .as_slice()
166            .map_err(|e| PyValueError::new_err(format!("Array not contiguous: {e}")))?;
167        let out = self
168            .inner
169            .forward_prepacked_2d(flat, n_inputs, words)
170            .map_err(PyValueError::new_err)?;
171        Ok(out.into_pyarray(py))
172    }
173}