Resolution-Gated Dual Masking / resolution_gated_masking.py
Mechanism confirmed, baseline not beaten
1"""Resolution-gated dual masking MVP with numerical check and synthetic sweep."""
2from __future__ import annotations
3import json
4from pathlib import Path
5import numpy as np
6
7
8def _codes(z):
9 z = np.asarray(z, dtype=np.int64)
10 if z.ndim == 1:
11 return z
12 # Binary candidate variables: encode a mask row as a mixed-radix integer.
13 return z.dot(1 << np.arange(z.shape[1], dtype=np.int64))
14
15
16def conditional_entropy(y, z=None):
17 y = np.asarray(y, dtype=np.int64)
18 if z is None:
19 z = np.zeros(len(y), dtype=np.int64)
20 keys = _codes(z)
21 # H(Y|Z) from one joint bincount; this is substantially faster than loops.
22 _, zi = np.unique(keys, return_inverse=True)
23 ny = int(y.max()) + 1
24 joint = np.bincount(zi * ny + y, minlength=int(zi.max() + 1) * ny).reshape(-1, ny)
25 totals = joint.sum(axis=1)
26 nz = joint > 0
27 probs = np.divide(joint, totals[:, None], out=np.zeros_like(joint, dtype=float), where=totals[:, None] > 0)
28 ent = -np.sum(np.where(nz, probs * np.log(np.maximum(probs, 1e-300)), 0.0), axis=1)
29 return float(np.sum(totals / len(y) * ent))
30
31
32def entropy_scores(y, X, mask=None):
33 y, X = np.asarray(y), np.asarray(X, dtype=np.int64)
34 mask = [] if mask is None else list(mask)
35 h0 = conditional_entropy(y, np.zeros(len(y), dtype=np.int64) if not mask else X[:, mask])
36 return np.array([h0 - conditional_entropy(y, X[:, mask + [j]])
37 for j in range(X.shape[1]) if j not in mask])
38
39
40def conditional_mean_predict(train_x, train_y, test_x):
41 train_x = np.asarray(train_x, dtype=np.int64); test_x = np.asarray(test_x, dtype=np.int64)
42 if train_x.ndim == 1: train_x = train_x[:, None]
43 if test_x.ndim == 1: test_x = test_x[:, None]
44 # Mixed-radix encoding is safe here because all features are binary.
45 kt = _codes(train_x); kv = _codes(test_x)
46 sums = np.bincount(kt, weights=train_y)
47 counts = np.bincount(kt)
48 glob = float(np.mean(train_y))
49 return np.divide(sums[kv], counts[kv], out=np.full(len(kv), glob), where=counts[kv] > 0)
50
51
52def risk_scores(ytr, Xtr, yva, Xva, mask=None):
53 mask = [] if mask is None else list(mask)
54 bxtr = np.zeros((len(ytr), 1), dtype=np.int64) if not mask else Xtr[:, mask]
55 bxva = np.zeros((len(yva), 1), dtype=np.int64) if not mask else Xva[:, mask]
56 base = np.mean((yva - conditional_mean_predict(bxtr, ytr, bxva)) ** 2)
57 out = []
58 for j in range(Xtr.shape[1]):
59 if j not in mask:
60 pred = conditional_mean_predict(Xtr[:, mask + [j]], ytr, Xva[:, mask + [j]])
61 out.append(base - np.mean((yva - pred) ** 2))
62 return np.array(out), base
63
64
65def bootstrap_gamma(y, X, B=30, seed=0):
66 rng = np.random.default_rng(seed)
67 vals = np.array([entropy_scores(y[ix], X[ix])
68 for ix in (rng.integers(0, len(y), len(y)) for _ in range(B))])
69 observed = entropy_scores(y, X)
70 j = int(np.argmax(observed))
71 return float(observed[j] / (np.std(vals[:, j], ddof=1) + 1e-12)), observed
72
73
74def gated_select(ytr, Xtr, yva, Xva, kappa_c=1.0, gamma_c=2.0, B=30, seed=0):
75 kappa = conditional_entropy(ytr) / np.log(2.0)
76 gamma, sh = bootstrap_gamma(ytr, Xtr, B=B, seed=seed)
77 sr, _ = risk_scores(ytr, Xtr, yva, Xva)
78 mh, mr = int(np.argmax(sh)), int(np.argmax(sr))
79 chosen = mh if (kappa < kappa_c and gamma > gamma_c) else mr
80 return dict(kappa=kappa, gamma=gamma, sh=sh, sr=sr, mh=mh, mr=mr, chosen=chosen)
81
82
83def generate(n, p, d, seed):
84 rng = np.random.default_rng(seed)
85 lag = rng.integers(0, 2, n)
86 X = np.column_stack([lag, rng.integers(0, 2, (n, d - 1))])
87 y = lag ^ (rng.random(n) < p).astype(int)
88 return X, y
89
90
91def run():
92 checks = []
93 for p in (0.0, .1, .25, .4, .5):
94 X, y = generate(100000, p, 3, 91)
95 empirical = conditional_entropy(y) - conditional_entropy(y, X[:, 0])
96 q = np.array([p, 1-p]); hb = -np.sum(q[q > 0] * np.log(q[q > 0]))
97 checks.append((p, empirical, np.log(2)-hb))
98 rows = []
99 for n in (256, 1024, 4096):
100 for p in (0.0, .1, .25, .4, .49):
101 for d in (3, 9, 17):
102 vals = {k: [] for k in ("entropy_recovery","risk_recovery","gate_recovery","risk_h","risk_r","risk_gate","kappa","gamma")}
103 for rep in range(6):
104 X,y = generate(n + n//2, p, d, 10000 + n + 100*d + rep)
105 Xtr, Xva, ytr, yva = X[:n], X[n:], y[:n], y[n:]
106 g = gated_select(ytr, Xtr, yva, Xva, B=30, seed=rep)
107 pred = []
108 for col in (g['mh'], g['mr'], g['chosen']):
109 pred.append(conditional_mean_predict(Xtr[:, [col]], ytr, Xva[:, [col]]))
110 vals["entropy_recovery"].append(g['mh'] == 0); vals["risk_recovery"].append(g['mr'] == 0); vals["gate_recovery"].append(g['chosen'] == 0)
111 vals["risk_h"].append(np.mean((yva-pred[0])**2)); vals["risk_r"].append(np.mean((yva-pred[1])**2)); vals["risk_gate"].append(np.mean((yva-pred[2])**2)); vals["kappa"].append(g['kappa']); vals["gamma"].append(g['gamma'])
112 rows.append(dict(n=n,p=p,d=d,**{k:float(np.mean(v)) for k,v in vals.items()}))
113 out = {"math_checks":checks, "results":rows}
114 Path("results.json").write_text(json.dumps(out, indent=2))
115 print(json.dumps({"math_checks":checks, "selected_summary": rows[:3]}, indent=2))
116
117if __name__ == "__main__": run()