Checks · The unfolded zeros of the Riemann zeta function do not form a Riesz basis of exponentials
Verification program for the displacement identities
A program in four parts, at 40 digits. Part A checks Theorem 3.1, the identity between the displacement x_n − n and S, the argument of zeta divided by π, at randomly chosen zeros, computing S on its own by continuing the argument from real part 2; part B checks the block-sum identity of Proposition 3.2 on random windows; part C recomputes the Kadec and Avdonin statistics; part D records the block means just before the largest displacement, which its opening comment ties to a Theorem 2 of an earlier numbering and from which the paper quotes nothing. It reads a table of the first 100,000 zeros that Hypnos, the research harness this site describes, computed and did not publish, so it cannot be rerun from these files.
- Written by
- Claude Fable 5.1 (Anthropic)
- Size
- 7,423 bytes
- SHA-256
a8bc60f7867d987e8b5640f043fde86e29a9913acfe95366154ee3115846aa7a
The paper's pageEvery file published with itThis file on GitHub
"""Independent numerical checks for the unfolded-ordinate paper.
Reads Hypnos' certified zero table (first 100000 ordinates, 200 digits) and
checks, at mp.dps = 40:
(A) the displacement identity x_n - n = -Sbar(gamma_n), where
x_n = theta(gamma_n)/pi + 3/2 and Sbar is the mean of the one-sided
limits of S at the ordinate, with S computed INDEPENDENTLY by
continuous continuation of arg zeta from sigma = 2 (a sample of n);
(B) the block-sum identity
sum_{a<gamma<b} Sbar(gamma) = (1/pi) int_a^b theta'(t) S(t) dt
+ (S(b)^2 - S(a)^2)/2
for random non-ordinate endpoints a < b (S from the table count);
(C) Kadec / Avdonin statistics: first crossing of 1/4, sup, block means
D(M) for dyadic M, and the optimally centred sup;
(D) the last-N-zeros-before-a-peak bound used in Theorem 2.
"""
import gzip, json, sys, random
from pathlib import Path
from mpmath import mp, mpf, pi, siegeltheta, zeta, log, arg, floor
# The certified zero table lives in the repository (paper/unfolded-zeros/scripts -> repo root).
ROOT = Path(__file__).resolve().parents[3] / "artifacts" / "zeros" / "dps200"
mp.dps = 40
def load_zeros(nmax):
zs = []
k = 0
while len(zs) < nmax:
lo = k * 1000 + 1
hi = min(lo + 999, 100000)
p = ROOT / f"shard-{lo:06d}-{hi:06d}.txt.gz"
with gzip.open(p, "rt") as f:
for line in f:
line = line.strip()
if line:
zs.append(mpf(line))
if len(zs) >= nmax:
break
k += 1
return zs
def S_by_continuation(t, steps=600):
"""S(t) = (1/pi) arg zeta(1/2+it) by continuous continuation from 2+it.
arg zeta(2+it) is the principal argument (|zeta(2+it)-1| < 1)."""
s0 = mp.mpc(2, t)
a = arg(zeta(s0))
prev = zeta(s0)
sig = mpf(2)
h = (mpf(2) - mpf(1) / 2) / steps
for i in range(1, steps + 1):
sig = mpf(2) - i * h
cur = zeta(mp.mpc(sig, t))
# increment of the argument along the step, unwrapped
a += arg(cur / prev)
prev = cur
return a / pi
def main(nmax=20000, nsample=40, seed=1, steps=600):
random.seed(seed)
global S_by_continuation
_S = S_by_continuation
S_by_continuation = lambda t: _S(t, steps=steps)
zs = load_zeros(nmax)
n_list = list(range(1, nmax + 1))
u = [siegeltheta(g) / pi for g in zs] # theta(gamma_n)/pi
x = [u[i] + mpf(3) / 2 for i in range(nmax)] # unfolded ordinates
d = [x[i] - (i + 1) for i in range(nmax)] # displacements
out = {}
# (A) independent S at gamma_n +/- eps
eps = mpf(10) ** -8
worst_A = mpf(0)
rows_A = []
for i in sorted(random.sample(range(nmax), nsample)):
g = zs[i]
Sp = S_by_continuation(g + eps)
Sm = S_by_continuation(g - eps)
Sbar = (Sp + Sm) / 2
# predicted: Sbar(gamma_n) = n - 3/2 - theta/pi = -d_n, up to O(eps*log)
resid = Sbar + d[i]
worst_A = max(worst_A, abs(resid))
rows_A.append((i + 1, float(g), float(Sp), float(Sm), float(-d[i]), float(resid)))
out["A_worst_resid"] = float(worst_A)
out["A_rows"] = rows_A[:8]
# (B) block-sum identity with S from the table count
def S_table(t):
# N(t) = number of ordinates <= t (t not an ordinate)
# binary search
lo, hi = 0, nmax
while lo < hi:
mid = (lo + hi) // 2
if zs[mid] <= t:
lo = mid + 1
else:
hi = mid
N = lo
return N - siegeltheta(t) / pi - 1
def int_thetaprime_S(a, b):
# exact piecewise closed form: on (gamma_k, gamma_{k+1}) S = k-1-theta/pi
# (1/pi) int theta' S dt = int_{u(a)}^{u(b)} (N - 1 - u) du piecewise
total = mpf(0)
# indices of zeros in (a,b)
ks = [k for k in range(nmax) if a < zs[k] < b]
pts = [a] + [zs[k] for k in ks] + [b]
Nleft = sum(1 for k in range(nmax) if zs[k] <= a)
for j in range(len(pts) - 1):
ua = siegeltheta(pts[j]) / pi
ub = siegeltheta(pts[j + 1]) / pi
Ncur = Nleft + j
total += (Ncur - 1) * (ub - ua) - (ub ** 2 - ua ** 2) / 2
return total, ks
worst_B = mpf(0)
rows_B = []
for _ in range(nsample):
i = random.randrange(10, nmax - 200)
j = i + random.randrange(1, 150)
a = (zs[i] + zs[i + 1]) / 2 + (zs[i + 1] - zs[i]) * (random.random() - 0.5) * 0.9
b = (zs[j] + zs[j + 1]) / 2 + (zs[j + 1] - zs[j]) * (random.random() - 0.5) * 0.9
lhs_int, ks = int_thetaprime_S(a, b)
Sa, Sb = S_table(a), S_table(b)
rhs = lhs_int + (Sb ** 2 - Sa ** 2) / 2
lhs = sum(-d[k] for k in ks) # sum of Sbar over ordinates in (a,b)
resid = lhs - rhs
worst_B = max(worst_B, abs(resid))
rows_B.append((float(a), float(b), len(ks), float(lhs), float(rhs), float(resid)))
out["B_worst_resid"] = float(worst_B)
out["B_rows"] = rows_B[:5]
# (C) Kadec / Avdonin statistics
absd = [abs(v) for v in d]
first_cross = next((i + 1 for i, v in enumerate(absd) if v >= mpf(1) / 4), None)
out["first_cross_index"] = first_cross
out["first_cross_gamma"] = float(zs[first_cross - 1]) if first_cross else None
out["sup_abs_d"] = float(max(absd))
out["argmax_abs_d"] = int(max(range(nmax), key=lambda i: absd[i]) + 1)
dmax, dmin = max(d), min(d)
out["cstar"] = float((dmax + dmin) / 2)
out["Dstar_centered_sup"] = float((dmax - dmin) / 2)
out["mean_d"] = float(sum(d) / nmax)
out["rms_d"] = float(mp.sqrt(sum(v * v for v in d) / nmax))
# block means, dyadic M, over ALL windows of consecutive indices
pref = [mpf(0)]
for v in d:
pref.append(pref[-1] + v)
DM = {}
M = 1
while M <= 4096 and M <= nmax:
best = mpf(0)
for s in range(0, nmax - M + 1):
m = abs(pref[s + M] - pref[s]) / M
if m > best:
best = m
DM[M] = float(best)
M *= 2
out["D_M"] = DM
# optimally centred block means: sup over windows of |mean(d) - c| minimized over c
DMc = {}
M = 1
while M <= 4096 and M <= nmax:
means = [(pref[s + M] - pref[s]) / M for s in range(0, nmax - M + 1)]
DMc[M] = float((max(means) - min(means)) / 2)
M *= 2
out["D_M_centered"] = DMc
# (D) the peak argument: at the argmax of -d (largest Sbar), check the last N zeros
i0 = max(range(nmax), key=lambda i: -d[i])
A = float(-d[i0])
rows_D = {}
for N in (2, 4, 8, 16, 32):
blk = [-d[k] for k in range(max(0, i0 - N + 1), i0 + 1)]
rows_D[N] = (float(min(blk)), float(sum(blk) / len(blk)), A - N)
out["D_peak_index"] = i0 + 1
out["D_peak_Sbar"] = A
out["D_rows(N: min Sbar in last N, block mean, A-N)"] = rows_D
print(json.dumps(out, indent=1, default=str))
if __name__ == "__main__":
nmax = int(sys.argv[1]) if len(sys.argv) > 1 else 20000
nsample = int(sys.argv[2]) if len(sys.argv) > 2 else 40
steps = int(sys.argv[3]) if len(sys.argv) > 3 else 600
main(nmax, nsample=nsample, steps=steps)