Skip to main content

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/// Reduce operation mode.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum ReduceMode {
157    Sum,
158    Max,
159}
160
161impl fmt::Display for ReduceMode {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::Sum => write!(f, "sum"),
165            Self::Max => write!(f, "max"),
166        }
167    }
168}
169
170// Operations
171
172/// A single operation in the SC compute graph.
173///
174/// Each variant produces exactly one value identified by `id`.
175/// Input operands reference values produced by earlier operations.
176#[derive(Debug, Clone, PartialEq)]
177pub enum ScOp {
178    // Data flow
179    /// Module input port. No operands; value comes from external I/O.
180    Input {
181        id: ValueId,
182        name: String,
183        ty: ScType,
184    },
185
186    /// Module output port. Consumes one value; no new value produced.
187    /// `id` is a dummy (not referenced by other ops).
188    Output {
189        id: ValueId,
190        name: String,
191        source: ValueId,
192    },
193
194    /// Compile-time constant.
195    Constant {
196        id: ValueId,
197        value: ScConst,
198        ty: ScType,
199    },
200
201    // Bitstream primitives
202    /// Encode a probability (Rate or FixedPoint) into a Bitstream.
203    /// Maps to `sc_bitstream_encoder` in HDL.
204    Encode {
205        id: ValueId,
206        /// Input probability value.
207        prob: ValueId,
208        /// Bitstream length.
209        length: usize,
210        /// LFSR seed parameter name (resolved from graph params).
211        seed: u16,
212    },
213
214    /// Bitwise AND of two bitstreams (stochastic multiply).
215    /// Maps to `sc_bitstream_synapse` in HDL.
216    BitwiseAnd {
217        id: ValueId,
218        lhs: ValueId,
219        rhs: ValueId,
220    },
221
222    /// Population count: count 1-bits in a bitstream.
223    /// Part of `sc_dotproduct_to_current` in HDL.
224    Popcount { id: ValueId, input: ValueId },
225
226    // Neuron
227    /// Single LIF neuron step.
228    /// Maps to `sc_lif_neuron` in HDL.
229    LifStep {
230        id: ValueId,
231        /// Input current (FixedPoint).
232        current: ValueId,
233        /// Leak coefficient (FixedPoint).
234        leak: ValueId,
235        /// Input gain coefficient (FixedPoint).
236        gain: ValueId,
237        /// External noise (FixedPoint, can be zero constant).
238        noise: ValueId,
239        /// Neuron parameters.
240        params: LifParams,
241    },
242
243    // Compound operations
244    /// Dense SC layer: N_INPUTS → N_NEURONS with full SC pipeline.
245    /// Maps to `sc_dense_layer_core` in HDL.
246    DenseForward {
247        id: ValueId,
248        /// Input values (`Vec<Rate>` or `Vec<FixedPoint>`).
249        inputs: ValueId,
250        /// Weight matrix (`Vec<Rate>` or `Vec<FixedPoint>`), row-major [n_neurons × n_inputs].
251        weights: ValueId,
252        /// Leak coefficient for all neurons.
253        leak: ValueId,
254        /// Gain coefficient for all neurons.
255        gain: ValueId,
256        /// Layer parameters.
257        params: DenseParams,
258    },
259
260    // L2: XOR encoding for hyperdimensional computing
261    /// Bitwise XOR of two bitstreams (HDC binding).
262    BitwiseXor {
263        id: ValueId,
264        lhs: ValueId,
265        rhs: ValueId,
266    },
267
268    // L3: Aggregation
269    /// Reduce a vector to a scalar (Sum or Max).
270    Reduce {
271        id: ValueId,
272        input: ValueId,
273        mode: ReduceMode,
274    },
275
276    // L8-L10: Graph message-passing
277    /// Graph forward: input features × adjacency → aggregated output.
278    GraphForward {
279        id: ValueId,
280        features: ValueId,
281        adjacency: ValueId,
282        n_nodes: usize,
283        n_features: usize,
284    },
285
286    // L7: Softmax attention
287    /// Softmax attention: Q·K^T/sqrt(d) → softmax → ·V.
288    SoftmaxAttention {
289        id: ValueId,
290        q: ValueId,
291        k: ValueId,
292        v: ValueId,
293        dim_k: usize,
294    },
295
296    // L4: Phase dynamics
297    /// Single Kuramoto integration step: dθ/dt = ω + ΣK sin(θ_m - θ_n).
298    KuramotoStep {
299        id: ValueId,
300        phases: ValueId,
301        omega: ValueId,
302        coupling: ValueId,
303        dt: f64,
304    },
305
306    // Arithmetic (post-processing)
307    /// Scale a value by a constant: output = input * factor.
308    Scale {
309        id: ValueId,
310        input: ValueId,
311        factor: f64,
312    },
313
314    /// Offset a value by a constant: output = input + offset.
315    Offset {
316        id: ValueId,
317        input: ValueId,
318        offset: f64,
319    },
320
321    /// Integer division by a constant (for rate computation).
322    DivConst {
323        id: ValueId,
324        input: ValueId,
325        divisor: u64,
326    },
327}
328
329impl ScOp {
330    /// Return the ValueId produced by this operation.
331    pub fn result_id(&self) -> ValueId {
332        match self {
333            Self::Input { id, .. }
334            | Self::Output { id, .. }
335            | Self::Constant { id, .. }
336            | Self::Encode { id, .. }
337            | Self::BitwiseAnd { id, .. }
338            | Self::BitwiseXor { id, .. }
339            | Self::Popcount { id, .. }
340            | Self::Reduce { id, .. }
341            | Self::LifStep { id, .. }
342            | Self::DenseForward { id, .. }
343            | Self::GraphForward { id, .. }
344            | Self::SoftmaxAttention { id, .. }
345            | Self::KuramotoStep { id, .. }
346            | Self::Scale { id, .. }
347            | Self::Offset { id, .. }
348            | Self::DivConst { id, .. } => *id,
349        }
350    }
351
352    /// Return all ValueIds consumed by this operation.
353    pub fn operands(&self) -> Vec<ValueId> {
354        match self {
355            Self::Input { .. } | Self::Constant { .. } => vec![],
356            Self::Output { source, .. } => vec![*source],
357            Self::Encode { prob, .. } => vec![*prob],
358            Self::BitwiseAnd { lhs, rhs, .. } | Self::BitwiseXor { lhs, rhs, .. } => {
359                vec![*lhs, *rhs]
360            }
361            Self::Popcount { input, .. } | Self::Reduce { input, .. } => vec![*input],
362            Self::LifStep {
363                current,
364                leak,
365                gain,
366                noise,
367                ..
368            } => vec![*current, *leak, *gain, *noise],
369            Self::DenseForward {
370                inputs,
371                weights,
372                leak,
373                gain,
374                ..
375            } => vec![*inputs, *weights, *leak, *gain],
376            Self::GraphForward {
377                features,
378                adjacency,
379                ..
380            } => vec![*features, *adjacency],
381            Self::SoftmaxAttention { q, k, v, .. } => vec![*q, *k, *v],
382            Self::KuramotoStep {
383                phases,
384                omega,
385                coupling,
386                ..
387            } => vec![*phases, *omega, *coupling],
388            Self::Scale { input, .. }
389            | Self::Offset { input, .. }
390            | Self::DivConst { input, .. } => {
391                vec![*input]
392            }
393        }
394    }
395
396    /// Human-readable operation name for the text format.
397    pub fn op_name(&self) -> &'static str {
398        match self {
399            Self::Input { .. } => "sc.input",
400            Self::Output { .. } => "sc.output",
401            Self::Constant { .. } => "sc.constant",
402            Self::Encode { .. } => "sc.encode",
403            Self::BitwiseAnd { .. } => "sc.and",
404            Self::BitwiseXor { .. } => "sc.xor",
405            Self::Popcount { .. } => "sc.popcount",
406            Self::Reduce { .. } => "sc.reduce",
407            Self::LifStep { .. } => "sc.lif_step",
408            Self::DenseForward { .. } => "sc.dense_forward",
409            Self::GraphForward { .. } => "sc.graph_forward",
410            Self::SoftmaxAttention { .. } => "sc.softmax_attention",
411            Self::KuramotoStep { .. } => "sc.kuramoto_step",
412            Self::Scale { .. } => "sc.scale",
413            Self::Offset { .. } => "sc.offset",
414            Self::DivConst { .. } => "sc.div_const",
415        }
416    }
417}
418
419// Graph
420
421/// A complete SC compute graph.
422///
423/// Operations are stored in topological order: every operand
424/// referenced by an operation must be defined by an earlier operation.
425#[derive(Debug, Clone, PartialEq)]
426pub struct ScGraph {
427    /// Module name (used as the SV module name during emission).
428    pub name: String,
429    /// Operations in topological (definition) order.
430    pub ops: Vec<ScOp>,
431    /// Next available ValueId counter.
432    pub(crate) next_id: u32,
433}
434
435impl ScGraph {
436    /// Create a new empty graph.
437    pub fn new(name: impl Into<String>) -> Self {
438        Self {
439            name: name.into(),
440            ops: Vec::new(),
441            next_id: 0,
442        }
443    }
444
445    /// Allocate a fresh ValueId.
446    pub fn fresh_id(&mut self) -> ValueId {
447        let id = ValueId(self.next_id);
448        self.next_id += 1;
449        id
450    }
451
452    /// Append an operation and return its result ValueId.
453    pub fn push(&mut self, op: ScOp) -> ValueId {
454        let id = op.result_id();
455        self.ops.push(op);
456        id
457    }
458
459    /// Number of operations.
460    pub fn len(&self) -> usize {
461        self.ops.len()
462    }
463
464    /// Whether the graph is empty.
465    pub fn is_empty(&self) -> bool {
466        self.ops.is_empty()
467    }
468
469    /// Collect all Input operations.
470    pub fn inputs(&self) -> Vec<&ScOp> {
471        self.ops
472            .iter()
473            .filter(|op| matches!(op, ScOp::Input { .. }))
474            .collect()
475    }
476
477    /// Collect all Output operations.
478    pub fn outputs(&self) -> Vec<&ScOp> {
479        self.ops
480            .iter()
481            .filter(|op| matches!(op, ScOp::Output { .. }))
482            .collect()
483    }
484}