Entropy-calibrated hyperbolic curvature / entropy_curvature.py
Mechanism confirmed, baseline not beaten
1import json
2import numpy as np
3from scipy.special import roots_hermitenorm, ndtr
4from scipy.optimize import brentq
5
6SEED = 1729
7LOG3 = np.log(3.0)
8
9def coth(s):
10 s = np.asarray(s, float)
11 out = np.empty_like(s)
12 small = np.abs(s) < 1e-5
13 out[small] = 1.0/s[small] + s[small]/3.0 - s[small]**3/45.0
14 out[small & (s == 0)] = np.inf
15 out[~small] = 1.0/np.tanh(s[~small])
16 return out
17
18def A(s):
19 s = np.asarray(s, float)
20 if s.ndim == 0:
21 if abs(float(s)) < 1e-5: return 1.0 + float(s)**2/3.0
22 return float(s)/np.tanh(float(s))
23 out = np.ones_like(s, dtype=float)
24 nz = s != 0
25 out[nz] = s[nz]*coth(s[nz])
26 return out
27
28class MasterCurve:
29 # F is standard normal radial fluctuation, a calibrated law for Z/W.
30 def __init__(self, n_w=18000, gh_order=32, seed=SEED):
31 rng = np.random.default_rng(seed)
32 self.w = rng.normal(size=(n_w, 3))
33 x, wt = roots_hermitenorm(gh_order)
34 self.z = x
35 self.wt = wt / np.sqrt(2*np.pi)
36
37 def entropy(self, lam):
38 lam = float(lam)
39 # q_i = E_z phi(z) prod_j Phi(-(z+lambda(wj-wi)))
40 q = np.zeros((len(self.w), 3))
41 for i in range(3):
42 delta = self.w - self.w[:, i:i+1]
43 # shape: triples, quadrature points, competitors
44 arg = self.z[None, :, None] + lam*delta[:, None, :]
45 surv = ndtr(-arg)
46 surv[:, :, i] = 1.0
47 q[:, i] = np.sum(self.wt[None, :] * np.prod(surv, axis=2), axis=1)
48 q = np.clip(q, 1e-14, 1.0)
49 return float(np.mean(-np.sum(q*np.log(q), axis=1)))
50
51 def table(self, grid):
52 return np.array([self.entropy(x) for x in grid])
53
54 def invert(self, h, grid, vals):
55 # Inversion only on a verified monotone interval; entropy decreases here.
56 order = np.argsort(vals)[::-1]
57 hh = np.asarray(vals)[order]
58 gg = np.asarray(grid)[order]
59 h = float(np.clip(h, hh[-1], hh[0]))
60 return float(np.interp(h, hh[::-1], gg[::-1]))
61
62def simulate_entropy(lam, n=120000, seed=SEED+1):
63 # This is the direct conditional entropy functional estimated by MC.
64 mc = MasterCurve(n_w=n, gh_order=24, seed=seed)
65 return mc.entropy(lam)
66
67def solve_s(lam_hat, d, tau):
68 base = np.sqrt(d)*tau
69 if base <= 0: return np.inf
70 if lam_hat <= base: return 0.0
71 return float(brentq(lambda s: base*A(s)-lam_hat, 0.0, max(2.0, lam_hat/base*2.0+1)))
72
73def run():
74 # Core math: curve, its validated local monotonicity, and exact amplification.
75 grid = np.linspace(0, 5, 26)
76 curve = MasterCurve(n_w=16000, gh_order=32)
77 hs = curve.table(grid)
78 diffs = np.diff(hs)
79 monotone_fraction = float(np.mean(diffs < 0))
80 # Compare predicted lambda_H with directly generated scores at several curvatures.
81 d, tau, mu = 32, 0.12, 2.0
82 rows = []
83 for s in [0.0, 0.5, 1.0, 2.0, 3.0]:
84 predicted = np.sqrt(d)*tau*A(s)
85 observed = simulate_entropy(predicted, n=30000, seed=SEED+int(10*s+1))
86 inferred = curve.invert(observed, grid, hs)
87 recovered_s = solve_s(inferred, d, tau)
88 rows.append(dict(s_true=s, lambda_pred=predicted, entropy=observed,
89 lambda_recovered=inferred, s_recovered=recovered_s))
90
91 # Controller experiment: entropy observations are noisy finite-sample measurements
92 # around the master curve, then curvature is updated by a slow EMA.
93 rng = np.random.default_rng(SEED+99)
94 controller = MasterCurve(n_w=20000, gh_order=32, seed=SEED+33)
95 # use dense calibration for inversion and stay in validated interval
96 cgrid = np.linspace(0, 5, 51); ch = controller.table(cgrid)
97 target_s = 1.5; target_lambda = np.sqrt(d)*tau*A(target_s)
98 target_h = controller.entropy(target_lambda)
99 starts = [0.15, 1.0, 4.0]
100 control = []
101 for k0 in starts:
102 k = k0
103 for step in range(20):
104 # standard error is deliberately representative of a small triplet batch
105 hobs = target_h + rng.normal(0, 0.012)
106 lamhat = controller.invert(hobs, cgrid, ch)
107 shat = solve_s(lamhat, d, tau)
108 khat = shat/mu
109 k = 0.8*k + 0.2*khat
110 control.append(dict(initial_kappa=k0, final_kappa=k,
111 target_kappa=target_s/mu,
112 abs_error=abs(k-target_s/mu)))
113
114 # Fixed-curvature controls are their initial values; report mean errors.
115 fixed_error = float(np.mean([abs(x-target_s/mu) for x in starts]))
116 adaptive_error = float(np.mean([x['abs_error'] for x in control]))
117 result = {
118 'seed': SEED, 'dimension': d, 'tau': tau, 'mu_R': mu,
119 'grid_entropy': [{'lambda':float(x), 'H':float(y)} for x,y in zip(grid,hs)],
120 'monotone_fraction': monotone_fraction,
121 'curvature_recovery': rows,
122 'controller': control,
123 'fixed_initial_mean_abs_kappa_error': fixed_error,
124 'adaptive_final_mean_abs_kappa_error': adaptive_error,
125 'claim_check': {'A_non_decreasing': bool(np.all(np.diff(A(grid)) >= -1e-12)),
126 'entropy_decreasing_fraction': monotone_fraction,
127 'entropy_range': [float(hs.min()), float(hs.max())]}
128 }
129 with open('results.json','w') as f: json.dump(result,f,indent=2)
130 print(json.dumps({'monotone_fraction':monotone_fraction,
131 'entropy_at_lambda_0':float(hs[0]), 'entropy_at_lambda_5':float(hs[-1]),
132 'fixed_mean_abs_kappa_error':fixed_error,
133 'adaptive_mean_abs_kappa_error':adaptive_error,
134 'controller':control}, indent=2))
135
136if __name__ == '__main__': run()
137
138# Separate robustness probe: angular scores with unequal variances violate the isotropic
139# master-curve assumption. We estimate the conditional entropy directly and invert
140# using the isotropic calibration table, without changing the fitted curve.
141def anisotropic_entropy(lam, scales=(1.0, 2.0, 0.5), n_w=12000, gh_order=24, seed=SEED+777):
142 rng = np.random.default_rng(seed)
143 w = rng.normal(size=(n_w, 3))
144 x, wt = roots_hermitenorm(gh_order)
145 wt = wt / np.sqrt(2*np.pi)
146 q = np.zeros((n_w, 3))
147 for i in range(3):
148 delta = w - w[:, i:i+1]
149 arg = x[None, :, None] + lam*delta[:, None, :] * np.asarray(scales)[None, None, :]
150 surv = ndtr(-arg)
151 surv[:, :, i] = 1.0
152 q[:, i] = np.sum(wt[None, :] * np.prod(surv, axis=2), axis=1)
153 q = np.clip(q, 1e-14, 1.0)
154 return float(np.mean(-np.sum(q*np.log(q), axis=1)))
155
156if __name__ == '__main__':
157 # The main experiment has already run above; append anisotropy metrics to JSON.
158 with open('results.json') as f: result = json.load(f)
159 grid = np.linspace(0, 5, 51)
160 iso = MasterCurve(n_w=20000, gh_order=32, seed=SEED+33)
161 vals = iso.table(grid)
162 anis = []
163 for lam in [0.5, 1.5, 3.0]:
164 h = anisotropic_entropy(lam)
165 inv = iso.invert(h, grid, vals)
166 anis.append({'lambda_true': lam, 'entropy_anisotropic': h,
167 'isotropic_inferred_lambda': inv,
168 'absolute_lambda_error': abs(inv-lam)})
169 result['anisotropy_probe'] = anis
170 with open('results.json','w') as f: json.dump(result,f,indent=2)
171 print(json.dumps({'anisotropy_probe': anis}, indent=2))