Skip to main content

sc_neurocore_engine/ir/
graph.rs

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