mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
332 lines
13 KiB
Python
332 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate deterministic SNN/NII test vectors.
|
|
|
|
Profiles:
|
|
synthetic Legacy/simple synthetic stream.
|
|
balanced5 Five-phase bring-up profile with recovery phase.
|
|
uc_records Derive observations from UnifiedCompression/RGFlow records.
|
|
|
|
The balanced5 profile is designed for hardware bring-up:
|
|
phase 0: lawful_stable
|
|
phase 1: near_miss_boundary
|
|
phase 2: reject_controlled
|
|
phase 3: reject_corridor
|
|
phase 4: recovery_lawful
|
|
|
|
Target distribution, when adaptive reference mode is available:
|
|
lawful: 35-45%
|
|
near_miss: 15-25%
|
|
reject: 35-45%
|
|
|
|
The generator can import snn_nii_reference.py and use it as a calibration oracle
|
|
so generated vectors are not silently masking FAMM saturation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import argparse, copy, importlib.util, json, sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
def clamp(x, lo, hi):
|
|
return lo if x < lo else hi if x > hi else x
|
|
|
|
|
|
def load_reference(config_path: str):
|
|
here = Path(__file__).resolve().parent
|
|
ref_path = here / "snn_nii_reference.py"
|
|
if not ref_path.exists():
|
|
return None, None
|
|
spec = importlib.util.spec_from_file_location("snn_nii_reference", ref_path)
|
|
ref = importlib.util.module_from_spec(spec)
|
|
sys.modules["snn_nii_reference"] = ref
|
|
spec.loader.exec_module(ref) # type: ignore[union-attr]
|
|
cfg = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
return ref, cfg
|
|
|
|
|
|
def event_from_uc_record(obj: dict, i: int) -> dict:
|
|
topo = obj.get("topology18", {})
|
|
rg = obj.get("rgflow", {})
|
|
observed = [
|
|
int(topo.get("theta", 0)) * 16,
|
|
int(topo.get("phi", 0)) * 32,
|
|
int(topo.get("tau", 0)) * 16,
|
|
(int(topo.get("C", 0)) - int(topo.get("A", 0))) * 32,
|
|
]
|
|
observed = [clamp(x, -512, 512) for x in observed]
|
|
return {
|
|
"i": i,
|
|
"source": "uc_records",
|
|
"observed": observed,
|
|
"coherence": int(float(rg.get("coherence", 0.5)) * 255),
|
|
"compression": int(float(rg.get("compression_gain", 0.5)) * 255),
|
|
"failure": 64 if rg.get("verdict") == "reject" else 16 if rg.get("verdict") == "near_miss" else 0,
|
|
"state18": int(topo.get("addr18", 0)),
|
|
}
|
|
|
|
|
|
def legacy_synthetic_event(i: int) -> dict:
|
|
phase = (i // 64) % 4
|
|
if phase == 0:
|
|
observed = [80, 40, 80, 20]
|
|
coherence, compression, failure = 210, 180, 0
|
|
elif phase == 1:
|
|
observed = [80 + (i % 64), 48, 80 - (i % 64), 24]
|
|
coherence, compression, failure = 155, 135, 16
|
|
elif phase == 2:
|
|
observed = [280, -200, 220, -160]
|
|
coherence, compression, failure = 105, 90, 64
|
|
else:
|
|
observed = [160 if i % 2 else 96, 64, 128 if i % 3 else 64, 32]
|
|
coherence, compression, failure = 145, 132, 40
|
|
return {
|
|
"i": i,
|
|
"source": "synthetic",
|
|
"observed": observed,
|
|
"coherence": coherence,
|
|
"compression": compression,
|
|
"failure": failure,
|
|
"state18": (i * 8191) & ((1 << 18) - 1),
|
|
}
|
|
|
|
|
|
PHASES = [
|
|
("lawful_stable", "lawful"),
|
|
("near_miss_boundary", "near_miss"),
|
|
("reject_controlled", "reject"),
|
|
("reject_corridor", "reject"),
|
|
("recovery_lawful", "lawful"),
|
|
]
|
|
|
|
|
|
def balanced_candidate_pool(family: str, i: int, phase: int, j: int) -> List[Tuple[List[int], int, int, int]]:
|
|
# Small pool on purpose: fast enough for generation, wide enough to expose all verdicts.
|
|
pools = {
|
|
"lawful": [
|
|
([80, 40, 80, 20], 235, 210, 0),
|
|
([82, 41, 79, 21], 245, 220, 0),
|
|
([76, 38, 84, 18], 225, 205, 4),
|
|
([88, 44, 72, 24], 215, 195, 8),
|
|
],
|
|
"near_miss": [
|
|
([116, 52, 96, 28], 165, 150, 18),
|
|
([128, 60, 104, 34], 175, 160, 22),
|
|
([100, 48, 92, 26], 155, 145, 28),
|
|
([140, 70, 120, 40], 180, 170, 36),
|
|
([112, 50, 96, 30], 150, 150, 42),
|
|
],
|
|
"reject": [
|
|
([150, -60, 130, -50], 145, 130, 52),
|
|
([170, -80, 150, -60], 135, 120, 62),
|
|
([190, -100, 160, -80], 125, 110, 72),
|
|
([210, -120, 180, -96], 115, 100, 82),
|
|
([230, -140, 190, -110], 105, 90, 92),
|
|
],
|
|
"recovery": [
|
|
([120, 60, 100, 30], 190, 175, 12),
|
|
([100, 50, 90, 25], 210, 190, 6),
|
|
([80, 40, 80, 20], 235, 210, 0),
|
|
([76, 38, 84, 18], 245, 220, 0),
|
|
],
|
|
}
|
|
rows = pools[family][:]
|
|
offset = (i * 17 + phase * 31 + j * 7) % len(rows)
|
|
return rows[offset:] + rows[:offset]
|
|
|
|
|
|
def init_ref_state(ref: Any, cfg: dict):
|
|
return (
|
|
ref.NIIState.init(cfg["delay_taps"], len(cfg["channels"])),
|
|
ref.MSNNState.init(cfg["msnn"]["neurons"]),
|
|
ref.FAMMState(),
|
|
)
|
|
|
|
|
|
def eval_candidate(ref: Any, cfg: dict, state, event: dict):
|
|
nii, msnn, famm = copy.deepcopy(state)
|
|
pred, surprise = ref.nii_step(nii, event["observed"], cfg)
|
|
msnn_out = ref.msnn_step(msnn, surprise, famm, cfg)
|
|
rg = ref.rgflow_step(surprise, msnn_out, event, famm, cfg)
|
|
flags = ref.famm_update(famm, rg, cfg)
|
|
return (nii, msnn, famm), pred, surprise, msnn_out, rg, flags
|
|
|
|
|
|
def choose_adaptive_event(ref: Any, cfg: dict, state, i: int, phase: int, j: int, counts: dict, n: int):
|
|
phase_name, phase_desired = PHASES[phase]
|
|
# If distribution is drifting, nudge desired verdict toward the target band.
|
|
progress = max(1, i)
|
|
ratios = {k: counts.get(k, 0) / progress for k in ["lawful", "near_miss", "reject"]}
|
|
if ratios["reject"] < 0.35 and phase in (2, 3):
|
|
desired = "reject"
|
|
elif ratios["near_miss"] < 0.15 and phase == 1:
|
|
desired = "near_miss"
|
|
elif ratios["lawful"] < 0.35 and phase in (0, 4):
|
|
desired = "lawful"
|
|
else:
|
|
desired = phase_desired
|
|
|
|
family = "recovery" if phase == 4 else desired
|
|
candidates = []
|
|
# Also allow adjacent family fallback; this avoids saturation while preserving target pressure.
|
|
families = [family]
|
|
if desired == "reject":
|
|
families += ["near_miss", "recovery"]
|
|
elif desired == "near_miss":
|
|
families += ["lawful", "reject"]
|
|
elif desired == "lawful":
|
|
families += ["near_miss"]
|
|
|
|
for fam in families:
|
|
for obs, coh, comp, fail in balanced_candidate_pool(fam, i, phase, j):
|
|
event = {
|
|
"i": i,
|
|
"source": "synthetic_5phase_balanced",
|
|
"profile": "balanced5",
|
|
"phase": phase,
|
|
"phase_name": phase_name,
|
|
"desired_verdict": desired,
|
|
"observed": obs[:],
|
|
"coherence": coh,
|
|
"compression": comp,
|
|
"failure": fail,
|
|
"state18": (i * 8191) & ((1 << 18) - 1),
|
|
}
|
|
st2, pred, sur, msnn_out, rg, flags = eval_candidate(ref, cfg, state, event)
|
|
candidates.append((event, st2, pred, sur, msnn_out, rg, flags))
|
|
|
|
best = None
|
|
best_score = 10**9
|
|
target_center = {"lawful": 0.40, "near_miss": 0.20, "reject": 0.40}
|
|
for item in candidates:
|
|
event, st2, pred, sur, msnn_out, rg, flags = item
|
|
verdict = rg["verdict"]
|
|
famm = st2[2]
|
|
trial_counts = counts.copy()
|
|
trial_counts[verdict] = trial_counts.get(verdict, 0) + 1
|
|
denom = i + 1
|
|
dist_penalty = sum(abs((trial_counts.get(k, 0) / denom) - target_center[k]) for k in target_center) * 1000
|
|
verdict_penalty = 0 if verdict == event["desired_verdict"] else 180
|
|
# Saturation is a failure-contract issue, not just a cost. Penalize very heavily.
|
|
saturation_penalty = 2000 if flags["any_saturated"] else 0
|
|
headroom_penalty = max(0, famm.frustration - 180) * 4 + max(0, famm.torsion - 180) * 4 + max(0, famm.basin - 220)
|
|
# Prefer controlled reject and middle-band near_miss, not extremes.
|
|
if verdict == "reject":
|
|
band_penalty = abs(int(rg["reject_pressure"]) - 180) / 4
|
|
elif verdict == "near_miss":
|
|
band_penalty = abs(int(rg["reject_pressure"]) - 80) / 4
|
|
else:
|
|
band_penalty = int(rg["reject_pressure"]) / 4
|
|
score = dist_penalty + verdict_penalty + saturation_penalty + headroom_penalty + band_penalty
|
|
if score < best_score:
|
|
best_score = score
|
|
best = item
|
|
assert best is not None
|
|
event, st2, pred, sur, msnn_out, rg, flags = best
|
|
event["expected_verdict"] = rg["verdict"]
|
|
event["expected_reject_pressure"] = rg["reject_pressure"]
|
|
event["expected_famm_after"] = {
|
|
"frustration": st2[2].frustration,
|
|
"basin": st2[2].basin,
|
|
"torsion": st2[2].torsion,
|
|
}
|
|
event["expected_famm_saturated"] = bool(flags["any_saturated"])
|
|
return event, st2
|
|
|
|
|
|
def balanced5_events(n: int, phase_len: int, ref: Any = None, cfg: dict = None) -> List[dict]:
|
|
rows = []
|
|
if ref is None or cfg is None:
|
|
# Static fallback, still 5 phases and recovery but no expected-verdict fields.
|
|
for i in range(n):
|
|
phase = (i // phase_len) % 5
|
|
j = i % phase_len
|
|
name, desired = PHASES[phase]
|
|
family = "recovery" if phase == 4 else desired
|
|
obs, coh, comp, fail = balanced_candidate_pool(family, i, phase, j)[0]
|
|
rows.append({
|
|
"i": i,
|
|
"source": "synthetic_5phase_balanced_static",
|
|
"profile": "balanced5",
|
|
"phase": phase,
|
|
"phase_name": name,
|
|
"desired_verdict": desired,
|
|
"observed": obs,
|
|
"coherence": coh,
|
|
"compression": comp,
|
|
"failure": fail,
|
|
"state18": (i * 8191) & ((1 << 18) - 1),
|
|
})
|
|
return rows
|
|
|
|
state = init_ref_state(ref, cfg)
|
|
counts = {"lawful": 0, "near_miss": 0, "reject": 0}
|
|
for i in range(n):
|
|
phase = (i // phase_len) % 5
|
|
j = i % phase_len
|
|
event, state = choose_adaptive_event(ref, cfg, state, i, phase, j, counts, n)
|
|
counts[event["expected_verdict"]] += 1
|
|
rows.append(event)
|
|
return rows
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--out", default="sample_vectors.jsonl")
|
|
ap.add_argument("--n", type=int, default=512)
|
|
ap.add_argument("--uc-records", default=None)
|
|
ap.add_argument("--profile", choices=["synthetic", "balanced5", "uc_records"], default="balanced5")
|
|
ap.add_argument("--phase-len", type=int, default=1)
|
|
ap.add_argument("--config", default="snn_test_config.json")
|
|
ap.add_argument("--no-adaptive", action="store_true")
|
|
ap.add_argument("--summary", default=None)
|
|
args = ap.parse_args()
|
|
|
|
rows = []
|
|
if args.profile == "uc_records" or args.uc_records:
|
|
if not args.uc_records:
|
|
raise SystemExit("--uc-records is required for uc_records profile")
|
|
with open(args.uc_records, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
try:
|
|
obj = json.loads(line)
|
|
except Exception:
|
|
continue
|
|
if obj.get("record_type") == "uc_rgflow_chunk" and "topology18" in obj:
|
|
rows.append(event_from_uc_record(obj, len(rows)))
|
|
if len(rows) >= args.n:
|
|
break
|
|
elif args.profile == "synthetic":
|
|
rows = [legacy_synthetic_event(i) for i in range(args.n)]
|
|
else:
|
|
ref, cfg = (None, None) if args.no_adaptive else load_reference(args.config)
|
|
rows = balanced5_events(args.n, args.phase_len, ref=ref, cfg=cfg)
|
|
|
|
with open(args.out, "w", encoding="utf-8") as out:
|
|
for row in rows:
|
|
out.write(json.dumps(row, separators=(",", ":")) + "\n")
|
|
|
|
if args.summary:
|
|
expected = {"lawful": 0, "near_miss": 0, "reject": 0, "unknown": 0}
|
|
sat = 0
|
|
phase_counts = {name: 0 for name, _ in PHASES}
|
|
for row in rows:
|
|
expected[row.get("expected_verdict", "unknown")] = expected.get(row.get("expected_verdict", "unknown"), 0) + 1
|
|
sat += 1 if row.get("expected_famm_saturated") else 0
|
|
phase_counts[row.get("phase_name", "unknown")] = phase_counts.get(row.get("phase_name", "unknown"), 0) + 1
|
|
summary = {
|
|
"profile": args.profile,
|
|
"n": len(rows),
|
|
"phase_len": args.phase_len,
|
|
"expected_verdicts": expected,
|
|
"expected_ratios": {k: (v / max(1, len(rows))) for k, v in expected.items()},
|
|
"expected_saturated_steps": sat,
|
|
"phase_counts": phase_counts,
|
|
"target": {"lawful": "35-45%", "near_miss": "15-25%", "reject": "35-45%"},
|
|
}
|
|
Path(args.summary).write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|