Skip to main content

sc_neurocore_engine/bindings/
poisson.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 — Poisson PyO3 binding
8
9//! Python binding for the homogeneous Poisson binary-bin generator.
10
11use numpy::{IntoPyArray, PyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::neurons;
17
18/// Register the homogeneous Poisson simulator with the extension module.
19pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20    module.add_class::<PyPoissonNeuron>()?;
21    module.add_function(wrap_pyfunction!(py_poisson_simulate, module)?)?;
22    Ok(())
23}
24
25#[pyclass(
26    name = "PoissonNeuron",
27    module = "sc_neurocore_engine.sc_neurocore_engine"
28)]
29#[derive(Clone)]
30pub struct PyPoissonNeuron {
31    inner: neurons::PoissonNeuron,
32}
33
34#[pymethods]
35impl PyPoissonNeuron {
36    #[new]
37    #[pyo3(signature = (rate_hz=100.0, dt_ms=1.0, seed=0xACE1))]
38    fn new(rate_hz: f64, dt_ms: f64, seed: u64) -> PyResult<Self> {
39        let inner = neurons::PoissonNeuron::new(rate_hz, dt_ms, seed);
40        if !inner.valid() {
41            return Err(PyValueError::new_err(
42                "invalid Poisson rate, timestep, or seed",
43            ));
44        }
45        Ok(Self { inner })
46    }
47    #[pyo3(signature = (rate_override=-1.0))]
48    fn step(&mut self, rate_override: f64) -> PyResult<i32> {
49        self.inner
50            .try_step(rate_override)
51            .map_err(PyValueError::new_err)
52    }
53    fn reset(&mut self) {
54        self.inner.reset();
55    }
56    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
57        let d = PyDict::new(py);
58        d.set_item("rate_hz", self.inner.rate_hz)?;
59        d.set_item("dt_ms", self.inner.dt_ms)?;
60        d.set_item("rng_state", self.inner.rng_state)?;
61        d.set_item("initial_seed", self.inner.initial_seed)?;
62        Ok(d.into_any().unbind())
63    }
64}
65
66/// Full-contract homogeneous-Poisson batch with the canonical LFSR16 trial.
67#[pyfunction]
68#[pyo3(signature = (rate_hz, dt_ms, rng_state, n_steps, rate_override=-1.0))]
69fn py_poisson_simulate<'py>(
70    py: Python<'py>,
71    rate_hz: f64,
72    dt_ms: f64,
73    rng_state: u16,
74    n_steps: usize,
75    rate_override: f64,
76) -> PyResult<(Bound<'py, PyArray1<u8>>, u16)> {
77    let mut neuron = crate::neurons::PoissonNeuron {
78        rate_hz,
79        dt_ms,
80        rng_state,
81        initial_seed: rng_state,
82    };
83    if !neuron.valid() || !rate_override.is_finite() {
84        return Err(PyValueError::new_err(
85            "invalid Poisson simulation state or rate override",
86        ));
87    }
88    let mut events = Vec::with_capacity(n_steps);
89    for _ in 0..n_steps {
90        let spike = neuron
91            .try_step(rate_override)
92            .map_err(PyValueError::new_err)?;
93        events.push(spike as u8);
94    }
95    Ok((events.into_pyarray(py), neuron.rng_state))
96}