sc_neurocore_engine/bindings/
bitstream.rs1use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray1};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::IntoPyObject;
15
16use crate::{bitstream, encoder, neuron, simd};
17
18pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
20 module.add_class::<Lfsr16>()?;
21 module.add_class::<BitstreamEncoder>()?;
22 module.add_class::<PyBitstreamAverager>()?;
23 module.add_function(wrap_pyfunction!(pack_bitstream, module)?)?;
24 module.add_function(wrap_pyfunction!(unpack_bitstream, module)?)?;
25 module.add_function(wrap_pyfunction!(popcount, module)?)?;
26 module.add_function(wrap_pyfunction!(pack_bitstream_numpy, module)?)?;
27 module.add_function(wrap_pyfunction!(popcount_numpy, module)?)?;
28 module.add_function(wrap_pyfunction!(unpack_bitstream_numpy, module)?)?;
29 module.add_function(wrap_pyfunction!(batch_encode, module)?)?;
30 module.add_function(wrap_pyfunction!(batch_encode_numpy, module)?)?;
31 Ok(())
32}
33
34#[pyclass(
35 name = "BitstreamAverager",
36 module = "sc_neurocore_engine.sc_neurocore_engine"
37)]
38pub struct PyBitstreamAverager {
39 inner: neuron::BitstreamAverager,
40}
41
42#[pymethods]
43impl PyBitstreamAverager {
44 #[new]
45 #[pyo3(signature = (window=1024))]
46 fn new(window: usize) -> Self {
47 Self {
48 inner: neuron::BitstreamAverager::new(window),
49 }
50 }
51
52 fn push(&mut self, bit: u8) {
53 self.inner.push(bit);
54 }
55
56 fn estimate(&self) -> f64 {
57 self.inner.estimate()
58 }
59
60 fn reset(&mut self) {
61 self.inner.reset();
62 }
63
64 #[getter]
65 fn window(&self) -> usize {
66 self.inner.window()
67 }
68}
69
70#[pyclass(module = "sc_neurocore_engine.sc_neurocore_engine")]
71pub struct Lfsr16 {
72 inner: encoder::Lfsr16,
73 seed_init: u16,
74}
75
76#[pymethods]
77impl Lfsr16 {
78 #[new]
79 #[pyo3(signature = (seed=0xACE1))]
80 fn new(seed: u16) -> PyResult<Self> {
81 if seed == 0 {
82 return Err(PyValueError::new_err("LFSR seed must be non-zero."));
83 }
84 Ok(Self {
85 inner: encoder::Lfsr16::new(seed),
86 seed_init: seed,
87 })
88 }
89
90 fn step(&mut self) -> u16 {
91 self.inner.step()
92 }
93
94 #[getter]
95 fn reg(&self) -> u16 {
96 self.inner.reg
97 }
98
99 #[getter]
100 fn width(&self) -> u32 {
101 self.inner.width
102 }
103
104 #[pyo3(signature = (seed=None))]
105 fn reset(&mut self, seed: Option<u16>) -> PyResult<()> {
106 let next = seed.unwrap_or(self.seed_init);
107 if next == 0 {
108 return Err(PyValueError::new_err("LFSR seed must be non-zero."));
109 }
110 self.inner = encoder::Lfsr16::new(next);
111 self.seed_init = next;
112 Ok(())
113 }
114}
115
116#[pyclass(module = "sc_neurocore_engine.sc_neurocore_engine")]
117pub struct BitstreamEncoder {
118 inner: encoder::BitstreamEncoder,
119 seed_init: u16,
120}
121
122#[pymethods]
123impl BitstreamEncoder {
124 #[new]
125 #[pyo3(signature = (data_width=16, seed=0xACE1))]
126 fn new(data_width: u32, seed: u16) -> PyResult<Self> {
127 if seed == 0 {
128 return Err(PyValueError::new_err("LFSR seed must be non-zero."));
129 }
130 Ok(Self {
131 inner: encoder::BitstreamEncoder::new(data_width, seed),
132 seed_init: seed,
133 })
134 }
135
136 fn step(&mut self, x_value: u16) -> u8 {
137 self.inner.step(x_value)
138 }
139
140 #[getter]
141 fn data_width(&self) -> u32 {
142 self.inner.data_width
143 }
144
145 #[getter]
146 fn reg(&self) -> u16 {
147 self.inner.lfsr.reg
148 }
149
150 #[pyo3(signature = (seed=None))]
151 fn reset(&mut self, seed: Option<u16>) -> PyResult<()> {
152 let next = seed.unwrap_or(self.seed_init);
153 if next == 0 {
154 return Err(PyValueError::new_err("LFSR seed must be non-zero."));
155 }
156 self.inner.reset(Some(next));
157 self.seed_init = next;
158 Ok(())
159 }
160}
161
162#[pyfunction]
164fn pack_bitstream(py: Python<'_>, bits: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
165 if let Ok(rows) = bits.extract::<Vec<Vec<u8>>>() {
166 let packed_rows: Vec<Vec<u64>> = rows.iter().map(|row| bitstream::pack(row).data).collect();
167 return Ok(packed_rows
168 .into_pyobject(py)
169 .map_err(|e| PyValueError::new_err(e.to_string()))?
170 .into_any()
171 .unbind());
172 }
173
174 let flat = bits
175 .extract::<Vec<u8>>()
176 .map_err(|_| PyValueError::new_err("Expected a 1-D or 2-D array of uint8 bits."))?;
177 Ok(bitstream::pack(&flat)
178 .data
179 .into_pyobject(py)
180 .map_err(|e| PyValueError::new_err(e.to_string()))?
181 .into_any()
182 .unbind())
183}
184
185#[pyfunction]
187#[pyo3(signature = (packed, original_length, original_shape=None))]
188fn unpack_bitstream(
189 py: Python<'_>,
190 packed: &Bound<'_, PyAny>,
191 original_length: usize,
192 original_shape: Option<(usize, usize)>,
193) -> PyResult<Py<PyAny>> {
194 if let Ok(rows) = packed.extract::<Vec<Vec<u64>>>() {
195 let batch = rows.len();
196 let per_batch_len = if let Some((expected_batch, length)) = original_shape {
197 if expected_batch != batch {
198 return Err(PyValueError::new_err(format!(
199 "original_shape batch {} does not match packed batch {}.",
200 expected_batch, batch
201 )));
202 }
203 length
204 } else {
205 original_length.checked_div(batch).unwrap_or(0)
206 };
207
208 let unpacked_rows: Vec<Vec<u8>> = rows
209 .into_iter()
210 .map(|row| {
211 bitstream::unpack(&bitstream::BitStreamTensor::from_words(row, per_batch_len))
212 })
213 .collect();
214 return Ok(unpacked_rows
215 .into_pyobject(py)
216 .map_err(|e| PyValueError::new_err(e.to_string()))?
217 .into_any()
218 .unbind());
219 }
220
221 let words = packed.extract::<Vec<u64>>().map_err(|_| {
222 PyValueError::new_err("Expected packed uint64 words as 1-D or 2-D sequence.")
223 })?;
224 let tensor = bitstream::BitStreamTensor::from_words(words, original_length);
225 Ok(bitstream::unpack(&tensor)
226 .into_pyobject(py)
227 .map_err(|e| PyValueError::new_err(e.to_string()))?
228 .into_any()
229 .unbind())
230}
231
232#[pyfunction]
234fn popcount(packed: &Bound<'_, PyAny>) -> PyResult<u64> {
235 if let Ok(array) = packed.extract::<PyReadonlyArray1<'_, u64>>() {
240 return Ok(simd::popcount_dispatch(array.as_slice()?));
241 }
242
243 if let Ok(rows) = packed.extract::<Vec<Vec<u64>>>() {
244 return Ok(rows
245 .iter()
246 .map(|row| simd::popcount_dispatch(row))
247 .sum::<u64>());
248 }
249
250 let words = packed.extract::<Vec<u64>>().map_err(|_| {
251 PyValueError::new_err("Expected packed uint64 words as 1-D or 2-D sequence.")
252 })?;
253 Ok(simd::popcount_dispatch(&words))
254}
255
256#[pyfunction]
259fn pack_bitstream_numpy<'py>(
260 py: Python<'py>,
261 bits: PyReadonlyArray1<'py, u8>,
262) -> PyResult<Bound<'py, PyArray1<u64>>> {
263 let slice = bits
264 .as_slice()
265 .map_err(|e| PyValueError::new_err(format!("Cannot read numpy array: {e}")))?;
266 let tensor = simd::pack_dispatch(slice);
267 Ok(tensor.data.into_pyarray(py))
268}
269
270#[pyfunction]
272fn popcount_numpy(packed: PyReadonlyArray1<'_, u64>) -> PyResult<u64> {
273 let words = packed
274 .as_slice()
275 .map_err(|e| PyValueError::new_err(format!("Cannot read numpy array: {e}")))?;
276 Ok(simd::popcount_dispatch(words))
277}
278
279#[pyfunction]
281fn unpack_bitstream_numpy<'py>(
282 py: Python<'py>,
283 packed: PyReadonlyArray1<'py, u64>,
284 original_length: usize,
285) -> PyResult<Bound<'py, PyArray1<u8>>> {
286 let words = packed
287 .as_slice()
288 .map_err(|e| PyValueError::new_err(format!("Cannot read numpy array: {e}")))?;
289 let tensor = bitstream::BitStreamTensor::from_words(words.to_vec(), original_length);
290 let bits = bitstream::unpack(&tensor);
291 Ok(bits.into_pyarray(py))
292}
293
294#[pyfunction]
298#[pyo3(signature = (probs, length=1024, seed=0xACE1))]
299fn batch_encode<'py>(
300 _py: Python<'py>,
301 probs: PyReadonlyArray1<'py, f64>,
302 length: usize,
303 seed: u64,
304) -> PyResult<Vec<Vec<u64>>> {
305 let prob_slice = probs
306 .as_slice()
307 .map_err(|e| PyValueError::new_err(format!("Cannot read probs: {e}")))?;
308 let words = length.div_ceil(64);
309
310 use rand::SeedableRng;
311 let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed);
312
313 let packed: Vec<Vec<u64>> = prob_slice
314 .iter()
315 .map(|&p| {
316 let mut data = bitstream::bernoulli_packed(p, length, &mut rng);
317 data.resize(words, 0);
318 data
319 })
320 .collect();
321
322 Ok(packed)
323}
324
325#[pyfunction]
329#[pyo3(signature = (probs, length=1024, seed=0xACE1))]
330fn batch_encode_numpy<'py>(
331 py: Python<'py>,
332 probs: PyReadonlyArray1<'py, f64>,
333 length: usize,
334 seed: u64,
335) -> PyResult<Bound<'py, PyArray2<u64>>> {
336 use rayon::prelude::*;
337
338 let prob_slice = probs
339 .as_slice()
340 .map_err(|e| PyValueError::new_err(format!("Cannot read probs: {e}")))?;
341 let words = length.div_ceil(64);
342 let n_probs = prob_slice.len();
343
344 let rows: Vec<Vec<u64>> = prob_slice
345 .par_iter()
346 .enumerate()
347 .map(|(idx, &p)| {
348 use rand::SeedableRng;
349
350 let prob_seed = seed.wrapping_add(idx as u64);
351 let mut rng = rand_xoshiro::Xoshiro256PlusPlus::seed_from_u64(prob_seed);
352 let mut row = bitstream::bernoulli_packed_simd(p, length, &mut rng);
353 row.resize(words, 0);
354 row
355 })
356 .collect();
357
358 let mut flat = Vec::with_capacity(n_probs * words);
359 for row in &rows {
360 flat.extend_from_slice(row);
361 }
362
363 let arr = ndarray::Array2::from_shape_vec((n_probs, words), flat)
364 .map_err(|e| PyValueError::new_err(format!("Shape construction failed: {e}")))?;
365 Ok(arr.into_pyarray(py))
366}