smoothed_novelty.py
7.7 kB · python · 198 lines
1import math2import subprocess3import time4import numpy as np56N = 30_000_0007JMIN = 8.08JMAX = 23.59PER_OCTAVE = 1610ZERO_HEIGHT = 30011PI2 = math.pi ** 212T0 = time.time()1314def clock(label):15 print(f"[{time.time() - T0:6.1f}s] {label}")1617def totients(n):18 phi = np.arange(n + 1, dtype=np.int32)19 rem = np.arange(n + 1, dtype=np.int32)20 r = math.isqrt(n)21 small = np.ones(r + 1, dtype=bool)22 small[:2] = False23 for p in range(2, math.isqrt(r) + 1):24 if small[p]:25 small[p * p::p] = False26 for p in np.flatnonzero(small):27 p = int(p)28 phi[p::p] -= phi[p::p] // p29 pk = p30 while pk <= n:31 rem[pk::pk] //= p32 pk *= p33 big = rem > 134 phi[big] -= phi[big] // rem[big]35 return phi3637def totients_brute(n):38 return np.array([sum(1 for a in range(1, k + 1) if math.gcd(a, k) == 1) for k in range(n + 1)])3940def totient_sum_mobius(x):41 mu = np.ones(x + 1, dtype=np.int64)42 mu[0] = 043 prime = np.ones(x + 1, dtype=bool)44 prime[:2] = False45 for p in range(2, x + 1):46 if prime[p]:47 prime[p * p::p] = False48 mu[p::p] *= -149 mu[p * p::p * p] = 050 total = 051 for d in range(1, x + 1):52 if mu[d]:53 m = x // d54 total += int(mu[d]) * m * (m + 1) // 255 return total5657def bump_c2(u):58 v = (u - 1.0) * (2.0 - u)59 return np.where((u > 1.0) & (u < 2.0), 64.0 * v ** 3, 0.0)6061def bump_cinf(u):62 v = (u - 1.0) * (2.0 - u)63 inside = (u > 1.0) & (u < 2.0)64 safe = np.where(inside, v, 1.0)65 return np.where(inside, np.exp(4.0 - 1.0 / safe), 0.0)6667C2_POLY = 64.0 * np.polynomial.polynomial.polypow([-2.0, 3.0, -1.0], 3)6869def mellin_c2(s):70 s = np.asarray(s, dtype=complex)71 total = np.zeros_like(s)72 for k, a in enumerate(C2_POLY):73 total += a * (2.0 ** (s + k) - 1.0) / (s + k)74 return total7576GL_X, GL_W = np.polynomial.legendre.leggauss(2000)77GL_U = 1.5 + 0.5 * GL_X78GL_F = bump_cinf(GL_U) * 0.5 * GL_W7980def mellin_cinf(s):81 s = np.asarray(s, dtype=complex)82 return np.array([np.sum(GL_F * np.exp((z - 1.0) * np.log(GL_U))) for z in s.ravel()]).reshape(s.shape)8384def zeros_from_pari(height):85 script = f"""default(realprecision, 30);86z = lfunzeros(1, {height});87for(k = 1, #z, r = 1/2 + I*z[k]; a = zeta(r - 1); b = lfun(1, r, 1); print(z[k], " ", real(a), " ", imag(a), " ", real(b), " ", imag(b)))88"""89 out = subprocess.run(["gp", "-q"], input=script, capture_output=True, text=True, check=True).stdout90 rows = np.array([[float(t) for t in line.split()] for line in out.strip().splitlines()])91 gamma = rows[:, 0]92 zeta_left = rows[:, 1] + 1j * rows[:, 2]93 zeta_prime = rows[:, 3] + 1j * rows[:, 4]94 return gamma, zeta_left, zeta_prime9596def smoothed_sum(phi, y, bump):97 lo = math.ceil(1.0 / y)98 hi = math.floor(2.0 / y)99 n = np.arange(lo, hi + 1, dtype=np.float64)100 return float(np.sum(phi[lo:hi + 1].astype(np.float64) * bump(n * y)))101102def indicator_sum(prefix, y):103 lo = math.ceil(1.0 / y)104 hi = math.floor(2.0 / y)105 return int(prefix[hi] - prefix[lo - 1])106107def fit(logq, logerr):108 a, b = np.polyfit(logq, logerr, 1)109 resid = logerr - (a * logq + b)110 return a, float(np.sqrt(np.mean(resid ** 2)))111112def octave_rms(j, err):113 octaves = np.floor(j).astype(int)114 rows = []115 for k in sorted(set(octaves)):116 sel = octaves == k117 if sel.sum() >= PER_OCTAVE // 2:118 rows.append((k, float(np.sqrt(np.mean(err[sel] ** 2)))))119 return np.array(rows)120121def slopes(name, j, err):122 logq = -2.0 * j * math.log(2.0)123 rows = octave_rms(j, err)124 ok = np.abs(err) > 0125 a_all, r_all = fit(logq[ok], np.log(np.abs(err[ok])))126 lq = -2.0 * (rows[:, 0] + 0.5) * math.log(2.0)127 le = np.log(rows[:, 1])128 half = len(rows) // 2129 a_lo, r_lo = fit(lq[:half], le[:half])130 a_hi, r_hi = fit(lq[half:], le[half:])131 a_oct, r_oct = fit(lq, le)132 print(f"{name}: octave rms slope in q {a_oct:.4f} (residual {r_oct:.3f}) over j in [{rows[0, 0]:.0f}, {rows[-1, 0] + 1:.0f}]")133 print(f"{name}: lower window j in [{rows[0, 0]:.0f}, {rows[half - 1, 0] + 1:.0f}] slope {a_lo:.4f} (residual {r_lo:.3f}); upper window j in [{rows[half, 0]:.0f}, {rows[-1, 0] + 1:.0f}] slope {a_hi:.4f} (residual {r_hi:.3f})")134 print(f"{name}: all-sample slope of log|E| in q {a_all:.4f} (residual {r_all:.3f}) on {int(ok.sum())} samples")135 return a_oct136137def main():138 clock(f"sieve to {N}")139 phi = totients(N)140 prefix = np.cumsum(phi, dtype=np.int64)141 clock("sieve done")142 brute = totients_brute(2000)143 print(f"brute-force totients agree to 2000: {bool(np.array_equal(brute, phi[:2001]))}")144 x = 10 ** 6145 print(f"totient sum at {x}: sieve {int(prefix[x])}, mobius route {totient_sum_mobius(x)}, equal {int(prefix[x]) == totient_sum_mobius(x)}")146 j = np.arange(JMIN, JMAX + 1e-9, 1.0 / PER_OCTAVE)147 y = 2.0 ** (-j)148 c_ind = 6.0 / PI2 * 1.5149 c_c2 = 6.0 / PI2 * (24.0 / 35.0)150 f2_cinf = float(np.sum(GL_F * GL_U))151 c_cinf = 6.0 / PI2 * f2_cinf152 print(f"mellin transforms at 2: indicator 3/2, c2 bump 24/35 = {24 / 35:.12f} (closed form {mellin_c2(2.0).real:.12f}), cinf bump {f2_cinf:.12f}")153 e_ind = np.array([yy * yy * indicator_sum(prefix, yy) - c_ind for yy in y])154 clock("indicator sums done")155 e_c2 = np.array([yy * yy * smoothed_sum(phi, yy, bump_c2) - c_c2 for yy in y])156 clock("c2 bump sums done")157 e_cinf = np.array([yy * yy * smoothed_sum(phi, yy, bump_cinf) - c_cinf for yy in y])158 clock("cinf bump sums done")159 yy = y[-1]160 lo = math.ceil(1.0 / yy)161 hi = math.floor(2.0 / yy)162 n = np.arange(lo, hi + 1, dtype=np.float64)163 prod = phi[lo:hi + 1].astype(np.float64) * bump_cinf(n * yy)164 noise = abs(float(np.sum(prod)) - math.fsum(prod.tolist())) * yy * yy165 print(f"summation noise at j = {JMAX}: |pairwise - fsum| scaled by y^2 is {noise:.3e} against |E_cinf| = {abs(e_cinf[-1]):.3e}")166 print()167 print("SLOPES")168 a_ind = slopes("indicator", j, e_ind)169 a_c2 = slopes("c2 bump", j, e_c2)170 a_cinf = slopes("cinf bump", j, e_cinf)171 print(f"verdict: indicator {a_ind:.3f} against 1/2, c2 bump {a_c2:.3f} against 3/4, cinf bump {a_cinf:.3f} against 3/4")172 print()173 print("EXPLICIT FORMULA")174 gamma, zeta_left, zeta_prime = zeros_from_pari(ZERO_HEIGHT)175 rho = 0.5 + 1j * gamma176 print(f"{len(gamma)} zeros to height {ZERO_HEIGHT} from PARI, first {gamma[0]:.6f}, last {gamma[-1]:.6f}")177 for name, mellin, err in (("c2 bump", mellin_c2, e_c2), ("cinf bump", mellin_cinf, e_cinf)):178 coef = mellin(rho) * zeta_left / zeta_prime179 print(f"{name}: |c_rho| at zeros 1, 2, 10, 50, {len(gamma)}: " + ", ".join(f"{abs(coef[k]):.3e}" for k in (0, 1, 9, 49, len(gamma) - 1)))180 power = np.polyfit(np.log(gamma), np.log(np.abs(coef)), 1)[0]181 print(f"{name}: least-squares power of |c_rho| against gamma on {len(gamma)} zeros: {power:.2f}")182 for K in (1, 10, 30, len(gamma)):183 model = np.array([2.0 * np.sum((coef[:K] * yy ** (2.0 - rho[:K])).real) for yy in y])184 resid = np.max(np.abs(err - model)) / np.max(np.abs(err))185 print(f"{name}: first {K:3d} zeros, max |E - sum| / max |E| = {resid:.3e}")186 amp = np.abs(err) / y ** 1.5187 print(f"{name}: |E|/y^(3/2) over the grid: min {amp.min():.4e}, max {amp.max():.4e}, bound 2 sum |c_rho| = {2 * np.sum(np.abs(coef)):.4e}")188 print()189 print("SHARP CUTOFF")190 primes = [p for p in (10 ** 6 + 3, 10 ** 7 + 19) if phi[p] == p - 1]191 for p in primes:192 before = prefix[p - 1] - 3.0 / PI2 * (p - 1) ** 2193 after = prefix[p] - 3.0 / PI2 * p ** 2194 print(f"prime {p}: totient error jumps from {before:.1f} to {after:.1f}, jump {after - before:.1f} against phi(p) - 3(2p - 1)/pi^2 = {p - 1 - 3.0 / PI2 * (2 * p - 1):.1f}")195 clock("done")196197if __name__ == "__main__":198 main()