sc_neurocore_engine/ir/graph.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 — SC Compute Graph data structures
8
9//! SC Compute Graph data structures.
10
11use std::fmt;
12
13// Types
14
15/// Type system for SC IR values.
16#[derive(Debug, Clone, PartialEq)]
17pub enum ScType {
18 /// Packed u64 bitstream of a given length.
19 Bitstream { length: usize },
20 /// Q-format signed fixed-point. E.g. `FixedPoint { width: 16, frac: 8 }` = Q8.8.
21 FixedPoint { width: u32, frac: u32 },
22 /// Floating-point probability in [0, 1].
23 Rate,
24 /// Unsigned integer of a given bit width.
25 UInt { width: u32 },
26 /// Signed integer of a given bit width.
27 SInt { width: u32 },
28 /// Boolean (1-bit).
29 Bool,
30 /// Vector of a base type.
31 Vec { element: Box<ScType>, count: usize },
32}
33
34impl ScType {
35 /// Return the bit width of this type for HDL emission.
36 pub fn bit_width(&self) -> usize {
37 match self {
38 Self::Bool => 1,
39 Self::Rate => 16, // mapped to Q8.8
40 Self::UInt { width } | Self::SInt { width } => *width as usize,
41 Self::FixedPoint { width, .. } => *width as usize,
42 Self::Bitstream { .. } => 1, // streaming 1-bit per cycle in current emitter
43 Self::Vec { element, count } => element.bit_width() * count,
44 }
45 }
46}
47
48impl fmt::Display for ScType {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::Bitstream { length } => write!(f, "bitstream<{length}>"),
52 Self::FixedPoint { width, frac } => write!(f, "fixed<{width},{frac}>"),
53 Self::Rate => write!(f, "rate"),
54 Self::UInt { width } => write!(f, "u{width}"),
55 Self::SInt { width } => write!(f, "i{width}"),
56 Self::Bool => write!(f, "bool"),
57 Self::Vec { element, count } => write!(f, "vec<{element},{count}>"),
58 }
59 }
60}
61
62// Value references (SSA-style)
63
64/// Unique identifier for a value produced by an operation.
65#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)]
66pub struct ValueId(pub u32);
67
68impl fmt::Display for ValueId {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "%{}", self.0)
71 }
72}
73
74// Constants
75
76/// Compile-time constant values embedded in the IR.
77#[derive(Debug, Clone, PartialEq)]
78pub enum ScConst {
79 /// Floating-point scalar.
80 F64(f64),
81 /// Signed integer scalar.
82 I64(i64),
83 /// Unsigned integer scalar.
84 U64(u64),
85 /// Flat vector of f64 (for weight matrices).
86 F64Vec(Vec<f64>),
87 /// Flat vector of i64 (for fixed-point arrays).
88 I64Vec(Vec<i64>),
89}
90
91// LIF neuron parameters (matches hdl/sc_lif_neuron.v)
92
93/// Parameters for the fixed-point LIF neuron.
94/// Maps 1:1 to `sc_lif_neuron` Verilog parameters.
95#[derive(Debug, Clone, PartialEq)]
96pub struct LifParams {
97 pub data_width: u32,
98 pub fraction: u32,
99 pub v_rest: i64,
100 pub v_reset: i64,
101 pub v_threshold: i64,
102 pub refractory_period: u32,
103}
104
105impl Default for LifParams {
106 fn default() -> Self {
107 Self {
108 data_width: 16,
109 fraction: 8,
110 v_rest: 0,
111 v_reset: 0,
112 v_threshold: 256, // 1.0 in Q8.8
113 refractory_period: 2,
114 }
115 }
116}
117
118// Dense layer parameters
119
120/// Parameters for a dense SC layer.
121/// Maps to `sc_dense_layer_core` Verilog module.
122#[derive(Debug, Clone, PartialEq)]
123pub struct DenseParams {
124 pub n_inputs: usize,
125 pub n_neurons: usize,
126 pub data_width: u32,
127 /// Bitstream length for SC encoding.
128 pub stream_length: usize,
129 /// Base LFSR seed for input encoders (per-input stride applied automatically).
130 pub input_seed_base: u16,
131 /// Base LFSR seed for weight encoders.
132 pub weight_seed_base: u16,
133 /// Input-to-current mapping: y_min in Q-format.
134 pub y_min: i64,
135 /// Input-to-current mapping: y_max in Q-format.
136 pub y_max: i64,
137}
138
139impl Default for DenseParams {
140 fn default() -> Self {
141 Self {
142 n_inputs: 3,
143 n_neurons: 7,
144 data_width: 16,
145 stream_length: 1024,
146 input_seed_base: 0xACE1,
147 weight_seed_base: 0xBEEF,
148 y_min: 0,
149 y_max: 256, // 1.0 in Q8.8
150 }
151 }
152}
153
154// DCLS layer parameters
155
156/// Parameters for a delay-coded learnable-spike layer.
157/// Maps to `sc_dcls_layer_core` and its Q8.8 tent kernel.
158#[derive(Debug, Clone, PartialEq)]
159pub struct DclsParams {
160 /// Number of delayed taps sampled from the input spike stream.
161 pub n_taps: usize,
162 /// Q-format data width.
163 pub data_width: u32,
164 /// Q-format fractional bits.
165 pub fraction: u32,
166 /// Delay-line depth in spike samples.
167 pub delay_depth: usize,
168 /// Bit width used to address the delay line.
169 pub ptr_width: u32,
170 /// Per-tap delay offsets, ordered low-to-high in the emitted bus.
171 pub tap_offsets: Vec<u32>,
172}
173
174impl Default for DclsParams {
175 fn default() -> Self {
176 Self {
177 n_taps: 3,
178 data_width: 16,
179 fraction: 8,
180 delay_depth: 31,
181 ptr_width: 5,
182 tap_offsets: vec![0, 1, 2],
183 }
184 }
185}
186
187/// Reduce operation mode.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum ReduceMode {
190 Sum,
191 Max,
192}
193
194impl fmt::Display for ReduceMode {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 match self {
197 Self::Sum => write!(f, "sum"),
198 Self::Max => write!(f, "max"),
199 }
200 }
201}
202
203// Operations
204
205/// A single operation in the SC compute graph.
206///
207/// Each variant produces exactly one value identified by `id`.
208/// Input operands reference values produced by earlier operations.
209#[derive(Debug, Clone, PartialEq)]
210pub enum ScOp {
211 // Data flow
212 /// Module input port. No operands; value comes from external I/O.
213 Input {
214 id: ValueId,
215 name: String,
216 ty: ScType,
217 },
218
219 /// Module output port. Consumes one value; no new value produced.
220 /// `id` is a dummy (not referenced by other ops).
221 Output {
222 id: ValueId,
223 name: String,
224 source: ValueId,
225 },
226
227 /// Compile-time constant.
228 Constant {
229 id: ValueId,
230 value: ScConst,
231 ty: ScType,
232 },
233
234 // Bitstream primitives
235 /// Encode a probability (Rate or FixedPoint) into a Bitstream.
236 /// Maps to `sc_bitstream_encoder` in HDL.
237 Encode {
238 id: ValueId,
239 /// Input probability value.
240 prob: ValueId,
241 /// Bitstream length.
242 length: usize,
243 /// LFSR seed parameter name (resolved from graph params).
244 seed: u16,
245 },
246
247 /// Bitwise AND of two bitstreams (stochastic multiply).
248 /// Maps to `sc_bitstream_synapse` in HDL.
249 BitwiseAnd {
250 id: ValueId,
251 lhs: ValueId,
252 rhs: ValueId,
253 },
254
255 /// Population count: count 1-bits in a bitstream.
256 /// Part of `sc_dotproduct_to_current` in HDL.
257 Popcount { id: ValueId, input: ValueId },
258
259 // Neuron
260 /// Single LIF neuron step.
261 /// Maps to `sc_lif_neuron` in HDL.
262 LifStep {
263 id: ValueId,
264 /// Input current (FixedPoint).
265 current: ValueId,
266 /// Leak coefficient (FixedPoint).
267 leak: ValueId,
268 /// Input gain coefficient (FixedPoint).
269 gain: ValueId,
270 /// External noise (FixedPoint, can be zero constant).
271 noise: ValueId,
272 /// Neuron parameters.
273 params: LifParams,
274 },
275
276 // Compound operations
277 /// Dense SC layer: N_INPUTS → N_NEURONS with full SC pipeline.
278 /// Maps to `sc_dense_layer_core` in HDL.
279 DenseForward {
280 id: ValueId,
281 /// Input values (`Vec<Rate>` or `Vec<FixedPoint>`).
282 inputs: ValueId,
283 /// Weight matrix (`Vec<Rate>` or `Vec<FixedPoint>`), row-major [n_neurons × n_inputs].
284 weights: ValueId,
285 /// Leak coefficient for all neurons.
286 leak: ValueId,
287 /// Gain coefficient for all neurons.
288 gain: ValueId,
289 /// Layer parameters.
290 params: DenseParams,
291 },
292
293 /// Delay-coded learnable-spike layer with a Q8.8 tent kernel.
294 /// Maps to `sc_dcls_layer_core` in HDL.
295 DclsLayer {
296 id: ValueId,
297 /// One-bit input spike stream.
298 spike: ValueId,
299 /// Per-tap Q8.8 weights, packed low-to-high.
300 weights: ValueId,
301 /// Q8.8 centre of the learnable tent in delay-index units.
302 centre: ValueId,
303 /// Q8.8 positive tent radius.
304 sigma: ValueId,
305 /// Layer parameters.
306 params: DclsParams,
307 },
308
309 // L2: XOR encoding for hyperdimensional computing
310 /// Bitwise XOR of two bitstreams (HDC binding).
311 BitwiseXor {
312 id: ValueId,
313 lhs: ValueId,
314 rhs: ValueId,
315 },
316
317 // L3: Aggregation
318 /// Reduce a vector to a scalar (Sum or Max).
319 Reduce {
320 id: ValueId,
321 input: ValueId,
322 mode: ReduceMode,
323 },
324
325 // L8-L10: Graph message-passing
326 /// Graph forward: input features × adjacency → aggregated output.
327 GraphForward {
328 id: ValueId,
329 features: ValueId,
330 adjacency: ValueId,
331 n_nodes: usize,
332 n_features: usize,
333 },
334
335 // L7: Softmax attention
336 /// Softmax attention: Q·K^T/sqrt(d) → softmax → ·V.
337 SoftmaxAttention {
338 id: ValueId,
339 q: ValueId,
340 k: ValueId,
341 v: ValueId,
342 dim_k: usize,
343 },
344
345 // L4: Phase dynamics
346 /// Single Kuramoto integration step: dθ/dt = ω + ΣK sin(θ_m - θ_n).
347 KuramotoStep {
348 id: ValueId,
349 phases: ValueId,
350 omega: ValueId,
351 coupling: ValueId,
352 dt: f64,
353 },
354
355 // Arithmetic (post-processing)
356 /// Scale a value by a constant: output = input * factor.
357 Scale {
358 id: ValueId,
359 input: ValueId,
360 factor: f64,
361 },
362
363 /// Offset a value by a constant: output = input + offset.
364 Offset {
365 id: ValueId,
366 input: ValueId,
367 offset: f64,
368 },
369
370 /// Integer division by a constant (for rate computation).
371 DivConst {
372 id: ValueId,
373 input: ValueId,
374 divisor: u64,
375 },
376}
377
378impl ScOp {
379 /// Return the ValueId produced by this operation.
380 pub fn result_id(&self) -> ValueId {
381 match self {
382 Self::Input { id, .. }
383 | Self::Output { id, .. }
384 | Self::Constant { id, .. }
385 | Self::Encode { id, .. }
386 | Self::BitwiseAnd { id, .. }
387 | Self::BitwiseXor { id, .. }
388 | Self::Popcount { id, .. }
389 | Self::Reduce { id, .. }
390 | Self::LifStep { id, .. }
391 | Self::DenseForward { id, .. }
392 | Self::DclsLayer { id, .. }
393 | Self::GraphForward { id, .. }
394 | Self::SoftmaxAttention { id, .. }
395 | Self::KuramotoStep { id, .. }
396 | Self::Scale { id, .. }
397 | Self::Offset { id, .. }
398 | Self::DivConst { id, .. } => *id,
399 }
400 }
401
402 /// Return all ValueIds consumed by this operation.
403 pub fn operands(&self) -> Vec<ValueId> {
404 match self {
405 Self::Input { .. } | Self::Constant { .. } => vec![],
406 Self::Output { source, .. } => vec![*source],
407 Self::Encode { prob, .. } => vec![*prob],
408 Self::BitwiseAnd { lhs, rhs, .. } | Self::BitwiseXor { lhs, rhs, .. } => {
409 vec![*lhs, *rhs]
410 }
411 Self::Popcount { input, .. } | Self::Reduce { input, .. } => vec![*input],
412 Self::LifStep {
413 current,
414 leak,
415 gain,
416 noise,
417 ..
418 } => vec![*current, *leak, *gain, *noise],
419 Self::DenseForward {
420 inputs,
421 weights,
422 leak,
423 gain,
424 ..
425 } => vec![*inputs, *weights, *leak, *gain],
426 Self::DclsLayer {
427 spike,
428 weights,
429 centre,
430 sigma,
431 ..
432 } => vec![*spike, *weights, *centre, *sigma],
433 Self::GraphForward {
434 features,
435 adjacency,
436 ..
437 } => vec![*features, *adjacency],
438 Self::SoftmaxAttention { q, k, v, .. } => vec![*q, *k, *v],
439 Self::KuramotoStep {
440 phases,
441 omega,
442 coupling,
443 ..
444 } => vec![*phases, *omega, *coupling],
445 Self::Scale { input, .. }
446 | Self::Offset { input, .. }
447 | Self::DivConst { input, .. } => {
448 vec![*input]
449 }
450 }
451 }
452
453 /// Human-readable operation name for the text format.
454 pub fn op_name(&self) -> &'static str {
455 match self {
456 Self::Input { .. } => "sc.input",
457 Self::Output { .. } => "sc.output",
458 Self::Constant { .. } => "sc.constant",
459 Self::Encode { .. } => "sc.encode",
460 Self::BitwiseAnd { .. } => "sc.and",
461 Self::BitwiseXor { .. } => "sc.xor",
462 Self::Popcount { .. } => "sc.popcount",
463 Self::Reduce { .. } => "sc.reduce",
464 Self::LifStep { .. } => "sc.lif_step",
465 Self::DenseForward { .. } => "sc.dense_forward",
466 Self::DclsLayer { .. } => "sc.dcls_layer",
467 Self::GraphForward { .. } => "sc.graph_forward",
468 Self::SoftmaxAttention { .. } => "sc.softmax_attention",
469 Self::KuramotoStep { .. } => "sc.kuramoto_step",
470 Self::Scale { .. } => "sc.scale",
471 Self::Offset { .. } => "sc.offset",
472 Self::DivConst { .. } => "sc.div_const",
473 }
474 }
475}
476
477// Graph
478
479/// A complete SC compute graph.
480///
481/// Operations are stored in topological order: every operand
482/// referenced by an operation must be defined by an earlier operation.
483#[derive(Debug, Clone, PartialEq)]
484pub struct ScGraph {
485 /// Module name (used as the SV module name during emission).
486 pub name: String,
487 /// Operations in topological (definition) order.
488 pub ops: Vec<ScOp>,
489 /// Next available ValueId counter.
490 pub(crate) next_id: u32,
491}
492
493impl ScGraph {
494 /// Create a new empty graph.
495 pub fn new(name: impl Into<String>) -> Self {
496 Self {
497 name: name.into(),
498 ops: Vec::new(),
499 next_id: 0,
500 }
501 }
502
503 /// Allocate a fresh ValueId.
504 pub fn fresh_id(&mut self) -> ValueId {
505 let id = ValueId(self.next_id);
506 self.next_id += 1;
507 id
508 }
509
510 /// Append an operation and return its result ValueId.
511 pub fn push(&mut self, op: ScOp) -> ValueId {
512 let id = op.result_id();
513 self.ops.push(op);
514 id
515 }
516
517 /// Number of operations.
518 pub fn len(&self) -> usize {
519 self.ops.len()
520 }
521
522 /// Whether the graph is empty.
523 pub fn is_empty(&self) -> bool {
524 self.ops.is_empty()
525 }
526
527 /// Collect all Input operations.
528 pub fn inputs(&self) -> Vec<&ScOp> {
529 self.ops
530 .iter()
531 .filter(|op| matches!(op, ScOp::Input { .. }))
532 .collect()
533 }
534
535 /// Collect all Output operations.
536 pub fn outputs(&self) -> Vec<&ScOp> {
537 self.ops
538 .iter()
539 .filter(|op| matches!(op, ScOp::Output { .. }))
540 .collect()
541 }
542}