Skip to main content

sc_neurocore_engine/bindings/
network_runner.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 — Network runner PyO3 binding
8
9//! Python bindings for heterogeneous network execution and named-model batches.
10
11use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::network_runner;
17
18/// Register network execution surfaces with the extension module.
19pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20    module.add_class::<PyNetworkRunner>()?;
21    module.add_function(wrap_pyfunction!(py_batch_simulate, module)?)?;
22    Ok(())
23}
24
25#[pyclass(
26    name = "NetworkRunner",
27    module = "sc_neurocore_engine.sc_neurocore_engine"
28)]
29pub struct PyNetworkRunner {
30    inner: network_runner::NetworkRunner,
31}
32
33#[pymethods]
34impl PyNetworkRunner {
35    #[new]
36    fn new() -> Self {
37        Self {
38            inner: network_runner::NetworkRunner::new(),
39        }
40    }
41
42    fn add_population(&mut self, model: &str, n: usize) -> PyResult<usize> {
43        let pop = network_runner::create_population(model, n).map_err(PyValueError::new_err)?;
44        Ok(self.inner.add_population(pop))
45    }
46
47    #[pyo3(signature = (src, tgt, row_offsets, col_indices, values, delay=0))]
48    fn add_projection(
49        &mut self,
50        src: usize,
51        tgt: usize,
52        row_offsets: Vec<i64>,
53        col_indices: Vec<i64>,
54        values: Vec<f64>,
55        delay: usize,
56    ) {
57        let ro: Vec<usize> = row_offsets.iter().map(|&x| x as usize).collect();
58        let ci: Vec<usize> = col_indices.iter().map(|&x| x as usize).collect();
59        let proj = network_runner::ProjectionRunner::new(src, tgt, ro, ci, values, delay);
60        self.inner.add_projection(proj);
61    }
62
63    fn step_population<'py>(
64        &mut self,
65        py: Python<'py>,
66        pop_index: usize,
67        currents: PyReadonlyArray1<'py, f64>,
68    ) -> PyResult<Py<PyAny>> {
69        let currents = currents.as_slice()?;
70        let (spikes, voltages) = self
71            .inner
72            .step_population_with_currents(pop_index, currents)
73            .map_err(PyValueError::new_err)?;
74        let dict = PyDict::new(py);
75        dict.set_item("spikes", spikes.into_pyarray(py))?;
76        dict.set_item("voltages", voltages.into_pyarray(py))?;
77        Ok(dict.into_any().unbind())
78    }
79
80    fn run<'py>(&mut self, py: Python<'py>, n_steps: usize) -> PyResult<Py<PyAny>> {
81        let results = self.inner.run(n_steps);
82        let dict = PyDict::new(py);
83        let spike_counts: Vec<u64> = results.spike_counts.iter().map(|&c| c as u64).collect();
84        dict.set_item("spike_counts", spike_counts.into_pyarray(py))?;
85        let spike_data: Vec<Py<PyArray1<u64>>> = results
86            .spike_data
87            .into_iter()
88            .map(|v: Vec<u64>| v.into_pyarray(py).unbind())
89            .collect();
90        dict.set_item("spike_data", spike_data)?;
91        let voltages: Vec<Py<PyArray1<f64>>> = results
92            .voltages
93            .into_iter()
94            .map(|v: Vec<f64>| v.into_pyarray(py).unbind())
95            .collect();
96        dict.set_item("voltages", voltages)?;
97        Ok(dict.into_any().unbind())
98    }
99
100    #[staticmethod]
101    fn supported_models() -> Vec<&'static str> {
102        network_runner::supported_models()
103    }
104}
105
106/// Run a named neuron model for n_steps with a current trace, returning
107/// voltage trace + spike indices. Entire simulation in Rust.
108#[pyfunction]
109fn py_batch_simulate<'py>(
110    py: Python<'py>,
111    model_name: &str,
112    n_steps: usize,
113    current_trace: PyReadonlyArray1<'py, f64>,
114) -> PyResult<Py<PyAny>> {
115    let mut neuron = network_runner::create_neuron(model_name).map_err(PyValueError::new_err)?;
116    let currents = current_trace.as_slice()?;
117    let steps = n_steps.min(currents.len());
118
119    let mut voltages = vec![0.0f64; steps];
120    let mut spikes: Vec<u64> = Vec::new();
121
122    for t in 0..steps {
123        let fired = neuron.step(currents[t]);
124        voltages[t] = neuron.soma_voltage();
125        if fired != 0 {
126            spikes.push(t as u64);
127        }
128    }
129
130    let d = PyDict::new(py);
131    d.set_item("voltages", voltages.into_pyarray(py))?;
132    d.set_item("spikes", spikes.into_pyarray(py))?;
133    d.set_item("n_steps", steps)?;
134    Ok(d.into_any().unbind())
135}