Skip to main content

sc_neurocore_engine/bindings/
dcls.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 — DCLS-max tent-kernel PyO3 binding
8
9//! Python binding for the bit-true batched DCLS-max tent contraction.
10
11use numpy::{IntoPyArray, PyReadonlyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::scpn;
17
18/// Register the batched DCLS-max tent contraction with the extension module.
19pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20    module.add_function(wrap_pyfunction!(py_dcls_max_forward_batch_q88, module)?)?;
21    Ok(())
22}
23
24/// Batched DCLS-max triangular (tent) contraction in bit-true Q8.8 arithmetic.
25///
26/// Parity contract with `sc_neurocore.scpn.dcls_tent_kernel`: this Rust path,
27/// the Mojo, Julia and Go backends, and the Python floor all return
28/// bit-identical arrays because the kernel is exact integer arithmetic.
29///
30/// `spikes` and `weights_q88` are row-major `n_channels * n_taps`; `centres_q88`
31/// and `sigmas_q88` carry one learnable `(centre, sigma)` per output channel.
32///
33/// Returns a dict with keys `outputs_q88` (int16), `accumulators_q16_16`
34/// (int32), `overflow` (bool), `active_tap_counts` (int64) and `max_gates_q88`
35/// (int16), each a 1-D array of length `n_channels`.
36#[pyfunction]
37#[pyo3(signature = (spikes, weights_q88, centres_q88, sigmas_q88, n_taps))]
38fn py_dcls_max_forward_batch_q88<'py>(
39    py: Python<'py>,
40    spikes: PyReadonlyArray1<'py, u8>,
41    weights_q88: PyReadonlyArray1<'py, i16>,
42    centres_q88: PyReadonlyArray1<'py, i16>,
43    sigmas_q88: PyReadonlyArray1<'py, i16>,
44    n_taps: usize,
45) -> PyResult<Py<PyAny>> {
46    let result = scpn::dcls_max_forward_batch_q88(
47        spikes.as_slice()?,
48        weights_q88.as_slice()?,
49        centres_q88.as_slice()?,
50        sigmas_q88.as_slice()?,
51        n_taps,
52    )
53    .map_err(|e| PyValueError::new_err(e.to_string()))?;
54    let active_tap_counts: Vec<i64> = result.active_tap_counts.iter().map(|&c| c as i64).collect();
55    let d = PyDict::new(py);
56    d.set_item("outputs_q88", result.outputs_q88.into_pyarray(py))?;
57    d.set_item(
58        "accumulators_q16_16",
59        result.accumulators_q16_16.into_pyarray(py),
60    )?;
61    d.set_item("overflow", result.overflow.into_pyarray(py))?;
62    d.set_item("active_tap_counts", active_tap_counts.into_pyarray(py))?;
63    d.set_item("max_gates_q88", result.max_gates_q88.into_pyarray(py))?;
64    Ok(d.into_any().unbind())
65}