quantum-cortex 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. cortex_c2/__init__.py +161 -0
  2. cortex_c2/consolidation.py +146 -0
  3. cortex_c2/router_ablation.py +254 -0
  4. cortex_c2/router_v2.py +109 -0
  5. cortex_c2b/__init__.py +724 -0
  6. cortex_c2b/companion_demo.py +203 -0
  7. cortex_c2b/crypto.py +102 -0
  8. cortex_c2b/external_arm.py +591 -0
  9. cortex_c2b/hm_lm.py +983 -0
  10. cortex_c2b/hm_protocol.py +308 -0
  11. cortex_c2b/lifecycle.py +454 -0
  12. cortex_c2b/lm_bridge.py +338 -0
  13. cortex_c2b/organ_use.py +254 -0
  14. cortex_c2b/read_path.py +218 -0
  15. cortex_c2b/reconstruction_probe.py +471 -0
  16. cortex_c2b/write_path.py +176 -0
  17. cortex_data/__init__.py +439 -0
  18. cortex_data/relay.py +174 -0
  19. cortex_eval/__init__.py +20 -0
  20. cortex_eval/ablation_mqar.py +187 -0
  21. cortex_eval/confirmation_run.py +133 -0
  22. cortex_eval/domain_map.py +127 -0
  23. cortex_eval/external_baseline.py +148 -0
  24. cortex_eval/ingest_confirmation.py +194 -0
  25. cortex_eval/mqar.py +122 -0
  26. cortex_eval/multiseed.py +148 -0
  27. cortex_eval/recurrent_base.py +197 -0
  28. cortex_eval/recurrent_ladder.py +159 -0
  29. cortex_eval/resumable_ladder.py +231 -0
  30. cortex_eval/resumable_rb.py +207 -0
  31. cortex_eval/run_mqar.py +127 -0
  32. cortex_eval/serial_position.py +116 -0
  33. quantum_cortex-1.0.0.dist-info/METADATA +209 -0
  34. quantum_cortex-1.0.0.dist-info/RECORD +41 -0
  35. quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
  36. quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
  37. quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
  38. quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
  39. quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
  40. quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
  41. train.py +885 -0
cortex_c2/__init__.py ADDED
@@ -0,0 +1,161 @@
1
+ """cortex_c2 — the multi-path C2 memory router (RES-18, ADR-006 D8), Slice A.
2
+
3
+ C2 is not one memory nor a choice between Hopfield and delta: it is a ROUTER of
4
+ memory paths on a guaranteed control floor. On any span, a memory is used only
5
+ where it beats the control; if all are below, the control is used — so C2 can
6
+ never degrade the model. (The ablation proved the floor is needed: the control
7
+ won 3 of 12 MQAR tiers.)
8
+
9
+ Slice A ships the FOUNDATION, retro-compatibility included from day one:
10
+ - a VERSIONED, stable Router interface (the contract every future version honours);
11
+ - RouterV1 = the upper-envelope oracle (picks, per span, the best of the known
12
+ path scores — the ceiling the learned router will chase: +21% vs control);
13
+ - version-tagged serialisation + a single-branch version dispatch (adding v2 is
14
+ one more branch, never a change to the existing one);
15
+ - a contract test (the guard) that any RouterVN must pass.
16
+
17
+ What is deliberately deferred (not by caution but by logic): ACTIVE migration
18
+ (transforming a v1 state into a v2 state) — impossible to write correctly before
19
+ v2 exists, but MADE POSSIBLE by the contract fixed here. The router-per-block
20
+ placement and soft/hard regime + breaker fallback are later slices (B, C).
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from abc import ABC, abstractmethod
25
+ from dataclasses import dataclass, field
26
+ from typing import Sequence
27
+
28
+ # Path identifiers — the control floor is always path 0.
29
+ PATH_CONTROL = 0
30
+ PATH_HOPFIELD = 1
31
+ PATH_DELTA = 2
32
+ PATH_NAMES = {PATH_CONTROL: "control", PATH_HOPFIELD: "hopfield", PATH_DELTA: "delta"}
33
+
34
+ CURRENT_ROUTER_VERSION = 1
35
+
36
+
37
+ @dataclass
38
+ class RouteDecision:
39
+ """The stable output contract of any Router version.
40
+
41
+ path : the chosen path id (PATH_CONTROL always available as the floor)
42
+ confidence: [0,1] — how sure the router is (drives soft/hard regime in Slice C)
43
+ weights : optional soft mixture over paths (None = hard choice of `path`)
44
+ """
45
+ path: int
46
+ confidence: float = 1.0
47
+ weights: Sequence[float] | None = None
48
+
49
+ def is_hard(self) -> bool:
50
+ return self.weights is None
51
+
52
+
53
+ class Router(ABC):
54
+ """The versioned, STABLE contract. Every future RouterVN implements exactly
55
+ this signature — versions EXTEND, they never break the interface. This is the
56
+ retro-compatibility promise, fixed at v1 so it precedes (not follows) the code.
57
+ """
58
+
59
+ version: int = 0 # each concrete version sets this
60
+
61
+ @abstractmethod
62
+ def route(self, *, span_ctx, path_scores: Sequence[float] | None = None,
63
+ state: dict | None = None) -> RouteDecision:
64
+ """Decide which path to use for a span.
65
+
66
+ span_ctx : opaque context handle for the span (features the router may use)
67
+ path_scores : optional known per-path scores (the oracle uses these; a
68
+ learned router predicts them from span_ctx instead)
69
+ state : optional persistent router state (version-tagged; see below)
70
+ Returns a RouteDecision honouring the RouteDecision contract.
71
+ """
72
+ raise NotImplementedError
73
+
74
+ # -- version-tagged serialisation (the retro-compat machinery, one branch) --
75
+ def dump_state(self, state: dict | None = None) -> dict:
76
+ """Serialise router state WITH its version tag, so any future loader can
77
+ dispatch on it. v1 state is trivial; the envelope carries the tag."""
78
+ return {"router_version": self.version, "state": state or {}}
79
+
80
+ @staticmethod
81
+ def load_state(blob: dict) -> dict:
82
+ """Version dispatch — currently one branch (v1). Adding v2 is one more
83
+ `elif`, never a change to this branch. Retro-compat by construction.
84
+ Active migration (v1→v2 transform) is deferred until a v2 exists — the
85
+ contract here makes it POSSIBLE, it does not implement it."""
86
+ v = blob.get("router_version")
87
+ if v == 1:
88
+ return blob.get("state", {})
89
+ # future: elif v == 2: return _migrate_or_read_v2(blob)
90
+ raise ValueError(f"unknown router_version {v!r} — no reader registered")
91
+
92
+
93
+ class RouterV1(Router):
94
+ """v1 = the upper-envelope ORACLE.
95
+
96
+ Given the known per-path scores for a span, pick the best path *that beats the
97
+ control floor*; if none beats it, choose the control. This is not a learned
98
+ router — it computes the CEILING (the ablation showed +21% vs control, +6%
99
+ over the best single path). Slice B replaces the oracle with a learned gate
100
+ that PREDICTS path_scores from span_ctx; it will implement this same interface.
101
+ """
102
+
103
+ version = 1
104
+
105
+ def __init__(self, floor_margin: float = 0.0):
106
+ # a memory must beat the control by at least floor_margin to be chosen
107
+ self.floor_margin = floor_margin
108
+
109
+ def route(self, *, span_ctx=None, path_scores: Sequence[float] | None = None,
110
+ state: dict | None = None) -> RouteDecision:
111
+ if not path_scores:
112
+ # no information → fall to the guaranteed floor (never degrade)
113
+ return RouteDecision(path=PATH_CONTROL, confidence=1.0)
114
+ scores = list(path_scores)
115
+ control = scores[PATH_CONTROL]
116
+ # candidate memories that clear the floor by the margin
117
+ best_path, best_score = PATH_CONTROL, control
118
+ for p in range(1, len(scores)):
119
+ if scores[p] > control + self.floor_margin and scores[p] > best_score:
120
+ best_path, best_score = p, scores[p]
121
+ # confidence = normalised gap to the runner-up (how decisive the choice is)
122
+ ordered = sorted(scores, reverse=True)
123
+ gap = (ordered[0] - ordered[1]) if len(ordered) > 1 else 1.0
124
+ conf = float(min(1.0, max(0.0, gap / (abs(ordered[0]) + 1e-6))))
125
+ return RouteDecision(path=best_path, confidence=conf)
126
+
127
+
128
+ def make_router(version: int = CURRENT_ROUTER_VERSION, **kw) -> Router:
129
+ """Factory — version dispatch for construction. Retro-compat in action:
130
+ v2 is one more branch, v1 is untouched (the contract fixed at v1 holds)."""
131
+ if version == 1:
132
+ return RouterV1(**kw)
133
+ if version == 2:
134
+ from cortex_c2.router_v2 import RouterV2 # torch-dependent, imported lazily
135
+ return RouterV2(**kw)
136
+ raise ValueError(f"no Router implementation for version {version}")
137
+
138
+
139
+ def upper_envelope_auc(per_tier_by_path: dict[str, list[float]]) -> dict:
140
+ """Compute the RouterV1-oracle result over a set of tiers: per tier, the
141
+ router picks max(control, hopfield, delta); the AUC of that choice is the
142
+ ceiling. Returns the envelope AUC and the routed path per tier."""
143
+ control = per_tier_by_path["control"]
144
+ hop = per_tier_by_path["hopfield"]
145
+ delta = per_tier_by_path["delta"]
146
+ n = len(control)
147
+ router = RouterV1()
148
+ routed_paths, routed_scores = [], []
149
+ for i in range(n):
150
+ d = router.route(path_scores=[control[i], hop[i], delta[i]])
151
+ routed_paths.append(PATH_NAMES[d.path])
152
+ routed_scores.append([control[i], hop[i], delta[i]][d.path])
153
+ env_auc = sum(routed_scores) / n
154
+ ctrl_auc = sum(control) / n
155
+ best_single = max(sum(hop) / n, sum(delta) / n, ctrl_auc)
156
+ return {
157
+ "envelope_auc": round(env_auc, 4),
158
+ "vs_control": round(env_auc - ctrl_auc, 4),
159
+ "vs_best_single_path": round(env_auc - best_single, 4),
160
+ "routed_paths": routed_paths,
161
+ }
@@ -0,0 +1,146 @@
1
+ """cortex_c2.consolidation — the consolidating, self-healing router controller.
2
+
3
+ Slice C of RES-18 (ADR-006 D8). This is where the founder's three-message design
4
+ becomes code, as ONE stateful object wrapping a Router:
5
+
6
+ 1. Control floor (message 1): a memory path is used only where it beats the
7
+ control; else the control. Enforced by the router; re-checked here.
8
+ 2. Regime = consolidation (message 2): a per-span-type decision that has been
9
+ LEARNED — the same path wins, confidently, over N passes — is HARDENED
10
+ (routed hard, one path, no soft mixture, minimal cost). Unsure decisions
11
+ stay SOFT (the router's mixture) and keep generating the learning signal.
12
+ Hardening is conservative (a high, declared confidence threshold + stability
13
+ count) — better stay soft too long than harden an error.
14
+ 3. Breaker fallback (message 3): when an active path degrades or diverges, do
15
+ not kill — RE-ROUTE to the best remaining path above the control, else the
16
+ control, and RE-SOFTEN the decision (re-open deliberation). A declared
17
+ HYSTERESIS (cooldown) prevents flapping between paths.
18
+
19
+ This is a textbook RES-17 type×state transition: a decision transitions
20
+ soft → hardened → (on breaker) softened-again. It unifies C1 (routing), RES-4
21
+ (confidence), RES-16 (consolidation + reminiscence), the endocrine explore/exploit
22
+ dial, basal-ganglia habit + hyperdirect veto, and the circuit breaker D7 — one
23
+ mechanism, seven windows.
24
+
25
+ Pure-Python controller (no torch here); it drives any Router.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass, field
30
+
31
+ from cortex_c2 import Router, RouteDecision, PATH_CONTROL, PATH_NAMES
32
+
33
+
34
+ @dataclass
35
+ class DecisionState:
36
+ """Per-span-type consolidation state — the RES-17 (type, state) cell."""
37
+ hardened_path: int | None = None # None = soft; an int = hardened to that path
38
+ stable_count: int = 0 # consecutive passes the same path won confidently
39
+ last_path: int | None = None
40
+ cooldown: int = 0 # hysteresis: passes remaining before a path may be re-chosen
41
+ banned_path: int | None = None # a path in cooldown after a breaker fallback
42
+
43
+
44
+ @dataclass
45
+ class ConsolidationConfig:
46
+ harden_confidence: float = 0.75 # min confidence to count a pass toward hardening
47
+ harden_stability: int = 5 # consecutive confident passes required to harden (conservative)
48
+ hysteresis_cooldown: int = 8 # passes a path stays banned after a fallback (anti-flapping)
49
+ clear_recovery_margin: float = 0.05 # a banned path must beat control by this to be re-eligible early
50
+
51
+
52
+ class ConsolidatingController:
53
+ """Wraps a Router and adds consolidation + breaker fallback, per span-type key.
54
+
55
+ `route(key, span_ctx, path_scores)` returns a RouteDecision:
56
+ - if the key is HARDENED and not in cooldown → a hard decision on the learned path;
57
+ - else the router's (soft) decision, with the control floor enforced, and
58
+ hardening bookkeeping updated.
59
+ `report_breaker(key, path)` is called when a path degraded/diverged on this key:
60
+ it re-routes (bans the path for a cooldown), re-softens, and never leaves a void.
61
+ """
62
+
63
+ def __init__(self, router: Router, cfg: ConsolidationConfig | None = None):
64
+ self.router = router
65
+ self.cfg = cfg or ConsolidationConfig()
66
+ self.states: dict = {}
67
+
68
+ def _state(self, key) -> DecisionState:
69
+ return self.states.setdefault(key, DecisionState())
70
+
71
+ def route(self, *, key, span_ctx=None, path_scores=None) -> RouteDecision:
72
+ st = self._state(key)
73
+
74
+ # HARDENED path: route hard, minimal cost — unless it is the banned path
75
+ if (st.hardened_path is not None and st.hardened_path != st.banned_path
76
+ and st.cooldown == 0):
77
+ self._tick_cooldown(st)
78
+ return RouteDecision(path=st.hardened_path, confidence=1.0) # hard, no mixture
79
+
80
+ # otherwise ask the router (soft), then apply the floor + bookkeeping
81
+ d = self.router.route(span_ctx=span_ctx, path_scores=path_scores)
82
+
83
+ # if the chosen path is banned (still in cooldown), pull to control floor
84
+ if st.banned_path is not None and st.cooldown > 0 and d.path == st.banned_path:
85
+ d = RouteDecision(path=PATH_CONTROL, confidence=d.confidence, weights=d.weights)
86
+
87
+ # consolidation bookkeeping: count confident agreement toward hardening
88
+ if d.confidence >= self.cfg.harden_confidence and d.path == st.last_path:
89
+ st.stable_count += 1
90
+ else:
91
+ st.stable_count = 1
92
+ st.last_path = d.path
93
+
94
+ # harden once stable enough (conservative) and not the banned path
95
+ if (st.stable_count >= self.cfg.harden_stability
96
+ and d.path != PATH_CONTROL
97
+ and not (st.banned_path == d.path and st.cooldown > 0)):
98
+ st.hardened_path = d.path
99
+
100
+ self._tick_cooldown(st) # decrement AFTER serving this pass
101
+ return d
102
+
103
+ def _tick_cooldown(self, st: DecisionState) -> None:
104
+ if st.cooldown > 0:
105
+ st.cooldown -= 1
106
+ if st.cooldown == 0:
107
+ st.banned_path = None
108
+
109
+ def report_breaker(self, *, key, path, path_scores=None) -> RouteDecision:
110
+ """A path degraded/diverged on this key → re-route with hysteresis.
111
+
112
+ Bans `path` for a cooldown (anti-flapping), re-softens (drops any
113
+ hardening), and returns the best remaining path above the control, else
114
+ the control. Never a void — completes the hyperdirect veto (redirect).
115
+ """
116
+ st = self._state(key)
117
+ st.banned_path = path
118
+ st.cooldown = self.cfg.hysteresis_cooldown
119
+ st.hardened_path = None # re-soften: re-open deliberation (RES-16 reminiscence)
120
+ st.stable_count = 0
121
+ st.last_path = None
122
+
123
+ # choose the fallback: best remaining path above control, else control
124
+ if path_scores:
125
+ control = path_scores[PATH_CONTROL]
126
+ best_path, best_score = PATH_CONTROL, control
127
+ for p in range(len(path_scores)):
128
+ if p == path:
129
+ continue # the banned path is out
130
+ if p != PATH_CONTROL and path_scores[p] > control and path_scores[p] > best_score:
131
+ best_path, best_score = p, path_scores[p]
132
+ return RouteDecision(path=best_path, confidence=0.5)
133
+ # no scores → fall to the guaranteed floor
134
+ return RouteDecision(path=PATH_CONTROL, confidence=1.0)
135
+
136
+ # -- introspection (RES-17: the system becomes legible) ------------------
137
+ def snapshot(self) -> dict:
138
+ """Inspectable view — how many keys are hardened, softened, in cooldown."""
139
+ hardened = sum(1 for s in self.states.values() if s.hardened_path is not None)
140
+ cooling = sum(1 for s in self.states.values() if s.cooldown > 0)
141
+ return {
142
+ "keys": len(self.states),
143
+ "hardened": hardened,
144
+ "soft": len(self.states) - hardened,
145
+ "in_cooldown": cooling,
146
+ }
@@ -0,0 +1,254 @@
1
+ """cortex_c2.router_ablation — Slice D: does the learned router reach the ceiling?
2
+
3
+ The closing question of N2 (RES-18, ADR-006 D8). Slice A gave the ORACLE ceiling
4
+ (+21% vs control, +6% over the best single path) — but the oracle *reads* the
5
+ truth. Slice B/C gave a router that *learns* to route from context alone and
6
+ consolidates. Slice D measures whether that learned router, in practice,
7
+ approaches the oracle ceiling — the test that validates (or not) that the whole
8
+ multi-path system actually works, not just in oracle.
9
+
10
+ Method (measure-first): build a synthetic routing task where each span-type has a
11
+ context feature and a known best path (mirroring the ablation's per-tier domains:
12
+ Hopfield-favoured, delta-favoured, control-favoured). Train RouterV2 on observed
13
+ per-path scores; drive it through the ConsolidatingController; and compare four
14
+ readings on held-out spans:
15
+ - control-only (always the floor)
16
+ - best single memory (whichever single path scores best on average)
17
+ - the ORACLE (RouterV1: reads truth — the ceiling)
18
+ - the LEARNED router (RouterV2 + controller: predicts, never reads truth)
19
+ The learned router's gap to the oracle is the Slice-D signal. Reaching most of
20
+ the oracle's lift over the best single path means RES-18 pays off in practice.
21
+
22
+ Pure enough to run on CPU in minutes; a fuller version trains on real MQAR spans.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import math
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+
32
+ import numpy as np
33
+
34
+ from cortex_c2 import PATH_CONTROL, PATH_HOPFIELD, PATH_DELTA, PATH_NAMES, RouterV1
35
+ from cortex_c2.consolidation import ConsolidatingController, ConsolidationConfig
36
+
37
+ REPO = Path(__file__).resolve().parents[1]
38
+ N_PATHS = 3
39
+
40
+
41
+ # --- the synthetic routing task (each span-type has a domain) ----------------
42
+ def _span_scores(ctx0: float) -> list[float]:
43
+ """Known per-path scores as a function of the context feature — mirrors the
44
+ ablation's finding that each path has a domain."""
45
+ if ctx0 < -0.33: # Hopfield's domain (dense recall)
46
+ return [0.10, 0.30, 0.16]
47
+ elif ctx0 > 0.33: # delta's domain (intermediate)
48
+ return [0.10, 0.16, 0.30]
49
+ else: # control's domain (memory hurts here)
50
+ return [0.28, 0.12, 0.12]
51
+
52
+
53
+ def _make_spans(n: int, seed: int):
54
+ rng = np.random.default_rng(seed)
55
+ ctx = rng.standard_normal((n, 8)).astype(np.float32)
56
+ scores = np.array([_span_scores(float(ctx[i, 0])) for i in range(n)], dtype=np.float32)
57
+ return ctx, scores
58
+
59
+
60
+ def _auc_of_choice(scores: np.ndarray, chosen_path: np.ndarray) -> float:
61
+ """Mean achieved score when each span uses its chosen path."""
62
+ return float(np.mean([scores[i, chosen_path[i]] for i in range(len(scores))]))
63
+
64
+
65
+ def run_router_ablation(train_n: int = 4000, test_n: int = 1000,
66
+ steps: int = 600, seed: int = 1337) -> dict:
67
+ import torch
68
+ from cortex_c2.router_v2 import RouterV2
69
+
70
+ # data
71
+ ctx_tr, sc_tr = _make_spans(train_n, seed)
72
+ ctx_te, sc_te = _make_spans(test_n, seed + 1)
73
+
74
+ # train the learned router (predict per-path scores from context)
75
+ torch.manual_seed(seed)
76
+ router = RouterV2(ctx_dim=8, floor_margin=0.0)
77
+ opt = torch.optim.Adam(router.parameters(), lr=1e-2)
78
+ Xtr = torch.from_numpy(ctx_tr); Ytr = torch.from_numpy(sc_tr)
79
+ bs = 128
80
+ for step in range(steps):
81
+ idx = torch.randint(0, train_n, (bs,))
82
+ loss = router.score_loss(Xtr[idx], Ytr[idx])
83
+ opt.zero_grad(); loss.backward(); opt.step()
84
+
85
+ # --- four readings on held-out spans ---
86
+ n = test_n
87
+ # 1) control-only
88
+ ctrl_choice = np.full(n, PATH_CONTROL)
89
+ auc_control = _auc_of_choice(sc_te, ctrl_choice)
90
+ # 2) best single memory (pick the path with best average score across spans)
91
+ avg = sc_te.mean(axis=0)
92
+ best_single_path = int(np.argmax(avg))
93
+ single_choice = np.full(n, best_single_path)
94
+ auc_best_single = _auc_of_choice(sc_te, single_choice)
95
+ # 3) oracle (RouterV1 reads truth)
96
+ oracle = RouterV1()
97
+ orc_choice = np.array([oracle.route(path_scores=sc_te[i].tolist()).path for i in range(n)])
98
+ auc_oracle = _auc_of_choice(sc_te, orc_choice)
99
+ # 4) learned router (RouterV2 + controller: predicts, never reads truth)
100
+ ctl = ConsolidatingController(router, ConsolidationConfig(harden_confidence=0.6,
101
+ harden_stability=3))
102
+ learned_choice = np.zeros(n, dtype=int)
103
+ Xte = torch.from_numpy(ctx_te)
104
+ for i in range(n):
105
+ # key = the span-type bucket (so consolidation has something to harden)
106
+ c0 = float(ctx_te[i, 0])
107
+ key = "hop" if c0 < -0.33 else "delta" if c0 > 0.33 else "ctrl"
108
+ d = ctl.route(key=key, span_ctx=Xte[i])
109
+ learned_choice[i] = d.path
110
+ auc_learned = _auc_of_choice(sc_te, learned_choice)
111
+
112
+ # gaps
113
+ lift_oracle = auc_oracle - auc_best_single
114
+ lift_learned = auc_learned - auc_best_single
115
+ frac_of_ceiling = (lift_learned / lift_oracle) if lift_oracle > 1e-9 else float("nan")
116
+
117
+ return {
118
+ "benchmark": "router_ablation",
119
+ "timestamp_utc": datetime.now(timezone.utc).isoformat(),
120
+ "regime": {"train_n": train_n, "test_n": test_n, "steps": steps, "seed": seed},
121
+ "auc": {
122
+ "control_only": round(auc_control, 4),
123
+ "best_single_memory": round(auc_best_single, 4),
124
+ "oracle_ceiling": round(auc_oracle, 4),
125
+ "learned_router": round(auc_learned, 4),
126
+ },
127
+ "best_single_path": PATH_NAMES[best_single_path],
128
+ "lift_over_best_single": {
129
+ "oracle": round(lift_oracle, 4),
130
+ "learned": round(lift_learned, 4),
131
+ "learned_fraction_of_oracle_ceiling": round(frac_of_ceiling, 3),
132
+ },
133
+ "controller": ctl.snapshot(),
134
+ "verdict": (
135
+ f"learned router captures {frac_of_ceiling*100:.0f}% of the oracle's lift "
136
+ f"over the best single path"
137
+ if lift_oracle > 1e-9 else
138
+ "no oracle lift on this task (paths do not separate)"
139
+ ),
140
+ "note": "RES-18 Slice D — the learned router predicts (never reads truth); "
141
+ "reaching most of the oracle ceiling means the multi-path design pays off. "
142
+ "Synthetic routing task; a fuller run trains on real MQAR spans.",
143
+ }
144
+
145
+
146
+ def write_artefact(result: dict, out_dir: str | Path) -> Path:
147
+ out = Path(out_dir); out.mkdir(parents=True, exist_ok=True)
148
+ stamp = result["timestamp_utc"][:19].replace(":", "").replace("-", "")
149
+ path = out / f"router-ablation-{stamp}.json"
150
+ path.write_text(json.dumps(result, indent=2) + "\n")
151
+ (out / "LATEST-router.json").write_text(json.dumps(result, indent=2) + "\n")
152
+ return path
153
+
154
+
155
+ def _cli():
156
+ ap = argparse.ArgumentParser(description="RES-18 router ablation (Slice D)")
157
+ ap.add_argument("--steps", type=int, default=600)
158
+ ap.add_argument("--out", default="metrics/mqar")
159
+ ap.add_argument("--quick", action="store_true")
160
+ args = ap.parse_args()
161
+ r = run_router_ablation(steps=200 if args.quick else args.steps,
162
+ train_n=1000 if args.quick else 4000)
163
+ path = write_artefact(r, args.out)
164
+ print("\n=== RES-18 router ablation (Slice D) ===")
165
+ print(json.dumps(r["auc"], indent=2))
166
+ print(r["verdict"])
167
+ print(f"controller: {json.dumps(r['controller'])}")
168
+ print(f"artefact: {path}")
169
+
170
+
171
+ if __name__ == "__main__":
172
+ _cli()
173
+
174
+
175
+ # ============================================================================ #
176
+ # (b) D19 Level 1b — the router on REAL MQAR spans, not the synthetic task #
177
+ # ============================================================================ #
178
+ def _real_span_ctx(kv: int, seq: int) -> np.ndarray:
179
+ """Context feature for a real MQAR span-type: normalised (kv, seq) + simple
180
+ interactions. The router must learn which path wins from THIS, never from
181
+ the truth. (8 dims to stay compatible with RouterV2(ctx_dim=8).)"""
182
+ k = math.log2(kv) / 5.0 # kv ∈ {4..32} → ~0.4..1.0
183
+ s = math.log2(seq) / 9.0 # seq ∈ {128..512} → ~0.78..1.0
184
+ return np.array([k, s, k * s, k * k, s * s, k - s, 1.0 - k, 1.0 - s], dtype=np.float32)
185
+
186
+
187
+ def run_router_ablation_on_real_mqar(curves_by_path: dict, steps: int = 800,
188
+ seed: int = 1337, holdout_frac: float = 0.34) -> dict:
189
+ """Train RouterV2 on real per-tier MQAR accuracies (from an ablation artefact)
190
+ and test on held-out tiers. curves_by_path = {"none": [...], "hopfield": [...],
191
+ "delta": [...]} where each item is {"kv_pairs","seq_len","accuracy"} (same tier
192
+ order). Held-out tiers are chosen by seed so the router never sees their scores.
193
+ """
194
+ import torch
195
+ from cortex_c2.router_v2 import RouterV2
196
+
197
+ tiers = [(t["kv_pairs"], t["seq_len"]) for t in curves_by_path["none"]]
198
+ n = len(tiers)
199
+ scores = np.array([[curves_by_path["none"][i]["accuracy"],
200
+ curves_by_path["hopfield"][i]["accuracy"],
201
+ curves_by_path["delta"][i]["accuracy"]] for i in range(n)],
202
+ dtype=np.float32)
203
+ ctx = np.array([_real_span_ctx(kv, sq) for kv, sq in tiers], dtype=np.float32)
204
+
205
+ rng = np.random.default_rng(seed)
206
+ perm = rng.permutation(n)
207
+ n_test = max(2, int(round(n * holdout_frac)))
208
+ test_idx, train_idx = perm[:n_test], perm[n_test:]
209
+
210
+ torch.manual_seed(seed)
211
+ router = RouterV2(ctx_dim=8, floor_margin=0.0)
212
+ opt = torch.optim.Adam(router.parameters(), lr=1e-2)
213
+ Xtr = torch.from_numpy(ctx[train_idx]); Ytr = torch.from_numpy(scores[train_idx])
214
+ for _ in range(steps):
215
+ loss = router.score_loss(Xtr, Ytr)
216
+ opt.zero_grad(); loss.backward(); opt.step()
217
+
218
+ # readings on held-out tiers
219
+ te_scores = scores[test_idx]
220
+ auc_control = float(te_scores[:, PATH_CONTROL].mean())
221
+ avg = te_scores.mean(axis=0); best_single_path = int(np.argmax(avg))
222
+ auc_best_single = float(te_scores[:, best_single_path].mean())
223
+ oracle = RouterV1()
224
+ orc = [oracle.route(path_scores=te_scores[i].tolist()).path for i in range(len(test_idx))]
225
+ auc_oracle = float(np.mean([te_scores[i, orc[i]] for i in range(len(test_idx))]))
226
+ Xte = torch.from_numpy(ctx[test_idx])
227
+ learned = [router.route(span_ctx=Xte[i]).path for i in range(len(test_idx))]
228
+ auc_learned = float(np.mean([te_scores[i, learned[i]] for i in range(len(test_idx))]))
229
+
230
+ lift_o = auc_oracle - auc_best_single
231
+ lift_l = auc_learned - auc_best_single
232
+ frac = (lift_l / lift_o) if lift_o > 1e-9 else float("nan")
233
+ return {
234
+ "benchmark": "router_ablation_real_mqar",
235
+ "timestamp_utc": datetime.now(timezone.utc).isoformat(),
236
+ "regime": {"n_tiers": n, "held_out": [tiers[i] for i in test_idx],
237
+ "train": [tiers[i] for i in train_idx], "steps": steps, "seed": seed},
238
+ "auc_heldout": {
239
+ "control_only": round(auc_control, 4),
240
+ "best_single_memory": round(auc_best_single, 4),
241
+ "oracle_ceiling": round(auc_oracle, 4),
242
+ "learned_router": round(auc_learned, 4),
243
+ },
244
+ "best_single_path": PATH_NAMES[best_single_path],
245
+ "learned_routes_heldout": [PATH_NAMES[p] for p in learned],
246
+ "oracle_routes_heldout": [PATH_NAMES[p] for p in orc],
247
+ "lift_over_best_single": {"oracle": round(lift_o, 4), "learned": round(lift_l, 4),
248
+ "learned_fraction_of_oracle_ceiling": round(frac, 3) if frac == frac else None},
249
+ "verdict": (f"on REAL MQAR held-out tiers the learned router captures "
250
+ f"{frac*100:.0f}% of the oracle lift" if frac == frac else
251
+ "no oracle lift on held-out tiers (paths do not separate there)"),
252
+ "note": "D19 Level 1b — router trained/tested on real per-tier MQAR accuracies "
253
+ "(held-out tiers never seen). Small-N caveat: 12 tiers only; repeat across seeds/holdouts.",
254
+ }
cortex_c2/router_v2.py ADDED
@@ -0,0 +1,109 @@
1
+ """cortex_c2.router_v2 — the learned soft router (RES-18, ADR-006 D8), Slice B.
2
+
3
+ Slice A's RouterV1 is an ORACLE: it *reads* the true per-path scores. That is the
4
+ ceiling, not a usable router — at inference there is no label saying "Hopfield
5
+ scores 0.06 here". RouterV2 is the real thing: a small head that **predicts** a
6
+ score per path *from the span context alone*, then routes by weighting. It learns
7
+ by a supervised signal — its predictions must correlate with the performances
8
+ actually observed (the ablation gives those). The central challenge of the whole
9
+ router: route without knowing the answer.
10
+
11
+ RouterV2 honours the *same* `Router` interface as v1 (retro-compat in action:
12
+ v2 is added, v1 stays; the version dispatch gains one branch). It emits a **soft**
13
+ RouteDecision (a weighted mixture over paths) with a confidence; Slice C turns
14
+ high-confidence decisions into hard routing and wires the breaker fallback.
15
+
16
+ The control floor (RES-18) is preserved: the predicted-best path is used only if
17
+ it clears the control's predicted score by a margin; else the mixture is pulled
18
+ toward the control. C2 can never be made to degrade the model by the router.
19
+
20
+ Depends on torch (kept out of cortex_c2/__init__.py so the core stays light).
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from typing import Sequence
25
+
26
+ import torch
27
+ import torch.nn as nn
28
+ import torch.nn.functional as F
29
+
30
+ from cortex_c2 import (Router, RouteDecision, PATH_CONTROL, PATH_HOPFIELD,
31
+ PATH_DELTA, PATH_NAMES)
32
+
33
+ N_PATHS = 3 # control, hopfield, delta
34
+
35
+
36
+ class RouterV2(nn.Module, Router):
37
+ """A learned gate that predicts per-path scores from a context vector.
38
+
39
+ - `ctx_dim`: dimensionality of the span-context feature the router sees.
40
+ - forward(ctx) -> predicted score per path (b, N_PATHS).
41
+ - route(span_ctx=<tensor>) -> a soft RouteDecision (weights = softmax over the
42
+ predicted scores, with the control floor enforced), plus a confidence.
43
+ - training: `score_loss(ctx, observed)` regresses predictions onto the
44
+ observed per-path performances (the supervised signal from the ablation).
45
+
46
+ Version dispatch: `version = 2`; `Router.load_state` gains its v2 branch in
47
+ Slice C when v2 carries persistent state. For now v2's "state" is its weights,
48
+ saved by the normal torch checkpoint — the contract's tag still applies.
49
+ """
50
+
51
+ version = 2
52
+
53
+ def __init__(self, ctx_dim: int = 32, hidden: int = 64,
54
+ floor_margin: float = 0.0, temperature: float = 1.0):
55
+ nn.Module.__init__(self)
56
+ self.ctx_dim = ctx_dim
57
+ self.floor_margin = floor_margin
58
+ self.temperature = temperature
59
+ self.net = nn.Sequential(
60
+ nn.Linear(ctx_dim, hidden),
61
+ nn.GELU(),
62
+ nn.Linear(hidden, N_PATHS),
63
+ )
64
+
65
+ # -- prediction ----------------------------------------------------------
66
+ def forward(self, ctx: torch.Tensor) -> torch.Tensor:
67
+ """ctx: (b, ctx_dim) -> predicted per-path score (b, N_PATHS)."""
68
+ return self.net(ctx)
69
+
70
+ # -- the Router contract -------------------------------------------------
71
+ def route(self, *, span_ctx=None, path_scores: Sequence[float] | None = None,
72
+ state: dict | None = None) -> RouteDecision:
73
+ """Route from the *predicted* scores. span_ctx is the context feature
74
+ (a tensor of shape (ctx_dim,) or (1, ctx_dim)); path_scores is ignored
75
+ (v2 predicts, it does not read the truth — that was v1's oracle)."""
76
+ if span_ctx is None:
77
+ return RouteDecision(path=PATH_CONTROL, confidence=1.0) # floor
78
+ ctx = span_ctx if span_ctx.dim() == 2 else span_ctx.unsqueeze(0)
79
+ with torch.no_grad():
80
+ pred = self.forward(ctx).squeeze(0) # (N_PATHS,)
81
+ # control floor: a memory competes only if it clears control + margin
82
+ adj = pred.clone()
83
+ control = pred[PATH_CONTROL]
84
+ for p in (PATH_HOPFIELD, PATH_DELTA):
85
+ if pred[p] <= control + self.floor_margin:
86
+ adj[p] = float("-inf") # remove from the mixture
87
+ weights = F.softmax(adj / self.temperature, dim=-1)
88
+ best = int(torch.argmax(weights).item())
89
+ # confidence = how peaked the mixture is (max weight, rescaled)
90
+ conf = float((weights.max() - 1.0 / N_PATHS) / (1.0 - 1.0 / N_PATHS))
91
+ conf = max(0.0, min(1.0, conf))
92
+ return RouteDecision(path=best, confidence=conf,
93
+ weights=weights.tolist())
94
+
95
+ # -- training signal -----------------------------------------------------
96
+ def score_loss(self, ctx: torch.Tensor, observed: torch.Tensor) -> torch.Tensor:
97
+ """MSE between predicted and observed per-path scores.
98
+
99
+ ctx : (b, ctx_dim)
100
+ observed : (b, N_PATHS) — the true per-path performances for those spans
101
+ (from the ablation / online measurement). This is how the
102
+ router learns to predict which path wins, from context alone.
103
+ """
104
+ pred = self.forward(ctx)
105
+ return F.mse_loss(pred, observed)
106
+
107
+
108
+ def make_router_v2(ctx_dim: int = 32, **kw) -> RouterV2:
109
+ return RouterV2(ctx_dim=ctx_dim, **kw)