Skip to content

Export

Model export to standard interchange formats.

Two ONNX-oriented export paths are maintained:

  • SCOnnxExporter — file-oriented exporter for SC networks that writes ONNX protobuf files when the optional onnx dependency is installed and JSON sidecars for lightweight deployment.
  • sc_neurocore.export.onnx_export.ONNXExporter — dependency-free graph exporter for SC-IR-style nodes. It emits a JSON-serializable ONNXGraph envelope using the custom sc.neurocore domain.
Python
from sc_neurocore.export import SCOnnxExporter

exporter = SCOnnxExporter()
exporter.export(model, "model.onnx")

For dependency-free graph export, use the SC-IR graph exporter directly:

Python
from sc_neurocore.export.onnx_export import ONNXExporter

graph = ONNXExporter().export(ir_graph, {"input_a": (128, 1024)})
payload = graph.to_dict()

Final graph output metadata follows the actual final emitted node. For example, a final SC_POPCOUNT node produces an ONNX int32 tensor (elem_type=6), while stochastic bitstream outputs remain bool tensors (elem_type=9). Mapped SC-IR node types that do not have a shape inference rule fail closed instead of silently emitting a guessed (1,) output.

For MLIR/SSA lowering, use the compiler exporter on the same graph-style surface:

Python
from sc_neurocore.export.compiler_export import CompilerExporter

mlir_text = CompilerExporter().export_to_mlir(ir_graph, {"input_a": (128, 1024)})

CompilerExporter supports the mlir target and validates the graph before emission. Empty graphs, duplicate node IDs, duplicate output edges, unsupported node types, wrong node arity, missing external input shapes, non-positive tensor dimensions, and output names that collide with graph inputs raise ValueError before SSA text is emitted. MLIR-facing input names are validated with the shared HDL identifier guard; invalid identifiers fail closed instead of being rewritten.

sc_neurocore.export.onnx_exporter

ONNX export for SC networks.

Supports two formats: - .onnx (protobuf) — standard ONNX ModelProto via onnx library - .json — legacy JSON schema (no external dependencies)

SC layers use stochastic bitstream ops not in the ONNX standard. We map them to a custom domain sc_neurocore with op types SC_Dense and SC_Custom.

SCOnnxExporter

Export SC networks to ONNX protobuf or legacy JSON.

Source code in src/sc_neurocore/export/onnx_exporter.py
Python
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
class SCOnnxExporter:
    """Export SC networks to ONNX protobuf or legacy JSON."""

    @staticmethod
    def export(layers: list[Any], filename: str) -> None:
        """Export *layers* to *filename*.

        File extension selects format: ``.onnx`` → protobuf,
        anything else → legacy JSON.
        """
        if filename.endswith(".onnx"):
            SCOnnxExporter._export_protobuf(layers, filename)
        else:
            SCOnnxExporter._export_json(layers, filename)

    # ── protobuf path ────────────────────────────────────────────

    @staticmethod
    def _export_protobuf(layers: list[Any], filename: str) -> None:
        try:
            import onnx
            from onnx import TensorProto, helper, numpy_helper
        except ImportError:
            from sc_neurocore.exceptions import SCDependencyError

            raise SCDependencyError(
                "ONNX protobuf export requires onnx: pip install sc-neurocore[full]"
            )

        nodes: list[Any] = []
        initializers: list[Any] = []
        prev = "input_0"
        n_in = layers[0].n_inputs

        for i, layer in enumerate(layers):
            out = f"output_{i}"
            attrs: dict[str, Any] = {
                "n_neurons": getattr(layer, "n_neurons", -1),
                "length": getattr(layer, "length", 256),
            }

            node = helper.make_node(
                _classify_op(layer),
                inputs=[prev],
                outputs=[out],
                name=f"Layer_{i}",
                domain=_CUSTOM_DOMAIN,
                **{k: v for k, v in attrs.items()},
            )
            nodes.append(node)

            if hasattr(layer, "weights"):
                w_name = f"Layer_{i}_weights"
                tensor = numpy_helper.from_array(
                    np.asarray(layer.weights, dtype=np.float32), name=w_name
                )
                initializers.append(tensor)
                node.input.append(w_name)

            prev = out

        input_vi = helper.make_tensor_value_info("input_0", TensorProto.FLOAT, ["batch", n_in])
        output_vi = helper.make_tensor_value_info(prev, TensorProto.FLOAT, None)

        graph = helper.make_graph(
            nodes,
            "sc_neurocore_graph",
            [input_vi],
            [output_vi],
            initializer=initializers,
        )

        opset_imports = [
            helper.make_opsetid("", 17),
            helper.make_opsetid(_CUSTOM_DOMAIN, _OPSET_VERSION),
        ]

        model = helper.make_model(graph, opset_imports=opset_imports)
        model.producer_name = "sc-neurocore"
        model.ir_version = 8

        onnx.save(model, filename)
        logger.info("Exported ONNX protobuf to %s", filename)

    # ── legacy JSON path ─────────────────────────────────────────

    @staticmethod
    def _export_json(layers: list[Any], filename: str) -> None:
        graph: dict[str, Any] = {
            "producer_name": "sc-neurocore",
            "producer_version": "2.0.0",
            "nodes": [],
            "inputs": [],
            "outputs": [],
        }

        graph["inputs"].append(
            {
                "name": "input_0",
                "type": "tensor(float)",
                "shape": ["batch", layers[0].n_inputs],
            }
        )

        prev = "input_0"
        for i, layer in enumerate(layers):
            out = f"output_{i}"
            node = {
                "op_type": _classify_op(layer),
                "name": f"Layer_{i}",
                "input": [prev],
                "output": [out],
                "attributes": {
                    "n_neurons": getattr(layer, "n_neurons", -1),
                    "length": getattr(layer, "length", 256),
                },
            }
            if hasattr(layer, "weights"):
                node["attributes"]["has_weights"] = True  # type: ignore[index]
                np.save(f"{filename}_layer_{i}_weights.npy", layer.weights)
                node["attributes"]["weights_file"] = f"{filename}_layer_{i}_weights.npy"  # type: ignore[index]
            graph["nodes"].append(node)
            prev = out

        graph["outputs"].append({"name": prev, "type": "tensor(float)"})

        try:
            with open(filename, "w") as f:
                json.dump(graph, f, indent=4)
        except OSError as exc:
            logger.error("Failed to export ONNX schema to %s: %s", filename, exc)
            raise

        logger.info("Exported ONNX-schema JSON to %s", filename)

export(layers, filename) staticmethod

Export layers to filename.

File extension selects format: .onnx → protobuf, anything else → legacy JSON.

Source code in src/sc_neurocore/export/onnx_exporter.py
Python
44
45
46
47
48
49
50
51
52
53
54
@staticmethod
def export(layers: list[Any], filename: str) -> None:
    """Export *layers* to *filename*.

    File extension selects format: ``.onnx`` → protobuf,
    anything else → legacy JSON.
    """
    if filename.endswith(".onnx"):
        SCOnnxExporter._export_protobuf(layers, filename)
    else:
        SCOnnxExporter._export_json(layers, filename)

sc_neurocore.export.onnx_export

Zero-dependency ONNX exporter for SC-NeuroCore IR graphs.

Maps SC-IR nodes to ONNX-compatible graph representation with custom operator set sc.neurocore. No ONNX runtime or protobuf dependency required — emits a self-contained dict-based graph that can be serialized to JSON or consumed by downstream tools.

ONNXTensorType dataclass

Tensor element type and static shape for the JSON ONNX model.

Parameters

elem_type: ONNX tensor element type id. shape: Static tensor dimensions.

Source code in src/sc_neurocore/export/onnx_export.py
Python
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@dataclass
class ONNXTensorType:
    """Tensor element type and static shape for the JSON ONNX model.

    Parameters
    ----------
    elem_type:
        ONNX tensor element type id.
    shape:
        Static tensor dimensions.
    """

    elem_type: int  # 1=float, 2=uint8, 3=int8, 6=int32, 7=int64, 9=bool
    shape: tuple[int, ...]

    def to_dict(self) -> dict[str, Any]:
        """Return the ONNX tensor-type dictionary representation."""
        return {
            "elem_type": self.elem_type,
            "shape": {"dim": [{"dim_value": d} for d in self.shape]},
        }

to_dict()

Return the ONNX tensor-type dictionary representation.

Source code in src/sc_neurocore/export/onnx_export.py
Python
43
44
45
46
47
48
def to_dict(self) -> dict[str, Any]:
    """Return the ONNX tensor-type dictionary representation."""
    return {
        "elem_type": self.elem_type,
        "shape": {"dim": [{"dim_value": d} for d in self.shape]},
    }

ONNXNode dataclass

Custom-domain ONNX node for a lowered stochastic-computing operation.

Parameters

op_type: ONNX operator type. domain: Operator domain. inputs: Input tensor names. outputs: Output tensor names. name: Stable node name. attributes: Optional scalar operator attributes.

Source code in src/sc_neurocore/export/onnx_export.py
Python
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@dataclass
class ONNXNode:
    """Custom-domain ONNX node for a lowered stochastic-computing operation.

    Parameters
    ----------
    op_type:
        ONNX operator type.
    domain:
        Operator domain.
    inputs:
        Input tensor names.
    outputs:
        Output tensor names.
    name:
        Stable node name.
    attributes:
        Optional scalar operator attributes.
    """

    op_type: str
    domain: str
    inputs: list[str]
    outputs: list[str]
    name: str
    attributes: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        """Return the ONNX node dictionary representation."""
        d: dict[str, Any] = {
            "op_type": self.op_type,
            "domain": self.domain,
            "input": self.inputs,
            "output": self.outputs,
            "name": self.name,
        }
        if self.attributes:
            d["attribute"] = [
                {"name": k, "type": "FLOAT" if isinstance(v, float) else "INT", "value": v}
                for k, v in self.attributes.items()
            ]
        return d

to_dict()

Return the ONNX node dictionary representation.

Source code in src/sc_neurocore/export/onnx_export.py
Python
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def to_dict(self) -> dict[str, Any]:
    """Return the ONNX node dictionary representation."""
    d: dict[str, Any] = {
        "op_type": self.op_type,
        "domain": self.domain,
        "input": self.inputs,
        "output": self.outputs,
        "name": self.name,
    }
    if self.attributes:
        d["attribute"] = [
            {"name": k, "type": "FLOAT" if isinstance(v, float) else "INT", "value": v}
            for k, v in self.attributes.items()
        ]
    return d

ONNXGraph dataclass

JSON-serializable ONNX model envelope.

Parameters

name: ONNX graph name. nodes: Lowered ONNX nodes. inputs: Named graph inputs and tensor types. outputs: Named graph outputs and tensor types. metadata: String metadata entries attached to the model.

Source code in src/sc_neurocore/export/onnx_export.py
Python
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
@dataclass
class ONNXGraph:
    """JSON-serializable ONNX model envelope.

    Parameters
    ----------
    name:
        ONNX graph name.
    nodes:
        Lowered ONNX nodes.
    inputs:
        Named graph inputs and tensor types.
    outputs:
        Named graph outputs and tensor types.
    metadata:
        String metadata entries attached to the model.
    """

    name: str
    nodes: list[ONNXNode] = field(default_factory=list)
    inputs: list[tuple[str, ONNXTensorType]] = field(default_factory=list)
    outputs: list[tuple[str, ONNXTensorType]] = field(default_factory=list)
    metadata: dict[str, str] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        """Return the complete ONNX model dictionary representation."""
        return {
            "ir_version": 9,
            "opset_import": [
                {"domain": "", "version": ONNX_OPSET_VERSION},
                {"domain": SCPN_DOMAIN, "version": SCPN_OPSET_VERSION},
            ],
            "graph": {
                "name": self.name,
                "node": [n.to_dict() for n in self.nodes],
                "input": [
                    {"name": name, "type": {"tensor_type": tt.to_dict()}}
                    for name, tt in self.inputs
                ],
                "output": [
                    {"name": name, "type": {"tensor_type": tt.to_dict()}}
                    for name, tt in self.outputs
                ],
            },
            "metadata_props": [{"key": k, "value": v} for k, v in self.metadata.items()],
        }

    def to_json(self, indent: int = 2) -> str:
        """Return the complete ONNX model as formatted JSON."""
        return json.dumps(self.to_dict(), indent=indent)

to_dict()

Return the complete ONNX model dictionary representation.

Source code in src/sc_neurocore/export/onnx_export.py
Python
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def to_dict(self) -> dict[str, Any]:
    """Return the complete ONNX model dictionary representation."""
    return {
        "ir_version": 9,
        "opset_import": [
            {"domain": "", "version": ONNX_OPSET_VERSION},
            {"domain": SCPN_DOMAIN, "version": SCPN_OPSET_VERSION},
        ],
        "graph": {
            "name": self.name,
            "node": [n.to_dict() for n in self.nodes],
            "input": [
                {"name": name, "type": {"tensor_type": tt.to_dict()}}
                for name, tt in self.inputs
            ],
            "output": [
                {"name": name, "type": {"tensor_type": tt.to_dict()}}
                for name, tt in self.outputs
            ],
        },
        "metadata_props": [{"key": k, "value": v} for k, v in self.metadata.items()],
    }

to_json(indent=2)

Return the complete ONNX model as formatted JSON.

Source code in src/sc_neurocore/export/onnx_export.py
Python
142
143
144
def to_json(self, indent: int = 2) -> str:
    """Return the complete ONNX model as formatted JSON."""
    return json.dumps(self.to_dict(), indent=indent)

ONNXExporter

Export SC-NeuroCore IR graphs to ONNX-compatible dictionaries.

Parameters

graph_name: Name assigned to the emitted ONNX graph.

Source code in src/sc_neurocore/export/onnx_export.py
Python
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
class ONNXExporter:
    """Export SC-NeuroCore IR graphs to ONNX-compatible dictionaries.

    Parameters
    ----------
    graph_name:
        Name assigned to the emitted ONNX graph.
    """

    def __init__(self, graph_name: str = "sc_network") -> None:
        self.graph_name = graph_name

    def _infer_type(self, node_type: str, shape: tuple[int, ...]) -> ONNXTensorType:
        if node_type == "SC_POPCOUNT":
            return ONNXTensorType(elem_type=6, shape=shape)  # int32
        return ONNXTensorType(elem_type=9, shape=shape)  # bool for SC bitstreams

    def _infer_shape(
        self,
        node_type: str,
        inputs: list[str],
        shapes: dict[str, tuple[int, ...]],
    ) -> tuple[int, ...]:
        if node_type in ("SC_AND", "SC_MUX", "LIF_MEMBRANE"):
            return shapes.get(inputs[0], (1,))
        if node_type == "SC_POPCOUNT":
            in_shape = shapes.get(inputs[0], (1,))
            return in_shape[:-1] + (1,) if len(in_shape) > 1 else (1,)
        raise ValueError(f"No ONNX shape rule for mapped SC-IR node type {node_type!r}")

    def export(
        self,
        ir_graph: Any,
        input_shapes: dict[str, tuple[int, ...]],
        metadata: dict[str, str] | None = None,
    ) -> ONNXGraph:
        """Convert an SC-IR graph to an ONNX graph representation.

        Parameters
        ----------
        ir_graph:
            SC-IR graph-like object with a ``nodes`` sequence.
        input_shapes:
            Mapping from input tensor names to static dimensions.
        metadata:
            Optional string metadata to attach to the emitted graph.

        Returns
        -------
        ONNXGraph
            JSON-serializable ONNX graph envelope.

        Raises
        ------
        ValueError
            If a mapped SC-IR operator has no shape inference rule.
        """
        from sc_neurocore.export.compiler_export import CompilerExporter

        exporter = CompilerExporter()
        sorted_nodes = exporter._topological_sort(ir_graph.nodes)

        graph = ONNXGraph(name=self.graph_name, metadata=metadata or {})

        # Register inputs
        for inp_name, shape in input_shapes.items():
            graph.inputs.append((inp_name, ONNXTensorType(elem_type=9, shape=shape)))

        # Track shapes for inference
        shapes: dict[str, tuple[int, ...]] = dict(input_shapes)

        # Convert nodes
        last_output = ""
        last_node_type = ""
        for node in sorted_nodes:
            op = SC_OP_MAP.get(node.type)
            if op is None:
                continue

            out_shape = self._infer_shape(node.type, list(node.inputs), shapes)
            shapes[node.output] = out_shape

            # Build ONNX node
            attrs: dict[str, Any] = {}
            if node.type == "LIF_MEMBRANE":
                attrs["threshold"] = getattr(node, "threshold", 1.0)
                attrs["leak"] = getattr(node, "leak", 0.9)

            onnx_node = ONNXNode(
                op_type=op,
                domain=SCPN_DOMAIN,
                inputs=list(node.inputs),
                outputs=[node.output],
                name=f"{op}_{node.id}",
                attributes=attrs,
            )
            graph.nodes.append(onnx_node)
            last_output = node.output
            last_node_type = node.type

        # Register final output
        if last_output and last_output in shapes:
            graph.outputs.append(
                (last_output, self._infer_type(last_node_type, shapes[last_output]))
            )

        return graph

export(ir_graph, input_shapes, metadata=None)

Convert an SC-IR graph to an ONNX graph representation.

Parameters

ir_graph: SC-IR graph-like object with a nodes sequence. input_shapes: Mapping from input tensor names to static dimensions. metadata: Optional string metadata to attach to the emitted graph.

Returns

ONNXGraph JSON-serializable ONNX graph envelope.

Raises

ValueError If a mapped SC-IR operator has no shape inference rule.

Source code in src/sc_neurocore/export/onnx_export.py
Python
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def export(
    self,
    ir_graph: Any,
    input_shapes: dict[str, tuple[int, ...]],
    metadata: dict[str, str] | None = None,
) -> ONNXGraph:
    """Convert an SC-IR graph to an ONNX graph representation.

    Parameters
    ----------
    ir_graph:
        SC-IR graph-like object with a ``nodes`` sequence.
    input_shapes:
        Mapping from input tensor names to static dimensions.
    metadata:
        Optional string metadata to attach to the emitted graph.

    Returns
    -------
    ONNXGraph
        JSON-serializable ONNX graph envelope.

    Raises
    ------
    ValueError
        If a mapped SC-IR operator has no shape inference rule.
    """
    from sc_neurocore.export.compiler_export import CompilerExporter

    exporter = CompilerExporter()
    sorted_nodes = exporter._topological_sort(ir_graph.nodes)

    graph = ONNXGraph(name=self.graph_name, metadata=metadata or {})

    # Register inputs
    for inp_name, shape in input_shapes.items():
        graph.inputs.append((inp_name, ONNXTensorType(elem_type=9, shape=shape)))

    # Track shapes for inference
    shapes: dict[str, tuple[int, ...]] = dict(input_shapes)

    # Convert nodes
    last_output = ""
    last_node_type = ""
    for node in sorted_nodes:
        op = SC_OP_MAP.get(node.type)
        if op is None:
            continue

        out_shape = self._infer_shape(node.type, list(node.inputs), shapes)
        shapes[node.output] = out_shape

        # Build ONNX node
        attrs: dict[str, Any] = {}
        if node.type == "LIF_MEMBRANE":
            attrs["threshold"] = getattr(node, "threshold", 1.0)
            attrs["leak"] = getattr(node, "leak", 0.9)

        onnx_node = ONNXNode(
            op_type=op,
            domain=SCPN_DOMAIN,
            inputs=list(node.inputs),
            outputs=[node.output],
            name=f"{op}_{node.id}",
            attributes=attrs,
        )
        graph.nodes.append(onnx_node)
        last_output = node.output
        last_node_type = node.type

    # Register final output
    if last_output and last_output in shapes:
        graph.outputs.append(
            (last_output, self._infer_type(last_node_type, shapes[last_output]))
        )

    return graph

sc_neurocore.export.compiler_export

SSA-based TVM/MLIR compiler frontend for SC-NeuroCore IR graphs.

Exports SNN dataflow graphs to MLIR text via topological traversal with strict SSA register allocation and shape inference.

SSAEnvironment

Manages Static Single Assignment (SSA) registers for MLIR/Relay.

Source code in src/sc_neurocore/export/compiler_export.py
Python
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class SSAEnvironment:
    """Manages Static Single Assignment (SSA) registers for MLIR/Relay."""

    def __init__(self) -> None:
        self.registers: dict[str, str] = {}
        self.counter: int = 0

    def allocate(self, edge_name: str) -> str:
        """Allocate and bind the next SSA register for an SC-IR edge."""
        reg = f"%{self.counter}"
        self.counter += 1
        self.registers[edge_name] = reg
        return reg

    def get(self, edge_name: str) -> str:
        """Return an allocated register or validate an external input register."""
        if edge_name not in self.registers:
            return f"%{sanitize_ident(edge_name, context='input name')}"
        return self.registers[edge_name]

allocate(edge_name)

Allocate and bind the next SSA register for an SC-IR edge.

Source code in src/sc_neurocore/export/compiler_export.py
Python
59
60
61
62
63
64
def allocate(self, edge_name: str) -> str:
    """Allocate and bind the next SSA register for an SC-IR edge."""
    reg = f"%{self.counter}"
    self.counter += 1
    self.registers[edge_name] = reg
    return reg

get(edge_name)

Return an allocated register or validate an external input register.

Source code in src/sc_neurocore/export/compiler_export.py
Python
66
67
68
69
70
def get(self, edge_name: str) -> str:
    """Return an allocated register or validate an external input register."""
    if edge_name not in self.registers:
        return f"%{sanitize_ident(edge_name, context='input name')}"
    return self.registers[edge_name]

ShapeInference

Infers tensor shapes dynamically across the SNN graph.

Source code in src/sc_neurocore/export/compiler_export.py
Python
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
class ShapeInference:
    """Infers tensor shapes dynamically across the SNN graph."""

    def __init__(self, input_shapes: Mapping[str, tuple[int, ...]]) -> None:
        self.shapes = dict(input_shapes)

    def infer(self, node: _IRNode) -> None:
        """Infer and store the output shape for one supported SC-IR node."""
        expected_arity = _NODE_INPUT_ARITY.get(node.type)
        if expected_arity is None:
            raise ValueError(f"Unsupported SC-IR node type {node.type!r} for MLIR export.")
        if len(node.inputs) != expected_arity:
            raise ValueError(
                f"SC-IR node {node.id!r} of type {node.type!r} expects "
                f"{expected_arity} input edge(s), got {len(node.inputs)}."
            )

        input_shapes = [self._shape_for(node, edge_name) for edge_name in node.inputs]
        if node.type == "SC_AND" or node.type == "SC_MUX":
            self.shapes[node.output] = input_shapes[0]
        elif node.type == "SC_POPCOUNT":
            self.shapes[node.output] = input_shapes[0][:-1] + (1,)
        elif node.type == "LIF_MEMBRANE":
            self.shapes[node.output] = input_shapes[0]

    def _shape_for(self, node: _IRNode, edge_name: str) -> tuple[int, ...]:
        try:
            return self.shapes[edge_name]
        except KeyError as exc:
            raise ValueError(
                f"Missing input shape for edge {edge_name!r} consumed by node {node.id!r}."
            ) from exc

infer(node)

Infer and store the output shape for one supported SC-IR node.

Source code in src/sc_neurocore/export/compiler_export.py
Python
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def infer(self, node: _IRNode) -> None:
    """Infer and store the output shape for one supported SC-IR node."""
    expected_arity = _NODE_INPUT_ARITY.get(node.type)
    if expected_arity is None:
        raise ValueError(f"Unsupported SC-IR node type {node.type!r} for MLIR export.")
    if len(node.inputs) != expected_arity:
        raise ValueError(
            f"SC-IR node {node.id!r} of type {node.type!r} expects "
            f"{expected_arity} input edge(s), got {len(node.inputs)}."
        )

    input_shapes = [self._shape_for(node, edge_name) for edge_name in node.inputs]
    if node.type == "SC_AND" or node.type == "SC_MUX":
        self.shapes[node.output] = input_shapes[0]
    elif node.type == "SC_POPCOUNT":
        self.shapes[node.output] = input_shapes[0][:-1] + (1,)
    elif node.type == "LIF_MEMBRANE":
        self.shapes[node.output] = input_shapes[0]

CompilerExporter

Export SC-IR graph-like objects to strict SSA MLIR text.

Source code in src/sc_neurocore/export/compiler_export.py
Python
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
class CompilerExporter:
    """Export SC-IR graph-like objects to strict SSA MLIR text."""

    def __init__(self, target: str = "mlir") -> None:
        """Create an exporter for a supported compiler backend target."""
        if target not in _SUPPORTED_TARGETS:
            supported = ", ".join(sorted(_SUPPORTED_TARGETS))
            raise ValueError(
                f"Unsupported compiler export target {target!r}; supported targets: {supported}."
            )
        self.target = target

    def _topological_sort(self, nodes: Sequence[_IRNode]) -> list[_IRNode]:
        """Kahn's algorithm for topological sorting of the DAG."""
        self._validate_unique_edges(nodes)
        in_degree = {n.id: 0 for n in nodes}
        node_map = {n.id: n for n in nodes}
        adj_list: dict[str, list[str]] = {n.id: [] for n in nodes}
        output_to_node_id = {n.output: n.id for n in nodes}

        for n in nodes:
            for inp in n.inputs:
                if inp in output_to_node_id:
                    src_id = output_to_node_id[inp]
                    adj_list[src_id].append(n.id)
                    in_degree[n.id] += 1

        queue = [n_id for n_id, deg in in_degree.items() if deg == 0]
        sorted_nodes = []

        while queue:
            curr_id = queue.pop(0)
            curr_node = node_map[curr_id]
            sorted_nodes.append(curr_node)

            for neighbor in adj_list[curr_id]:
                in_degree[neighbor] -= 1
                if in_degree[neighbor] == 0:
                    queue.append(neighbor)

        if len(sorted_nodes) != len(nodes):
            raise ValueError("Cycle detected in SNN IR graph. Cannot lower to SSA.")

        return sorted_nodes

    def _format_mlir_type(self, shape: tuple[int, ...], dtype: str = "i1") -> str:
        """Render an MLIR scalar or tensor type for a validated static shape."""
        if any(dim <= 0 for dim in shape):
            raise ValueError(f"MLIR tensor dimensions must be positive; got {shape!r}.")
        if not shape or shape == (1,):
            return dtype
        dims = "x".join(map(str, shape))
        return f"tensor<{dims}x{dtype}>"

    def export_to_mlir(
        self, ir_graph: _IRGraph, input_shapes: Mapping[str, tuple[int, ...]]
    ) -> str:
        """Emit strict SSA MLIR text via topological traversal."""
        input_shape_map = dict(input_shapes)
        nodes = tuple(ir_graph.nodes)
        self._validate_output_input_collisions(nodes, input_shape_map)
        sorted_nodes = self._topological_sort(nodes)
        if not sorted_nodes:
            raise ValueError(
                "Cannot export MLIR for a graph with no nodes; expected at least one node."
            )
        self._validate_export_contract(sorted_nodes, input_shape_map)

        ssa = SSAEnvironment()
        shape_inf = ShapeInference(input_shape_map)
        safe_input_names = {
            inp: sanitize_ident(inp, context="input name") for inp in input_shape_map
        }

        mlir_lines = ["module {"]

        sig_args = ", ".join(
            [
                f"%{safe_input_names[inp]}: {self._format_mlir_type(shape)}"
                for inp, shape in input_shape_map.items()
            ]
        )
        mlir_lines.append(f"  func.func @sc_network_forward({sig_args}) {{")

        last_reg = ""
        last_shape = ""

        for node in sorted_nodes:
            shape_inf.infer(node)
            out_shape = shape_inf.shapes[node.output]
            out_type = self._format_mlir_type(
                out_shape, "i1" if "POPCOUNT" not in node.type else "i32"
            )

            in_regs = [ssa.get(inp) for inp in node.inputs]
            out_reg = ssa.allocate(node.output)

            last_reg = out_reg
            last_shape = out_type

            if node.type == "SC_AND":
                mlir_lines.append(
                    f"    {out_reg} = scpn.and {in_regs[0]}, {in_regs[1]} : {out_type}"
                )
            elif node.type == "SC_MUX":
                mlir_lines.append(
                    f"    {out_reg} = scpn.mux {in_regs[0]}, {in_regs[1]}, {in_regs[2]} : {out_type}"
                )
            elif node.type == "SC_POPCOUNT":
                in_type = self._format_mlir_type(shape_inf.shapes[node.inputs[0]], "i1")
                mlir_lines.append(
                    f"    {out_reg} = scpn.popcount {in_regs[0]} : ({in_type}) -> {out_type}"
                )
            elif node.type == "LIF_MEMBRANE":
                th = getattr(node, "threshold", 1.0)
                lk = getattr(node, "leak", 0.9)
                mlir_lines.append(
                    f"    {out_reg} = scpn.lif {in_regs[0]} {{threshold={th}, leak={lk}}} : {out_type}"
                )

        mlir_lines.append(f"    return {last_reg} : {last_shape}")
        mlir_lines.append("  }")
        mlir_lines.append("}")
        return "\n".join(mlir_lines)

    def _validate_unique_edges(self, nodes: Sequence[_IRNode]) -> None:
        seen_node_ids: set[str] = set()
        seen_outputs: set[str] = set()
        for node in nodes:
            if node.id in seen_node_ids:
                raise ValueError(f"Duplicate node id {node.id!r} in SC-IR graph.")
            if node.output in seen_outputs:
                raise ValueError(f"Duplicate output edge {node.output!r} in SC-IR graph.")
            seen_node_ids.add(node.id)
            seen_outputs.add(node.output)

    def _validate_output_input_collisions(
        self, nodes: Sequence[_IRNode], input_shapes: Mapping[str, tuple[int, ...]]
    ) -> None:
        graph_inputs = set(input_shapes)
        for node in nodes:
            if node.output in graph_inputs:
                raise ValueError(f"SC-IR output edge {node.output!r} collides with graph input.")

    def _validate_export_contract(
        self, nodes: Sequence[_IRNode], input_shapes: Mapping[str, tuple[int, ...]]
    ) -> None:
        produced_edges = {node.output for node in nodes}
        graph_inputs = set(input_shapes)
        for node in nodes:
            for edge_name in node.inputs:
                if edge_name not in produced_edges and edge_name not in graph_inputs:
                    raise ValueError(
                        f"Missing input shape for external edge {edge_name!r} "
                        f"consumed by node {node.id!r}."
                    )

__init__(target='mlir')

Create an exporter for a supported compiler backend target.

Source code in src/sc_neurocore/export/compiler_export.py
Python
110
111
112
113
114
115
116
117
def __init__(self, target: str = "mlir") -> None:
    """Create an exporter for a supported compiler backend target."""
    if target not in _SUPPORTED_TARGETS:
        supported = ", ".join(sorted(_SUPPORTED_TARGETS))
        raise ValueError(
            f"Unsupported compiler export target {target!r}; supported targets: {supported}."
        )
    self.target = target

export_to_mlir(ir_graph, input_shapes)

Emit strict SSA MLIR text via topological traversal.

Source code in src/sc_neurocore/export/compiler_export.py
Python
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def export_to_mlir(
    self, ir_graph: _IRGraph, input_shapes: Mapping[str, tuple[int, ...]]
) -> str:
    """Emit strict SSA MLIR text via topological traversal."""
    input_shape_map = dict(input_shapes)
    nodes = tuple(ir_graph.nodes)
    self._validate_output_input_collisions(nodes, input_shape_map)
    sorted_nodes = self._topological_sort(nodes)
    if not sorted_nodes:
        raise ValueError(
            "Cannot export MLIR for a graph with no nodes; expected at least one node."
        )
    self._validate_export_contract(sorted_nodes, input_shape_map)

    ssa = SSAEnvironment()
    shape_inf = ShapeInference(input_shape_map)
    safe_input_names = {
        inp: sanitize_ident(inp, context="input name") for inp in input_shape_map
    }

    mlir_lines = ["module {"]

    sig_args = ", ".join(
        [
            f"%{safe_input_names[inp]}: {self._format_mlir_type(shape)}"
            for inp, shape in input_shape_map.items()
        ]
    )
    mlir_lines.append(f"  func.func @sc_network_forward({sig_args}) {{")

    last_reg = ""
    last_shape = ""

    for node in sorted_nodes:
        shape_inf.infer(node)
        out_shape = shape_inf.shapes[node.output]
        out_type = self._format_mlir_type(
            out_shape, "i1" if "POPCOUNT" not in node.type else "i32"
        )

        in_regs = [ssa.get(inp) for inp in node.inputs]
        out_reg = ssa.allocate(node.output)

        last_reg = out_reg
        last_shape = out_type

        if node.type == "SC_AND":
            mlir_lines.append(
                f"    {out_reg} = scpn.and {in_regs[0]}, {in_regs[1]} : {out_type}"
            )
        elif node.type == "SC_MUX":
            mlir_lines.append(
                f"    {out_reg} = scpn.mux {in_regs[0]}, {in_regs[1]}, {in_regs[2]} : {out_type}"
            )
        elif node.type == "SC_POPCOUNT":
            in_type = self._format_mlir_type(shape_inf.shapes[node.inputs[0]], "i1")
            mlir_lines.append(
                f"    {out_reg} = scpn.popcount {in_regs[0]} : ({in_type}) -> {out_type}"
            )
        elif node.type == "LIF_MEMBRANE":
            th = getattr(node, "threshold", 1.0)
            lk = getattr(node, "leak", 0.9)
            mlir_lines.append(
                f"    {out_reg} = scpn.lif {in_regs[0]} {{threshold={th}, leak={lk}}} : {out_type}"
            )

    mlir_lines.append(f"    return {last_reg} : {last_shape}")
    mlir_lines.append("  }")
    mlir_lines.append("}")
    return "\n".join(mlir_lines)