diff --git a/.github/workflows/neural-fabric.yml b/.github/workflows/neural-fabric.yml index b97a966..30593e5 100644 --- a/.github/workflows/neural-fabric.yml +++ b/.github/workflows/neural-fabric.yml @@ -43,7 +43,7 @@ jobs: python-version: "3.12" - name: Install numerical stack - run: python -m pip install --upgrade pip pytest numpy scipy scikit-learn matplotlib + run: python -m pip install --upgrade pip pytest numpy scipy scikit-learn matplotlib jsonschema - name: Run Neural Fabric CI run: make -f mk/neural-fabric.mk neural-fabric-ci diff --git a/mk/neural-fabric.mk b/mk/neural-fabric.mk index f1471d5..6ada279 100644 --- a/mk/neural-fabric.mk +++ b/mk/neural-fabric.mk @@ -4,20 +4,18 @@ neural-fabric-static: python3 -m json.tool schemas/neural-fabric/model-family.v1.json >/dev/null python3 -m json.tool schemas/neural-fabric/targeting-experiment.v1.json >/dev/null python3 -m json.tool schemas/neural-fabric/targeting-result.v1.json >/dev/null + python3 -m json.tool schemas/neural-fabric/intervention-event.v1.json >/dev/null python3 -m compileall packages/superconscious-core/superconscious_core/neural_fabric scripts research/activation-time-targeting/code >/dev/null -# NOTE: neural-fabric-smoke / neural-fabric-full depend on the results-validation -# and experiment-harness that are not yet committed (scripts/validate-neural-fabric-results.py, -# scripts/check-capacity-bounds.py, tests/neural_fabric, research/.../run_suite.py). They are -# kept here as the roadmap for that harness and are intentionally NOT part of neural-fabric-ci -# until those files land, so CI gates only what this module actually delivers. neural-fabric-smoke: python3 scripts/validate-neural-fabric-results.py research/activation-time-targeting python3 scripts/check-capacity-bounds.py --m 60 --C 0.4 --s 0.1 python3 -m pytest tests/neural_fabric -q +# neural-fabric-full regenerates the committed reference results/events in place +# (deterministic) and re-validates them, so it doubles as a reproducibility check. neural-fabric-full: - python3 research/activation-time-targeting/code/run_suite.py --out research/activation-time-targeting/results/generated + python3 research/activation-time-targeting/code/run_suite.py --out research/activation-time-targeting python3 scripts/validate-neural-fabric-results.py research/activation-time-targeting -neural-fabric-ci: neural-fabric-static +neural-fabric-ci: neural-fabric-static neural-fabric-smoke diff --git a/research/activation-time-targeting/code/run_suite.py b/research/activation-time-targeting/code/run_suite.py new file mode 100644 index 0000000..6665481 --- /dev/null +++ b/research/activation-time-targeting/code/run_suite.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Deterministic, CPU-only activation-time targeting suite. + +Each experiment exercises a Neural Fabric reference primitive and emits a +``targeting-result.v1`` document under ``/results``. Interventions +additionally emit ``intervention-event.v1`` audit records under ``/events`` +-- the doctrine governs every intervention by an audit event. The experiments +are intentionally ``toy_model_confirmed``: they demonstrate that steering and +capacity monitoring work *without updating weights* (``weights_updated`` is +always ``false``), not that they are production-safe. + + python3 research/activation-time-targeting/code/run_suite.py \ + --out research/activation-time-targeting +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys + +import numpy as np + +_ROOT = pathlib.Path(__file__).resolve().parents[3] +sys.path.insert(0, str(_ROOT / "packages" / "superconscious-core")) + +from superconscious_core.neural_fabric.hopfield import ( # noqa: E402 + hopfield_retrieve, + logit_boost, +) +from superconscious_core.neural_fabric.may_wigner import ( # noqa: E402 + classify_may_wigner, + may_wigner_number, +) + +EPISTEMIC_STATUS = "toy_model_confirmed" + + +def experiment_hopfield_logit_boost() -> tuple[dict, list[dict]]: + """NF-ATT-001: steer a Hopfield readout via logit boosting, no weight update.""" + patterns = np.eye(4) + query = np.full(4, 0.25) + target_idx = 1 + _, base_weights = hopfield_retrieve(patterns, query, beta=1.0) + base_prob = float(base_weights[target_idx]) + + rows: list[dict] = [] + events: list[dict] = [] + prev = -1.0 + monotone = True + for strength in (0.0, 1.0, 2.0, 3.0): + dist = logit_boost(patterns, query, target_idx=target_idx, strength=strength, beta=1.0) + prob = float(dist[target_idx]) + monotone = monotone and (prob >= prev - 1e-12) + prev = prob + rows.append({"strength": strength, "target_prob": round(prob, 6)}) + if strength > 0.0: + events.append( + { + "event_id": f"NF-ATT-001-EV{len(events) + 1:03d}", + "experiment_id": "NF-ATT-001", + "intervention_type": "logit_boost", + "target_locus": "attention-logits", + "weights_updated": False, + "params": {"target_idx": target_idx, "strength": strength, "beta": 1.0}, + "observed": {"target_prob": round(prob, 6)}, + "epistemic_status": EPISTEMIC_STATUS, + } + ) + + boosted_prob = rows[-1]["target_prob"] + argmax_is_target = bool( + np.argmax(logit_boost(patterns, query, target_idx=target_idx, strength=3.0, beta=1.0)) == target_idx + ) + result = { + "experiment_id": "NF-ATT-001", + "weights_updated": False, + "epistemic_status": EPISTEMIC_STATUS, + "claim_invariants": [ + "weights_updated == false", + "intervention is activation-time only", + "target probability is monotone non-decreasing in boost strength", + ], + "metrics": { + "baseline_target_prob": round(base_prob, 6), + "boosted_target_prob": boosted_prob, + "target_prob_lift": round(boosted_prob - base_prob, 6), + "argmax_is_target_after_boost": argmax_is_target, + "lift_is_monotone": bool(monotone), + }, + "rows": rows, + } + return result, events + + +def experiment_may_wigner_sweep() -> tuple[dict, list[dict]]: + """NF-ATT-002: sweep active-feature count and locate the stability boundaries.""" + C, s = 0.4, 0.1 + rows: list[dict] = [] + boundaries: dict[str, int | None] = {"warn": None, "error": None, "stop": None} + max_ok_m = 0 + for m in range(10, 401, 10): + value = may_wigner_number(m, C, s) + cls = classify_may_wigner(value) + if cls == "ok": + max_ok_m = m + for level in ("warn", "error", "stop"): + if cls == level and boundaries[level] is None: + boundaries[level] = m + rows.append({"m": m, "may_wigner": round(value, 6), "classification": cls}) + + result = { + "experiment_id": "NF-ATT-002", + "weights_updated": False, + "epistemic_status": EPISTEMIC_STATUS, + "claim_invariants": [ + "weights_updated == false", + "control number is s*sqrt(m*C)", + "classification is monotone non-decreasing in m", + ], + "metrics": { + "C": C, + "s": s, + "max_ok_m": max_ok_m, + "m_first_warn": boundaries["warn"] if boundaries["warn"] is not None else -1, + "m_first_error": boundaries["error"] if boundaries["error"] is not None else -1, + "m_first_stop": boundaries["stop"] if boundaries["stop"] is not None else -1, + }, + "rows": rows, + } + return result, [] # capacity monitoring is observation, not intervention + + +EXPERIMENTS = (experiment_hopfield_logit_boost, experiment_may_wigner_sweep) + + +def _write(path: pathlib.Path, doc: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Activation-time targeting suite") + p.add_argument( + "--out", + type=pathlib.Path, + default=_ROOT / "research" / "activation-time-targeting", + help="suite root; results/ and events/ are written beneath it", + ) + args = p.parse_args(argv) + + for build in EXPERIMENTS: + result, events = build() + exp_id = result["experiment_id"] + _write(args.out / "results" / f"{exp_id}.result.json", result) + print(f"[ok] {exp_id} result -> {args.out / 'results' / f'{exp_id}.result.json'}") + for event in events: + _write(args.out / "events" / f"{event['event_id']}.event.json", event) + if events: + print(f"[ok] {exp_id} emitted {len(events)} intervention event(s)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/research/activation-time-targeting/events/NF-ATT-001-EV001.event.json b/research/activation-time-targeting/events/NF-ATT-001-EV001.event.json new file mode 100644 index 0000000..6204ef4 --- /dev/null +++ b/research/activation-time-targeting/events/NF-ATT-001-EV001.event.json @@ -0,0 +1,16 @@ +{ + "epistemic_status": "toy_model_confirmed", + "event_id": "NF-ATT-001-EV001", + "experiment_id": "NF-ATT-001", + "intervention_type": "logit_boost", + "observed": { + "target_prob": 0.475367 + }, + "params": { + "beta": 1.0, + "strength": 1.0, + "target_idx": 1 + }, + "target_locus": "attention-logits", + "weights_updated": false +} diff --git a/research/activation-time-targeting/events/NF-ATT-001-EV002.event.json b/research/activation-time-targeting/events/NF-ATT-001-EV002.event.json new file mode 100644 index 0000000..a961678 --- /dev/null +++ b/research/activation-time-targeting/events/NF-ATT-001-EV002.event.json @@ -0,0 +1,16 @@ +{ + "epistemic_status": "toy_model_confirmed", + "event_id": "NF-ATT-001-EV002", + "experiment_id": "NF-ATT-001", + "intervention_type": "logit_boost", + "observed": { + "target_prob": 0.711235 + }, + "params": { + "beta": 1.0, + "strength": 2.0, + "target_idx": 1 + }, + "target_locus": "attention-logits", + "weights_updated": false +} diff --git a/research/activation-time-targeting/events/NF-ATT-001-EV003.event.json b/research/activation-time-targeting/events/NF-ATT-001-EV003.event.json new file mode 100644 index 0000000..3eabe7b --- /dev/null +++ b/research/activation-time-targeting/events/NF-ATT-001-EV003.event.json @@ -0,0 +1,16 @@ +{ + "epistemic_status": "toy_model_confirmed", + "event_id": "NF-ATT-001-EV003", + "experiment_id": "NF-ATT-001", + "intervention_type": "logit_boost", + "observed": { + "target_prob": 0.870049 + }, + "params": { + "beta": 1.0, + "strength": 3.0, + "target_idx": 1 + }, + "target_locus": "attention-logits", + "weights_updated": false +} diff --git a/research/activation-time-targeting/experiments/NF-ATT-001.experiment.json b/research/activation-time-targeting/experiments/NF-ATT-001.experiment.json new file mode 100644 index 0000000..5f1c15e --- /dev/null +++ b/research/activation-time-targeting/experiments/NF-ATT-001.experiment.json @@ -0,0 +1,15 @@ +{ + "experiment_id": "NF-ATT-001", + "title": "Hopfield logit-boost steering without weight update", + "model_family": "hopfield-associative", + "target_locus": "attention-logits", + "intervention_type": "logit_boost", + "weights_updated": false, + "seed_policy": "deterministic-fixed-inputs", + "metrics": ["baseline_target_prob", "boosted_target_prob", "target_prob_lift", "lift_is_monotone"], + "artifacts": { + "results": ["results/NF-ATT-001.result.json"] + }, + "claim_invariants": ["weights_updated == false", "intervention is activation-time only"], + "epistemic_status": "toy_model_confirmed" +} diff --git a/research/activation-time-targeting/experiments/NF-ATT-002.experiment.json b/research/activation-time-targeting/experiments/NF-ATT-002.experiment.json new file mode 100644 index 0000000..9d1ab93 --- /dev/null +++ b/research/activation-time-targeting/experiments/NF-ATT-002.experiment.json @@ -0,0 +1,15 @@ +{ + "experiment_id": "NF-ATT-002", + "title": "May-Wigner capacity sweep over active-feature count", + "model_family": "hopfield-associative", + "target_locus": "feature-activation-density", + "intervention_type": "capacity_monitor", + "weights_updated": false, + "seed_policy": "deterministic-fixed-inputs", + "metrics": ["max_ok_m", "m_first_warn", "m_first_error", "m_first_stop"], + "artifacts": { + "results": ["results/NF-ATT-002.result.json"] + }, + "claim_invariants": ["weights_updated == false", "control number is s*sqrt(m*C)"], + "epistemic_status": "toy_model_confirmed" +} diff --git a/research/activation-time-targeting/families/hopfield-associative.family.json b/research/activation-time-targeting/families/hopfield-associative.family.json new file mode 100644 index 0000000..dfefc71 --- /dev/null +++ b/research/activation-time-targeting/families/hopfield-associative.family.json @@ -0,0 +1,15 @@ +{ + "family_id": "hopfield-associative", + "substrate": { + "default": "cpu-float64", + "allowed": ["cpu-float64", "cpu-float32"] + }, + "structure": { + "class": "modern-hopfield", + "replay_invariant": "deterministic-given-inputs" + }, + "target_loci": ["query-vector", "attention-logits", "feature-activation-density"], + "intervention_types": ["query_injection", "logit_boost", "capacity_monitor"], + "may_wigner": {"C": 0.4, "s": 0.1, "boundary": 1.0}, + "epistemic_status": "toy_model_confirmed" +} diff --git a/research/activation-time-targeting/results/NF-ATT-001.result.json b/research/activation-time-targeting/results/NF-ATT-001.result.json new file mode 100644 index 0000000..21dca4b --- /dev/null +++ b/research/activation-time-targeting/results/NF-ATT-001.result.json @@ -0,0 +1,35 @@ +{ + "claim_invariants": [ + "weights_updated == false", + "intervention is activation-time only", + "target probability is monotone non-decreasing in boost strength" + ], + "epistemic_status": "toy_model_confirmed", + "experiment_id": "NF-ATT-001", + "metrics": { + "argmax_is_target_after_boost": true, + "baseline_target_prob": 0.25, + "boosted_target_prob": 0.870049, + "lift_is_monotone": true, + "target_prob_lift": 0.620049 + }, + "rows": [ + { + "strength": 0.0, + "target_prob": 0.25 + }, + { + "strength": 1.0, + "target_prob": 0.475367 + }, + { + "strength": 2.0, + "target_prob": 0.711235 + }, + { + "strength": 3.0, + "target_prob": 0.870049 + } + ], + "weights_updated": false +} diff --git a/research/activation-time-targeting/results/NF-ATT-002.result.json b/research/activation-time-targeting/results/NF-ATT-002.result.json new file mode 100644 index 0000000..645680a --- /dev/null +++ b/research/activation-time-targeting/results/NF-ATT-002.result.json @@ -0,0 +1,220 @@ +{ + "claim_invariants": [ + "weights_updated == false", + "control number is s*sqrt(m*C)", + "classification is monotone non-decreasing in m" + ], + "epistemic_status": "toy_model_confirmed", + "experiment_id": "NF-ATT-002", + "metrics": { + "C": 0.4, + "m_first_error": 190, + "m_first_stop": 230, + "m_first_warn": 130, + "max_ok_m": 120, + "s": 0.1 + }, + "rows": [ + { + "classification": "ok", + "m": 10, + "may_wigner": 0.2 + }, + { + "classification": "ok", + "m": 20, + "may_wigner": 0.282843 + }, + { + "classification": "ok", + "m": 30, + "may_wigner": 0.34641 + }, + { + "classification": "ok", + "m": 40, + "may_wigner": 0.4 + }, + { + "classification": "ok", + "m": 50, + "may_wigner": 0.447214 + }, + { + "classification": "ok", + "m": 60, + "may_wigner": 0.489898 + }, + { + "classification": "ok", + "m": 70, + "may_wigner": 0.52915 + }, + { + "classification": "ok", + "m": 80, + "may_wigner": 0.565685 + }, + { + "classification": "ok", + "m": 90, + "may_wigner": 0.6 + }, + { + "classification": "ok", + "m": 100, + "may_wigner": 0.632456 + }, + { + "classification": "ok", + "m": 110, + "may_wigner": 0.663325 + }, + { + "classification": "ok", + "m": 120, + "may_wigner": 0.69282 + }, + { + "classification": "warn", + "m": 130, + "may_wigner": 0.72111 + }, + { + "classification": "warn", + "m": 140, + "may_wigner": 0.748331 + }, + { + "classification": "warn", + "m": 150, + "may_wigner": 0.774597 + }, + { + "classification": "warn", + "m": 160, + "may_wigner": 0.8 + }, + { + "classification": "warn", + "m": 170, + "may_wigner": 0.824621 + }, + { + "classification": "warn", + "m": 180, + "may_wigner": 0.848528 + }, + { + "classification": "error", + "m": 190, + "may_wigner": 0.87178 + }, + { + "classification": "error", + "m": 200, + "may_wigner": 0.894427 + }, + { + "classification": "error", + "m": 210, + "may_wigner": 0.916515 + }, + { + "classification": "error", + "m": 220, + "may_wigner": 0.938083 + }, + { + "classification": "stop", + "m": 230, + "may_wigner": 0.959166 + }, + { + "classification": "stop", + "m": 240, + "may_wigner": 0.979796 + }, + { + "classification": "stop", + "m": 250, + "may_wigner": 1.0 + }, + { + "classification": "stop", + "m": 260, + "may_wigner": 1.019804 + }, + { + "classification": "stop", + "m": 270, + "may_wigner": 1.03923 + }, + { + "classification": "stop", + "m": 280, + "may_wigner": 1.058301 + }, + { + "classification": "stop", + "m": 290, + "may_wigner": 1.077033 + }, + { + "classification": "stop", + "m": 300, + "may_wigner": 1.095445 + }, + { + "classification": "stop", + "m": 310, + "may_wigner": 1.113553 + }, + { + "classification": "stop", + "m": 320, + "may_wigner": 1.131371 + }, + { + "classification": "stop", + "m": 330, + "may_wigner": 1.148913 + }, + { + "classification": "stop", + "m": 340, + "may_wigner": 1.16619 + }, + { + "classification": "stop", + "m": 350, + "may_wigner": 1.183216 + }, + { + "classification": "stop", + "m": 360, + "may_wigner": 1.2 + }, + { + "classification": "stop", + "m": 370, + "may_wigner": 1.216553 + }, + { + "classification": "stop", + "m": 380, + "may_wigner": 1.232883 + }, + { + "classification": "stop", + "m": 390, + "may_wigner": 1.249 + }, + { + "classification": "stop", + "m": 400, + "may_wigner": 1.264911 + } + ], + "weights_updated": false +} diff --git a/schemas/neural-fabric/intervention-event.v1.json b/schemas/neural-fabric/intervention-event.v1.json new file mode 100644 index 0000000..523063b --- /dev/null +++ b/schemas/neural-fabric/intervention-event.v1.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.socioprophet.ai/neural-fabric/intervention-event.v1.json", + "title": "Neural Fabric Intervention Event", + "description": "Audit record for a single activation-time intervention. Interventions never update weights.", + "type": "object", + "required": ["event_id", "experiment_id", "intervention_type", "target_locus", "weights_updated", "params", "epistemic_status"], + "additionalProperties": false, + "properties": { + "event_id": {"type": "string", "pattern": "^NF-ATT-[0-9]{3}-EV[0-9]{3}$"}, + "experiment_id": {"type": "string", "pattern": "^NF-ATT-[0-9]{3}$"}, + "intervention_type": {"type": "string", "minLength": 1}, + "target_locus": {"type": "string", "minLength": 1}, + "weights_updated": {"type": "boolean", "const": false}, + "params": {"type": "object", "additionalProperties": {"type": ["number", "integer", "string", "boolean"]}}, + "observed": {"type": "object", "additionalProperties": {"type": ["number", "integer", "string", "boolean"]}}, + "epistemic_status": {"enum": ["toy_model_confirmed", "open_weight_validated", "production_validated"]} + } +} diff --git a/scripts/check-capacity-bounds.py b/scripts/check-capacity-bounds.py new file mode 100644 index 0000000..efdce0e --- /dev/null +++ b/scripts/check-capacity-bounds.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Check a May-Wigner capacity number against the stability policy. + +The control number is ``s * sqrt(m * C)`` (see +``superconscious_core.neural_fabric.may_wigner``). The stability boundary is +1.0; governance should warn well before it. This is a deterministic, CPU-only +gate used by ``neural-fabric-smoke``: it computes the number for the supplied +``--m/--C/--s`` and exits non-zero when the classification is worse than the +allowed ceiling. + + python3 scripts/check-capacity-bounds.py --m 60 --C 0.4 --s 0.1 +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "packages" / "superconscious-core")) + +from superconscious_core.neural_fabric.may_wigner import ( # noqa: E402 + classify_may_wigner, + may_wigner_number, +) + +# Order of increasing severity; used to compare against --max-class. +_SEVERITY = ["ok", "warn", "error", "stop"] + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="May-Wigner capacity gate") + p.add_argument("--m", type=float, required=True, help="active feature count") + p.add_argument("--C", type=float, required=True, help="feature co-activation density") + p.add_argument("--s", type=float, required=True, help="effective interaction scale") + p.add_argument("--warn", type=float, default=0.70) + p.add_argument("--error", type=float, default=0.85) + p.add_argument("--stop", type=float, default=0.95) + p.add_argument( + "--max-class", + choices=_SEVERITY, + default="warn", + help="highest classification allowed to pass (default: warn)", + ) + p.add_argument("--json", action="store_true", help="emit machine-readable JSON") + args = p.parse_args(argv) + + value = may_wigner_number(args.m, args.C, args.s) + cls = classify_may_wigner(value, warn=args.warn, error=args.error, stop=args.stop) + ok = _SEVERITY.index(cls) <= _SEVERITY.index(args.max_class) + + report = { + "m": args.m, + "C": args.C, + "s": args.s, + "may_wigner": round(value, 6), + "boundary": 1.0, + "classification": cls, + "max_class": args.max_class, + "pass": ok, + } + if args.json: + print(json.dumps(report, sort_keys=True)) + else: + print( + f"may_wigner={value:.6f} boundary=1.0 class={cls} " + f"max_class={args.max_class} -> {'PASS' if ok else 'FAIL'}" + ) + if not ok: + print( + f"[FAIL] capacity classification '{cls}' exceeds allowed '{args.max_class}'", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate-neural-fabric-results.py b/scripts/validate-neural-fabric-results.py new file mode 100644 index 0000000..54d58e4 --- /dev/null +++ b/scripts/validate-neural-fabric-results.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Validate Neural Fabric activation-time targeting artifacts. + +Given a suite root (e.g. ``research/activation-time-targeting``) this checks: + +* ``families/*.family.json`` against ``model-family.v1.json`` +* ``experiments/*.experiment.json`` against ``targeting-experiment.v1.json`` +* ``results/**/*.result.json`` against ``targeting-result.v1.json`` + +plus the cross-cutting doctrine invariants: every experiment and result declares +``weights_updated == false``, and every result's ``experiment_id`` resolves to a +committed experiment. Exits non-zero on the first failure it can report. + + python3 scripts/validate-neural-fabric-results.py research/activation-time-targeting +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys + +from jsonschema import Draft202012Validator + +_REPO = pathlib.Path(__file__).resolve().parents[1] +_SCHEMA_DIR = _REPO / "schemas" / "neural-fabric" + +_SETS = ( + ("families", "*.family.json", "model-family.v1.json"), + ("experiments", "*.experiment.json", "targeting-experiment.v1.json"), + ("results", "*.result.json", "targeting-result.v1.json"), + ("events", "*.event.json", "intervention-event.v1.json"), +) + + +def _load(path: pathlib.Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _validator(schema_name: str) -> Draft202012Validator: + return Draft202012Validator(_load(_SCHEMA_DIR / schema_name)) + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Validate neural-fabric suite artifacts") + p.add_argument("root", type=pathlib.Path, help="suite root, e.g. research/activation-time-targeting") + args = p.parse_args(argv) + root: pathlib.Path = args.root + + if not root.is_dir(): + print(f"[FAIL] suite root not found: {root}", file=sys.stderr) + return 2 + + errors: list[str] = [] + experiment_ids: set[str] = set() + result_experiment_ids: list[tuple[pathlib.Path, str]] = [] + checked = 0 + + for subdir, glob, schema_name in _SETS: + validator = _validator(schema_name) + for path in sorted((root / subdir).rglob(glob)): + checked += 1 + doc = _load(path) + for err in sorted(validator.iter_errors(doc), key=lambda e: e.path): + loc = "/".join(str(x) for x in err.path) or "" + errors.append(f"{path}: {loc}: {err.message}") + # doctrine invariant: activation-time targeting never updates weights + if subdir in ("experiments", "results", "events") and doc.get("weights_updated") is not False: + errors.append(f"{path}: weights_updated must be false (activation-time only)") + if subdir == "experiments": + experiment_ids.add(doc.get("experiment_id", "")) + if subdir in ("results", "events"): + result_experiment_ids.append((path, doc.get("experiment_id", ""))) + + for path, exp_id in result_experiment_ids: + if exp_id and exp_id not in experiment_ids: + errors.append(f"{path}: references experiment_id '{exp_id}' with no committed experiment") + + if checked == 0: + print(f"[FAIL] no artifacts found under {root}", file=sys.stderr) + return 2 + if errors: + for e in errors: + print(f"[FAIL] {e}", file=sys.stderr) + print(f"{len(errors)} neural-fabric artifact validation failure(s).", file=sys.stderr) + return 1 + print(f"[OK] {checked} neural-fabric artifact(s) valid under {root}.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/neural_fabric/test_hopfield.py b/tests/neural_fabric/test_hopfield.py new file mode 100644 index 0000000..d18f16c --- /dev/null +++ b/tests/neural_fabric/test_hopfield.py @@ -0,0 +1,56 @@ +"""Invariants for the Hopfield activation-time intervention primitives.""" +from __future__ import annotations + +import numpy as np +import pytest + +from superconscious_core.neural_fabric.hopfield import ( + hopfield_retrieve, + logit_boost, + query_injection, +) + + +def _orthonormal_patterns() -> np.ndarray: + return np.eye(4) + + +def test_retrieval_weights_form_a_distribution() -> None: + patterns = _orthonormal_patterns() + _, weights = hopfield_retrieve(patterns, patterns[0], beta=1.0) + assert weights.shape == (4,) + assert np.all(weights >= 0) + assert weights.sum() == pytest.approx(1.0) + + +def test_high_beta_concentrates_on_nearest_pattern() -> None: + patterns = _orthonormal_patterns() + retrieved, weights = hopfield_retrieve(patterns, patterns[2], beta=50.0) + assert int(np.argmax(weights)) == 2 + assert retrieved == pytest.approx(patterns[2], abs=1e-6) + + +def test_shape_mismatch_is_rejected() -> None: + with pytest.raises(ValueError): + hopfield_retrieve(np.eye(4), np.zeros(3)) + with pytest.raises(ValueError): + hopfield_retrieve(np.zeros(4), np.zeros(4)) # rank-1 patterns + + +def test_query_injection_shifts_query_toward_target() -> None: + q = np.zeros(4) + target = np.array([0.0, 1.0, 0.0, 0.0]) + out = query_injection(q, target, strength=2.0) + assert out == pytest.approx(np.array([0.0, 2.0, 0.0, 0.0])) + # Injection must not mutate the caller's query in place. + assert q == pytest.approx(np.zeros(4)) + + +def test_logit_boost_raises_target_probability_without_touching_weights() -> None: + patterns = _orthonormal_patterns() + query = np.full(4, 0.25) + _, base_weights = hopfield_retrieve(patterns, query, beta=1.0) + boosted = logit_boost(patterns, query, target_idx=1, strength=3.0, beta=1.0) + assert boosted.sum() == pytest.approx(1.0) + assert boosted[1] > base_weights[1] + assert int(np.argmax(boosted)) == 1 diff --git a/tests/neural_fabric/test_may_wigner.py b/tests/neural_fabric/test_may_wigner.py new file mode 100644 index 0000000..361a5a2 --- /dev/null +++ b/tests/neural_fabric/test_may_wigner.py @@ -0,0 +1,52 @@ +"""Invariants for the May-Wigner capacity monitor.""" +from __future__ import annotations + +from math import sqrt + +import pytest + +from superconscious_core.neural_fabric.may_wigner import ( + classify_may_wigner, + may_wigner_number, +) + + +def test_control_number_matches_closed_form() -> None: + assert may_wigner_number(60, 0.4, 0.1) == pytest.approx(0.1 * sqrt(60 * 0.4)) + assert may_wigner_number(0, 1, 1) == 0.0 + assert may_wigner_number(4, 1, 1) == pytest.approx(2.0) + + +def test_control_number_is_monotone_in_each_argument() -> None: + base = may_wigner_number(50, 0.4, 0.1) + assert may_wigner_number(80, 0.4, 0.1) > base + assert may_wigner_number(50, 0.6, 0.1) > base + assert may_wigner_number(50, 0.4, 0.2) > base + + +def test_negative_inputs_are_rejected() -> None: + for bad in ((-1, 1, 1), (1, -1, 1), (1, 1, -1)): + with pytest.raises(ValueError): + may_wigner_number(*bad) + + +@pytest.mark.parametrize( + "value,expected", + [ + (0.0, "ok"), + (0.69, "ok"), + (0.70, "warn"), + (0.84, "warn"), + (0.85, "error"), + (0.94, "error"), + (0.95, "stop"), + (1.5, "stop"), + ], +) +def test_classification_thresholds(value: float, expected: str) -> None: + assert classify_may_wigner(value) == expected + + +def test_smoke_operating_point_is_stable() -> None: + # The point exercised by neural-fabric-smoke must stay comfortably below warn. + assert classify_may_wigner(may_wigner_number(60, 0.4, 0.1)) == "ok" diff --git a/tests/neural_fabric/test_result_invariants.py b/tests/neural_fabric/test_result_invariants.py new file mode 100644 index 0000000..017d619 --- /dev/null +++ b/tests/neural_fabric/test_result_invariants.py @@ -0,0 +1,61 @@ +"""Invariants for the committed activation-time targeting artifacts. + +These pin the reference results/events so a regression in the suite or the +primitives is caught in CI, and they enforce the core doctrine (activation-time +interventions never update weights). +""" +from __future__ import annotations + +import json +import pathlib +import re + +import pytest + +_REPO = pathlib.Path(__file__).resolve().parents[2] +_SUITE = _REPO / "research" / "activation-time-targeting" +_EXP_ID = re.compile(r"^NF-ATT-[0-9]{3}$") +_EVENT_ID = re.compile(r"^NF-ATT-[0-9]{3}-EV[0-9]{3}$") + + +def _load(rel: str) -> dict: + return json.loads((_SUITE / rel).read_text(encoding="utf-8")) + + +def _all(subdir: str, glob: str) -> list[pathlib.Path]: + return sorted((_SUITE / subdir).glob(glob)) + + +def test_reference_results_exist() -> None: + assert _all("results", "*.result.json"), "no committed reference results" + + +@pytest.mark.parametrize("path", _all("results", "*.result.json"), ids=lambda p: p.stem) +def test_result_never_updates_weights(path: pathlib.Path) -> None: + doc = json.loads(path.read_text(encoding="utf-8")) + assert doc["weights_updated"] is False + assert _EXP_ID.match(doc["experiment_id"]) + assert doc["epistemic_status"] == "toy_model_confirmed" + + +@pytest.mark.parametrize("path", _all("events", "*.event.json"), ids=lambda p: p.stem) +def test_event_is_a_non_weight_updating_audit_record(path: pathlib.Path) -> None: + doc = json.loads(path.read_text(encoding="utf-8")) + assert doc["weights_updated"] is False + assert _EVENT_ID.match(doc["event_id"]) + assert doc["event_id"].startswith(doc["experiment_id"]) + + +def test_logit_boost_experiment_lifts_target_monotonically() -> None: + m = _load("results/NF-ATT-001.result.json")["metrics"] + assert m["target_prob_lift"] > 0 + assert m["lift_is_monotone"] is True + assert m["argmax_is_target_after_boost"] is True + + +def test_may_wigner_sweep_locates_stability_boundaries() -> None: + m = _load("results/NF-ATT-002.result.json")["metrics"] + # sweep should stay stable at small m and cross into the stop band by m=400. + assert m["max_ok_m"] > 0 + assert m["m_first_warn"] > m["max_ok_m"] >= 0 + assert m["m_first_stop"] >= m["m_first_error"] >= m["m_first_warn"]