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
@@ -0,0 +1,338 @@
1
+ """cortex_c2b.lm_bridge -- the journal in the decode loop (ADR-008; REPRISE step 3).
2
+
3
+ Until 2026-09-12 the journal was measured as an ORGAN: a hash-seeded cue encoder
4
+ and a reference skill probe stood in for the model. This module wires the organ
5
+ into the language model itself, under the nine invariants validated by the
6
+ founder before any code was written (ADR-008):
7
+
8
+ 1. N1 intact at step zero: the reader's output projection is zero-initialised,
9
+ so a model with the journal on produces N1's logits at initialisation,
10
+ window or no window; a parent checkpoint loads its weights unchanged.
11
+ 2. One read path, the organ's: cues go to `JournalPath.retrieve`; nothing is
12
+ duplicated on the model side, nothing scans.
13
+ 3. The journal enters as CONTENT, through a zero-init cross attention over
14
+ the retrieved bytes embedded with the model's own byte embeddings -- not
15
+ as prompt tokens. Journal OFF = the reader absent = N1 exactly.
16
+ 4. The router decides the read, on the floor: the reader's contribution is
17
+ multiplied by the path 4 gate; with no score the gate is zero.
18
+ 5. The cue encoder is learned (a projection of the mean pooled residual
19
+ stream to a unit vector), trained by contrast: the cue of a query must be
20
+ nearest to the cue of its own statement among the batch. One encoder for
21
+ writing and reading; the DG/CA3/CA1 write gate is unchanged.
22
+ 6. Organ use is taught, not assumed: `cortex_c2b.organ_use` builds the
23
+ curriculum; the target of every example is computed from what the journal
24
+ actually returned (cite what was shown, else abstain).
25
+ 7. Two processes, persistence declared: session A writes, session B reopens
26
+ the sealed journal and the checkpoint from the disk alone.
27
+ 8. A failure is attributable: retrieval hit (the encoder), attention mass on
28
+ the retrieved bytes (the reader), attribute exactness given a valid
29
+ citation (the answer) are reported next to the verdict.
30
+ 9. Everything is a flag, a ledger record, and a CPU smoke test.
31
+
32
+ The contract of an episodic answer (ADR-008, point d, decided 2026-09-12):
33
+ `<EPI>` marks an episodic query; the answer is `<CITE>` + a one byte LOCAL
34
+ LABEL of the retrieved episode (the harness maps it back to the pointer and
35
+ verifies it) + `<ANS>` + the attribute bytes + newline, or `<UNKNOWN>` +
36
+ newline. Four of the eight reserved oracle tokens (NOW-1) are taken; the
37
+ vocabulary size does not change, so the N1 checkpoint loads as is.
38
+
39
+ Pure torch over the numpy organ. Written from scratch (our filon).
40
+ """
41
+ from __future__ import annotations
42
+
43
+ from dataclasses import dataclass
44
+
45
+ import numpy as np
46
+ import torch
47
+ import torch.nn as nn
48
+ import torch.nn.functional as F
49
+
50
+ from cortex_c2b import Journal, CUE_DIM
51
+ from cortex_c2b.write_path import WritePath, WriteReport
52
+ from cortex_c2b.read_path import JournalPath
53
+
54
+ # ---- the contract tokens: 4 of the 8 reserved oracle tokens (ids 256..263) ----
55
+ TOK_EPI, TOK_CITE, TOK_ANS, TOK_UNKNOWN = 256, 257, 258, 259
56
+ CONTRACT_TOKENS = {"EPI": TOK_EPI, "CITE": TOK_CITE, "ANS": TOK_ANS, "UNKNOWN": TOK_UNKNOWN}
57
+ LABELS = b"ABCDEFGH" # local labels of the retrieved episodes, at most 8 per read
58
+ NEWLINE = 10
59
+
60
+
61
+ # ---------------------------------------------------------------------------- #
62
+ # The cue encoder -- learned, one for writing and reading (invariant 5) #
63
+ # ---------------------------------------------------------------------------- #
64
+ class CueEncoder(nn.Module):
65
+ """Mean pooled residual stream -> linear -> unit vector of `cue_dim`."""
66
+
67
+ def __init__(self, n_embd: int, cue_dim: int = CUE_DIM):
68
+ super().__init__()
69
+ self.proj = nn.Linear(n_embd, cue_dim, bias=False)
70
+
71
+ def forward(self, h: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
72
+ if mask is None:
73
+ pooled = h.mean(dim=1)
74
+ else:
75
+ m = mask.to(h.dtype).unsqueeze(-1)
76
+ pooled = (h * m).sum(dim=1) / m.sum(dim=1).clamp(min=1.0)
77
+ return F.normalize(self.proj(pooled), dim=-1, eps=1e-6)
78
+
79
+
80
+ def contrastive_loss(query_cues: torch.Tensor, statement_cues: torch.Tensor,
81
+ temperature: float = 0.1) -> torch.Tensor:
82
+ """InfoNCE in both directions: row i of the queries must pick row i of the
83
+ statements among the batch (and back). The signal that teaches the encoder
84
+ to put a question and its own episode at the same address."""
85
+ logits = query_cues @ statement_cues.t() / temperature
86
+ target = torch.arange(logits.size(0), device=logits.device)
87
+ return 0.5 * (F.cross_entropy(logits, target) + F.cross_entropy(logits.t(), target))
88
+
89
+
90
+ # ---------------------------------------------------------------------------- #
91
+ # The reader -- zero-init cross attention over the read window (invariant 3) #
92
+ # ---------------------------------------------------------------------------- #
93
+ class JournalReader(nn.Module):
94
+ """Queries from the residual stream, keys and values from the embedded bytes
95
+ of the read window (plus a learned null slot: "nothing to read"). The output
96
+ projection starts at zero, so the model begins as N1 and learns to read.
97
+ `forward` also returns the attention mass on the real bytes (excluding the
98
+ null slot): the reader's diagnostic (invariant 8)."""
99
+
100
+ def __init__(self, n_embd: int, n_head: int, window_len: int):
101
+ super().__init__()
102
+ self.h = n_head
103
+ self.ln_x = nn.LayerNorm(n_embd)
104
+ self.ln_w = nn.LayerNorm(n_embd)
105
+ self.q = nn.Linear(n_embd, n_embd, bias=False)
106
+ self.kv = nn.Linear(n_embd, 2 * n_embd, bias=False)
107
+ self.out = nn.Linear(n_embd, n_embd, bias=False)
108
+ self.null = nn.Parameter(torch.zeros(1, 1, n_embd))
109
+ self.wpe = nn.Embedding(window_len, n_embd) # order within the window
110
+ nn.init.zeros_(self.out.weight) # the no-op law
111
+ self.scale = (n_embd // n_head) ** -0.5
112
+
113
+ def forward(self, x: torch.Tensor, window_emb: torch.Tensor, window_mask: torch.Tensor):
114
+ b, t, c = x.shape
115
+ r = window_emb.size(1)
116
+ pos = torch.arange(r, device=x.device)
117
+ w = torch.cat([self.null.expand(b, 1, c), window_emb + self.wpe(pos)[None]], dim=1) # (b, r+1, c)
118
+ m = torch.cat([torch.ones(b, 1, dtype=torch.bool, device=x.device), window_mask.bool()], dim=1)
119
+ hq = self.q(self.ln_x(x)).view(b, t, self.h, c // self.h).transpose(1, 2) # (b,h,t,d)
120
+ k, v = self.kv(self.ln_w(w)).split(c, dim=2)
121
+ k = k.view(b, r + 1, self.h, c // self.h).transpose(1, 2)
122
+ v = v.view(b, r + 1, self.h, c // self.h).transpose(1, 2)
123
+ att = (hq @ k.transpose(-2, -1)) * self.scale
124
+ att = att.masked_fill(~m[:, None, None, :], float("-inf"))
125
+ p = torch.softmax(att, dim=-1)
126
+ read = (p @ v).transpose(1, 2).contiguous().view(b, t, c)
127
+ mass = p[..., 1:].sum(dim=-1).mean() # attention on real bytes, null slot excluded
128
+ self.last_attended = read # RES-21: what was read, before the zero init projection
129
+ self.last_attn = p.detach() # (b, h, t, r + 1): the pointer policy reads it at decode
130
+ return self.out(read), mass
131
+
132
+
133
+ class MatchHead(nn.Module):
134
+ """RES-21 (ADR-008, amendment of 2026-09-14): a small head that answers ONE question at
135
+ the decision position, "is the queried entity's episode in the window?", from what
136
+ the model had before reading (the residual stream at the journal block) and what it
137
+ read (the reader's attended values, before the zero init projection). Trained
138
+ directly on that label, which the harness knows exactly for every curriculum example
139
+ (the own pointer among the window's labels, never the ground truth alone), and
140
+ coupled to the decision token's logits: + coupling * m on <CITE>, - coupling * m on
141
+ <UNKNOWN>. The last layer starts at zero, so the model begins as its parent (m = 0,
142
+ no bias) and learns the comparison from there."""
143
+
144
+ def __init__(self, n_embd: int):
145
+ super().__init__()
146
+ self.ln_pre = nn.LayerNorm(n_embd)
147
+ self.ln_read = nn.LayerNorm(n_embd)
148
+ self.up = nn.Linear(2 * n_embd, n_embd)
149
+ self.out = nn.Linear(n_embd, 1)
150
+ nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias) # the no-op law
151
+
152
+ def forward(self, pre: torch.Tensor, attended: torch.Tensor) -> torch.Tensor:
153
+ z = torch.cat([self.ln_pre(pre), self.ln_read(attended)], dim=-1)
154
+ return self.out(torch.nn.functional.gelu(self.up(z))).squeeze(-1) # (b,) a logit: present > 0
155
+
156
+
157
+ # ---------------------------------------------------------------------------- #
158
+ # The router's gate on the read (invariant 4) #
159
+ # ---------------------------------------------------------------------------- #
160
+ PATH_JOURNAL = 4 # the journal is path 4 of the RES-18 router (ADR-007)
161
+
162
+
163
+ def journal_gate(route: str, decision=None) -> float:
164
+ """The multiplier on the reader's contribution. `pinned_on` / `pinned_off`
165
+ are the two arms of the protocol (declared, recorded); `router` takes the
166
+ path 4 weight of a RouteDecision -- a hard decision for another path, or no
167
+ decision at all, gives zero: the floor guarantee at the model scale."""
168
+ if route == "pinned_on":
169
+ return 1.0
170
+ if route == "pinned_off":
171
+ return 0.0
172
+ if route != "router":
173
+ raise ValueError(f"unknown journal_route {route!r}")
174
+ if decision is None:
175
+ return 0.0
176
+ if decision.weights is not None:
177
+ return float(decision.weights[PATH_JOURNAL]) if len(decision.weights) > PATH_JOURNAL else 0.0
178
+ return float(decision.confidence) if decision.path == PATH_JOURNAL else 0.0
179
+
180
+
181
+ # ---------------------------------------------------------------------------- #
182
+ # The read window -- labelled, budgeted, in a seeded random order #
183
+ # ---------------------------------------------------------------------------- #
184
+ def build_read_window(items: list[tuple[str, bytes]], budget: int, rng: np.random.Generator,
185
+ marks: list[float] | None = None) -> tuple[bytes, dict[int, str]]:
186
+ """`items` = (pointer, payload) as retrieved. Returns the window bytes
187
+ (`A:<payload>\\n` per item, order shuffled with `rng` so a position is never a
188
+ shortcut, each payload cut to its share of `budget`) and the label byte ->
189
+ pointer map the harness uses to verify a citation.
190
+ `marks` (RES-23, the familiarity mark, v9): one number per item, the organ's
191
+ own similarity for that line; the prefix becomes `A(0.97):` (two decimals,
192
+ clamped at zero) and each payload gives six more bytes to it inside the same
193
+ budget. None keeps the window byte for byte as every earlier run wrote it."""
194
+ items = list(items)[: len(LABELS)]
195
+ if not items:
196
+ return b"", {}
197
+ if marks is not None and len(marks) != len(items):
198
+ raise ValueError("one mark per item")
199
+ order = rng.permutation(len(items))
200
+ per = max(1, budget // len(items) - (9 if marks is not None else 3)) # 'X:' or 'X(0.97):', and the newline
201
+ out, labels = [], {}
202
+ for slot, i in enumerate(order):
203
+ pointer, payload = items[i]
204
+ label = LABELS[slot]
205
+ labels[label] = pointer
206
+ prefix = bytes([label]) + (f"({max(0.0, min(1.0, float(marks[i]))):.2f})".encode("ascii") if marks is not None else b"") + b":"
207
+ out.append(prefix + payload[:per] + b"\n")
208
+ return b"".join(out), labels
209
+
210
+
211
+ def encode_query(text: str) -> list[int]:
212
+ return [TOK_EPI] + list(text.encode("utf-8"))
213
+
214
+
215
+ def encode_target(label: int | None, attr: str | None) -> list[int]:
216
+ """The contract: cite a label and answer, or abstain."""
217
+ if label is None:
218
+ return [TOK_UNKNOWN, NEWLINE]
219
+ return [TOK_CITE, label, TOK_ANS] + list(attr.encode("utf-8")) + [NEWLINE]
220
+
221
+
222
+ def parse_contract(tokens: list[int]) -> tuple[str, int | None, str | None]:
223
+ """('unknown'|'cite'|'malformed', label, attribute). A malformed answer is a
224
+ claim without the contract's shape -- counted against the model, never for it."""
225
+ toks = list(tokens)
226
+ if toks and toks[0] == TOK_UNKNOWN:
227
+ return "unknown", None, None
228
+ if len(toks) >= 3 and toks[0] == TOK_CITE and toks[2] == TOK_ANS and toks[1] in LABELS:
229
+ body = toks[3:]
230
+ if NEWLINE in body:
231
+ body = body[: body.index(NEWLINE)]
232
+ try:
233
+ return "cite", toks[1], bytes(b for b in body if b < 256).decode("utf-8")
234
+ except UnicodeDecodeError:
235
+ return "malformed", toks[1], None
236
+ return "malformed", None, None
237
+
238
+
239
+ # ---------------------------------------------------------------------------- #
240
+ # The bridge -- the torch model above, the numpy organ below (invariant 2) #
241
+ # ---------------------------------------------------------------------------- #
242
+ @dataclass
243
+ class ReadResult:
244
+ window: bytes
245
+ labels: dict[int, str] # label byte -> pointer
246
+ pointers: list[str] # what the organ returned, in retrieval order
247
+ scores: list[float]
248
+
249
+
250
+ class JournalBridge:
251
+ """Holds the organ (journal, write path, router path) and talks to the model
252
+ through two calls it must expose: `journal_hidden(idx, mask)` (the residual
253
+ stream at the journal block, before the reader) and `cue_encoder`.
254
+
255
+ The model never sees a pointer or a payload except through the read window;
256
+ the organ never sees a tensor except the cue."""
257
+
258
+ def __init__(self, model, journal: Journal, write_path: WritePath | None = None,
259
+ journal_path: JournalPath | None = None, k: int = 4, budget_bytes: int = 256,
260
+ seed: int = 0, shuffle_seed: int | None = None, device=None, mark: bool = False):
261
+ """`seed` is the scope's separation seed (DG) and the index seed -- it must
262
+ be the same at every open of a journal (Decision 8: a journal written under
263
+ another separation is refused). `shuffle_seed` only drives the order of
264
+ the read window and may differ between sessions."""
265
+ self.model = model
266
+ self.j = journal
267
+ if model.cue_encoder is None or model.cue_encoder.proj.out_features != journal.cue_dim:
268
+ raise ValueError("the model's cue encoder must produce the journal's cue dimension "
269
+ f"({journal.cue_dim}); set journal_cue_dim accordingly")
270
+ self.wp = write_path or WritePath(journal, seed=seed)
271
+ self.jp = journal_path or JournalPath(journal, seed=seed)
272
+ self.k, self.budget = k, budget_bytes
273
+ self.mark = mark # RES-23: the organ's similarity in every window line
274
+ self.rng = np.random.default_rng(seed if shuffle_seed is None else shuffle_seed)
275
+ self.device = device or next(model.parameters()).device
276
+ self.pointer_of: dict[str, str] = {} # entity -> pointer, for the curriculum's targets
277
+
278
+ # ---- tensors ----------------------------------------------------------------
279
+ def tokens(self, sequences: list[list[int]]) -> tuple[torch.Tensor, torch.Tensor]:
280
+ n = max(len(s) for s in sequences)
281
+ idx = torch.zeros(len(sequences), n, dtype=torch.long)
282
+ mask = torch.zeros(len(sequences), n, dtype=torch.bool)
283
+ for i, s in enumerate(sequences):
284
+ idx[i, : len(s)] = torch.tensor(s, dtype=torch.long); mask[i, : len(s)] = True
285
+ return idx.to(self.device), mask.to(self.device)
286
+
287
+ def cue_tensor(self, sequences: list[list[int]]) -> torch.Tensor:
288
+ """Cues with gradient (for the contrastive loss)."""
289
+ idx, mask = self.tokens(sequences)
290
+ h = self.model.journal_hidden(idx)
291
+ return self.model.cue_encoder(h, mask)
292
+
293
+ def cues(self, sequences: list[list[int]]) -> np.ndarray:
294
+ """Cues for the organ (no gradient)."""
295
+ with torch.no_grad():
296
+ return self.cue_tensor(sequences).float().cpu().numpy()
297
+
298
+ # ---- the organ ---------------------------------------------------------------
299
+ def write(self, text: str, schema: str, now: float, entity: str | None = None) -> WriteReport:
300
+ cue = self.cues([list(text.encode("utf-8"))])[0]
301
+ rep = self.wp.write(cue, text.encode("utf-8"), schema, now=now)
302
+ if rep.admitted:
303
+ self.jp.on_write(rep.entry)
304
+ if entity is not None:
305
+ self.pointer_of[entity] = rep.entry.pointer
306
+ return rep
307
+
308
+ def read(self, cue: np.ndarray) -> ReadResult:
309
+ hits = self.jp.retrieve(cue, k=self.k)
310
+ items = [(e.pointer, payload) for e, payload, _ in hits]
311
+ scores = [float(s) for _, _, s in hits]
312
+ window, labels = build_read_window(items, self.budget, self.rng, marks=scores if self.mark else None)
313
+ return ReadResult(window=window, labels=labels, pointers=[p for p, _ in items], scores=scores)
314
+
315
+ def marks_for(self, cue: np.ndarray, pointers: list[str]) -> list[float]:
316
+ """The organ's similarity between `cue` and each episode, whatever route put
317
+ the line in a window (a plain read, a paired negative, a forced bootstrap,
318
+ a shape filler): the same computation as the index's scoring."""
319
+ return [self.jp.cue_similarity(cue, p) for p in pointers]
320
+
321
+ def window_for(self, cue: np.ndarray, items: list[tuple[str, bytes]], rng: np.random.Generator
322
+ ) -> tuple[bytes, dict[int, str]]:
323
+ """A window over arbitrary items, marked with the organ's similarities when
324
+ the bridge marks, unmarked otherwise; the curriculum's windows go through here."""
325
+ marks = self.marks_for(cue, [p for p, _ in items]) if self.mark else None
326
+ return build_read_window(items, self.budget, rng, marks=marks)
327
+
328
+ def window_tensors(self, windows: list[bytes], window_len: int) -> tuple[torch.Tensor, torch.Tensor]:
329
+ """Byte windows -> (tokens, mask), each cut to `window_len`; an empty
330
+ window is one masked position (the reader keeps its null slot)."""
331
+ r = max(1, min(window_len, max((len(w) for w in windows), default=1)))
332
+ tok = torch.zeros(len(windows), r, dtype=torch.long)
333
+ mask = torch.zeros(len(windows), r, dtype=torch.bool)
334
+ for i, w in enumerate(windows):
335
+ w = w[:r]
336
+ if w:
337
+ tok[i, : len(w)] = torch.tensor(list(w), dtype=torch.long); mask[i, : len(w)] = True
338
+ return tok.to(self.device), mask.to(self.device)
@@ -0,0 +1,254 @@
1
+ """cortex_c2b.organ_use -- teaching the model to use its own organ (NOW-8; ADR-008 invariant 6).
2
+
3
+ The rule that combines abstention and citation, decided by the founder on
4
+ 2026-09-12: **the target of every example is computed from what the journal
5
+ actually returned, never from the ground truth alone.** If the episode that
6
+ holds the answer is among the retrieved ones, the target cites its local label
7
+ and gives the attribute; otherwise the target is `<UNKNOWN>`, even though the
8
+ fact exists in the journal. The model is never rewarded for citing what it was
9
+ not shown, so no invented citation enters the signal; while the cue encoder is
10
+ poor the model learns silence first (one token), and as the contrastive loss
11
+ improves retrieval, citations replace abstentions by themselves.
12
+
13
+ Three mitigations, all declared and hashed in the run's configuration:
14
+ - an asymmetric weight on the decision token of an abstention target
15
+ (answering instead of abstaining costs more than the reverse);
16
+ - negative examples (entities never planted) from the first step;
17
+ - a bootstrap for the cold start: on a missed retrieval, with a declared
18
+ probability that decays to zero, the right episode is FORCED into the
19
+ window and the example is marked `forced` -- counted in the diagnostics,
20
+ never as recall.
21
+
22
+ Decontamination (invariant 5): the training facts come from the same generator
23
+ family as the frozen protocol, with disjoint seeds, and every entity name is
24
+ checked against the protocol's facts and its negative control; a collision is
25
+ removed and reported. Written from scratch (our filon).
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass, field
30
+
31
+ import numpy as np
32
+
33
+ from cortex_c2b.hm_protocol import Fact, generate_facts, N_FACTS, N_NEGCTRL
34
+ from cortex_c2b.lm_bridge import (JournalBridge, encode_query, encode_target, build_read_window,
35
+ TOK_UNKNOWN)
36
+
37
+ EVAL_SEEDS = (0, 10_000) # the frozen protocol's facts and its negative control
38
+
39
+
40
+ # ---------------------------------------------------------------------------- #
41
+ # Facts: disjoint seeds, checked entities #
42
+ # ---------------------------------------------------------------------------- #
43
+ def decontaminate(train: list[Fact], eval_facts: list[Fact]) -> tuple[list[Fact], list[str]]:
44
+ """Remove every training fact whose entity appears in the evaluation facts."""
45
+ banned = {f.entity for f in eval_facts}
46
+ kept = [f for f in train if f.entity not in banned]
47
+ removed = [f.entity for f in train if f.entity in banned]
48
+ return kept, removed
49
+
50
+
51
+ def training_facts(n: int, seed: int = 100_000, n_negatives: int = 50) -> tuple[list[Fact], list[Fact], dict]:
52
+ """Planted facts and never-planted negatives for training, both disjoint
53
+ from the frozen protocol's sets (seeds 0 and 10_000). Returns
54
+ (facts, negatives, report)."""
55
+ if seed in EVAL_SEEDS or seed + 1 in EVAL_SEEDS:
56
+ raise ValueError("training seeds must differ from the frozen protocol's seeds (0 and 10_000)")
57
+ eval_facts = [f for s in EVAL_SEEDS for f in generate_facts(N_FACTS if s == 0 else N_NEGCTRL, s)[0]]
58
+ facts, gen_hash = generate_facts(n, seed)
59
+ negs, _ = generate_facts(n_negatives, seed + 1)
60
+ facts, removed_f = decontaminate(facts, eval_facts)
61
+ negs, removed_n = decontaminate(negs, eval_facts + facts)
62
+ report = {"generator_hash": gen_hash, "seed": seed, "n_facts": len(facts), "n_negatives": len(negs),
63
+ "removed_collisions": removed_f + removed_n}
64
+ return facts, negs, report
65
+
66
+
67
+ # ---------------------------------------------------------------------------- #
68
+ # The pool: facts planted through the normal gated path under the CURRENT encoder
69
+ # ---------------------------------------------------------------------------- #
70
+ def plant_pool(bridge: JournalBridge, facts: list[Fact], now: float = 0.0) -> dict:
71
+ """Write every fact as an episode (statement bytes, schema, cue from the
72
+ model). Returns admission statistics; `bridge.pointer_of` maps entities to
73
+ their pointers, which is how a target knows whether the right episode was
74
+ among the retrieved ones."""
75
+ admitted = 0
76
+ for i, f in enumerate(facts):
77
+ rep = bridge.write(f.statement, f.schema, now=now + i, entity=f.entity)
78
+ admitted += int(rep.admitted)
79
+ return {"planted": len(facts), "admitted": admitted}
80
+
81
+
82
+ # ---------------------------------------------------------------------------- #
83
+ # Examples #
84
+ # ---------------------------------------------------------------------------- #
85
+ @dataclass
86
+ class Example:
87
+ kind: str # answer | abstain | negative | forced
88
+ query: list[int] # <EPI> + question bytes
89
+ target: list[int] # the contract
90
+ window: bytes
91
+ labels: dict[int, str] = field(default_factory=dict)
92
+ retrieved_ok: bool = False # the right episode was among the retrieved (the encoder's success)
93
+ entity: str = ""
94
+ attr: str = ""
95
+ statement: list[int] = field(default_factory=list) # the episode's bytes, for the contrastive pair
96
+
97
+ @property
98
+ def tokens(self) -> list[int]:
99
+ return self.query + self.target
100
+
101
+ def weights(self, asym: float) -> list[float]:
102
+ """Loss on the target only; the decision token of an abstention target
103
+ carries the asymmetric weight."""
104
+ w = [0.0] * len(self.query) + [1.0] * len(self.target)
105
+ if self.target and self.target[0] == TOK_UNKNOWN:
106
+ w[len(self.query)] = float(asym)
107
+ return w
108
+
109
+
110
+ def _label_of(labels: dict[int, str], pointer: str) -> int | None:
111
+ for label, p in labels.items():
112
+ if p == pointer:
113
+ return label
114
+ return None
115
+
116
+
117
+ def make_example(bridge: JournalBridge, fact: Fact, negative: bool, forced_frac: float,
118
+ rng: np.random.Generator, paraphrase: bool = False) -> Example:
119
+ """One example, its target computed from the actual read (the rule above)."""
120
+ question = fact.paraphrase if paraphrase else fact.query
121
+ query = encode_query(question)
122
+ cue = bridge.cues([query])[0]
123
+ read = bridge.read(cue)
124
+ statement = list(fact.statement.encode("utf-8"))
125
+ if negative:
126
+ return Example("negative", query, encode_target(None, None), read.window, read.labels,
127
+ False, fact.entity, fact.attr, statement)
128
+ pointer = bridge.pointer_of.get(fact.entity)
129
+ label = _label_of(read.labels, pointer) if pointer else None
130
+ if label is not None:
131
+ return Example("answer", query, encode_target(label, fact.attr), read.window, read.labels,
132
+ True, fact.entity, fact.attr, statement)
133
+ if pointer and forced_frac > 0 and rng.random() < forced_frac:
134
+ # the bootstrap: the right episode is forced into the window (replacing the last slot)
135
+ items = [(p, bridge.j.payloads.get(p)) for p in read.pointers][: max(0, bridge.k - 1)]
136
+ items.append((pointer, bridge.j.payloads.get(pointer)))
137
+ window, labels = bridge.window_for(cue, items, rng)
138
+ return Example("forced", query, encode_target(_label_of(labels, pointer), fact.attr), window, labels,
139
+ False, fact.entity, fact.attr, statement)
140
+ return Example("abstain", query, encode_target(None, None), read.window, read.labels,
141
+ False, fact.entity, fact.attr, statement)
142
+
143
+
144
+ def make_paired_negative(bridge: JournalBridge, fact: Fact, rng: np.random.Generator,
145
+ paraphrase: bool = False, keep_shape: bool = False) -> Example:
146
+ """The minimal pair (v5 lever, run 6f10e8cbfebf): the SAME planted fact, its own episode
147
+ WITHHELD from the window; target abstain. Four runs showed the decision to cite or abstain
148
+ following the share of negatives seen in training (a class prior) far more than the
149
+ window's content; with pairs the prior earns nothing, and the only way down the loss is
150
+ to check whether the queried entity is in the read.
151
+
152
+ `keep_shape=False` is the pair AS RUN in v5 (`db028e6a4262`, INVALID): the own pointer
153
+ removed from the k retrieved, the window built from what is left, so it has k - 1 lines
154
+ whenever the episode had been retrieved (over 90 % of the time late in the run). That is
155
+ a shape cue, cheaper than a comparison, and absent from the frozen protocol's windows,
156
+ which always have k lines. The pair was not minimal; kept as written so the v5 run stays
157
+ reproducible under its hash. `keep_shape=True` is the v6 lever: k + 1 retrieved, the own
158
+ dropped, k lines kept, so the pair's two halves have the same shape and differ only by the
159
+ episode's presence."""
160
+ question = fact.paraphrase if paraphrase else fact.query
161
+ query = encode_query(question)
162
+ cue = bridge.cues([query])[0]
163
+ own = bridge.pointer_of.get(fact.entity)
164
+ items = withheld_items(bridge, cue, own, rng, keep_shape=keep_shape)
165
+ window, labels = bridge.window_for(cue, items, rng)
166
+ return Example("negative", query, encode_target(None, None), window, labels, False,
167
+ fact.entity, fact.attr, list(fact.statement.encode("utf-8")))
168
+
169
+
170
+ def withheld_items(bridge: JournalBridge, cue: np.ndarray, own: str | None, rng: np.random.Generator,
171
+ keep_shape: bool) -> list[tuple[str, bytes]]:
172
+ """The window items of a paired negative. `keep_shape=False`: the read's items minus the own
173
+ pointer, one line fewer than the read whenever the own was retrieved (v5 as run). `keep_shape=True`:
174
+ the same number of lines as the read would have shown: the k + 1 th candidate fills the slot when the
175
+ index returns one, else another planted episode drawn at random (the index is a locality sensitive
176
+ hash and may return fewer than k candidates, so the fill is by construction, not by luck)."""
177
+ if not keep_shape:
178
+ read = bridge.read(cue)
179
+ return [(p, bridge.j.payloads.get(p)) for p in read.pointers if p != own]
180
+ hits = bridge.jp.retrieve(cue, k=bridge.k + 1)
181
+ top = [(e.pointer, payload) for e, payload, _ in hits]
182
+ shown = min(len(top), bridge.k) # what a plain read shows
183
+ items = [(p, pl) for p, pl in top if p != own][: bridge.k]
184
+ if len(items) < shown: # the own was shown and no k + 1 th candidate exists
185
+ seen = {p for p, _ in top}
186
+ others = sorted(p for p in bridge.pointer_of.values() if p != own and p not in seen)
187
+ if others:
188
+ p = others[int(rng.integers(len(others)))]
189
+ items.append((p, bridge.j.payloads.get(p)))
190
+ return items
191
+
192
+
193
+ def make_batch(bridge: JournalBridge, facts: list[Fact], negatives: list[Fact], n: int,
194
+ rng: np.random.Generator, negatives_frac: float = 0.25, forced_frac: float = 0.5,
195
+ paraphrase_frac: float = 0.5, paired: bool = False, keep_shape: bool = False) -> list[Example]:
196
+ """`paired`: negatives are minimal pairs of the planted facts (the episode withheld)
197
+ instead of separate never planted entities; `keep_shape`: the pair keeps k lines (v6)."""
198
+ out = []
199
+ for _ in range(n):
200
+ if rng.random() < negatives_frac and (paired or negatives):
201
+ if paired:
202
+ f = facts[int(rng.integers(len(facts)))]
203
+ out.append(make_paired_negative(bridge, f, rng, paraphrase=rng.random() < paraphrase_frac,
204
+ keep_shape=keep_shape))
205
+ else:
206
+ f = negatives[int(rng.integers(len(negatives)))]
207
+ out.append(make_example(bridge, f, True, 0.0, rng, paraphrase=rng.random() < paraphrase_frac))
208
+ else:
209
+ f = facts[int(rng.integers(len(facts)))]
210
+ out.append(make_example(bridge, f, False, forced_frac, rng, paraphrase=rng.random() < paraphrase_frac))
211
+ return out
212
+
213
+
214
+ def own_in_window(e: Example) -> bool:
215
+ """RES-21's label: the queried entity's own episode is among the window's labels. Computed
216
+ from what the journal returned (the pointer is the content hash of the statement's bytes),
217
+ never from the ground truth alone: a positive whose episode was not retrieved is a 0."""
218
+ from cortex_c2b import content_hash
219
+ return content_hash(bytes(e.statement)) in set(e.labels.values())
220
+
221
+
222
+ def match_tensors(examples: list[Example], device):
223
+ """(targets (b,) float 0/1, decision positions (b,) long): the position at which the
224
+ decision token is predicted is the last token of the query."""
225
+ import torch
226
+ t = torch.tensor([1.0 if own_in_window(e) else 0.0 for e in examples], device=device)
227
+ d = torch.tensor([len(e.query) - 1 for e in examples], dtype=torch.long, device=device)
228
+ return t, d
229
+
230
+
231
+ def batch_stats(examples: list[Example]) -> dict:
232
+ n = max(1, len(examples))
233
+ kinds = {k: sum(e.kind == k for e in examples) for k in ("answer", "abstain", "negative", "forced")}
234
+ planted = [e for e in examples if e.kind != "negative"]
235
+ return {**{f"n_{k}": v for k, v in kinds.items()},
236
+ "retrieval_hit": (sum(e.retrieved_ok for e in planted) / len(planted)) if planted else 0.0,
237
+ "abstain_target_rate": sum(e.target[0] == TOK_UNKNOWN for e in examples) / n}
238
+
239
+
240
+ def collate(bridge: JournalBridge, examples: list[Example], asym: float, window_len: int):
241
+ """Tensors for one training step: (idx, targets, weights, window, window_mask,
242
+ query_sequences, statement_sequences). Targets are the next token; the
243
+ weight of a position is the weight of the token it predicts."""
244
+ import torch
245
+ seqs = [e.tokens for e in examples]
246
+ idx, _ = bridge.tokens([s[:-1] for s in seqs])
247
+ tgt, _ = bridge.tokens([s[1:] for s in seqs])
248
+ n = idx.size(1)
249
+ w = torch.zeros(len(examples), n)
250
+ for i, e in enumerate(examples):
251
+ ww = e.weights(asym)[1:] # aligned with the predicted token
252
+ w[i, : len(ww)] = torch.tensor(ww)
253
+ window, wmask = bridge.window_tensors([e.window for e in examples], window_len)
254
+ return idx, tgt, w.to(idx.device), window, wmask, [e.query for e in examples], [e.statement for e in examples]