Ellipsoidal-Preserving Spherical Feature Stabilizer / experiment.py
Failed on benchmark
1import json
2import math
3import numpy as np
4try:
5 from scipy.special import sph_harm
6except ImportError:
7 from scipy.special import sph_harm_y
8 def sph_harm(m, n, phi, theta):
9 # New SciPy API takes (degree, order, polar, azimuth).
10 return sph_harm_y(n, m, theta, phi)
11
12from sklearn.linear_model import LogisticRegression
13from sklearn.model_selection import train_test_split
14from sklearn.metrics import accuracy_score
15
16SEED = 635
17rng = np.random.default_rng(SEED)
18
19
20def fibonacci_sphere(m):
21 # Nearly uniform quadrature points on S^2.
22 k = np.arange(m, dtype=float)
23 z = 1.0 - 2.0 * (k + 0.5) / m
24 phi = (math.pi * (3.0 - math.sqrt(5.0)) * k) % (2.0 * math.pi)
25 r = np.sqrt(np.maximum(0.0, 1.0 - z * z))
26 return np.column_stack((r * np.cos(phi), r * np.sin(phi), z))
27
28
29def real_harmonics(points, lmax):
30 # Real orthonormal spherical harmonics, evaluated in scipy's convention.
31 x, y, z = points.T
32 theta = np.arccos(np.clip(z, -1, 1))
33 phi = np.arctan2(y, x) % (2 * np.pi)
34 cols, degrees = [], []
35 for ell in range(lmax + 1):
36 for mm in range(-ell, ell + 1):
37 a = sph_harm(abs(mm), ell, phi, theta)
38 if mm < 0:
39 h = math.sqrt(2.0) * (-1 if mm % 2 else 1) * a.imag
40 elif mm == 0:
41 h = a.real
42 else:
43 h = math.sqrt(2.0) * (-1 if mm % 2 else 1) * a.real
44 cols.append(h)
45 degrees.append(ell)
46 return np.asarray(cols).T.real, np.asarray(degrees)
47
48
49def make_funk_matrix(points, neighbors=120):
50 # For each u, average samples closest to u-perpendicular (small |u.v|).
51 dots = np.abs(points @ points.T)
52 idx = np.argpartition(dots, neighbors, axis=1)[:, :neighbors]
53 A = np.zeros_like(dots)
54 rows = np.arange(len(points))[:, None]
55 # A local strip is a simple positive approximation to the great-circle average.
56 weights = np.exp(-(dots[rows, idx] / 0.055) ** 2)
57 weights /= weights.sum(axis=1, keepdims=True)
58 A[rows, idx] = weights
59 return A
60
61
62def projectors(H, degrees):
63 # Weighted-by-sampling least-squares projectors; Fibonacci weights are uniform.
64 p0 = degrees == 0
65 p2 = degrees == 2
66 B0, B2 = H[:, p0], H[:, p2]
67 P0 = B0 @ np.linalg.pinv(B0)
68 P2 = B2 @ np.linalg.pinv(B2)
69 return P0, P2
70
71
72def phi_squared(f, A):
73 # In n=3, c_3/kappa_2 = 1 and Phi(f)=R(f^2).
74 z = A @ (f ** 2)
75 return A @ (z ** 2)
76
77
78def rel_norm(x):
79 return float(np.sqrt(np.mean(x * x)))
80
81
82def diagnostic(points, A, H, degrees):
83 P0, P2 = projectors(H, degrees)
84 Pge4 = np.eye(len(points)) - P0 - P2
85 results = {}
86 # Small perturbations make the finite difference equal to the claimed derivative.
87 for ell in (2, 4, 6):
88 j = np.flatnonzero(degrees == ell)[0]
89 h = H[:, j] / rel_norm(H[:, j])
90 eps = 1e-3
91 out = (phi_squared(1.0 + eps * h, A) - 1.0) / eps
92 if ell == 2:
93 ratio = rel_norm(P2 @ out) / rel_norm(h)
94 results['degree2_multiplier'] = ratio
95 else:
96 ratio = rel_norm(Pge4 @ out) / rel_norm(h)
97 results[f'degree{ell}_multiplier'] = ratio
98 # Direct nonlinear test, with mixed ellipsoidal and high-frequency perturbation.
99 h2 = H[:, np.flatnonzero(degrees == 2)[2]] / rel_norm(H[:, np.flatnonzero(degrees == 2)[2]])
100 h4 = H[:, np.flatnonzero(degrees == 4)[1]] / rel_norm(H[:, np.flatnonzero(degrees == 4)[1]])
101 eps = 1e-2
102 before2, before4 = rel_norm(P2 @ (eps*h2)), rel_norm(Pge4 @ (eps*h4))
103 after = phi_squared(1 + eps*h2 + eps*h4, A) - 1
104 results['mixed_degree2_ratio'] = rel_norm(P2 @ after) / before2
105 results['mixed_high_ratio'] = rel_norm(Pge4 @ after) / before4
106 results['predicted_high_multiplier'] = 9.0 / 16.0
107 results['predicted_gap'] = 7.0 / 16.0
108 return results
109
110
111def toy_classification(points, A, H, degrees, n_samples=1200):
112 # Same tiny supervised task: infer the sign of an ellipsoidal (degree-2) coefficient
113 # amid high-frequency angular contamination. Compare raw features with the proposed map.
114 i2 = np.flatnonzero(degrees == 2)[2]
115 hi = np.flatnonzero(degrees >= 4)
116 X, Xmap, y = [], [], []
117 for _ in range(n_samples):
118 sign = 1 if rng.random() > 0.5 else -1
119 f = np.ones(len(points))
120 f += sign * 0.18 * H[:, i2] / rel_norm(H[:, i2])
121 coeff = rng.normal(0, 0.055, size=len(hi))
122 f += H[:, hi] @ coeff / math.sqrt(len(hi))
123 f = np.maximum(f, 0.03)
124 X.append(f - 1.0)
125 Xmap.append(phi_squared(f, A) - 1.0)
126 y.append(sign > 0)
127 X, Xmap, y = np.asarray(X), np.asarray(Xmap), np.asarray(y)
128 xa, xb, ya, yb = train_test_split(X, y, test_size=.3, random_state=SEED, stratify=y)
129 ma = LogisticRegression(C=1.0, max_iter=200, random_state=SEED).fit(xa, ya)
130 xma, xmb, _, _ = train_test_split(Xmap, y, test_size=.3, random_state=SEED, stratify=y)
131 # split indices above are independently random but identically seeded; labels align.
132 mm = LogisticRegression(C=1.0, max_iter=200, random_state=SEED).fit(xma, ya)
133 return {'raw_accuracy': float(accuracy_score(yb, ma.predict(xb))),
134 'idea_accuracy': float(accuracy_score(yb, mm.predict(xmb)))}
135
136
137def main():
138 m = 1400
139 points = fibonacci_sphere(m)
140 H, degrees = real_harmonics(points, 6)
141 A = make_funk_matrix(points)
142 d = diagnostic(points, A, H, degrees)
143 c = toy_classification(points, A, H, degrees)
144 out = {'seed': SEED, 'points': m, 'neighbor_count': 120,
145 'diagnostic': d, 'classification': c}
146 print(json.dumps(out, indent=2, sort_keys=True))
147
148
149if __name__ == '__main__':
150 main()