neuralos-snn 0.1.0-alpha.7

Spiking Neural Network library, no_std, i16 fixed-point — NeuralOS sovereignty stack core
Documentation
//! NIR format gate (Phase 1 / R7, slice 1) — the import/export gate
//! over the REFERENCE-emitted fixtures, in the family style of
//! `ternary_format_gate`: test vectors derived from the pinned
//! reference implementation's own outputs, never from our
//! re-implementation of ourselves.
//!
//! Reference: `neuromorphs/NIR` @ 7883c3c85f1be27ed113ccc9e8d6ab47ab541df4
//! (BSD-3; clone at `nir-ref/`, gitignored; fixtures regenerated by
//! `tools/gen_nir_fixtures.py` — positive = the reference's own
//! `to_dict()` emissions, negative = minimal mutations, one error
//! class each).
//!
//! Container note of record: at the pinned sha the reference
//! serializes via HDF5 (h5py); the dict schema is container-
//! independent and this gate carries its JSON encoding (the
//! historical NIR container). HDF5 `.nir` IO is slice 2 (std-side).
//!
//! Gates:
//! 1. the reference chain imports with EXACT dyadic quantization
//!    (weights [0.5,-1.0,0.25] → [16384,-32767,8192] @ scale 1/32767;
//!    LIF −70/−55/−80 mV quanta, τ=20 ms, R=100 MΩ, C=200 pF);
//! 2. every negative fixture rejects with its named error (13 cases);
//! 3. the chain assembles onto a real `SpikingNeuralNetwork` and
//!    SPIKES under deterministic drive (the minimal real path);
//! 4. export is byte-stable and re-imports state-identically
//!    (idempotence; provenance rides `metadata.neuralos`, the
//!    reference-native round-trip channel).
//!
//! Exits non-zero on any gate failure. No models, no network — the
//! fixtures are embedded.

use neuralos_snn::nir::{
    nir_export, nir_scan, NirBuffers, NirError, NirImport, NirImportOptions, NirLif, NirNode,
    NirNodeKind, NirNote, EXPORT_VERSION, NIR_REF_SHA,
};

const CHAIN: &str = include_str!("../tests/nir_fixtures/chain.json");
const CHAIN_VRESET: &str = include_str!("../tests/nir_fixtures/chain_vreset_absent.json");
/// One negative fixture: (label, document, error check).
type NegCase = (&'static str, &'static str, fn(&NirError) -> bool);

const NEGATIVES: &[NegCase] = &[
    (
        "affine",
        include_str!("../tests/nir_fixtures/neg_affine.json"),
        |e| matches!(e, NirError::UnsupportedNodeKind("Affine")),
    ),
    (
        "unknown kind",
        include_str!("../tests/nir_fixtures/neg_unknown_kind.json"),
        |e| matches!(e, NirError::UnsupportedNodeKind("CubaLIF")),
    ),
    (
        "tau zero",
        include_str!("../tests/nir_fixtures/neg_tau_zero.json"),
        |e| matches!(e, NirError::BadNumber("tau")),
    ),
    (
        "threshold quantizes to 0",
        include_str!("../tests/nir_fixtures/neg_threshold_zero_quant.json"),
        |e| matches!(e, NirError::ThresholdZero),
    ),
    (
        "tau below dt",
        include_str!("../tests/nir_fixtures/neg_tau_below_dt.json"),
        |e| matches!(e, NirError::TauBelowDt),
    ),
    (
        "potential out of range",
        include_str!("../tests/nir_fixtures/neg_potential_out_of_range.json"),
        |e| matches!(e, NirError::PotentialOutOfRange("v_leak")),
    ),
    (
        "missing field",
        include_str!("../tests/nir_fixtures/neg_missing_field.json"),
        |e| matches!(e, NirError::MissingField("v_threshold")),
    ),
    (
        "unknown endpoint",
        include_str!("../tests/nir_fixtures/neg_unknown_endpoint.json"),
        |e| matches!(e, NirError::UnknownEdgeEndpoint("ghost")),
    ),
    (
        "duplicate edge",
        include_str!("../tests/nir_fixtures/neg_duplicate_edge.json"),
        |e| matches!(e, NirError::DuplicateEdge),
    ),
    (
        "ragged weight",
        include_str!("../tests/nir_fixtures/neg_ragged_weight.json"),
        |e| matches!(e, NirError::BadShape("weight")),
    ),
    (
        "param length mismatch (population)",
        include_str!("../tests/nir_fixtures/neg_param_length.json"),
        |e| matches!(e, NirError::BadShape("LIF param")),
    ),
    (
        "missing version",
        include_str!("../tests/nir_fixtures/neg_missing_version.json"),
        |e| matches!(e, NirError::MissingField("version")),
    ),
    (
        "escaped name",
        include_str!("../tests/nir_fixtures/neg_escaped_name.json"),
        |e| matches!(e, NirError::EscapedOrNonAsciiString(_)),
    ),
];

fn fail(msg: &str) -> ! {
    eprintln!("NIR FORMAT GATE: FAIL — {msg}");
    std::process::exit(1);
}

fn main() {
    println!(
        "=== NIR format gate — reference-emitted vectors, slice 1 (Input/Linear/LIF/Output) ==="
    );
    println!("ref     : neuromorphs/NIR @ {NIR_REF_SHA}");
    println!("export  : version block \"{EXPORT_VERSION}\" (one block, derived values rendered)");
    println!();

    // ---- gate 1: reference chain, exact quantization ----------------
    let scan = nir_scan(CHAIN.as_bytes()).unwrap_or_else(|e| fail(&e.to_string()));
    println!(
        "fixture : chain.json — {} nodes / {} edges / {} weight cells, version \"{}\"",
        scan.node_count, scan.edge_count, scan.weight_cells, scan.version
    );
    let opts = NirImportOptions::default();
    let g = NirImport::from_json(CHAIN.as_bytes(), opts)
        .unwrap_or_else(|e| fail(&format!("chain import: {e}")));
    let pop = g.nodes[2].lif.expect("lif");
    let lif = g.lifs[pop.offset];
    let lin = g.nodes[1].linear.expect("linear");
    print!(
        "quant   : LIF tau {} us · R {} MOhm · leak {}/thr {}/reset {} quanta · C {} pF",
        lif.tau_us,
        lif.resistance_mohm,
        lif.leak_q,
        lif.threshold_q,
        lif.reset_q,
        lif.capacitance_pf
    );
    if lif.max_v_err_v > 0.0 || lif.tau_err_s > 0.0 {
        print!(
            "  (lossy: dv<={:e} V, dtau<={:e} s)",
            lif.max_v_err_v, lif.tau_err_s
        );
    }
    println!();
    println!(
        "quant   : Linear {}x{} @ scale {:.6e} — weights {:?}",
        lin.rows, lin.cols, lin.scale, g.weights
    );
    if g.weights != vec![16384, -32767, 8192] {
        fail("dyadic weights must quantize to [16384, -32767, 8192]");
    }
    if (lif.tau_us, lif.leak_q, lif.threshold_q, lif.reset_q) != (20_000, -70, -55, -80) {
        fail("LIF quanta must be exactly (20000 us, -70, -55, -80)");
    }
    println!("gate 1  : PASS — reference emission quantizes exactly");

    // absent-v_reset semantics (reference from_dict) + notes
    let scan2 = nir_scan(CHAIN_VRESET.as_bytes()).unwrap();
    let mut nodes = vec![blank(); scan2.node_count];
    let mut edges = vec![(0u32, 0u32); scan2.edge_count];
    let mut weights = vec![0i16; scan2.weight_cells];
    let mut lifs2 = vec![NirLif::default(); scan2.lif_neurons];
    let mut scratch = vec![0f64; scan2.weight_cells + 5 * scan2.lif_neurons];
    let report = {
        let mut bufs = NirBuffers {
            nodes: &mut nodes,
            edges: &mut edges,
            weights: &mut weights,
            lifs: &mut lifs2,
            scratch: &mut scratch,
        };
        neuralos_snn::nir::nir_import(CHAIN_VRESET.as_bytes(), opts, &mut bufs)
            .unwrap_or_else(|e| fail(&format!("absent-v_reset import: {e}")))
    };
    if report.notes[NirNote::VResetDefaulted as usize] == 0 {
        fail("absent v_reset must default with a note");
    }
    if report.notes[NirNote::QuantizationLoss as usize] == 0 {
        fail("non-dyadic weights (0.1) must note their loss");
    }
    println!("gate 1b : PASS — absent v_reset defaults (noted), lossy weights noted ({} loss notes total)",
        report.note_count());

    // ---- gate 2: negative fixtures, named errors --------------------
    for (label, doc, check) in NEGATIVES {
        let err = NirImport::from_json(doc.as_bytes(), opts).expect_err(label);
        if !check(&err) {
            fail(&format!("{label}: wrong error — {err}"));
        }
    }
    println!(
        "gate 2  : PASS — {} negative fixtures reject with named errors",
        NEGATIVES.len()
    );

    // ---- gate 3: assemble + fire ------------------------------------
    let (mut net, enc) = g
        .build_chain_network()
        .unwrap_or_else(|e| fail(&format!("assembly: {e}")));
    let mut spikes = 0usize;
    let mut first_spike_step = usize::MAX;
    for t in 0..100u32 {
        let fired = net
            .step(&enc.encode(&[4, 0, 0]))
            .unwrap_or_else(|e| fail(&e.to_string()));
        if !fired.is_empty() && first_spike_step == usize::MAX {
            first_spike_step = t as usize;
        }
        spikes += fired.len();
    }
    if spikes == 0 {
        fail("the imported chain must spike under sustained drive");
    }
    println!("gate 3  : PASS — chain fires on the substrate: {spikes} spikes / 100 steps, first at step {first_spike_step}");

    // ---- gate 4: export byte-stability + idempotence ----------------
    let mut out = vec![0u8; 4096];
    let n = nir_export(&g.nodes, &g.edges, &g.weights, &g.lifs, opts, &mut out)
        .unwrap_or_else(|e| fail(&format!("export: {e}")));
    out.truncate(n);
    let mut out2 = vec![0u8; 4096];
    let n2 = nir_export(&g.nodes, &g.edges, &g.weights, &g.lifs, opts, &mut out2).unwrap();
    if out != out2[..n2] {
        fail("export must be byte-stable");
    }
    let g2 = NirImport::from_json(&out, opts).unwrap_or_else(|e| fail(&format!("re-import: {e}")));
    if g2.weights != g.weights {
        fail("re-imported weights differ");
    }
    let pop2 = g2.nodes[2].lif.unwrap();
    let l2 = g2.lifs[pop2.offset];
    if (l2.tau_us, l2.threshold_q, l2.leak_q, l2.reset_q)
        != (lif.tau_us, lif.threshold_q, lif.leak_q, lif.reset_q)
    {
        fail("re-imported LIF quanta differ");
    }
    println!("gate 4  : PASS — export byte-stable ({n} B), re-import state-identical, provenance in metadata");
    println!();
    println!("NIR FORMAT GATE: PASS — 4/4 gates on reference-emitted vectors");

    // debugging affordance (fork-family precedent): dump the exported
    // bytes for external consumption, e.g. re-loading them through
    // the pinned reference implementation's own loader.
    if let Ok(path) = std::env::var("NEURALOS_NIR_DUMP") {
        std::fs::write(&path, &out).unwrap_or_else(|e| fail(&format!("dump: {e}")));
        println!("dumped  : {path} ({n} B)");
    }
}

fn blank() -> NirNode<'static> {
    NirNode {
        name: "",
        kind: NirNodeKind::Input,
        shape: [0; 4],
        shape_len: 0,
        lif: None,
        linear: None,
    }
}