shell.py

6.6 kB · python · 204 lines

1from fractions import Fraction2from math import isqrt3import mpmath4import numpy as np5import sympy as sp67N2 = 2000008N_ROT = 60009DPS = 3010EXPONENTS = (2, 3, 4)1112def parity_classes(limit):13    bound = isqrt(limit)14    keys = []15    for a in range(-bound, bound + 1):16        square = a * a17        inner = isqrt(limit - square)18        b = np.arange(-inner, inner + 1, dtype=np.int64)19        norms = square + b * b20        slot = (a & 1) * 2 + (b & 1)21        keys.append(slot * (limit + 1) + norms)22    flat = np.bincount(np.concatenate(keys), minlength=4 * (limit + 1))23    return flat.reshape(4, limit + 1).astype(np.int64)2425def jacobi_r2(limit):26    out = np.zeros(limit + 1, dtype=np.int64)27    for d in range(1, limit + 1, 4):28        out[d::d] += 429    for d in range(3, limit + 1, 4):30        out[d::d] -= 431    return out3233def shifted(source, step, limit):34    out = np.zeros(limit + 1, dtype=np.int64)35    out[::step] = source[: limit // step + 1]36    return out3738def doubling_identity(cls, total, limit):39    predicted = shifted(total, 4, limit)40    return int(np.count_nonzero(cls[0][1:] != predicted[1:]))4142def rotation_identity(cls, limit):43    mixed = cls[1] + cls[2]44    predicted = shifted(mixed, 2, limit)45    return int(np.count_nonzero(cls[3][1:] != predicted[1:]))4647def swap_identity(cls):48    return int(np.count_nonzero(cls[1][1:] != cls[2][1:]))4950def odd_odd_points(norm):51    out = []52    bound = isqrt(norm)53    for i in range(-bound, bound + 1):54        if i % 2 == 0:55            continue56        rest = norm - i * i57        if rest < 0:58            continue59        j = isqrt(rest)60        if j * j != rest or j % 2 == 0:61            continue62        out.append((i, j))63        out.append((i, -j))64    return out6566def rotation_as_a_map(cls, limit):67    moved = 068    faults = 069    for norm in range(2, limit + 1, 2):70        points = odd_odd_points(norm)71        images = set()72        for i, j in points:73            u = (i + j) // 274            v = (i - j) // 275            if u * u + v * v != norm // 2 or (u & 1) == (v & 1):76                faults += 177            images.add((u, v))78        moved += len(points)79        if len(images) != len(points) or len(points) != int(cls[3][norm]):80            faults += 181        half = norm // 282        if len(images) != int(cls[1][half]) + int(cls[2][half]):83            faults += 184    return moved, faults8586def pinned_polynomial():87    t, whole = sp.symbols("t S")88    ee, eo, oe, oo = sp.symbols("S_ee S_eo S_oe S_oo")89    solution = sp.solve(90        [91            ee - t**2 * whole,92            oo - t * (eo + oe),93            eo - oe,94            ee + eo + oe + oo - whole,95        ],96        [ee, eo, oe, oo],97        dict=True,98    )[0]99    a_ee, a_eo, a_oe, a_oo = sp.symbols("a_ee a_eo a_oe a_oo")100    filled = (101        a_ee * solution[ee]102        + a_eo * solution[eo]103        + a_oe * solution[oe]104        + a_oo * solution[oo]105    )106    claimed = whole * (107        a_ee * t**2 + a_oo * t * (1 - t) + (a_eo + a_oe) * (1 - t) / 2108    )109    return sp.simplify(filled - claimed) == 0110111def dirichlet_beta(s):112    return (mpmath.zeta(s, mpmath.mpf(1) / 4) - mpmath.zeta(s, mpmath.mpf(3) / 4)) / 4**s113114def shell_sums(cls, total, limit):115    live = np.nonzero(total[: limit + 1])[0]116    live = live[live > 0]117    totals = {s: [mpmath.mpf(0)] * 4 for s in EXPONENTS}118    columns = [cls[slot] for slot in range(4)]119    for norm in live.tolist():120        base = mpmath.mpf(norm)121        powers = {2: base * base}122        powers[3] = powers[2] * base123        powers[4] = powers[3] * base124        for slot in range(4):125            weight = int(columns[slot][norm])126            if weight == 0:127                continue128            value = mpmath.mpf(weight)129            for s in EXPONENTS:130                totals[s][slot] += value / powers[s]131    return totals132133def indicators(code):134    return [(code >> k) & 1 for k in range(4)]135136def q_polynomial(a, s):137    t = Fraction(1, 2**s)138    return a[0] * t * t + a[3] * t * (1 - t) + Fraction(a[1] + a[2], 2) * (1 - t)139140def to_mpf(value):141    return mpmath.mpf(value.numerator) / mpmath.mpf(value.denominator)142143def closed_form_gaps(totals, limit):144    zeta = {s: mpmath.zeta(s) for s in EXPONENTS}145    beta = {s: dirichlet_beta(s) for s in EXPONENTS}146    worst = {}147    for s in EXPONENTS:148        biggest = mpmath.mpf(0)149        for code in range(1, 16):150            a = indicators(code)151            predicted = 4 * zeta[s] * beta[s] * to_mpf(q_polynomial(a, s))152            measured = mpmath.fsum(153                totals[s][slot] for slot in range(4) if a[slot]154            )155            tail = mpmath.mpf(0)156            if s == 2:157                tail = sum(a) * mpmath.pi / (4 * limit)158            gap = abs(measured + tail - predicted)159            biggest = max(biggest, gap)160        worst[s] = biggest161    return worst162163def sci(value):164    return mpmath.nstr(value, 2, strip_zeros=False)165166def main():167    mpmath.mp.dps = DPS168    cls = parity_classes(N2)169    total = cls.sum(axis=0)170    print(f"DOMAIN  parity classes of Z^2 to n = {N2}, rotation as a map to n = {N_ROT}")171    print()172    print("JACOBI")173    jacobi = jacobi_r2(N2)174    bad = int(np.count_nonzero(total[1:] != jacobi[1:]))175    print(f"  r2(n) = 4 (d1(n) - d3(n))            mismatches {bad} of {N2}")176    print()177    print("THE THREE SHELL RELATIONS AS INTEGER IDENTITIES ON THE COEFFICIENTS")178    bad_ee = doubling_identity(cls, total, N2)179    bad_oo = rotation_identity(cls, N2)180    bad_eo = swap_identity(cls)181    print(f"  S_ee = t^2 S    r2_ee(n) = r2(n/4)    mismatches {bad_ee} of {N2}")182    print(f"  S_oo = t S_mix  r2_oo(n) = r2_mix(n/2) mismatches {bad_oo} of {N2}")183    print(f"  S_eo = S_oe     r2_eo(n) = r2_oe(n)   mismatches {bad_eo} of {N2}")184    print()185    print("THE 45 DEGREE ROTATION AS A MAP")186    moved, faults = rotation_as_a_map(cls, N_ROT)187    print(f"  (i,j) -> ((i+j)/2, (i-j)/2) on every odd-odd point of even norm to n = {N_ROT}")188    print(f"  odd-odd points transported {moved}, faults {faults}")189    print()190    print("THE THREE RELATIONS PIN Q_c")191    print(f"  solving the four linear relations returns Q_c exactly: {pinned_polynomial()}")192    print()193    print("SELF TEST OF THE BETA VALUES")194    print(f"  beta(2) against Catalan   {sci(abs(dirichlet_beta(2) - mpmath.catalan))}")195    print(f"  beta(3) against pi^3/32   {sci(abs(dirichlet_beta(3) - mpmath.pi**3 / 32))}")196    print()197    print("THE CLOSED FORM AGAINST TRUNCATED LATTICE SUMS")198    totals = shell_sums(cls, total, N2)199    worst = closed_form_gaps(totals, N2)200    for s in EXPONENTS:201        note = " after the leading tail k pi / (4N)" if s == 2 else ""202        print(f"  s = {s}  worst gap over the 15 nonempty designs {sci(worst[s])}{note}")203204main()