eisenstein_stack.py
10.5 kB · python · 360 lines
1from fractions import Fraction2from math import isqrt, lcm, log3from pathlib import Path45from sympy import N as N_6from sympy import Poly, Rational, Symbol, cos, cyclotomic_poly, minimal_polynomial, sin, sqrt7from sympy import pi as spi8from sympy import zeta as szeta910HERE = Path(__file__).resolve().parent1112X = Symbol("x")13CYC = 36014PHI360 = Poly(cyclotomic_poly(CYC, X), X, domain="QQ")15SPOT_DEGREES = [0, 30, 45, 60, 90, 120, 137, 180, 240, 270, 300, 359]16STACK_N = 5017COUNT_NS = [50, 200, 800, 1200, 2000, 3200]18CIRCLE_T = 40019CSL_M = 10020ROT_BOUND = 6021ROT_REACH = 2022FIG_N = 6023FIG_W = 90024FIG_H = 60025FIG_OUT = HERE / "eisenstein-stack.png"26ROOT3 = 3.0 ** 0.52728SURF = (255, 255, 255)29BLUE = (0, 140, 255)30GRAY = (186, 186, 191)31INK = (0, 0, 0)3233def enorm(z):34 a, b = z35 return a * a - a * b + b * b3637def emul(z, w):38 a, b = z39 c, d = w40 return (a * c - b * d, a * d + b * c - b * d)4142def econj(z):43 a, b = z44 return (a - b, -b)4546def edivides(w, z):47 p = emul(z, econj(w))48 n = enorm(w)49 return p[0] % n == 0 and p[1] % n == 05051def equo(z, w):52 p = emul(z, econj(w))53 n = enorm(w)54 return (p[0] // n, p[1] // n)5556def enearest(z, w):57 p = emul(z, econj(w))58 n = enorm(w)59 return ((2 * p[0] + n) // (2 * n), (2 * p[1] + n) // (2 * n))6061def egcd(z, w):62 while w != (0, 0):63 q = enearest(z, w)64 t = emul(q, w)65 z, w = w, (z[0] - t[0], z[1] - t[1])66 return z6768def associates(z):69 out = [z]70 for _ in range(2):71 a, b = out[-1]72 out.append((-b, a - b))73 return out + [(-a, -b) for a, b in out]7475def ecanon(z):76 if z == (0, 0):77 return z78 for c in associates(z):79 if c[0] > 0 and 0 <= c[1] < c[0]:80 return c81 return z8283def classes_up_to(n):84 out = set()85 r = isqrt(4 * n // 3) + 286 for a in range(-r, r + 1):87 for b in range(-r, r + 1):88 if (a, b) == (0, 0):89 continue90 if enorm((a, b)) <= n:91 out.add(ecanon((a, b)))92 return sorted(out, key=lambda z: (enorm(z), z))9394def hex_classes_closed(t):95 s = 096 j = 097 while 3 * j + 1 <= t:98 s += t // (3 * j + 1) - t // (3 * j + 2)99 j += 1100 return s101102def hex_classes_direct(t):103 return len(classes_up_to(t))104105def eisenstein_primes_dividing(z):106 n = enorm(z)107 rational = []108 m = n109 q = 2110 while q * q <= m:111 if m % q == 0:112 while m % q == 0:113 m //= q114 rational.append(q)115 q += 1116 if m > 1:117 rational.append(m)118 primes = []119 for q in rational:120 if q == 3:121 primes.append(ecanon((1, -1)))122 elif q % 3 == 2:123 primes.append((q, 0))124 else:125 r = 2126 root = None127 while root is None:128 c = pow(r, (q - 1) // 3, q)129 if (c * c + c + 1) % q == 0:130 root = c131 r += 1132 p = egcd((q, 0), (root, -1))133 primes.append(ecanon(p))134 primes.append(ecanon(econj(p)))135 return [p for p in primes if edivides(p, z)]136137def eisenstein_totient(z):138 v = enorm(z)139 if v == 1:140 return 1141 for p in eisenstein_primes_dividing(z):142 v = v // enorm(p) * (enorm(p) - 1)143 return v144145def totient_sum(n_max):146 return sum(eisenstein_totient(z) for z in classes_up_to(n_max))147148def layer_nodes(z):149 n = enorm(z)150 c = econj(z)151 out = set()152 for m in range(n):153 for k in range(n):154 p, q = emul((m, k), c)155 out.add((Fraction(p % n, n), Fraction(q % n, n)))156 return out157158def literal_stack(n_max):159 hits = {}160 for z in classes_up_to(n_max):161 for p in layer_nodes(z):162 hits[p] = hits.get(p, 0) + 1163 return hits164165def reduced_denominator(node):166 x, y = node167 d = lcm(x.denominator, y.denominator)168 u = (x.numerator * (d // x.denominator), y.numerator * (d // y.denominator))169 g = egcd(u, (d, 0))170 return ecanon(equo((d, 0), g))171172def closed_brightness(n_max, d):173 return hex_classes_closed(n_max // enorm(d))174175def cyc_reduce(terms):176 acc = {}177 for e, c in terms:178 k = (e % CYC,)179 acc[k] = acc.get(k, 0) + c180 acc = {k: v for k, v in acc.items() if v}181 if not acc:182 return Poly(0, X, domain="QQ")183 return Poly(acc, X, domain="QQ").rem(PHI360)184185def field_cos_sin(d):186 c = cyc_reduce([(d, 1), (-d, 1)])187 s = cyc_reduce([(d + 120, 1), (d + 240, -1), (-d + 120, -1), (-d + 240, 1)])188 return c.degree() <= 0, s.degree() <= 0189190def field_rotation_degrees():191 return [d for d in range(CYC) if all(field_cos_sin(d))]192193def spot_check_degrees():194 out = []195 for d in SPOT_DEGREES:196 c = minimal_polynomial(cos(spi * Rational(d, 180)), X, polys=True).degree()197 s = minimal_polynomial(sin(spi * Rational(d, 180)) / sqrt(3), X, polys=True).degree()198 out.append((d, c == 1, s == 1, field_cos_sin(d)))199 return out200201def rotation_hits(bound, reach):202 seen = set()203 for a in range(-reach, reach + 1):204 for b in range(-reach, reach + 1):205 if (a, b) == (0, 0):206 continue207 n = enorm((a, b))208 seen.add((Fraction(2 * a * a - 2 * a * b - b * b, 2 * n), Fraction(2 * a * b - b * b, 2 * n)))209 direct = set()210 for r in range(1, bound + 1):211 for p in range(-r, r + 1):212 rest = r * r - p * p213 if rest < 0 or rest % 3:214 continue215 s = isqrt(rest // 3)216 if 3 * s * s == rest:217 for t in (s, -s):218 direct.add((Fraction(p, r), Fraction(t, r)))219 return len(direct), direct <= seen220221def chi3(n):222 return (0, 1, -1)[n % 3]223224def dirichlet_mul(u, v, m):225 out = [0] * (m + 1)226 for i in range(1, m + 1):227 if u[i] == 0:228 continue229 for j in range(1, m // i + 1):230 out[i * j] += u[i] * v[j]231 return out232233def csl_zeta_ratio(m):234 a = [0] * (m + 1)235 for d in range(1, m + 1):236 c = chi3(d)237 if c:238 for n in range(d, m + 1, d):239 a[n] += c240 inv_square = [0] * (m + 1)241 for k in range(1, isqrt(m) + 1):242 f = k243 mu = 1244 q = 2245 while q * q <= f:246 if f % q == 0:247 f //= q248 if f % q == 0:249 mu = 0250 break251 mu = -mu252 q += 1253 if mu and f > 1:254 mu = -mu255 inv_square[k * k] = mu256 inv_three = [0] * (m + 1)257 e = 1258 j = 0259 while 3 ** j <= m:260 inv_three[3 ** j] = e261 e = -e262 j += 1263 return dirichlet_mul(dirichlet_mul(a, inv_square, m), inv_three, m)264265def csl_euler_product(m):266 out = [0] * (m + 1)267 out[1] = 1268 for q in range(2, m + 1):269 if q % 3 != 1 or any(q % r == 0 for r in range(2, isqrt(q) + 1)):270 continue271 local = [0] * (m + 1)272 local[1] = 1273 k = q274 while k <= m:275 local[k] = 2276 k *= q277 out = dirichlet_mul(out, local, m)278 return out279280def draw(n_max):281 from PIL import Image, ImageDraw282283 stack = literal_stack(n_max)284 top = max(stack.values())285 ss = 3286 w, h = FIG_W * ss, FIG_H * ss287 img = Image.new("RGB", (w, h), SURF)288 pen = ImageDraw.Draw(img)289 unit = 560.0 * ss290 ox = (w - 1.5 * unit) / 2 + unit / 2291 oy = (h + unit * ROOT3 / 2) / 2292 corners = [(0, 0), (1, 0), (1, 1), (0, 1)]293 place = lambda x, y: (ox + unit * (x - y / 2), oy - unit * y * ROOT3 / 2)294 pen.line([place(*c) for c in corners] + [place(0, 0)], fill=GRAY, width=2 * ss)295 for node, b in sorted(stack.items(), key=lambda kv: kv[1]):296 px, py = place(float(node[0]), float(node[1]))297 r = (1.4 + 7.0 * (b / top) ** 0.5) * ss298 pen.ellipse([px - r, py - r, px + r, py + r], fill=BLUE if b < top else INK)299 small = img.resize((FIG_W, FIG_H), Image.LANCZOS)300 small.convert("P", palette=Image.ADAPTIVE, colors=48).save(FIG_OUT, optimize=True)301 return len(stack), top302303def main():304 print("ring Z[omega], omega^2 = -1 - omega, norm a^2 - ab + b^2, units 6")305 print("coordinates in the basis 1, omega; fundamental domain the unit square of that chart")306307 rots = field_rotation_degrees()308 print("field rotation degrees", *rots)309 print("field rotation count", len(rots))310 spots = spot_check_degrees()311 print("spot degrees", len(spots))312 print("spot minpoly agrees with cyclotomic", all((c, s) == r for _, c, s, r in spots))313 hits, covered = rotation_hits(ROT_BOUND, ROT_REACH)314 print("rational rotations denominator <=", ROT_BOUND, hits)315 print("all are w/conj(w), Eisenstein w in box", ROT_REACH, covered)316317 print("hex circle count closed equals direct", all(hex_classes_closed(t) == hex_classes_direct(t) for t in range(CIRCLE_T + 1)))318 print("hex circle counts t=1..12", *[hex_classes_closed(t) for t in range(1, 13)])319320 stack = literal_stack(STACK_N)321 layers = classes_up_to(STACK_N)322 print("stack norm bound", STACK_N)323 print("stack layers", len(layers))324 print("stack nodes", len(stack))325 print("eisenstein totient sum", totient_sum(STACK_N))326 print("nodes equal totient sum", len(stack) == totient_sum(STACK_N))327 bad = 0328 for node, b in stack.items():329 if closed_brightness(STACK_N, reduced_denominator(node)) != b:330 bad += 1331 print("brightness comparisons", len(stack))332 print("brightness mismatches", bad)333 print("origin brightness", stack[(Fraction(0), Fraction(0))], "max", max(stack.values()))334335 lval = (szeta(2, Rational(1, 3)) - szeta(2, Rational(2, 3))) / 9336 z2 = spi ** 2 / 6337 rho = spi / (3 * sqrt(3))338 const = rho / (2 * z2 * lval)339 print("zeta(2)", "%.15f" % float(N_(z2, 30)))340 print("L(2, chi_-3)", "%.15f" % float(N_(lval, 30)))341 print("zeta_K(2) = zeta(2) L(2, chi_-3)", "%.15f" % float(N_(z2 * lval, 30)))342 print("residue of zeta_K at 1 = pi/(3 sqrt 3)", "%.15f" % float(N_(rho, 30)))343 c = float(N_(const, 30))344 print("constant pi/(6 sqrt3 zeta(2) L(2, chi_-3))", "%.15f" % c)345 for n in COUNT_NS:346 s = totient_sum(n)347 r = s / (n * n)348 print("node count N", n, "=", s, "ratio N^2", "%.6f" % r, "deviation", "%+.6f" % (r - c), "scaled by N/log N", "%+.3f" % ((r - c) * n / log(n)))349350 ratio = csl_zeta_ratio(CSL_M)351 euler = csl_euler_product(CSL_M)352 print("csl series bound", CSL_M)353 print("csl zeta ratio equals euler product", ratio == euler)354 print("csl nonzero coefficients", *[(n, ratio[n]) for n in range(1, CSL_M + 1) if ratio[n]])355356 nodes, top = draw(FIG_N)357 print("figure norm bound", FIG_N, "nodes", nodes, "brightest", top)358 print("figure bytes", FIG_OUT.stat().st_size)359360main()