1use crate::ir;
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14
15pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
17 m.add_class::<PyScGraph>()?;
18 m.add_class::<PyScGraphBuilder>()?;
19 m.add_function(wrap_pyfunction!(ir_verify, m)?)?;
20 m.add_function(wrap_pyfunction!(ir_print, m)?)?;
21 m.add_function(wrap_pyfunction!(ir_parse, m)?)?;
22 m.add_function(wrap_pyfunction!(ir_emit_sv, m)?)?;
23 Ok(())
24}
25
26#[pyclass(name = "ScGraph", module = "sc_neurocore_engine.sc_neurocore_engine")]
29pub struct PyScGraph {
30 inner: ir::graph::ScGraph,
31}
32
33#[pymethods]
34impl PyScGraph {
35 fn len(&self) -> usize {
37 self.inner.len()
38 }
39
40 fn __len__(&self) -> usize {
41 self.inner.len()
42 }
43
44 fn is_empty(&self) -> bool {
46 self.inner.is_empty()
47 }
48
49 #[getter]
51 fn name(&self) -> &str {
52 &self.inner.name
53 }
54
55 fn num_inputs(&self) -> usize {
57 self.inner.inputs().len()
58 }
59
60 fn num_outputs(&self) -> usize {
62 self.inner.outputs().len()
63 }
64
65 fn __repr__(&self) -> String {
66 format!("ScGraph('{}', ops={})", self.inner.name, self.inner.len())
67 }
68}
69
70#[pyclass(
71 name = "ScGraphBuilder",
72 module = "sc_neurocore_engine.sc_neurocore_engine"
73)]
74pub struct PyScGraphBuilder {
75 inner: Option<ir::builder::ScGraphBuilder>,
76}
77
78impl PyScGraphBuilder {
79 fn builder_mut(&mut self) -> PyResult<&mut ir::builder::ScGraphBuilder> {
80 self.inner
81 .as_mut()
82 .ok_or_else(|| PyValueError::new_err("Builder already consumed by build()."))
83 }
84}
85
86#[pymethods]
87impl PyScGraphBuilder {
88 #[new]
89 fn new(name: String) -> Self {
90 Self {
91 inner: Some(ir::builder::ScGraphBuilder::new(name)),
92 }
93 }
94
95 fn input(&mut self, name: &str, ty: &str) -> PyResult<u32> {
97 let sc_type = parse_sc_type(ty)?;
98 Ok(self.builder_mut()?.input(name, sc_type).0)
99 }
100
101 fn output(&mut self, name: &str, source_id: u32) -> PyResult<u32> {
103 Ok(self
104 .builder_mut()?
105 .output(name, ir::graph::ValueId(source_id))
106 .0)
107 }
108
109 fn constant_f64(&mut self, value: f64, ty: &str) -> PyResult<u32> {
111 let sc_type = parse_sc_type(ty)?;
112 Ok(self
113 .builder_mut()?
114 .constant(ir::graph::ScConst::F64(value), sc_type)
115 .0)
116 }
117
118 fn constant_i64(&mut self, value: i64, ty: &str) -> PyResult<u32> {
120 let sc_type = parse_sc_type(ty)?;
121 Ok(self
122 .builder_mut()?
123 .constant(ir::graph::ScConst::I64(value), sc_type)
124 .0)
125 }
126
127 fn constant_f64_vec(&mut self, values: Vec<f64>, ty: &str) -> PyResult<u32> {
129 let sc_type = parse_sc_type(ty)?;
130 Ok(self
131 .builder_mut()?
132 .constant(ir::graph::ScConst::F64Vec(values), sc_type)
133 .0)
134 }
135
136 fn kuramoto_step(
141 &mut self,
142 phases_id: u32,
143 omega_id: u32,
144 coupling_id: u32,
145 dt: f64,
146 ) -> PyResult<u32> {
147 Ok(self
148 .builder_mut()?
149 .kuramoto_step(
150 ir::graph::ValueId(phases_id),
151 ir::graph::ValueId(omega_id),
152 ir::graph::ValueId(coupling_id),
153 dt,
154 )
155 .0)
156 }
157
158 fn graph_forward(
163 &mut self,
164 features_id: u32,
165 adjacency_id: u32,
166 n_nodes: usize,
167 n_features: usize,
168 ) -> PyResult<u32> {
169 Ok(self
170 .builder_mut()?
171 .graph_forward(
172 ir::graph::ValueId(features_id),
173 ir::graph::ValueId(adjacency_id),
174 n_nodes,
175 n_features,
176 )
177 .0)
178 }
179
180 fn softmax_attention(
186 &mut self,
187 q_id: u32,
188 k_id: u32,
189 v_id: u32,
190 dim_k: usize,
191 ) -> PyResult<u32> {
192 Ok(self
193 .builder_mut()?
194 .softmax_attention(
195 ir::graph::ValueId(q_id),
196 ir::graph::ValueId(k_id),
197 ir::graph::ValueId(v_id),
198 dim_k,
199 )
200 .0)
201 }
202
203 fn encode(&mut self, prob_id: u32, length: usize, seed: u64) -> PyResult<u32> {
205 let seed = u16::try_from(seed)
206 .map_err(|_| PyValueError::new_err(format!("Seed out of range for u16: {seed}")))?;
207 Ok(self
208 .builder_mut()?
209 .encode(ir::graph::ValueId(prob_id), length, seed)
210 .0)
211 }
212
213 fn bitwise_and(&mut self, lhs_id: u32, rhs_id: u32) -> PyResult<u32> {
215 Ok(self
216 .builder_mut()?
217 .bitwise_and(ir::graph::ValueId(lhs_id), ir::graph::ValueId(rhs_id))
218 .0)
219 }
220
221 fn popcount(&mut self, input_id: u32) -> PyResult<u32> {
223 Ok(self.builder_mut()?.popcount(ir::graph::ValueId(input_id)).0)
224 }
225
226 #[pyo3(signature = (
228 current_id,
229 leak_id,
230 gain_id,
231 noise_id,
232 data_width=16,
233 fraction=8,
234 v_rest=0,
235 v_reset=0,
236 v_threshold=256,
237 refractory_period=2
238 ))]
239 #[allow(clippy::too_many_arguments)]
240 fn lif_step(
241 &mut self,
242 current_id: u32,
243 leak_id: u32,
244 gain_id: u32,
245 noise_id: u32,
246 data_width: u32,
247 fraction: u32,
248 v_rest: i64,
249 v_reset: i64,
250 v_threshold: i64,
251 refractory_period: u32,
252 ) -> PyResult<u32> {
253 let params = ir::graph::LifParams {
254 data_width,
255 fraction,
256 v_rest,
257 v_reset,
258 v_threshold,
259 refractory_period,
260 };
261 Ok(self
262 .builder_mut()?
263 .lif_step(
264 ir::graph::ValueId(current_id),
265 ir::graph::ValueId(leak_id),
266 ir::graph::ValueId(gain_id),
267 ir::graph::ValueId(noise_id),
268 params,
269 )
270 .0)
271 }
272
273 #[pyo3(signature = (
275 inputs_id,
276 weights_id,
277 leak_id,
278 gain_id,
279 n_inputs=3,
280 n_neurons=7,
281 data_width=16,
282 stream_length=1024,
283 seed_base=0xACE1u64,
284 y_min=0,
285 y_max=65535
286 ))]
287 #[allow(clippy::too_many_arguments)]
288 fn dense_forward(
289 &mut self,
290 inputs_id: u32,
291 weights_id: u32,
292 leak_id: u32,
293 gain_id: u32,
294 n_inputs: usize,
295 n_neurons: usize,
296 data_width: u32,
297 stream_length: usize,
298 seed_base: u64,
299 y_min: i64,
300 y_max: i64,
301 ) -> PyResult<u32> {
302 let input_seed_base = u16::try_from(seed_base).map_err(|_| {
303 PyValueError::new_err(format!("seed_base out of range for u16: {seed_base}"))
304 })?;
305 let params = ir::graph::DenseParams {
306 n_inputs,
307 n_neurons,
308 data_width,
309 stream_length,
310 input_seed_base,
311 weight_seed_base: input_seed_base.wrapping_add(1),
312 y_min,
313 y_max,
314 };
315 Ok(self
316 .builder_mut()?
317 .dense_forward(
318 ir::graph::ValueId(inputs_id),
319 ir::graph::ValueId(weights_id),
320 ir::graph::ValueId(leak_id),
321 ir::graph::ValueId(gain_id),
322 params,
323 )
324 .0)
325 }
326
327 fn scale(&mut self, input_id: u32, factor: f64) -> PyResult<u32> {
329 Ok(self
330 .builder_mut()?
331 .scale(ir::graph::ValueId(input_id), factor)
332 .0)
333 }
334
335 fn offset(&mut self, input_id: u32, offset_val: f64) -> PyResult<u32> {
337 Ok(self
338 .builder_mut()?
339 .offset(ir::graph::ValueId(input_id), offset_val)
340 .0)
341 }
342
343 fn div_const(&mut self, input_id: u32, divisor: u64) -> PyResult<u32> {
345 Ok(self
346 .builder_mut()?
347 .div_const(ir::graph::ValueId(input_id), divisor)
348 .0)
349 }
350
351 fn build(&mut self) -> PyResult<PyScGraph> {
353 let builder = self
354 .inner
355 .take()
356 .ok_or_else(|| PyValueError::new_err("Builder already consumed by build()."))?;
357 Ok(PyScGraph {
358 inner: builder.build(),
359 })
360 }
361}
362
363#[pyfunction]
365fn ir_verify(graph: PyRef<'_, PyScGraph>) -> Option<Vec<String>> {
366 match ir::verify::verify(&graph.inner) {
367 Ok(()) => None,
368 Err(errors) => Some(errors.iter().map(|e| e.to_string()).collect()),
369 }
370}
371
372#[pyfunction]
374fn ir_print(graph: PyRef<'_, PyScGraph>) -> String {
375 ir::printer::print(&graph.inner)
376}
377
378#[pyfunction]
380fn ir_parse(text: &str) -> PyResult<PyScGraph> {
381 ir::parser::parse(text)
382 .map(|graph| PyScGraph { inner: graph })
383 .map_err(|e| PyValueError::new_err(e.to_string()))
384}
385
386#[pyfunction]
388fn ir_emit_sv(graph: PyRef<'_, PyScGraph>) -> PyResult<String> {
389 ir::emit_sv::emit(&graph.inner).map_err(PyValueError::new_err)
390}
391
392fn parse_sc_type(s: &str) -> PyResult<ir::graph::ScType> {
397 let s = s.trim();
398 let lower = s.to_ascii_lowercase();
399 match lower.as_str() {
400 "bool" => Ok(ir::graph::ScType::Bool),
401 "rate" => Ok(ir::graph::ScType::Rate),
402 "u32" => Ok(ir::graph::ScType::UInt { width: 32 }),
403 "u64" => Ok(ir::graph::ScType::UInt { width: 64 }),
404 "i16" => Ok(ir::graph::ScType::SInt { width: 16 }),
405 "i32" => Ok(ir::graph::ScType::SInt { width: 32 }),
406 "bitstream" => Ok(ir::graph::ScType::Bitstream { length: 0 }),
407 _ => {
408 if let Some(width) = lower.strip_prefix('u') {
409 if let Ok(width) = width.parse::<u32>() {
410 return Ok(ir::graph::ScType::UInt { width });
411 }
412 }
413 if let Some(width) = lower.strip_prefix('i') {
414 if let Ok(width) = width.parse::<u32>() {
415 return Ok(ir::graph::ScType::SInt { width });
416 }
417 }
418 if let Some(inner) = lower
419 .strip_prefix("bitstream<")
420 .and_then(|r| r.strip_suffix('>'))
421 {
422 let length = inner.parse::<usize>().map_err(|_| {
423 PyValueError::new_err(format!("Invalid bitstream length: '{inner}'"))
424 })?;
425 return Ok(ir::graph::ScType::Bitstream { length });
426 }
427 if let Some(inner) = lower
428 .strip_prefix("fixed<")
429 .and_then(|r| r.strip_suffix('>'))
430 {
431 let parts: Vec<&str> = inner.split(',').collect();
432 if parts.len() != 2 {
433 return Err(PyValueError::new_err(format!(
434 "fixed type needs 2 params: '{s}'"
435 )));
436 }
437 let width = parts[0].trim().parse::<u32>().map_err(|_| {
438 PyValueError::new_err(format!("Invalid fixed width: '{}'", parts[0]))
439 })?;
440 let frac = parts[1].trim().parse::<u32>().map_err(|_| {
441 PyValueError::new_err(format!("Invalid fixed frac: '{}'", parts[1]))
442 })?;
443 return Ok(ir::graph::ScType::FixedPoint { width, frac });
444 }
445 if let Some(inner) = lower.strip_prefix("vec<").and_then(|r| r.strip_suffix('>')) {
446 if let Some(comma_pos) = inner.rfind(',') {
447 let inner_ty_str = &inner[..comma_pos];
448 let count_str = inner[comma_pos + 1..].trim();
449 let inner_ty = parse_sc_type(inner_ty_str)?;
450 let count = count_str.parse::<usize>().map_err(|_| {
451 PyValueError::new_err(format!("Invalid vec count: '{count_str}'"))
452 })?;
453 return Ok(ir::graph::ScType::Vec {
454 element: Box::new(inner_ty),
455 count,
456 });
457 }
458 }
459 Err(PyValueError::new_err(format!("Unknown IR type: '{s}'")))
460 }
461 }
462}