muster-memory 0.1.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.
@@ -0,0 +1,47 @@
1
+ """The harness's reranker score cache, read-only (harness/lme_pf29.py:1774-1848).
2
+
3
+ A file is named `<corpus signature>__<knob digest>.json`. The corpus signature hashes the exact unit list, so a
4
+ cut that names an existing file has produced, byte for byte, the units the published run scored.
5
+ """
6
+ import hashlib
7
+ import json
8
+ import os
9
+
10
+ RERANKER_NAME = "BAAI/bge-reranker-v2-m3"
11
+ # The knob string of every final leg except FactConsolidation (len_legs.json env: CEML=2048 CEBS=16 CE_SUBMAX=0).
12
+ KNOBS_FINAL = "CEML=2048|CEBS=16|CE_SUBMAX=0|CE_SUB_TOK=|CE_SLIDE=|name=" + RERANKER_NAME
13
+
14
+
15
+ def ce_sig(units, name=RERANKER_NAME):
16
+ h = hashlib.sha1()
17
+ h.update(("ce1|%s|%d\n" % (name, len(units))).encode())
18
+ for t in units:
19
+ h.update(hashlib.sha1(t.encode("utf-8", "replace")).digest())
20
+ return h.hexdigest()[:16]
21
+
22
+
23
+ def knob_digest(knobs=KNOBS_FINAL):
24
+ return hashlib.sha1(knobs.encode()).hexdigest()[:8]
25
+
26
+
27
+ def qkey(query):
28
+ return hashlib.sha1(query.encode("utf-8", "replace")).hexdigest()[:16]
29
+
30
+
31
+ class LegacyCECache:
32
+ """{question key: {unit index: score}} for one corpus, from a directory of harness cache files."""
33
+
34
+ def __init__(self, directory, *, knobs=KNOBS_FINAL):
35
+ self.directory, self.knobs, self.suffix = directory, knobs, knob_digest(knobs)
36
+
37
+ def path_for(self, units):
38
+ return os.path.join(self.directory, "%s__%s.json" % (ce_sig(units), self.suffix))
39
+
40
+ def has(self, units):
41
+ return os.path.exists(self.path_for(units))
42
+
43
+ def load(self, units):
44
+ p = self.path_for(units)
45
+ if not os.path.exists(p):
46
+ return {}
47
+ return json.load(open(p))
muster/compat/env.py ADDED
@@ -0,0 +1,39 @@
1
+ """The environment exact mode needs, and a record of the one a run used.
2
+
3
+ Exact mode iterates sets the way the paper's harness did, which is reproducible only with string hashing fixed
4
+ (PYTHONHASHSEED=0 at interpreter start; `python -I` and `-E` ignore the variable). The published equality was
5
+ measured with the versions in EXACT_VERSIONS on CPython 3.12.13.
6
+ """
7
+ import importlib.metadata
8
+ import platform
9
+ import sys
10
+
11
+ from ..errors import EnvironmentRefused
12
+
13
+ EXACT_PYTHON = "3.12.13"
14
+ EXACT_VERSIONS = {"tiktoken": "0.13.0", "numpy": "2.4.4", "torch": "2.11.0", "sentence-transformers": "5.3.0",
15
+ "transformers": "5.5.0", "tokenizers": "0.22.2", "nltk": "3.10.0", "rank-bm25": "0.2.2"}
16
+
17
+
18
+ def versions():
19
+ out = {"python": platform.python_version(), "hash_randomization": sys.flags.hash_randomization}
20
+ for k in EXACT_VERSIONS:
21
+ try:
22
+ out[k] = importlib.metadata.version(k)
23
+ except importlib.metadata.PackageNotFoundError:
24
+ out[k] = None
25
+ return out
26
+
27
+
28
+ def check_environment(*, strict_versions=False):
29
+ """Raise EnvironmentRefused if exact mode cannot reproduce the paper here; return the mismatches otherwise."""
30
+ if sys.flags.hash_randomization:
31
+ raise EnvironmentRefused("exact mode needs string hashing fixed: start Python as "
32
+ "`PYTHONHASHSEED=0 python ...` (not with -I or -E)")
33
+ v = versions()
34
+ bad = {k: (v.get(k), want) for k, want in EXACT_VERSIONS.items() if v.get(k) != want}
35
+ if v["python"] != EXACT_PYTHON:
36
+ bad["python"] = (v["python"], EXACT_PYTHON)
37
+ if bad and strict_versions:
38
+ raise EnvironmentRefused("exact mode was verified with other versions: %s" % bad)
39
+ return bad
@@ -0,0 +1,140 @@
1
+ """The harness's intermediate renders, from which the published R4 contexts were made (exact mode only).
2
+
3
+ The paper's R4 contexts were produced in two steps: the harness rendered the selected units with an intermediate
4
+ grouping (bgroup for prose stores, seg3 for BEAM and LoCoMo), and render_rule.py then recovered the unit ids from that
5
+ text and rendered one block per parent. Where a store holds several units with identical text, the recovery can name
6
+ another copy than the one selected, and the published context follows the copy. Reproducing a published context byte
7
+ for byte therefore needs both steps. Lifted from harness/lme_pf29.py:271-333 (bgroup) and 525-715 (seg3, HMARK off).
8
+ """
9
+ import math
10
+
11
+
12
+ def rank_pos(order):
13
+ """First position of each unit in a ranking."""
14
+ pos = {}
15
+ for n, i in enumerate(order):
16
+ pos.setdefault(i, n)
17
+ return pos
18
+
19
+
20
+ def render_bgroup(fed, txt, pos, far, parent, ents, hubs, cap):
21
+ fed = sorted(fed)
22
+ lim = max(1, cap if cap > 0 else 10)
23
+ if len(fed) <= lim:
24
+ return "\n".join("Memory %d:\n%s" % (n + 1, txt(i))
25
+ for n, i in enumerate(sorted(fed, key=lambda j: (pos.get(j, far), j))))
26
+ E = {i: (ents[i] - hubs) if i < len(ents) else set() for i in fed}
27
+ _ov = [len(E[fed[a]] & E[fed[b]]) for a in range(len(fed)) for b in range(a + 1, len(fed))]
28
+ PB = (max(_ov) if _ov else 0) + 1
29
+ groups = {i: [i] for i in fed}
30
+
31
+ def sim(a, b):
32
+ s = 0
33
+ for x in groups[a]:
34
+ for y in groups[b]:
35
+ s += len(E[x] & E[y]) + (PB if parent[x] == parent[y] else 0)
36
+ return s / float(len(groups[a]) * len(groups[b]))
37
+
38
+ size_cap = int(math.ceil(len(fed) / float(lim)))
39
+ while len(groups) > lim:
40
+ ks = sorted(groups)
41
+ pick, best = None, -1.0
42
+ for ia in range(len(ks)):
43
+ for ib in range(ia + 1, len(ks)):
44
+ a, b = ks[ia], ks[ib]
45
+ if len(groups[a]) + len(groups[b]) > size_cap:
46
+ continue
47
+ s = sim(a, b)
48
+ if s > best:
49
+ pick, best = (a, b), s
50
+ if pick is None:
51
+ size_cap += 1
52
+ continue
53
+ a, b = pick
54
+ groups[a] = groups[a] + groups[b]
55
+ del groups[b]
56
+ out = sorted(groups.values(), key=lambda m: (min(pos.get(i, far) for i in m), m[0]))
57
+ return "\n".join("Memory %d:\n%s" % (n + 1, "\n".join(txt(i) for i in sorted(m))) for n, m in enumerate(out))
58
+
59
+
60
+ def render_seg3(fed, txt, parent, ents, hubs, cap):
61
+ """seg3 (doc_first) with HMARK off: an ordered segmentation of the sorted fed units, merged by priority levels."""
62
+ fed = sorted(fed)
63
+ P = len({parent[i] for i in fed})
64
+ lim = min(len(fed), max(-cap, P)) if cap < 0 else max(1, cap if cap > 0 else 10)
65
+ if len(fed) <= lim:
66
+ return "\n".join("Memory %d:\n%s" % (n + 1, txt(i)) for n, i in enumerate(fed))
67
+ E = {i: (ents[i] - hubs) if i < len(ents) else set() for i in fed}
68
+ D = {}
69
+ for i, p in enumerate(parent):
70
+ if i < len(ents):
71
+ D.setdefault(p, set()).update(ents[i])
72
+ D = {p: (s - hubs) for p, s in D.items()}
73
+ blocks = [[i] for i in fed]
74
+
75
+ def pars(b):
76
+ return {parent[i] for i in b}
77
+
78
+ def jac(sa, sb):
79
+ u = len(sa | sb)
80
+ return (len(sa & sb) / float(u)) if u else 0.0
81
+
82
+ def b1(a, b):
83
+ s = 0
84
+ for x in a:
85
+ for y in b:
86
+ s += len(E[x] & E[y])
87
+ return s / float(len(a) * len(b))
88
+
89
+ def b2(a, b):
90
+ return jac(set().union(*[E[i] for i in a]), set().union(*[E[i] for i in b]))
91
+
92
+ def b3(a, b):
93
+ sa = set().union(*[D.get(p, set()) for p in pars(a)]) if a else set()
94
+ sb = set().union(*[D.get(p, set()) for p in pars(b)]) if b else set()
95
+ return jac(sa, sb)
96
+
97
+ _sing = [(x, y) for ix, x in enumerate(fed) for y in fed[ix + 1:] if parent[x] != parent[y]]
98
+ tau_i = (sum(b1([x], [y]) for x, y in _sing) / len(_sing)) if _sing else 0.0
99
+ _pl = sorted({parent[i] for i in fed}, key=lambda p: min(i for i in fed if parent[i] == p))
100
+ _grp = {p: [i for i in fed if parent[i] == p] for p in _pl}
101
+ _pp = [(a, b) for ia, a in enumerate(_pl) for b in _pl[ia + 1:]]
102
+ tau_b = (sum(b2(_grp[a], _grp[b]) for a, b in _pp) / len(_pp)) if _pp else 0.0
103
+ tau_c = (sum(b3(_grp[a], _grp[b]) for a, b in _pp) / len(_pp)) if _pp else 0.0
104
+ LEVELS = [(1, b1, tau_i, True), (1, b1, 0.0, True), (2, b2, tau_b, False), (2, b2, 0.0, False),
105
+ (3, b3, tau_c, False), (3, b3, 0.0, False), (4, None, None, False)]
106
+ size_cap = int(math.ceil(len(fed) / float(lim)))
107
+ while len(blocks) > lim:
108
+ got = False
109
+ for lv, bond, thr, same_only in LEVELS:
110
+ if lv == 4:
111
+ pool = [j for j in range(len(blocks) - 1) if len(blocks[j]) + len(blocks[j + 1]) <= size_cap]
112
+ if not pool:
113
+ continue
114
+ k = min(pool, key=lambda j: len(blocks[j]) + len(blocks[j + 1]))
115
+ blocks[k] = blocks[k] + blocks[k + 1]
116
+ del blocks[k + 1]
117
+ got = True
118
+ break
119
+ cand = []
120
+ for j in range(len(blocks) - 1):
121
+ A, B = blocks[j], blocks[j + 1]
122
+ one = (len(pars(A)) == 1 and pars(A) == pars(B))
123
+ if same_only != one:
124
+ continue
125
+ if not one and len(A) + len(B) > size_cap:
126
+ continue
127
+ s = bond(A, B)
128
+ if s < thr or (s <= 0 and not (lv == 1 and thr == 0.0)):
129
+ continue
130
+ cand.append((s, j))
131
+ if not cand:
132
+ continue
133
+ s, j = max(cand, key=lambda t: (t[0], -t[1]))
134
+ blocks[j] = blocks[j] + blocks[j + 1]
135
+ del blocks[j + 1]
136
+ got = True
137
+ break
138
+ if not got:
139
+ size_cap += 1
140
+ return "\n".join("Memory %d:\n%s" % (n + 1, "\n".join(txt(i) for i in m)) for n, m in enumerate(blocks))
@@ -0,0 +1,75 @@
1
+ """Recover the selected unit ids from a rendered harness context (harness/render_parent.py:13-43,
2
+ harness/rerender_nomerge.py:53-55, harness/render_rule.py:28-29), so R4 can be rendered from what a published run fed.
3
+ """
4
+ import collections
5
+ import re
6
+
7
+ HDR = re.compile(r"(?m)^Memory \d+:\n")
8
+
9
+
10
+ def index80(units):
11
+ idx = {}
12
+ for i, s in enumerate(units):
13
+ idx.setdefault(s[:80], []).append(i)
14
+ return idx
15
+
16
+
17
+ def blocks_of(context):
18
+ """The block bodies of a "Memory n:" context, joiners stripped (render_rule.py:28-29)."""
19
+ parts = HDR.split(context)[1:]
20
+ return [b[:-1] if (n < len(parts) - 1 and b.endswith("\n")) else b for n, b in enumerate(parts)]
21
+
22
+
23
+ def decompose(block, units, idx):
24
+ """Unit ids of one block; among same-text units, the first one after the block's previous unit."""
25
+ ids, p = [], 0
26
+ while p < len(block):
27
+ cand = [i for i in idx.get(block[p:p + 80], []) if block.startswith(units[i], p)
28
+ and (p + len(units[i]) == len(block) or block[p + len(units[i])] == "\n")]
29
+ if not cand:
30
+ return None
31
+ L = max(len(units[j]) for j in cand)
32
+ cand = [j for j in cand if len(units[j]) == L]
33
+ after = [j for j in cand if not ids or j > ids[-1]]
34
+ i = min(after) if after else min(cand)
35
+ ids.append(i)
36
+ p += len(units[i])
37
+ if p < len(block):
38
+ p += 1
39
+ return ids
40
+
41
+
42
+ def resolve(dec, units, parent):
43
+ """Where one id was given to two places (a repeated passage), move the extra one to an unused copy of the same
44
+ text, preferably in the parent of its block-mates."""
45
+ used = collections.Counter(i for d in dec for i in d)
46
+ copies = collections.defaultdict(list)
47
+ for j, s in enumerate(units):
48
+ copies[s].append(j)
49
+ for d in dec:
50
+ for p, i in enumerate(d):
51
+ if used[i] < 2:
52
+ continue
53
+ mates = {parent[x] for x in d if x != i}
54
+ free = [j for j in copies[units[i]] if used[j] == 0]
55
+ alt = [j for j in free if mates and parent[j] in mates] or free
56
+ if alt:
57
+ used[i] -= 1
58
+ d[p] = alt[0]
59
+ used[alt[0]] += 1
60
+ return dec
61
+
62
+
63
+ def recover_ids(context, units, parent, idx=None):
64
+ """The ids a harness context holds, or raise ValueError (a header-less context, a failed decomposition)."""
65
+ if not HDR.search(context):
66
+ raise ValueError("context has no 'Memory n:' header; nothing to recover")
67
+ idx = idx if idx is not None else index80(units)
68
+ dec = [decompose(b, units, idx) for b in blocks_of(context)]
69
+ if any(d is None for d in dec):
70
+ raise ValueError("unit decomposition failed")
71
+ dec = resolve(dec, units, parent)
72
+ ids = [i for d in dec for i in d]
73
+ if len(set(ids)) != len(ids):
74
+ raise ValueError("a unit id is used twice")
75
+ return ids
@@ -0,0 +1,142 @@
1
+ """Check the library against the paper without the paper's machine: tables of hashes, no benchmark text.
2
+
3
+ muster verify cuts --data DIR every row of the 24 final legs: unit count, unit-list hash, parent-label hash
4
+
5
+ DIR holds the benchmark files at the paths the legs name (data/..., data_all/data/..., beam/...), each checked against
6
+ gate/data.sha256 first. The table (gate/cut_table.json) was written by tools/make_cut_table.py on the experiment machine
7
+ from the same library code, after the cut had been checked row by row against the harness (tests/studio/).
8
+ """
9
+ import hashlib
10
+ import json
11
+ import os
12
+
13
+ from .. import presets
14
+ from ..cut import cut
15
+ from .ce_cache import ce_sig
16
+
17
+
18
+ def parent_hash(parent):
19
+ h = hashlib.sha1()
20
+ for p in parent:
21
+ h.update(("%s:%s\n" % (type(p).__name__, p)).encode())
22
+ return h.hexdigest()[:16]
23
+
24
+
25
+ def text_hash(text):
26
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
27
+
28
+
29
+ def row_signature(text, plan):
30
+ c = cut(text, plan)
31
+ return {"units": len(c.units), "unit_sig": ce_sig(list(c.units)), "parent_sig": parent_hash(c.parent),
32
+ "parents": len(set(c.parent)), "text_sha256": text_hash(text)}
33
+
34
+
35
+ def _sha256(path):
36
+ h = hashlib.sha256()
37
+ with open(path, "rb") as f:
38
+ for b in iter(lambda: f.read(1 << 20), b""):
39
+ h.update(b)
40
+ return h.hexdigest()
41
+
42
+
43
+ def load_data_hashes(path):
44
+ out = {}
45
+ for line in open(path):
46
+ if line.strip() and not line.startswith("#"):
47
+ sha, rel = line.split()
48
+ out[rel] = sha
49
+ return out
50
+
51
+
52
+ def verify_cuts(data_dir, table_path, hashes_path=None, *, legs=None, out=print):
53
+ """Each row's store text is checked against the table's text hash before it is cut, so a file rebuilt with
54
+ another parquet writer still verifies, and a wrong file is reported, never compared."""
55
+ import pandas as pd
56
+ table = json.load(open(table_path))
57
+ ok = bad = skipped = 0
58
+ for tag, rows in sorted(table["legs"].items()):
59
+ if legs and tag not in legs:
60
+ continue
61
+ leg = presets.legs()[tag]
62
+ path = os.path.join(data_dir, leg["parq"])
63
+ if not os.path.exists(path):
64
+ out("SKIP %-16s %s not found" % (tag, leg["parq"]))
65
+ skipped += len(rows)
66
+ continue
67
+ df = pd.read_parquet(path)
68
+ plan = presets.get(tag)
69
+ n_wrong = 0
70
+ for r, want in rows.items():
71
+ text = str(df["context"].iloc[int(r)]) if int(r) < len(df) else None
72
+ if text is None or ("text_sha256" in want and text_hash(text) != want["text_sha256"]):
73
+ n_wrong += 1
74
+ skipped += 1
75
+ continue
76
+ got = row_signature(text, plan)
77
+ if "text_sha256" not in want:
78
+ got.pop("text_sha256")
79
+ same = got == want
80
+ ok += same
81
+ bad += not same
82
+ if not same:
83
+ out("DIFF %-16s row %s: %s != %s" % (tag, r, got, want))
84
+ out("%-16s %d rows checked%s" % (tag, len(rows) - n_wrong,
85
+ (", %d skipped: store text differs from the published one" % n_wrong) if n_wrong else ""))
86
+ out("cut verification: %d equal, %d different, %d skipped (missing or different data)" % (ok, bad, skipped))
87
+ return bad == 0 and ok > 0
88
+
89
+
90
+ def verify_contexts(data_dir, ctx_table_path, cut_table_path, *, legs=None, cache_dirs=(), models=None, nq=None,
91
+ out=print):
92
+ """Rebuild each leg's stores under its preset, search every question in exact mode and compare the context hash
93
+ with gate/ctx_table.jsonl. With the published reranker caches (cache_dirs) the comparison is exact on CPU;
94
+ without them, scores are computed live and near-equal scores may come out in another order."""
95
+ import pandas as pd
96
+ from ..memory import build
97
+ from ..models import Models
98
+ from .ce_cache import LegacyCECache
99
+ want = {}
100
+ for line in open(ctx_table_path):
101
+ r = json.loads(line)
102
+ want.setdefault(r["leg"], {})[(r["row"], r["qi"])] = r["ctxhash"]
103
+ texts = {tag: {int(r): v.get("text_sha256") for r, v in rows.items()}
104
+ for tag, rows in json.load(open(cut_table_path))["legs"].items()}
105
+ total = equal = 0
106
+ for tag in sorted(want):
107
+ if legs and tag not in legs:
108
+ continue
109
+ leg = presets.legs()[tag]
110
+ path = os.path.join(data_dir, leg["parq"])
111
+ if not os.path.exists(path):
112
+ out("SKIP %-16s %s not found" % (tag, leg["parq"]))
113
+ continue
114
+ plan = presets.get(tag)
115
+ m = models or Models(device="cpu", encoder_spec=plan.encoder, reranker_spec=plan.reranker)
116
+ df = pd.read_parquet(path)
117
+ t = e = 0
118
+ for row in leg["rows"]:
119
+ keys = sorted(k for k in want[tag] if k[0] == row)[:nq] if nq else sorted(k for k in want[tag] if k[0] == row)
120
+ if not keys:
121
+ continue
122
+ text = str(df["context"].iloc[row])
123
+ if texts.get(tag, {}).get(row) and text_hash(text) != texts[tag][row]:
124
+ out("SKIP %-16s row %d: store text differs from the published one" % (tag, row))
125
+ continue
126
+ mem = build(text, plan, models=m)
127
+ cache = {}
128
+ for d in cache_dirs:
129
+ c = LegacyCECache(d, knobs=plan.compat["knobs"])
130
+ if c.has(list(mem.units)):
131
+ cache = c.load(list(mem.units))
132
+ break
133
+ qs = list(df["questions"].iloc[row])
134
+ for k in keys:
135
+ ctx = mem.context(str(qs[k[1]]), cache=cache, models=m)
136
+ t += 1
137
+ e += ctx.ctxhash == want[tag][k]
138
+ total += t
139
+ equal += e
140
+ out("%-16s %5d questions, %5d contexts equal" % (tag, t, e))
141
+ out("context verification: %d of %d equal" % (equal, total))
142
+ return total > 0 and equal == total