Upload experiments/e8_theta_h4_decomposition.py with huggingface_hub
Browse files
experiments/e8_theta_h4_decomposition.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
E8 Theta Series Decomposition Under H4 Projection
|
| 4 |
+
|
| 5 |
+
The E8 theta function Theta_E8(q) = 1 + 240q + 2160q^2 + 6720q^3 + ...
|
| 6 |
+
counts lattice vectors at each squared norm (using norm^2/2 as index).
|
| 7 |
+
|
| 8 |
+
Under E8 = H4 + H4', each vector v decomposes as v = v_H4 + v_H4'.
|
| 9 |
+
Its norm splits: |v|^2 = |v_H4|^2 + |v_H4'|^2.
|
| 10 |
+
|
| 11 |
+
But H4 norms live in Q(sqrt(5)), not in Q. So the decomposition
|
| 12 |
+
produces a TWO-VARIABLE theta series over the golden ring Z[phi].
|
| 13 |
+
|
| 14 |
+
This connects E8 modular forms to HILBERT MODULAR FORMS of Q(sqrt(5)).
|
| 15 |
+
The explicit decomposition may reveal structure nobody has written down.
|
| 16 |
+
|
| 17 |
+
Strategy: enumerate E8 lattice vectors at norms 2,4,6,8,10,
|
| 18 |
+
project each to H4+H4', record the (norm_H4, norm_H4') pairs
|
| 19 |
+
in exact Q(sqrt(5)) arithmetic.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import os
|
| 23 |
+
import time
|
| 24 |
+
import json
|
| 25 |
+
from collections import Counter
|
| 26 |
+
from itertools import product as cartprod
|
| 27 |
+
|
| 28 |
+
os.environ["OMP_NUM_THREADS"] = "2"
|
| 29 |
+
|
| 30 |
+
print("=" * 65)
|
| 31 |
+
print(" E8 THETA SERIES: H4 DECOMPOSITION")
|
| 32 |
+
print(" Exact Q(sqrt(5)) arithmetic")
|
| 33 |
+
print("=" * 65)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# ββ Q(sqrt(5)) arithmetic ββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
# Represent a + b*sqrt(5) as (a, b) with a, b rational (we use
|
| 38 |
+
# integers * 4 to avoid fractions from the 2x-scaled E8 roots).
|
| 39 |
+
|
| 40 |
+
class QSqrt5:
|
| 41 |
+
"""Exact element of Q(sqrt(5)): a + b*sqrt(5), stored as (a, b)
|
| 42 |
+
with integer numerators (denominator tracked separately)."""
|
| 43 |
+
__slots__ = ('a', 'b')
|
| 44 |
+
|
| 45 |
+
def __init__(self, a=0, b=0):
|
| 46 |
+
self.a = a # rational part (integer)
|
| 47 |
+
self.b = b # sqrt(5) coefficient (integer)
|
| 48 |
+
|
| 49 |
+
def __add__(self, other):
|
| 50 |
+
return QSqrt5(self.a + other.a, self.b + other.b)
|
| 51 |
+
|
| 52 |
+
def __eq__(self, other):
|
| 53 |
+
return self.a == other.a and self.b == other.b
|
| 54 |
+
|
| 55 |
+
def __hash__(self):
|
| 56 |
+
return hash((self.a, self.b))
|
| 57 |
+
|
| 58 |
+
def __repr__(self):
|
| 59 |
+
if self.b == 0:
|
| 60 |
+
return f"{self.a}"
|
| 61 |
+
if self.a == 0:
|
| 62 |
+
return f"{self.b}*sqrt5"
|
| 63 |
+
return f"({self.a}+{self.b}*sqrt5)"
|
| 64 |
+
|
| 65 |
+
def approx(self):
|
| 66 |
+
return self.a + self.b * 2.2360679774997896 # sqrt(5)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ββ E8 lattice vector enumeration βββββββββββββββββββββββββββββββββ
|
| 70 |
+
|
| 71 |
+
def enumerate_e8_shell(norm_sq):
|
| 72 |
+
"""Enumerate all E8 lattice vectors with given squared norm.
|
| 73 |
+
|
| 74 |
+
E8 lattice (D8+ form): vectors are either
|
| 75 |
+
Type A: all integers, even sum
|
| 76 |
+
Type B: all half-integers, even sum
|
| 77 |
+
Squared norm = sum of squares.
|
| 78 |
+
|
| 79 |
+
We use 2x-scaling: actual coordinates * 2 -> all integers.
|
| 80 |
+
Scaled squared norm = 4 * actual squared norm.
|
| 81 |
+
So norm_sq=2 (actual) -> scaled_norm_sq=8.
|
| 82 |
+
"""
|
| 83 |
+
target = norm_sq * 4 # scaled
|
| 84 |
+
vectors = []
|
| 85 |
+
|
| 86 |
+
# Type A: coordinates are even integers (0, +-2, +-4, ...)
|
| 87 |
+
# Sum must be even (always true since all even)
|
| 88 |
+
# Sum of squares = target
|
| 89 |
+
_enumerate_type_a([], 8, target, vectors)
|
| 90 |
+
|
| 91 |
+
# Type B: coordinates are odd integers (+-1, +-3, +-5, ...)
|
| 92 |
+
# Sum must be even
|
| 93 |
+
_enumerate_type_b([], 8, target, vectors)
|
| 94 |
+
|
| 95 |
+
return vectors
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _enumerate_type_a(partial, remaining, target, results):
|
| 99 |
+
"""Enumerate 8D vectors with even integer coords, sum of squares = target."""
|
| 100 |
+
if remaining == 0:
|
| 101 |
+
if target == 0:
|
| 102 |
+
# Check even sum (always true for even integers, but verify)
|
| 103 |
+
s = sum(partial)
|
| 104 |
+
if s % 4 == 0: # in 2x scaling, "even sum" means sum divisible by 4
|
| 105 |
+
results.append(tuple(partial))
|
| 106 |
+
return
|
| 107 |
+
|
| 108 |
+
# Maximum absolute value for remaining coordinates
|
| 109 |
+
max_val = int(target ** 0.5)
|
| 110 |
+
# Only even values
|
| 111 |
+
for v in range(0, max_val + 1, 2):
|
| 112 |
+
v_sq = v * v
|
| 113 |
+
if v_sq > target:
|
| 114 |
+
break
|
| 115 |
+
if v == 0:
|
| 116 |
+
_enumerate_type_a(partial + [0], remaining - 1, target, results)
|
| 117 |
+
else:
|
| 118 |
+
for sign in [v, -v]:
|
| 119 |
+
_enumerate_type_a(partial + [sign], remaining - 1, target - v_sq, results)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _enumerate_type_b(partial, remaining, target, results):
|
| 123 |
+
"""Enumerate 8D vectors with odd integer coords, sum of squares = target."""
|
| 124 |
+
if remaining == 0:
|
| 125 |
+
if target == 0:
|
| 126 |
+
s = sum(partial)
|
| 127 |
+
if s % 4 == 0: # even sum condition in 2x scaling
|
| 128 |
+
results.append(tuple(partial))
|
| 129 |
+
return
|
| 130 |
+
|
| 131 |
+
max_val = int(target ** 0.5)
|
| 132 |
+
# Only odd values
|
| 133 |
+
for v in range(1, max_val + 1, 2):
|
| 134 |
+
v_sq = v * v
|
| 135 |
+
if v_sq > target:
|
| 136 |
+
break
|
| 137 |
+
for sign in [v, -v]:
|
| 138 |
+
_enumerate_type_b(partial + [sign], remaining - 1, target - v_sq, results)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ββ H4 projection with exact Q(sqrt(5)) arithmetic βββββββββββββββ
|
| 142 |
+
|
| 143 |
+
def project_exact(vec):
|
| 144 |
+
"""Project 8D vector to H4 using exact Q(sqrt(5)) arithmetic.
|
| 145 |
+
|
| 146 |
+
Projection: (x1,...,x8) -> (x1 + phi*x5, ..., x4 + phi*x8)
|
| 147 |
+
where phi = (1+sqrt(5))/2.
|
| 148 |
+
|
| 149 |
+
In our 2x-scaled coords, xi are integers.
|
| 150 |
+
Result: 4 components, each of form (a + b*sqrt(5)) where
|
| 151 |
+
a = 2*xi + xj (integer, from 2*xi + (1/2)*2*xj = 2*xi + xj)
|
| 152 |
+
Wait, let me be careful.
|
| 153 |
+
|
| 154 |
+
phi = (1+sqrt(5))/2
|
| 155 |
+
Component i: vec[i] + phi * vec[i+4]
|
| 156 |
+
= vec[i] + (1+sqrt(5))/2 * vec[i+4]
|
| 157 |
+
= vec[i] + vec[i+4]/2 + vec[i+4]*sqrt(5)/2
|
| 158 |
+
|
| 159 |
+
In 2x-scaled: actual vec[i] = scaled[i]/2
|
| 160 |
+
So: scaled[i]/2 + phi * scaled[i+4]/2
|
| 161 |
+
= scaled[i]/2 + (1+sqrt(5))/2 * scaled[i+4]/2
|
| 162 |
+
= (scaled[i] + scaled[i+4])/4 + scaled[i+4]*sqrt(5)/4
|
| 163 |
+
|
| 164 |
+
To stay in integers, multiply everything by 4:
|
| 165 |
+
4 * component_i = (scaled[i] + scaled[i+4]) + scaled[i+4]*sqrt(5)
|
| 166 |
+
|
| 167 |
+
So norm in Q(sqrt(5)):
|
| 168 |
+
|4*comp_i|^2 = (a + b*sqrt(5))^2 = a^2 + 5b^2 + 2ab*sqrt(5)
|
| 169 |
+
Total 4^2 * |proj|^2 = sum_i (a_i^2 + 5*b_i^2) + sqrt(5)*sum_i(2*a_i*b_i)
|
| 170 |
+
"""
|
| 171 |
+
components = []
|
| 172 |
+
for i in range(4):
|
| 173 |
+
a = vec[i] + vec[i + 4] # rational part * 4
|
| 174 |
+
b = vec[i + 4] # sqrt(5) part * 4
|
| 175 |
+
components.append((a, b))
|
| 176 |
+
|
| 177 |
+
# Compute |proj|^2 * 16 in Q(sqrt(5))
|
| 178 |
+
rat_part = 0
|
| 179 |
+
sqrt5_part = 0
|
| 180 |
+
for a, b in components:
|
| 181 |
+
rat_part += a * a + 5 * b * b
|
| 182 |
+
sqrt5_part += 2 * a * b
|
| 183 |
+
|
| 184 |
+
return QSqrt5(rat_part, sqrt5_part), components
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def project_conjugate_exact(vec):
|
| 188 |
+
"""Project to H4' using phi_bar = (1-sqrt(5))/2.
|
| 189 |
+
|
| 190 |
+
4 * component_i = (scaled[i] + scaled[i+4]) - scaled[i+4]*sqrt(5)
|
| 191 |
+
-> just negate the sqrt(5) coefficient.
|
| 192 |
+
"""
|
| 193 |
+
rat_part = 0
|
| 194 |
+
sqrt5_part = 0
|
| 195 |
+
for i in range(4):
|
| 196 |
+
a = vec[i] + vec[i + 4]
|
| 197 |
+
b = -vec[i + 4] # negated!
|
| 198 |
+
rat_part += a * a + 5 * b * b
|
| 199 |
+
sqrt5_part += 2 * a * b
|
| 200 |
+
|
| 201 |
+
return QSqrt5(rat_part, sqrt5_part)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# ββ Main computation ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 205 |
+
|
| 206 |
+
# Known theta series coefficients: vectors at norm^2 = 2n
|
| 207 |
+
# a(n) for E8: 1, 240, 2160, 6720, 17520, 30240, 60480, 82560, ...
|
| 208 |
+
expected_counts = {
|
| 209 |
+
1: 240,
|
| 210 |
+
2: 2160,
|
| 211 |
+
3: 6720,
|
| 212 |
+
4: 17520,
|
| 213 |
+
5: 30240,
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
all_decompositions = {}
|
| 217 |
+
|
| 218 |
+
for shell in range(1, 6):
|
| 219 |
+
norm_sq = 2 * shell
|
| 220 |
+
|
| 221 |
+
print(f"\n--- Shell {shell}: norm^2 = {norm_sq} ---")
|
| 222 |
+
t0 = time.time()
|
| 223 |
+
vectors = enumerate_e8_shell(norm_sq)
|
| 224 |
+
elapsed = time.time() - t0
|
| 225 |
+
print(f" Found {len(vectors)} vectors ({elapsed:.1f}s)")
|
| 226 |
+
|
| 227 |
+
if shell in expected_counts:
|
| 228 |
+
expected = expected_counts[shell]
|
| 229 |
+
status = "OK" if len(vectors) == expected else f"MISMATCH (expected {expected})"
|
| 230 |
+
print(f" Expected: {expected} -> {status}")
|
| 231 |
+
|
| 232 |
+
if len(vectors) == 0:
|
| 233 |
+
continue
|
| 234 |
+
|
| 235 |
+
# Project each vector and record (norm_H4, norm_H4') in Q(sqrt(5))
|
| 236 |
+
decomp = Counter() # (norm_h4, norm_h4_bar) -> count
|
| 237 |
+
for v in vectors:
|
| 238 |
+
nh4, _ = project_exact(v)
|
| 239 |
+
nh4bar = project_conjugate_exact(v)
|
| 240 |
+
decomp[(nh4, nh4bar)] += 1
|
| 241 |
+
|
| 242 |
+
print(f" Distinct (norm_H4, norm_H4') pairs: {len(decomp)}")
|
| 243 |
+
print(f" Decomposition:")
|
| 244 |
+
for (nh4, nh4bar), count in sorted(decomp.items(),
|
| 245 |
+
key=lambda x: x[0][0].approx()):
|
| 246 |
+
# The two norms should sum to the original norm * 16
|
| 247 |
+
total = QSqrt5(nh4.a + nh4bar.a, nh4.b + nh4bar.b)
|
| 248 |
+
print(f" H4={nh4!r:>20s} H4'={nh4bar!r:>20s} "
|
| 249 |
+
f"sum={total!r:>15s} count={count:>5d}")
|
| 250 |
+
|
| 251 |
+
all_decompositions[shell] = {
|
| 252 |
+
str((str(k[0]), str(k[1]))): v
|
| 253 |
+
for k, v in decomp.items()
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
# Safety: don't run too long
|
| 257 |
+
if time.time() - t0 > 300: # 5 min max per shell
|
| 258 |
+
print(" Time limit reached, stopping")
|
| 259 |
+
break
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
# ββ Analysis: look for patterns βββββββββββββββββββββββββββββββββββ
|
| 263 |
+
|
| 264 |
+
print(f"\n\n" + "=" * 65)
|
| 265 |
+
print(f" PATTERN ANALYSIS")
|
| 266 |
+
print(f"=" * 65)
|
| 267 |
+
|
| 268 |
+
print(f"""
|
| 269 |
+
The norm decomposition n = n_H4 + n_H4' lives in Q(sqrt(5)).
|
| 270 |
+
If the counts at each (n_H4, n_H4') pair follow a pattern
|
| 271 |
+
related to Hilbert modular forms of Q(sqrt(5)), that would
|
| 272 |
+
connect E8 theta series to the arithmetic of the golden field.
|
| 273 |
+
|
| 274 |
+
Key question: do the counts factorize? I.e., is
|
| 275 |
+
#{'{'}v : |v_H4|^2=a, |v_H4'|^2=b{'}'} = f(a) * g(b)
|
| 276 |
+
for some functions f, g? If yes, Theta_E8 = Theta_H4 * Theta_H4'
|
| 277 |
+
as Hilbert modular forms. If no, there's a non-trivial mixing term.
|
| 278 |
+
""")
|
| 279 |
+
|
| 280 |
+
# Save results
|
| 281 |
+
out_path = os.path.join(os.path.dirname(__file__), "e8_theta_decomp.json")
|
| 282 |
+
with open(out_path, "w") as f:
|
| 283 |
+
json.dump(all_decompositions, f, indent=2)
|
| 284 |
+
print(f"Results saved to {out_path}")
|