Skip to content

SC-NIR API

SC-NIR is the SC-NeuroCore metadata layer for stochastic-computing semantics that plain NIR does not encode. It records bitstream length, stochastic encoding, stream signal kind, fixed-point precision, deterministic transform metadata, optional online-learning contracts for weight streams, random-source metadata, stream correlation constraints, and explicit hierarchy instance boundaries before a model reaches hardware compilation or experiment handoff.

The schema is intentionally strict. Unknown fields, missing fields, duplicate stream identifiers, invalid random-source metadata, dangling correlation references, and hierarchy ports that do not match an existing stream are rejected rather than ignored.

JSON Schema

The reference schema is tracked at:

Text Only
schemas/scnir/scnir.schema.json

Current schema version:

Text Only
sc-neurocore.scnir.v0.7

Each stream entry must provide:

Field Purpose
stream_id Stable identifier used by correlation constraints
layer Producer layer or graph node name
bitstream_length Positive integer SC stream length
encoding unipolar, bipolar, low-discrepancy, replay, LFSR, or hardware-source encoding
signal_kind Logical stream role: spike, analogue_state, or weight
delay_steps Explicit unit-delay count as a scalar integer or exact source-width integer vector; feed-forward streams use 0
transforms Ordered deterministic transforms applied to the stream, currently threshold comparators
precision Signedness, total bits, fractional bits, accumulator bits, rounding, overflow
source LFSR, Sobol, Halton, replay, or hardware source metadata
correlation_constraints Pairwise policy metadata between streams
online_learning null or a validated Online O(1) learning annotation on weight streams only

The top-level hierarchy array records bounded nested-hardware contracts. Each entry must provide a stable instance_id, synthesisable module_name, and at least one port. Each port records port_name, direction, stream_id, signal_kind, and bit_width; the referenced stream must exist and its signal_kind must match the port. Single-input/single-output nested NIR graphs that are inlined for flat HDL lowering preserve their original boundary as a hierarchy instance, with ports generated from the SC-NIR streams produced by the inlined contents.

CLI

Validate a document:

Bash
sc-neurocore scnir validate model.scnir.json

Upgrade a supported document to the current canonical schema:

Bash
sc-neurocore scnir upgrade model.scnir.json --output upgraded.scnir.json

The sc-neurocore.scnir.v0.1 upgrade path adds explicit delay_steps=0 to legacy streams, every pre-v0.3 upgrade adds signal_kind inferred from the stream identifier, and every pre-v0.4 upgrade adds an explicit empty transforms list. Version v0.5 keeps scalar delay metadata compatible and adds per-source delay vectors for heterogeneous NIR Delay lowering. Version v0.6 adds top-level hierarchy instance and port metadata; legacy documents upgrade with an empty hierarchy list. Version v0.7 adds optional online_learning annotations for weight streams and rejects those annotations on spike or analogue-state streams. The typed validator and deterministic JSON writer then produce sc-neurocore.scnir.v0.7. Unknown schema versions fail closed so migration support must be added deliberately when the schema evolves.

Export SC-NIR metadata from a NIR graph:

Bash
sc-neurocore scnir export model.nir --output model.scnir.json --T 1024

Exit code 0 means the document passed the SC-NIR validator. Exit code 1 means validation, upgrade, export, or input loading failed.

FPGA Compilation Integration

compile_network_to_fpga(...) constructs SC-NIR metadata for the lowered NeuronGraph before emitting top-level RTL. The returned NetworkCompilationResult exposes scnir_document, scnir_source_modules, and scnir_source_manifest. The generated top module includes deterministic handoff localparams:

Verilog
localparam integer SCNIR_BITSTREAM_LENGTH = 1024;
localparam integer SCNIR_STREAM_COUNT = 2;
localparam integer SCNIR_SOURCE_MODULE_COUNT = 2;

scnir_source_modules is keyed by emitted Verilog module name and contains the standalone LFSR-16 or Sobol-16 source RTL generated from each SC-NIR stream. The compile-nir CLI writes the full validated document as scnir_document.json in the output directory so dense exported-network runs can be reproduced from the same stream metadata that drove source generation. scnir_source_manifest records the stream identifier, module name, source family, seed, bitstream length, encoding, signal kind, recurrent delay steps, precision, transform metadata, optional online-learning metadata, and source-specific metadata used for each module. CLI manifests also record the selected interconnect, Q-format, total neuron count, total synapse count, SC-NIR stream count, scnir_signal_kinds counts, scnir_signal_routes, and scnir_external_inputs so AER/event-driven and mixed analogue/spiking output directories carry machine-readable compile evidence. FPGA compilation marks non-spiking LI/CubaLI/integrator population streams as analogue_state, so mixed analogue/spiking NIR graphs expose voltage-state handoff metadata instead of being mislabeled as spike streams; combined mixed AER graphs record analogue-state streams as direct MAC routes and spike streams as weighted event routes. FPGA compilation currently materialises LFSR-16 and Sobol-16 source families because both expose the standard threshold[15:0]/bit_out contract; unsupported source families fail closed instead of being emitted through an incompatible HDL interface.

NIR Primitive Compatibility

The executable compatibility matrix is exposed by scnir_compatibility_matrix() and checked against the parser's declared NODE_MAP support. It separates parser execution from SC-NIR/FPGA handoff so documentation cannot claim hardware closure for primitives that are currently parser-only or only closed under a bounded shape/port contract.

NIR primitive SC-NIR / FPGA level Stream metadata HDL handoff
Input, Output boundary none external input/output buses
LIF, IF, CubaLIF metadata and HDL signal_kind=spike, encoding=unipolar canonical ODE module plus direct or AER interconnect
LI, CubaLI, I metadata and HDL signal_kind=analogue_state, encoding=bipolar canonical ODE or integrator state-update module with direct analogue-state MAC routing
Affine, Linear metadata and HDL signal_kind=weight, encoding=bipolar; recurrent or explicit delayed streams carry delay_steps weight ROM plus direct or weighted-event interconnect
Scale metadata and HDL when adjacent to Affine/Linear folded into the downstream weight stream as connection gain folded fixed-point gain in direct/AER weight terms
Flatten metadata and HDL when exact shape metadata preserves element count adjacent to Affine/Linear folded into the downstream weight stream as shape_preserving_flatten fixed-point weight indexing with exact flattened width checks
Threshold metadata and HDL when adjacent to Affine/Linear with scalar or exact-width thresholds weight stream carries a threshold transform with source or destination position fixed-point comparator before weighted-event contribution or destination current
Delay metadata and HDL for scalar or exact source-width source-side delays feeding Affine/Linear population connections downstream weight stream carries scalar delay_steps>=0 or vector delay_steps=[...] direct-interconnect register chains with per-source delay taps for spike and analogue-state sources
Conv1d metadata and HDL when input_shape is explicit and output is flattened into a destination population lowered to a signal_kind=weight stream as convolution_lowered_weight dense Toeplitz-style fixed-point MAC terms through the weight path
Conv2d metadata and HDL when exact spatial input shape is explicit and output is flattened into a destination population lowered to a signal_kind=weight stream as convolution_lowered_weight dense 2D convolution fixed-point MAC terms through the weight path
SumPool2d, AvgPool2d metadata and HDL when exact CHW shape metadata is present and output is flattened into a destination population lowered to a signal_kind=weight stream as pool2d_lowered_weight dense pooling fixed-point MAC terms through the weight path
nested NIRGraph metadata and HDL for single-input/single-output subgraphs and exact one-edge-per-port multi-port subgraphs, including exact multi-output boundaries, inlined into the parent graph; ambiguous multi-port hierarchy fails closed namespaced stream IDs from the inlined subgraph contents plus a top-level hierarchy instance whose ports reference those streams namespaced inline fixed-point terms with stable external input-bus lanes plus standalone hierarchy boundary module artefacts, top-level contract instances, and packed hierarchy weight outputs consumed by the top-level MAC

Use validate_scnir_compatibility_matrix() in tests or release checks to fail when parser support changes without a corresponding compatibility row. Use build_scnir_compatibility_audit() for release evidence bundles that need the validated matrix, support-level counts, explicit parser-only and metadata-only closure blocker lists, the locally closed handoff primitive list, the exact evidence file set, file sizes, per-evidence SHA-256 digests, and a canonical matrix SHA-256 digest in one versioned JSON object. A closed_for_local_handoff audit status is limited to the SC-NIR/HDL software handoff and is emitted with external_hardware_evidence_status=not_claimed until separate Vivado, PYNQ, or physical hardware evidence is attached.

Python API

Python
from sc_neurocore.ir import (
    SCNIR_HDL_HANDOFF_MANIFEST_VERSION,
    SCNIR_PREVIOUS_SCHEMA_VERSION,
    SCNIR_SCHEMA_VERSION,
    SCNIR_COMPATIBILITY_AUDIT_VERSION,
    SCNIRCompatibilityRow,
    SCNIRConversionConfig,
    SCNIRDocument,
    SCNIRHierarchyInstance,
    SCNIRHierarchyPort,
    SCNIRPrecision,
    SCNIRSignalKind,
    SCNIRSource,
    SCNIRStream,
    build_scnir_compatibility_audit,
    build_scnir_source_bundle,
    build_scnir_from_neuron_graph,
    export_scnir_from_nir,
    load_scnir,
    scnir_compatibility_matrix,
    scnir_compatibility_matrix_dicts,
    validate_scnir_compatibility_matrix,
    validate_scnir_dict,
    write_scnir,
    upgrade_scnir_dict,
)

sc_neurocore.ir.scnir_schema

SC-aware NIR metadata schema and validator.

The SC-NIR layer records stochastic-computing semantics that plain NIR does not carry: stream length, encoding, fixed-point precision, random-source metadata, deterministic stream transforms, and correlation constraints. Validation is intentionally fail-closed so unrecognised or under-specified metadata cannot silently reach hardware generation.

SCNIR_SCHEMA_VERSION = 'sc-neurocore.scnir.v0.7' module-attribute

SCNIR_PREVIOUS_SCHEMA_VERSION = 'sc-neurocore.scnir.v0.5' module-attribute

SCNIRSignalKind = Literal['spike', 'analogue_state', 'weight'] module-attribute

SCNIRValidationError

Bases: ValueError

Raised when an SC-NIR payload violates the fail-closed contract.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
103
104
class SCNIRValidationError(ValueError):
    """Raised when an SC-NIR payload violates the fail-closed contract."""

SCNIRPrecision dataclass

Fixed-point interpretation attached to one stochastic stream.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
107
108
109
110
111
112
113
114
115
116
@dataclass(frozen=True, slots=True)
class SCNIRPrecision:
    """Fixed-point interpretation attached to one stochastic stream."""

    signed: bool
    total_bits: int
    fractional_bits: int
    accumulator_bits: int
    rounding: SCNIRRounding
    overflow: SCNIROverflow

SCNIRSource dataclass

Random or deterministic source metadata for a stochastic stream.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
119
120
121
122
123
124
125
126
127
128
129
130
@dataclass(frozen=True, slots=True)
class SCNIRSource:
    """Random or deterministic source metadata for a stochastic stream."""

    kind: SCNIRSourceKind
    seed: int | None = None
    lfsr_polynomial: str | None = None
    tap_mask: int | None = None
    sobol_dimension: int | None = None
    halton_base: int | None = None
    replay_uri: str | None = None
    hardware_id: str | None = None

SCNIRCorrelationConstraint dataclass

Correlation rule between two stochastic streams.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
133
134
135
136
137
138
139
140
@dataclass(frozen=True, slots=True)
class SCNIRCorrelationConstraint:
    """Correlation rule between two stochastic streams."""

    peer_stream_id: str
    policy: SCNIRCorrelationPolicy
    max_abs_correlation: float | None = None
    seed_domain: str | None = None

SCNIRHierarchyPort dataclass

One typed port on a hierarchical SC-NIR hardware instance.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
170
171
172
173
174
175
176
177
178
@dataclass(frozen=True, slots=True)
class SCNIRHierarchyPort:
    """One typed port on a hierarchical SC-NIR hardware instance."""

    port_name: str
    direction: SCNIRHierarchyPortDirection
    stream_id: str
    signal_kind: SCNIRSignalKind
    bit_width: int

SCNIRHierarchyInstance dataclass

One hierarchy instance boundary for future nested hardware handoff.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
181
182
183
184
185
186
187
@dataclass(frozen=True, slots=True)
class SCNIRHierarchyInstance:
    """One hierarchy instance boundary for future nested hardware handoff."""

    instance_id: str
    module_name: str
    ports: Sequence[SCNIRHierarchyPort]

SCNIRStream dataclass

SC metadata for one logical stochastic bitstream.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@dataclass(frozen=True, slots=True)
class SCNIRStream:
    """SC metadata for one logical stochastic bitstream."""

    stream_id: str
    layer: str
    bitstream_length: int
    encoding: SCNIREncoding
    precision: SCNIRPrecision
    source: SCNIRSource
    signal_kind: SCNIRSignalKind = "spike"
    delay_steps: SCNIRDelaySteps = 0
    transforms: Sequence[SCNIRStreamTransform] = field(default_factory=tuple)
    correlation_constraints: Sequence[SCNIRCorrelationConstraint] = field(default_factory=tuple)
    online_learning: Mapping[str, Any] | None = None

SCNIRDocument dataclass

Top-level SC-NIR metadata document.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
190
191
192
193
194
195
196
197
@dataclass(frozen=True, slots=True)
class SCNIRDocument:
    """Top-level SC-NIR metadata document."""

    producer: str
    streams: Sequence[SCNIRStream]
    hierarchy: Sequence[SCNIRHierarchyInstance] = field(default_factory=tuple)
    schema_version: str = SCNIR_SCHEMA_VERSION

validate_scnir_dict(payload)

Validate a decoded SC-NIR payload or raise SCNIRValidationError.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
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
def validate_scnir_dict(payload: Mapping[str, Any]) -> None:
    """Validate a decoded SC-NIR payload or raise ``SCNIRValidationError``."""

    _expect_keys(payload, {"schema_version", "producer", "streams", "hierarchy"}, "document")
    if payload["schema_version"] != SCNIR_SCHEMA_VERSION:
        raise SCNIRValidationError(
            f"schema_version must be {SCNIR_SCHEMA_VERSION!r}, got {payload['schema_version']!r}"
        )
    _expect_non_empty_string(payload["producer"], "producer")
    streams = _expect_sequence(payload["streams"], "streams")
    if not streams:
        raise SCNIRValidationError("streams must contain at least one stream")

    stream_ids: set[str] = set()
    stream_signal_kinds: dict[str, SCNIRSignalKind] = {}
    stream_payloads: list[Mapping[str, Any]] = []
    for index, item in enumerate(streams):
        stream = _expect_mapping(item, f"streams[{index}]")
        _validate_stream(stream, f"streams[{index}]")
        stream_id = cast(str, stream["stream_id"])
        if stream_id in stream_ids:
            raise SCNIRValidationError(f"duplicate stream_id {stream_id!r}")
        stream_ids.add(stream_id)
        stream_signal_kinds[stream_id] = cast(SCNIRSignalKind, stream["signal_kind"])
        stream_payloads.append(stream)

    for index, stream in enumerate(stream_payloads):
        constraints = _expect_sequence(
            stream.get("correlation_constraints", ()), f"streams[{index}].correlation_constraints"
        )
        for c_index, item in enumerate(constraints):
            constraint = _expect_mapping(
                item, f"streams[{index}].correlation_constraints[{c_index}]"
            )
            peer = cast(str, constraint["peer_stream_id"])
            if peer not in stream_ids:
                raise SCNIRValidationError(
                    f"streams[{index}].correlation_constraints[{c_index}].peer_stream_id "
                    f"{peer!r} does not reference an existing stream"
                )

    _validate_hierarchy(
        _expect_sequence(payload["hierarchy"], "hierarchy"),
        stream_signal_kinds,
    )

scnir_from_dict(payload)

Build a typed SC-NIR document from a decoded mapping.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def scnir_from_dict(payload: Mapping[str, Any]) -> SCNIRDocument:
    """Build a typed SC-NIR document from a decoded mapping."""

    validate_scnir_dict(payload)
    streams_payload = _expect_sequence(payload["streams"], "streams")
    streams = tuple(
        _stream_from_dict(_expect_mapping(item, f"streams[{index}]"))
        for index, item in enumerate(streams_payload)
    )
    return SCNIRDocument(
        schema_version=cast(str, payload["schema_version"]),
        producer=cast(str, payload["producer"]),
        streams=streams,
        hierarchy=tuple(
            _hierarchy_instance_from_dict(_expect_mapping(item, f"hierarchy[{index}]"))
            for index, item in enumerate(_expect_sequence(payload["hierarchy"], "hierarchy"))
        ),
    )

scnir_to_dict(document)

Convert a typed SC-NIR document to deterministic JSON-ready data.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
267
268
269
270
271
272
273
274
275
276
277
def scnir_to_dict(document: SCNIRDocument) -> dict[str, Any]:
    """Convert a typed SC-NIR document to deterministic JSON-ready data."""

    payload: dict[str, Any] = {
        "schema_version": document.schema_version,
        "producer": document.producer,
        "streams": [_stream_to_dict(stream) for stream in document.streams],
        "hierarchy": [_hierarchy_instance_to_dict(instance) for instance in document.hierarchy],
    }
    validate_scnir_dict(payload)
    return payload

load_scnir(path)

Load and validate an SC-NIR JSON document.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
280
281
282
283
284
def load_scnir(path: str | Path) -> SCNIRDocument:
    """Load and validate an SC-NIR JSON document."""

    raw = json.loads(Path(path).read_text(encoding="utf-8"))
    return scnir_from_dict(_expect_mapping(raw, "document"))

write_scnir(path, document)

Write an SC-NIR JSON document after validating it.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
287
288
289
290
291
292
293
294
def write_scnir(path: str | Path, document: SCNIRDocument) -> None:
    """Write an SC-NIR JSON document after validating it."""

    payload = scnir_to_dict(document)
    Path(path).write_text(
        json.dumps(payload, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )

upgrade_scnir_dict(payload)

Upgrade supported SC-NIR payloads to the current canonical schema.

Version v0.1 did not encode recurrent connection delay, and versions before v0.3 did not distinguish spiking, analogue-state, and weight streams. Version v0.4 added explicit stream transform metadata for threshold comparators. Version v0.5 permits delay_steps to be either a scalar integer or a per-source-column integer vector. Version v0.6 adds top-level hierarchy instance and port metadata. Version v0.7 adds optional validated per-weight-stream online-learning annotations. Legacy upgrades insert the missing fields before validating through the typed schema. Current documents are canonicalised through the same deterministic writer.

Source code in src/sc_neurocore/ir/scnir_schema.py
Python
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
def upgrade_scnir_dict(payload: Mapping[str, Any]) -> dict[str, Any]:
    """Upgrade supported SC-NIR payloads to the current canonical schema.

    Version ``v0.1`` did not encode recurrent connection delay, and versions
    before ``v0.3`` did not distinguish spiking, analogue-state, and weight
    streams.  Version ``v0.4`` added explicit stream transform metadata for
    threshold comparators.  Version ``v0.5`` permits ``delay_steps`` to be
    either a scalar integer or a per-source-column integer vector.  Version
    ``v0.6`` adds top-level hierarchy instance and port metadata.  Version
    ``v0.7`` adds optional validated per-weight-stream online-learning
    annotations.  Legacy upgrades insert the missing fields before validating
    through the typed schema.  Current documents are canonicalised through the
    same deterministic writer.
    """

    version = payload.get("schema_version")
    if not isinstance(version, str) or version not in SCNIR_SUPPORTED_SCHEMA_VERSIONS:
        raise SCNIRValidationError(f"unsupported SC-NIR schema_version {version!r}")
    if version in {
        SCNIR_LEGACY_SCHEMA_VERSION,
        SCNIR_V02_SCHEMA_VERSION,
        SCNIR_V03_SCHEMA_VERSION,
        SCNIR_V04_SCHEMA_VERSION,
        SCNIR_PREVIOUS_SCHEMA_VERSION,
        SCNIR_V06_SCHEMA_VERSION,
    }:
        upgraded: dict[str, Any] = dict(payload)
        upgraded["schema_version"] = SCNIR_SCHEMA_VERSION
        streams = _expect_sequence(upgraded.get("streams"), "streams")
        upgraded_streams: list[dict[str, Any]] = []
        for index, stream in enumerate(streams):
            stream_payload = dict(_expect_mapping(stream, f"streams[{index}]"))
            if "delay_steps" not in stream_payload:
                stream_payload["delay_steps"] = 0
            if "signal_kind" not in stream_payload:
                stream_payload["signal_kind"] = _infer_legacy_signal_kind(
                    str(stream_payload.get("stream_id", ""))
                )
            if "transforms" not in stream_payload:
                stream_payload["transforms"] = []
            if "online_learning" not in stream_payload:
                stream_payload["online_learning"] = None
            upgraded_streams.append(stream_payload)
        upgraded["streams"] = upgraded_streams
        if "hierarchy" not in upgraded:
            upgraded["hierarchy"] = []
        return scnir_to_dict(scnir_from_dict(upgraded))
    return scnir_to_dict(scnir_from_dict(payload))

sc_neurocore.ir.scnir_convert

Conversion utilities that attach SC-NIR metadata to NIR-derived graphs.

SCNIRConversionConfig dataclass

Configuration for deterministic SC-NIR metadata export.

Source code in src/sc_neurocore/ir/scnir_convert.py
Python
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
@dataclass(frozen=True, slots=True)
class SCNIRConversionConfig:
    """Configuration for deterministic SC-NIR metadata export."""

    bitstream_length: int
    data_width: int = 16
    fraction: int = 8
    accumulator_bits: int | None = None
    base_seed: int = 1
    source_kind: Literal["lfsr", "sobol", "halton"] = "lfsr"
    rounding: SCNIRRounding = "nearest_even"
    overflow: SCNIROverflow = "saturate"
    seed_domain: str = "scnir-default"
    max_abs_correlation: float = 0.0
    producer: str = "sc-neurocore"
    online_learning: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not isinstance(self.bitstream_length, int) or self.bitstream_length <= 0:
            raise ValueError("bitstream_length must be a positive integer")
        if not isinstance(self.data_width, int) or self.data_width <= 0:
            raise ValueError("data_width must be a positive integer")
        if not isinstance(self.fraction, int) or self.fraction < 0:
            raise ValueError("fraction must be a non-negative integer")
        if self.fraction >= self.data_width:
            raise ValueError("fraction must be smaller than data_width")
        if self.accumulator_bits is not None and self.accumulator_bits < self.data_width:
            raise ValueError("accumulator_bits must be greater than or equal to data_width")
        if not isinstance(self.base_seed, int) or not 0 <= self.base_seed <= _MAX_SEED:
            raise ValueError("base_seed must fit in uint64")
        if not 0.0 <= self.max_abs_correlation <= 1.0:
            raise ValueError("max_abs_correlation must be in [0, 1]")
        if not self.seed_domain:
            raise ValueError("seed_domain must be non-empty")
        if not self.producer:
            raise ValueError("producer must be non-empty")

    @property
    def resolved_accumulator_bits(self) -> int:
        """Accumulator width used by exported precision metadata."""

        return self.accumulator_bits if self.accumulator_bits is not None else self.data_width * 2

resolved_accumulator_bits property

Accumulator width used by exported precision metadata.

build_scnir_from_neuron_graph(neuron_graph, *, config)

Build an SC-NIR document from an existing NIR-derived NeuronGraph.

Source code in src/sc_neurocore/ir/scnir_convert.py
Python
 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
def build_scnir_from_neuron_graph(
    neuron_graph: Any,
    *,
    config: SCNIRConversionConfig,
) -> SCNIRDocument:
    """Build an SC-NIR document from an existing NIR-derived NeuronGraph."""

    streams: list[SCNIRStream] = []
    pop_stream_ids: dict[str, str] = {}

    for pop in neuron_graph.populations:
        neuron_type = str(pop.neuron_type)
        signal_kind = _population_signal_kind(neuron_type)
        stream_id = _population_stream_id(str(pop.name), signal_kind=signal_kind)
        pop_stream_ids[str(pop.name)] = stream_id
        streams.append(
            SCNIRStream(
                stream_id=stream_id,
                layer=str(pop.name),
                bitstream_length=config.bitstream_length,
                encoding=_population_encoding(neuron_type),
                precision=_precision(config, signed=False),
                source=_source(config, len(streams)),
                signal_kind=signal_kind,
                delay_steps=0,
                correlation_constraints=(),
            )
        )

    for conn in neuron_graph.connections:
        dst = str(conn.dst)
        dst_stream_id = pop_stream_ids.get(dst)
        if dst_stream_id is None:
            raise ValueError(f"Connection destination {dst!r} has no population stream")

        stream_index = len(streams)
        streams.append(
            SCNIRStream(
                stream_id=_connection_stream_id(str(conn.src), dst),
                layer=dst,
                bitstream_length=config.bitstream_length,
                encoding="bipolar",
                precision=_precision(config, signed=True),
                source=_source(config, stream_index),
                signal_kind="weight",
                delay_steps=_connection_delay_steps(conn),
                transforms=_connection_transforms(conn),
                online_learning=_online_learning_annotation(
                    config, _connection_stream_id(str(conn.src), dst)
                ),
                correlation_constraints=(
                    SCNIRCorrelationConstraint(
                        peer_stream_id=dst_stream_id,
                        policy="max_correlation",
                        max_abs_correlation=config.max_abs_correlation,
                        seed_domain=config.seed_domain,
                    ),
                ),
            )
        )

    return SCNIRDocument(
        producer=config.producer,
        streams=tuple(streams),
        hierarchy=_hierarchy_from_graph(neuron_graph, tuple(streams)),
    )

export_scnir_from_nir(model_path, *, output_path, config, dt=1.0)

Read a NIR model, export SC-NIR metadata, and write it to JSON.

Source code in src/sc_neurocore/ir/scnir_convert.py
Python
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
def export_scnir_from_nir(
    model_path: str | Path,
    *,
    output_path: str | Path,
    config: SCNIRConversionConfig,
    dt: float = 1.0,
) -> SCNIRDocument:
    """Read a NIR model, export SC-NIR metadata, and write it to JSON."""

    import nir as nir_lib

    from sc_neurocore.nir_bridge import from_nir, from_scnetwork

    graph = nir_lib.read(str(model_path))
    network = from_nir(graph, dt=dt)
    neuron_graph = from_scnetwork(network, dt=dt)
    document = build_scnir_from_neuron_graph(neuron_graph, config=config)
    write_scnir(output_path, document)
    return document

sc_neurocore.ir.scnir_compatibility

Executable compatibility matrix for NIR primitives in the SC-NIR pipeline.

The NIR parser supports more primitives than the current SC-NIR to FPGA handoff path can lower. This module makes that distinction explicit and machine-checkable so documentation, tests, and future closure gates cannot silently over-claim support.

SCNIR_COMPATIBILITY_AUDIT_VERSION = 'sc-neurocore.scnir.compatibility-audit.v0.2' module-attribute

SCNIRCompatibilityRow dataclass

One compatibility row for a NIR primitive.

Source code in src/sc_neurocore/ir/scnir_compatibility.py
Python
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@dataclass(frozen=True, slots=True)
class SCNIRCompatibilityRow:
    """One compatibility row for a NIR primitive."""

    nir_primitive: str
    support_level: SCNIRSupportLevel
    parser_node: str
    neuron_graph_lowering: str
    scnir_stream_metadata: tuple[str, ...]
    source_metadata: tuple[str, ...]
    hdl_support: str
    audit_evidence: tuple[str, ...]
    limitation: str

    def as_dict(self) -> dict[str, object]:
        """Return a deterministic JSON-ready row."""

        payload = asdict(self)
        payload["scnir_stream_metadata"] = list(self.scnir_stream_metadata)
        payload["source_metadata"] = list(self.source_metadata)
        payload["audit_evidence"] = list(self.audit_evidence)
        return payload

as_dict()

Return a deterministic JSON-ready row.

Source code in src/sc_neurocore/ir/scnir_compatibility.py
Python
49
50
51
52
53
54
55
56
def as_dict(self) -> dict[str, object]:
    """Return a deterministic JSON-ready row."""

    payload = asdict(self)
    payload["scnir_stream_metadata"] = list(self.scnir_stream_metadata)
    payload["source_metadata"] = list(self.source_metadata)
    payload["audit_evidence"] = list(self.audit_evidence)
    return payload

scnir_compatibility_matrix()

Return the deterministic SC-NIR compatibility matrix.

Source code in src/sc_neurocore/ir/scnir_compatibility.py
Python
335
336
337
338
def scnir_compatibility_matrix() -> tuple[SCNIRCompatibilityRow, ...]:
    """Return the deterministic SC-NIR compatibility matrix."""

    return _MATRIX

scnir_compatibility_matrix_dicts()

Return the matrix as deterministic JSON-ready dictionaries.

Source code in src/sc_neurocore/ir/scnir_compatibility.py
Python
341
342
343
344
def scnir_compatibility_matrix_dicts() -> tuple[dict[str, object], ...]:
    """Return the matrix as deterministic JSON-ready dictionaries."""

    return tuple(row.as_dict() for row in _MATRIX)

build_scnir_compatibility_audit(evidence_root)

Build a versioned closure-audit report for the SC-NIR compatibility matrix.

The report is intentionally derived from the executable matrix after validation, so release automation consumes the same data that enforces parser coverage and evidence-path freshness.

Source code in src/sc_neurocore/ir/scnir_compatibility.py
Python
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def build_scnir_compatibility_audit(evidence_root: str | Path) -> dict[str, object]:
    """Build a versioned closure-audit report for the SC-NIR compatibility matrix.

    The report is intentionally derived from the executable matrix after
    validation, so release automation consumes the same data that enforces
    parser coverage and evidence-path freshness.
    """

    root = Path(evidence_root).resolve()
    validate_scnir_compatibility_matrix(evidence_root=root)

    support_level_counts: dict[str, int] = {}
    for row in _MATRIX:
        support_level_counts[row.support_level] = support_level_counts.get(row.support_level, 0) + 1
    parser_only_primitives = sorted(
        row.nir_primitive for row in _MATRIX if row.support_level == "parser_only"
    )
    metadata_only_primitives = sorted(
        row.nir_primitive for row in _MATRIX if row.support_level == "metadata_only"
    )
    boundary_primitives = sorted(
        row.nir_primitive for row in _MATRIX if row.support_level == "boundary"
    )
    closed_handoff_primitives = sorted(
        row.nir_primitive for row in _MATRIX if row.support_level == "metadata_and_hdl"
    )
    closure_blocker_count = len(parser_only_primitives) + len(metadata_only_primitives)
    evidence_paths = sorted({path for row in _MATRIX for path in row.audit_evidence})
    evidence_files = []
    for path in evidence_paths:
        evidence_file = root / path
        evidence_bytes = evidence_file.read_bytes()
        evidence_files.append(
            {
                "path": path,
                "sha256": hashlib.sha256(evidence_bytes).hexdigest(),
                "size_bytes": len(evidence_bytes),
            }
        )
    matrix = list(scnir_compatibility_matrix_dicts())
    matrix_bytes = (json.dumps(matrix, sort_keys=True, separators=(",", ":")) + "\n").encode(
        "utf-8"
    )

    return {
        "schema_version": SCNIR_COMPATIBILITY_AUDIT_VERSION,
        "status": "valid",
        "evidence_root": str(root),
        "primitive_count": len(_MATRIX),
        "support_level_counts": dict(sorted(support_level_counts.items())),
        "closure_status": ("closed_for_local_handoff" if closure_blocker_count == 0 else "blocked"),
        "closure_blocker_count": closure_blocker_count,
        "parser_only_primitives": parser_only_primitives,
        "metadata_only_primitives": metadata_only_primitives,
        "boundary_primitives": boundary_primitives,
        "closed_handoff_primitives": closed_handoff_primitives,
        "requires_external_hardware_evidence": True,
        "external_hardware_evidence_status": "not_claimed",
        "external_hardware_evidence_note": (
            "Local SC-NIR/HDL handoff closure does not claim Vivado, PYNQ, "
            "or physical hardware evidence."
        ),
        "audit_evidence_file_count": len(evidence_paths),
        "audit_evidence_paths": evidence_paths,
        "audit_evidence_files": evidence_files,
        "matrix_sha256": hashlib.sha256(matrix_bytes).hexdigest(),
        "matrix": matrix,
    }

validate_scnir_compatibility_matrix(evidence_root=None)

Fail if the matrix drifts from parser-declared support or stale evidence paths.

Parameters

evidence_root: Optional repository root used to verify that every audit_evidence path in the matrix resolves to an existing file.

Source code in src/sc_neurocore/ir/scnir_compatibility.py
Python
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def validate_scnir_compatibility_matrix(evidence_root: str | Path | None = None) -> None:
    """Fail if the matrix drifts from parser-declared support or stale evidence paths.

    Parameters
    ----------
    evidence_root:
        Optional repository root used to verify that every ``audit_evidence``
        path in the matrix resolves to an existing file.
    """

    from sc_neurocore.nir_bridge.node_map import NODE_MAP

    matrix_primitives = {row.nir_primitive for row in _MATRIX}
    parser_primitives = {primitive.__name__ for primitive in NODE_MAP}
    missing = sorted(parser_primitives - matrix_primitives)
    stale = sorted(matrix_primitives - parser_primitives - {"NIRGraph"})
    if missing:
        raise ValueError(f"SC-NIR compatibility matrix misses parser primitives: {missing}")
    if stale:
        raise ValueError(f"SC-NIR compatibility matrix contains stale primitives: {stale}")

    seen: set[str] = set()
    for row in _MATRIX:
        if row.nir_primitive in seen:
            raise ValueError(f"duplicate SC-NIR compatibility row: {row.nir_primitive}")
        seen.add(row.nir_primitive)
        if row.support_level == "metadata_and_hdl" and not row.scnir_stream_metadata:
            raise ValueError(f"{row.nir_primitive} claims HDL support without stream metadata")
        if not row.audit_evidence:
            raise ValueError(f"{row.nir_primitive} has no audit evidence pointer")
        if evidence_root is not None:
            root = Path(evidence_root)
            missing_evidence = [
                evidence_path
                for evidence_path in row.audit_evidence
                if not (root / evidence_path).is_file()
            ]
            if missing_evidence:
                raise ValueError(
                    f"{row.nir_primitive} has missing audit evidence paths: {missing_evidence}"
                )

sc_neurocore.ir.scnir_hdl

Materialise SC-NIR source metadata into concrete HDL artefacts.

SCNIRHDLSourceManifestEntry dataclass

Serialisable manifest row for one emitted stochastic source module.

Source code in src/sc_neurocore/ir/scnir_hdl.py
Python
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
@dataclass(frozen=True, slots=True)
class SCNIRHDLSourceManifestEntry:
    """Serialisable manifest row for one emitted stochastic source module."""

    stream_id: str
    layer: str
    module_name: str
    source_kind: SCNIRHDLSourceKind
    seed: int
    bitstream_length: int
    encoding: str
    signal_kind: str
    delay_steps: int | tuple[int, ...]
    total_bits: int
    fractional_bits: int
    transforms: tuple[dict[str, object], ...] = ()
    online_learning: Mapping[str, Any] | None = None
    lfsr_polynomial: str | None = None
    tap_mask: int | None = None
    sobol_dimension: int | None = None

    def as_dict(self) -> dict[str, object]:
        """Return a deterministic JSON-ready representation."""

        return {
            "stream_id": self.stream_id,
            "layer": self.layer,
            "module_name": self.module_name,
            "source_kind": self.source_kind,
            "seed": self.seed,
            "bitstream_length": self.bitstream_length,
            "encoding": self.encoding,
            "signal_kind": self.signal_kind,
            "delay_steps": (
                self.delay_steps if isinstance(self.delay_steps, int) else list(self.delay_steps)
            ),
            "total_bits": self.total_bits,
            "fractional_bits": self.fractional_bits,
            "transforms": [dict(transform) for transform in self.transforms],
            "online_learning": (
                dict(self.online_learning) if self.online_learning is not None else None
            ),
            "lfsr_polynomial": self.lfsr_polynomial,
            "tap_mask": self.tap_mask,
            "sobol_dimension": self.sobol_dimension,
        }

as_dict()

Return a deterministic JSON-ready representation.

Source code in src/sc_neurocore/ir/scnir_hdl.py
Python
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
def as_dict(self) -> dict[str, object]:
    """Return a deterministic JSON-ready representation."""

    return {
        "stream_id": self.stream_id,
        "layer": self.layer,
        "module_name": self.module_name,
        "source_kind": self.source_kind,
        "seed": self.seed,
        "bitstream_length": self.bitstream_length,
        "encoding": self.encoding,
        "signal_kind": self.signal_kind,
        "delay_steps": (
            self.delay_steps if isinstance(self.delay_steps, int) else list(self.delay_steps)
        ),
        "total_bits": self.total_bits,
        "fractional_bits": self.fractional_bits,
        "transforms": [dict(transform) for transform in self.transforms],
        "online_learning": (
            dict(self.online_learning) if self.online_learning is not None else None
        ),
        "lfsr_polynomial": self.lfsr_polynomial,
        "tap_mask": self.tap_mask,
        "sobol_dimension": self.sobol_dimension,
    }

SCNIRHDLSourceBundle dataclass

Concrete HDL source modules plus the manifest that explains them.

Source code in src/sc_neurocore/ir/scnir_hdl.py
Python
74
75
76
77
78
79
80
81
82
83
84
@dataclass(frozen=True, slots=True)
class SCNIRHDLSourceBundle:
    """Concrete HDL source modules plus the manifest that explains them."""

    modules: dict[str, str]
    manifest: tuple[SCNIRHDLSourceManifestEntry, ...]

    def manifest_dicts(self) -> tuple[dict[str, object], ...]:
        """Return deterministic JSON-ready manifest rows."""

        return tuple(entry.as_dict() for entry in self.manifest)

manifest_dicts()

Return deterministic JSON-ready manifest rows.

Source code in src/sc_neurocore/ir/scnir_hdl.py
Python
81
82
83
84
def manifest_dicts(self) -> tuple[dict[str, object], ...]:
    """Return deterministic JSON-ready manifest rows."""

    return tuple(entry.as_dict() for entry in self.manifest)

build_scnir_source_bundle(document)

Emit deterministic HDL source modules for every SC-NIR stream.

Only source kinds with the standard threshold-bit output contract are materialised here. Unsupported SC-NIR source kinds fail closed instead of being lowered to semantically incompatible RTL.

Source code in src/sc_neurocore/ir/scnir_hdl.py
Python
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def build_scnir_source_bundle(document: SCNIRDocument) -> SCNIRHDLSourceBundle:
    """Emit deterministic HDL source modules for every SC-NIR stream.

    Only source kinds with the standard threshold-bit output contract are
    materialised here. Unsupported SC-NIR source kinds fail closed instead of
    being lowered to semantically incompatible RTL.
    """

    scnir_to_dict(document)
    modules: dict[str, str] = {}
    manifest: list[SCNIRHDLSourceManifestEntry] = []

    for index, stream in enumerate(document.streams):
        module_name = _module_name_for_stream(stream, index)
        entry, verilog = _emit_stream_source(stream, module_name=module_name)
        if module_name in modules:
            raise ValueError(f"duplicate SC-NIR source module name {module_name!r}")
        modules[module_name] = verilog
        manifest.append(entry)

    return SCNIRHDLSourceBundle(modules=modules, manifest=tuple(manifest))

sc_neurocore.ir.scnir_handoff_audit

Executable audit checks for compile-nir SC-NIR HDL handoff artefacts.

SCNIR_HDL_HANDOFF_MANIFEST_VERSION = 'sc-neurocore.scnir.hdl-sources.v0.2' module-attribute

SCNIRHDLHandoffAuditReport dataclass

Deterministic summary of a validated SC-NIR HDL handoff directory.

Source code in src/sc_neurocore/ir/scnir_handoff_audit.py
Python
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
@dataclass(frozen=True, slots=True)
class SCNIRHDLHandoffAuditReport:
    """Deterministic summary of a validated SC-NIR HDL handoff directory."""

    directory: str
    module_name: str
    schema_version: str
    manifest_schema_version: str
    bitstream_length: int
    interconnect: str
    q_format: str
    stream_count: int
    source_module_count: int
    hierarchy_instance_count: int
    hierarchy_port_count: int
    hierarchy_instances: dict[str, dict[str, object]]
    external_input_count: int
    external_inputs: tuple[dict[str, int | str], ...]
    total_neurons: int
    total_synapses: int
    signal_kinds: dict[str, int]
    signal_routes: dict[str, str]
    artefacts: tuple[str, ...]

    def as_dict(self) -> dict[str, Any]:
        """Return a stable JSON-ready report."""

        return {
            "status": "valid",
            "directory": self.directory,
            "module_name": self.module_name,
            "schema_version": self.schema_version,
            "manifest_schema_version": self.manifest_schema_version,
            "bitstream_length": self.bitstream_length,
            "interconnect": self.interconnect,
            "q_format": self.q_format,
            "stream_count": self.stream_count,
            "source_module_count": self.source_module_count,
            "hierarchy_instance_count": self.hierarchy_instance_count,
            "hierarchy_port_count": self.hierarchy_port_count,
            "hierarchy_instances": self.hierarchy_instances,
            "external_input_count": self.external_input_count,
            "external_inputs": [dict(row) for row in self.external_inputs],
            "total_neurons": self.total_neurons,
            "total_synapses": self.total_synapses,
            "signal_kinds": self.signal_kinds,
            "signal_routes": self.signal_routes,
            "artefacts": list(self.artefacts),
        }

as_dict()

Return a stable JSON-ready report.

Source code in src/sc_neurocore/ir/scnir_handoff_audit.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
def as_dict(self) -> dict[str, Any]:
    """Return a stable JSON-ready report."""

    return {
        "status": "valid",
        "directory": self.directory,
        "module_name": self.module_name,
        "schema_version": self.schema_version,
        "manifest_schema_version": self.manifest_schema_version,
        "bitstream_length": self.bitstream_length,
        "interconnect": self.interconnect,
        "q_format": self.q_format,
        "stream_count": self.stream_count,
        "source_module_count": self.source_module_count,
        "hierarchy_instance_count": self.hierarchy_instance_count,
        "hierarchy_port_count": self.hierarchy_port_count,
        "hierarchy_instances": self.hierarchy_instances,
        "external_input_count": self.external_input_count,
        "external_inputs": [dict(row) for row in self.external_inputs],
        "total_neurons": self.total_neurons,
        "total_synapses": self.total_synapses,
        "signal_kinds": self.signal_kinds,
        "signal_routes": self.signal_routes,
        "artefacts": list(self.artefacts),
    }

audit_scnir_hdl_handoff(directory)

Validate a compile-nir HDL output directory and return an audit report.

The audit is intentionally structural and fail-closed: every SC-NIR stream must have exactly one matching source-manifest row and emitted source module, aggregate counts must match the typed document, and top-level SC-NIR localparams must agree with the JSON handoff metadata.

Source code in src/sc_neurocore/ir/scnir_handoff_audit.py
Python
 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
175
176
177
178
179
180
181
182
183
def audit_scnir_hdl_handoff(directory: str | Path) -> SCNIRHDLHandoffAuditReport:
    """Validate a ``compile-nir`` HDL output directory and return an audit report.

    The audit is intentionally structural and fail-closed: every SC-NIR stream
    must have exactly one matching source-manifest row and emitted source
    module, aggregate counts must match the typed document, and top-level
    SC-NIR localparams must agree with the JSON handoff metadata.
    """

    root = Path(directory)
    if not root.is_dir():
        raise SCNIRHDLHandoffAuditError(f"handoff directory does not exist: {root}")

    document_path = root / "scnir_document.json"
    manifest_path = root / "scnir_source_manifest.json"
    _require_file(document_path, "SC-NIR document")
    _require_file(manifest_path, "SC-NIR source manifest")

    try:
        document = load_scnir(document_path)
    except Exception as exc:
        raise SCNIRHDLHandoffAuditError(f"invalid scnir_document.json: {exc}") from exc

    manifest = _load_manifest(manifest_path)
    _verify_manifest_header(manifest)

    module_name = _expect_non_empty_string(manifest, "module_name")
    top_module_path = root / f"{module_name}.v"
    _require_file(top_module_path, "top module")
    _require_file(root / "sc_nir_weight_rom.v", "weight ROM")

    streams = tuple(document.streams)
    sources = _expect_mapping_sequence(manifest, "sources")
    stream_count = _expect_int(manifest, "scnir_stream_count")
    if stream_count != len(streams):
        raise SCNIRHDLHandoffAuditError(
            f"scnir_stream_count {stream_count} does not match document stream count {len(streams)}"
        )
    if len(sources) != len(streams):
        raise SCNIRHDLHandoffAuditError(
            f"sources length {len(sources)} does not match document stream count {len(streams)}"
        )

    bitstream_length = _expect_int(manifest, "bitstream_length")
    source_modules = _verify_source_rows(root, document, sources)
    signal_kinds = _signal_kind_counts(document)
    signal_routes = _signal_routes(
        document, interconnect=_expect_non_empty_string(manifest, "interconnect")
    )
    hierarchy_instances = _hierarchy_instances(document)
    hierarchy_port_count = sum(
        len(cast(list[object], instance["ports"])) for instance in hierarchy_instances.values()
    )
    _expect_equal(manifest.get("scnir_signal_kinds"), signal_kinds, "scnir_signal_kinds")
    _expect_equal(manifest.get("scnir_signal_routes"), signal_routes, "scnir_signal_routes")
    _expect_equal(
        manifest.get("scnir_hierarchy_instance_count"),
        len(hierarchy_instances),
        "scnir_hierarchy_instance_count",
    )
    _expect_equal(
        manifest.get("scnir_hierarchy_port_count"),
        hierarchy_port_count,
        "scnir_hierarchy_port_count",
    )
    hierarchy_modules = _verify_hierarchy_modules(root, document)
    top_module = top_module_path.read_text(encoding="utf-8")
    _verify_hierarchy_top_instances(top_module, document)
    external_inputs = _external_inputs(manifest)
    _expect_top_localparam(top_module, "SCNIR_BITSTREAM_LENGTH", bitstream_length)
    _expect_top_localparam(top_module, "SCNIR_STREAM_COUNT", len(streams))
    _expect_top_localparam(top_module, "SCNIR_SOURCE_MODULE_COUNT", len(sources))

    artefacts = tuple(
        sorted(
            {
                "scnir_document.json",
                "scnir_source_manifest.json",
                "sc_nir_weight_rom.v",
                f"{module_name}.v",
                *source_modules,
                *hierarchy_modules,
            }
        )
    )
    return SCNIRHDLHandoffAuditReport(
        directory=str(root),
        module_name=module_name,
        schema_version=document.schema_version,
        manifest_schema_version=cast(str, manifest["schema_version"]),
        bitstream_length=bitstream_length,
        interconnect=cast(str, manifest["interconnect"]),
        q_format=_expect_non_empty_string(manifest, "q_format"),
        stream_count=len(streams),
        source_module_count=len(sources),
        hierarchy_instance_count=len(hierarchy_instances),
        hierarchy_port_count=hierarchy_port_count,
        hierarchy_instances=hierarchy_instances,
        external_input_count=len(external_inputs),
        external_inputs=external_inputs,
        total_neurons=_expect_int(manifest, "total_neurons"),
        total_synapses=_expect_int(manifest, "total_synapses"),
        signal_kinds=signal_kinds,
        signal_routes=signal_routes,
        artefacts=artefacts,
    )

write_scnir_hdl_handoff_audit(directory, output_path)

Validate a handoff directory and write the JSON audit report.

Source code in src/sc_neurocore/ir/scnir_handoff_audit.py
Python
186
187
188
189
190
191
192
193
194
195
196
197
def write_scnir_hdl_handoff_audit(
    directory: str | Path,
    output_path: str | Path,
) -> SCNIRHDLHandoffAuditReport:
    """Validate a handoff directory and write the JSON audit report."""

    report = audit_scnir_hdl_handoff(directory)
    Path(output_path).write_text(
        json.dumps(report.as_dict(), indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )
    return report