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");
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!();
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");
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());
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()
);
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}");
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");
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,
}
}