Proximal Spherical Cubic Step / proximal_cubic.py
Mechanism confirmed, baseline not beaten
1import json
2import numpy as np
3
4
5def cubic_tensor(c, g, H, C):
6 """Return A such that A[(1,s)]^3 = c+g.s+.5*s'Hs+C[s,s,s]/6."""
7 g, H, C = np.asarray(g), np.asarray(H), np.asarray(C)
8 n = len(g)
9 A = np.zeros((n + 1,) * 3, dtype=float)
10 A[0, 0, 0] = c
11 for i in range(n):
12 for p in ((0, 0, i + 1), (0, i + 1, 0), (i + 1, 0, 0)):
13 A[p] = g[i] / 3.0
14 for i in range(n):
15 for j in range(n):
16 # Three placements of the zero index sum to (1/2) H_ij s_i s_j.
17 for p in ((0, i + 1, j + 1), (i + 1, 0, j + 1),
18 (i + 1, j + 1, 0)):
19 A[p] = (0.5 * H[i, j]) / 3.0
20 A[1:, 1:, 1:] = C / 6.0
21 return A
22
23
24def model(c, g, H, C, s):
25 return c + g @ s + .5 * s @ H @ s + np.einsum('ijk,i,j,k', C, s, s, s) / 6.
26
27
28def contract(A, u, v):
29 return np.einsum('ijk,j,k->i', A, u, v)
30
31
32def pam(A, sweeps=30, beta=0.8, seed=0):
33 rng = np.random.default_rng(seed)
34 d = A.shape[0]
35 U = [rng.normal(size=d) for _ in range(3)]
36 U = [u / np.linalg.norm(u) for u in U]
37 history = []
38 def F():
39 return np.einsum('ijk,i,j,k', A, *U)
40 for _ in range(sweeps):
41 for block in range(3):
42 old = U[block].copy()
43 before = F() # proximal term is zero at the old block
44 others = [U[i] for i in range(3)]
45 a = contract(A, others[(block + 1) % 3], others[(block + 2) % 3])
46 z = beta * old - a
47 if np.linalg.norm(z) < 1e-14:
48 continue
49 U[block] = z / np.linalg.norm(z)
50 after = F() + .5 * beta * np.sum((U[block] - old) ** 2)
51 history.append((after - before, before, after))
52 return U, np.asarray(history)
53
54
55def run():
56 rng = np.random.default_rng(12)
57 n = 4
58 c = .37
59 g = rng.normal(size=n)
60 H = rng.normal(size=(n, n)); H = (H + H.T) / 2
61 C0 = rng.normal(size=(n, n, n))
62 C = sum(C0.transpose(p) for p in ((0,1,2),(1,0,2),(2,1,0),(0,2,1),(1,2,0),(2,0,1))) / 6
63 A = cubic_tensor(c, g, H, C)
64
65 # Prediction 1: exact homogeneous representation.
66 rep_errors = []
67 for _ in range(1000):
68 s = rng.normal(size=n)
69 rep_errors.append(abs(np.einsum('ijk,i,j,k', A, np.r_[1., s], np.r_[1., s], np.r_[1., s]) - model(c, g, H, C, s)))
70 representation_max_error = float(max(rep_errors))
71
72 # Prediction 2 sweep: exact block solves never increase their proximal objective,
73 # for every beta; larger beta is the predicted stronger proximal stabilization.
74 beta_sweep = {}
75 for beta in (0.0, 0.1, 0.8, 5.0):
76 _, ch = pam(A, sweeps=40, beta=beta, seed=4)
77 beta_sweep[str(beta)] = {'max_increase': float(np.max(ch[:, 0])),
78 'final_objective': float(ch[-1, 2])}
79 max_block_increase = beta_sweep['0.8']['max_increase']
80 final_F = beta_sweep['0.8']['final_objective']
81
82 # Prediction 3 sweep: for a quartic mismatch q*r^4/4, rho decreases with
83 # radius and its acceptance boundary is the positive root of rho=eta.
84 sdir = -g / np.linalg.norm(g)
85 eta = .25
86 radii = np.geomspace(.03, 2.0, 400)
87 aa = -(g @ sdir); bb = -.5 * (sdir @ H @ sdir)
88 dd = -np.einsum('ijk,i,j,k', C, sdir, sdir, sdir) / 6
89 q_sweep = {}
90 for q in (.2, .8, 1.7, 4.0):
91 observed = []
92 for r in radii:
93 dec = aa*r + bb*r*r + dd*r**3
94 rho = (dec - q*r**4/4) / dec
95 observed.append(rho >= eta)
96 roots = np.roots([q/4, -(1-eta)*dd, -(1-eta)*bb, -(1-eta)*aa])
97 pos = sorted(float(z.real) for z in roots if abs(z.imag)<1e-8 and z.real>1e-12)
98 rstar = pos[0] if pos else float('nan')
99 accepted = np.where(np.asarray(observed))[0]
100 observed_boundary = float(radii[accepted[-1]]) if len(accepted) else 0.0
101 q_sweep[str(q)] = {'predicted_radius': rstar,
102 'observed_grid_boundary': observed_boundary,
103 'relative_grid_error': abs(observed_boundary-rstar)/rstar,
104 'pattern_matches': bool(np.all(np.asarray(observed)==(radii<=rstar)))}
105 rstar = q_sweep['1.7']['predicted_radius']
106 first_rejected = float(radii[np.where(~np.asarray([((aa*r+bb*r*r+dd*r**3)-1.7*r**4/4)/(aa*r+bb*r*r+dd*r**3)>=eta for r in radii]))[0][0]])
107 transition_matches = q_sweep['1.7']['pattern_matches']
108
109 # Secondary same-objective comparison: fixed-step gradient descent vs guarded cubic-direction steps.
110 def f(x): return model(c, g, H, C, x) + q * np.sum(x**2)**2 / 4
111 def gradient(x): return g + H @ x + .5 * np.einsum('ijk,j,k->i', C, x, x) + q * np.sum(x*x) * x
112 def safeguarded(x, radius):
113 gg = gradient(x); ss = radius * (-gg / (np.linalg.norm(gg) + 1e-12))
114 if model(c, g, H, C, x + ss) <= model(c, g, H, C, x) and f(x + ss) < f(x): return x + ss, True
115 return x, False
116 xg = np.zeros(n); xs = np.zeros(n); lr = .08; rejects = 0
117 for _ in range(80):
118 xg -= lr * gradient(xg)
119 xs, ok = safeguarded(xs, .18); rejects += int(not ok)
120 return {'prediction_1_representation_max_error': representation_max_error,
121 'prediction_2_max_proximal_block_increase': max_block_increase,
122 'prediction_2_final_recorded_block_objective': final_F,
123 'prediction_2_beta_sweep': beta_sweep,
124 'prediction_3_eta': eta, 'prediction_3_quartic_q': 1.7,
125 'prediction_3_predicted_transition_radius': rstar,
126 'prediction_3_first_rejected_grid_radius': first_rejected,
127 'prediction_3_quartic_strength_sweep': q_sweep,
128 'prediction_3_acceptance_patterns_match': transition_matches,
129 'mini_final_objective_gradient': float(f(xg)),
130 'mini_final_objective_safeguarded': float(f(xs)),
131 'mini_safeguarded_rejections': rejects, 'mini_steps': 80}
132
133if __name__ == '__main__':
134 result = run()
135 with open('results.json', 'w') as fp: json.dump(result, fp, indent=2)
136 print(json.dumps(result, indent=2))