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// 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}