Current task: —

E-010-suzuki-W-function.py

Experiments: E-010 · script updated 08-18-2026

E-010-suzuki-W-zeros.csv — 2.7 KB, updated 08-18-2026

page 1 of 1 · 500/page · 48 rows
# ahlambdathetajz_jgamma_jz_over_gamma
2 2.0 0.005 -1.0 0.0 1 1.5781071480 14.1347251417 0.111648
3 2.0 0.005 -1.0 0.0 2 3.1562797086 21.0220396388 0.150141
4 2.0 0.005 -1.0 0.0 3 4.7345899540 25.0108575801 0.189301
5 2.0 0.005 -1.0 0.0 4 6.3131262144 30.4248761259 0.207499
6 2.0 0.005 -1.0 0.0 5 7.8920088439 32.9350615877 0.239623
7 2.0 0.005 -1.0 0.0 6 9.4714258113 37.5861781588 0.251992
8 2.0 0.005 -1.0 0.0 7 11.0517451194 40.9187190121 0.270090
9 2.0 0.005 -1.0 0.0 8 12.6342158953 43.3270732809 0.291601
10 2.0 0.005 -1.0 0.0 9 14.1505897285 48.0051508812 0.294772
11 2.0 0.005 -1.0 0.0 10 15.7950949427 49.7738324777 0.317337
12 2.0 0.005 -1.0 0.0 11 17.3870146997 52.9703214777 0.328241
13 2.0 0.005 -1.0 0.0 12 18.9953025663 56.4462476971 0.336520
14 4.0 0.005 -1.0 0.0 1 0.7864646704 14.1347251417 0.055641
15 4.0 0.005 -1.0 0.0 2 1.5729312318 21.0220396388 0.074823
16 4.0 0.005 -1.0 0.0 3 2.3594016187 25.0108575801 0.094335
17 4.0 0.005 -1.0 0.0 4 3.1458778549 30.4248761259 0.103398
18 4.0 0.005 -1.0 0.0 5 3.9323621057 32.9350615877 0.119397
19 4.0 0.005 -1.0 0.0 6 4.7188567404 37.5861781588 0.125548
20 4.0 0.005 -1.0 0.0 7 5.5053644057 40.9187190121 0.134544
21 4.0 0.005 -1.0 0.0 8 6.2918881303 43.3270732809 0.145218
22 4.0 0.005 -1.0 0.0 9 7.0784314675 48.0051508812 0.147451
23 4.0 0.005 -1.0 0.0 10 7.8649986715 49.7738324777 0.158015
24 4.0 0.005 -1.0 0.0 11 8.6515950185 52.9703214777 0.163329
25 4.0 0.005 -1.0 0.0 12 9.4382272966 56.4462476971 0.167207
26 6.0 0.005 -1.0 0.0 1 0.5239397465 14.1347251417 0.037068
27 6.0 0.005 -1.0 0.0 2 1.0478796490 21.0220396388 0.049847
28 6.0 0.005 -1.0 0.0 3 1.5718198646 25.0108575801 0.062846
29 6.0 0.005 -1.0 0.0 4 2.0957605530 30.4248761259 0.068883
30 6.0 0.005 -1.0 0.0 5 2.6197018778 32.9350615877 0.079541
31 6.0 0.005 -1.0 0.0 6 3.1436440079 37.5861781588 0.083638
32 6.0 0.005 -1.0 0.0 7 3.6675871192 40.9187190121 0.089631
33 6.0 0.005 -1.0 0.0 8 4.1915313960 43.3270732809 0.096742
34 6.0 0.005 -1.0 0.0 9 4.7154770341 48.0051508812 0.098229
35 6.0 0.005 -1.0 0.0 10 5.2394242398 49.7738324777 0.105265
36 6.0 0.005 -1.0 0.0 11 5.7633732415 52.9703214777 0.108804
37 6.0 0.005 -1.0 0.0 12 6.2873242773 56.4462476971 0.111386
38 8.0 0.005 -1.0 0.0 1 0.3928980474 14.1347251417 0.027797
39 8.0 0.005 -1.0 0.0 2 0.7857961992 21.0220396388 0.037380
40 8.0 0.005 -1.0 0.0 3 1.1786945601 25.0108575801 0.047127
41 8.0 0.005 -1.0 0.0 4 1.5715932361 30.4248761259 0.051655
42 8.0 0.005 -1.0 0.0 5 1.9644923352 32.9350615877 0.059647
43 8.0 0.005 -1.0 0.0 6 2.3573919674 37.5861781588 0.062720
44 8.0 0.005 -1.0 0.0 7 2.7502922464 40.9187190121 0.067214
45 8.0 0.005 -1.0 0.0 8 3.1431932894 43.3270732809 0.072546
46 8.0 0.005 -1.0 0.0 9 3.5360952182 48.0051508812 0.073661
47 8.0 0.005 -1.0 0.0 10 3.9289981595 49.7738324777 0.078937
48 8.0 0.005 -1.0 0.0 11 4.3219022473 52.9703214777 0.081591
49 8.0 0.005 -1.0 0.0 12 4.7148076221 56.4462476971 0.083527

E-010-suzuki-W-zeros.json — 3.9 KB, updated 08-18-2026

JSON sidecar (click to expand)
{
  "experiment": "E-010",
  "task": "3.4c cont. (D-019 §4): zeros of Suzuki W(a,theta;z)",
  "description": "deficiency vectors v_pm from Galerkin resolvent (E-009b machinery, bordered mean-zero solve); W(a,theta;z) per eq. 1.11; real-gauge zero finding on (0,60]; comparison with zeta zero ordinates",
  "script": "research/numerics/scripts/E-010-suzuki-W-function.py",
  "parameters": {
    "a_sweep": [
      2.0,
      4.0,
      6.0,
      8.0
    ],
    "h_sweep": 0.005,
    "h_conv": [
      0.01,
      0.005,
      0.0025
    ],
    "lambdas": [
      -0.5,
      -1.0,
      -2.0
    ],
    "thetas": [
      0.0,
      1.5707963267948966,
      3.141592653589793
    ],
    "z_max": 60.0
  },
  "checks": {
    "v_minus_symmetry": true,
    "T_norms_agree": true,
    "theta0_real_gauge": true,
    "h_convergence_first_zero": true
  },
  "key_values": {
    "gamma_1..6": [
      14.134725141734695,
      21.022039638771556,
      25.01085758014569,
      30.424876125859512,
      32.93506158773919,
      37.586178158825675
    ],
    "a=2.0": [
      1.5781071479559565,
      3.156279708637713,
      4.734589954016487,
      6.313126214353641,
      7.892008843882063,
      9.471425811293946,
      11.051745119397912,
      12.634215895261782,
      14.150589728489816,
      15.79509494269096,
      17.38701469967809,
      18.99530256627463
    ],
    "a=4.0": [
      0.78646467037824,
      1.5729312318082147,
      2.3594016186812166,
      3.1458778549196453,
      3.9323621057289406,
      4.7188567403698025,
      5.505364405687678,
      6.291888130339955,
      7.078431467455094,
      7.864998671463607,
      8.651595018454742,
      9.438227296565794
    ],
    "a=6.0": [
      0.5239397465249559,
      1.0478796489834181,
      1.571819864560504,
      2.0957605529866874,
      2.6197018778056727,
      3.143644007875613,
      3.667587119190764,
      4.191531396047481,
      4.715477034091082,
      5.23942423978668,
      5.7633732415498,
      6.287324277308421
    ],
    "a=8.0": [
      0.3928980474237194,
      0.7857961991893093,
      1.1786945600926104,
      1.5715932361283171,
      1.9644923351809158,
      2.357391967431892,
      2.7502922464497574,
      3.143193289370707,
      3.5360952181834935,
      3.92899815947609,
      4.321902247297599,
      4.714807622114
    ],
    "h_conv_first_zero": [
      0.7867814646177885,
      0.78646467037824,
      0.7862960897194806
    ],
    "lambda_sensitivity": {
      "-0.5": [
        0.786549044345905,
        1.573100135109588,
        2.359655366162599,
        3.14621692956192
      ],
      "-1.0": [
        0.78646467037824,
        1.5729312318082147,
        2.3594016186812166,
        3.1458778549196453
      ],
      "-2.0": [
        0.786360915811139,
        1.5727234886518329,
        2.359089412936245,
        3.1454604602135774
      ]
    },
    "theta_sensitivity": {
      "0.0": [
        0.78646467037824,
        1.5729312318082147,
        2.3594016186812166,
        3.1458778549196453,
        3.9323621057289406,
        4.7188567403698025
      ],
      "1.5707963267948966": [
        0.9831559835641227,
        1.785143661842912,
        2.5782009073997156,
        3.368499578391118,
        4.157616557181577,
        4.94614687241914
      ],
      "3.141592653589793": [
        1.1256469139766037,
        1.935274990583408,
        2.731660080425844,
        3.5238773590800987,
        4.314268719282322,
        5.103729551791709
      ]
    }
  },
  "caveats": [
    "theta=0 extension has no known zeta-alignment guarantee (Q-S3: no distinguished theta in the paper)",
    "small a (<=8) — Cor 1.6 concerns a -> infinity; zeros need not be near gamma_j yet; trend with a is the observable"
  ],
  "elapsed_seconds": 1471.5,
  "timestamp_utc": "2026-08-18T03:47:24.129033+00:00",
  "row_count": 48
}

Source code

Python script (click to expand)
"""E-010 — Task 3.4c cont. (D-019 §4): zeros of Suzuki's W(a,theta;z).

Suzuki (arXiv:2606.09096v1): the zeta-aligned operators are the self-
adjoint extensions Dbar_{a,theta} of the minimal first-order operator
D_a = i d/dx in H(T_a) (T_a = A_a - lambda I, lambda < lambda_a).
Deficiency indices (1,1); deficiency vectors (Lemma 6.2):

  (T_a v_pm)(x) = e^{pm x}  on (-a,a),   v_pm in L^2_0 (mean-zero)

Characteristic function (eq. 1.11):
  W(a,theta;z) = (z-i) int v_+ e^{izx} dx + e^{i theta}(z+i) int v_- e^{izx} dx
Thm 1.5 [PROVED]: zeros of W = eigenvalues of Dbar_{a,theta}, ALL REAL.
Cor 1.6 [CONDITIONAL]: if e^{phi(a,z)} W(a,theta;z) ->
z^2 xi(1/2 - iz)/xi'(1/2 - iz) uniformly on compacts, then RH.

This experiment: Galerkin v_pm from the E-009b-validated machinery
(solve (Q - lambda M) c = b_pm with mean-zero bordered system), then
W on the real line and its zeros, vs the zero ordinates gamma_j (E-002).

Real gauges (v_+(x) = v_-(-x) by evenness of T_a, so with
V(z) = int v_+ e^{izx} dx, for real z):
  theta = 0:  W = 2 (z Re V + Im V)          (real)
  theta = pi: W/i = 2 (z Im V - Re V)         (real)
Other theta: |W| minima (zeros are real by Thm 1.5, so minima ~ 0).

Runs:
  * a-sweep: a in {2,4,6,8}, h = 0.005, theta = 0, lambda = -1
  * h-convergence at a = 4: h in {0.01, 0.005, 0.0025}
  * lambda-sensitivity at a = 4: lambda in {-0.5, -1, -2}
  * theta-sensitivity at a = 4: theta in {0, pi/2, pi}
Zeros: sign-change scan on [0.01, 60] (grid 2e4) + bisection.

Checks: v_- symmetry (c_- ~ reversed c_+); T-norm agreement; theta=0
imaginary gauge ~ 0; h-convergence of zeros at a = 4 (rel < 1e-3).

Outputs: research/numerics/data/E-010-suzuki-W-zeros.csv (+ .json)
"""
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

import numpy as np
import scipy.linalg
import scipy.optimize
import mpmath as mp
from numpy.polynomial.legendre import leggauss

REPO = Path(__file__).resolve().parents[3]
DATA = REPO / "research" / "numerics" / "data"
ZEROS_CSV = DATA / "E-002-zeros-first-10000.csv"
CSV = DATA / "E-010-suzuki-W-zeros.csv"
JSON = DATA / "E-010-suzuki-W-zeros.json"

A_CONST = 0.5 * (np.log(2 * np.pi) + np.euler_gamma - 1.0)
T_MAXG = 14.0                     # g needed on [0, 2a], a <= 7
GL_N = 12
T_LERCH = 0.05                    # below this, Lerch via mpmath
A_SWEEP = [2.0, 4.0, 6.0, 7.0]
H_SWEEP = 0.005
H_CONV = [0.01, 0.005]
A_CONV = 4.0
LAMBDAS = [-0.5, -2.0]
THETAS = [0.0, np.pi / 2, np.pi]
Z_MAX = 60.0
N_GRID = 20000
N_ZEROS = 12


# ---------------------------------------------------------- g (as E-009b)
def mangoldt_sieve(n_max: int) -> np.ndarray:
    lam_log = np.zeros(n_max + 1)
    is_comp = np.zeros(n_max + 1, dtype=bool)
    for p in range(2, n_max + 1):
        if not is_comp[p]:
            if p * p <= n_max:
                is_comp[p * p :: p] = True
            pk = p
            while pk <= n_max:
                lam_log[pk] = np.log(p)
                pk *= p
    return lam_log


print("sieving von Mangoldt to e^16 ...", flush=True)
N_MAXG = int(np.exp(T_MAXG)) + 2
LAM_LOG = mangoldt_sieve(N_MAXG)
NS = np.arange(2, N_MAXG + 1)
LOGS = np.log(NS)
LAM = LAM_LOG[2:] / np.sqrt(NS)
C1 = np.cumsum(LAM)
C0 = np.cumsum(LAM * LOGS)

mp.dps = 30
DIG_COEFF = float(-(mp.digamma(mp.mpf(1) / 4) - mp.log(mp.pi)) / 2)
PHI1 = float(mp.lerchphi(1, 2, mp.mpf(1) / 4))
mp.dps = 25
KK = np.arange(0, 400)
DENOM = (KK + 0.25) ** 2


def prime_sum_vec(t: np.ndarray) -> np.ndarray:
    m = np.searchsorted(LOGS, t, side="right")
    out = np.zeros_like(t)
    pos = m > 0
    idx = m[pos] - 1
    out[pos] = C1[idx] * t[pos] - C0[idx]
    return out


def lerch_comb(t: np.ndarray) -> np.ndarray:
    out = np.empty_like(t)
    fast = t > T_LERCH          # z = e^{-2t} < 0.905: 400-term series
    if fast.any():
        z = np.exp(-2.0 * t[fast])
        phi = (np.power.outer(z, KK) / DENOM).sum(axis=1)
        out[fast] = -0.25 * (PHI1 - np.exp(-t[fast] / 2) * phi)
    if (~fast).any():
        for i in np.flatnonzero(~fast):
            ti = mp.mpf(float(t[i]))
            out[i] = float(-0.25 * (mp.lerchphi(1, 2, mp.mpf(1) / 4)
                                    - mp.exp(-ti / 2)
                                    * mp.lerchphi(mp.exp(-2 * ti), 2,
                                                  mp.mpf(1) / 4)))
    return out


def g_float(t: np.ndarray) -> np.ndarray:
    t = np.asarray(t, dtype=float)
    g = -4.0 * (np.exp(t / 2) + np.exp(-t / 2) - 2.0)
    g += prime_sum_vec(t)
    g += DIG_COEFF * t
    g += lerch_comb(t)
    g[t == 0.0] = 0.0
    return g


def sigma_vec(t: np.ndarray) -> np.ndarray:
    t = np.asarray(t, dtype=float)
    with np.errstate(divide="ignore", invalid="ignore"):
        return np.where(t > 0, 0.5 * t * np.log(t) + A_CONST * t, 0.0)


def F_sing(t: float) -> float:
    if t <= 0:
        return 0.0
    return (A_CONST * t**3 / 6.0 + t**3 * np.log(t) / 12.0
            - 5.0 * t**3 / 72.0)


def S_sing(k: int, h: float) -> float:
    return (F_sing((k + 1) * h) - 2.0 * F_sing(k * h)
            + F_sing(abs(k - 1) * h))


XN, XW = leggauss(GL_N)


def S(k: int, h: float) -> float:
    def quad(lo, hi, peak):
        n_lo = int(np.ceil(np.exp(lo)))
        n_hi = int(np.floor(np.exp(hi)))
        pts = sorted({lo, peak, hi} |
                     {float(np.log(n))
                      for n in range(max(n_lo, 2), n_hi + 1)
                      if lo < np.log(n) < hi})
        tot = 0.0
        for p0, p1 in zip(pts, pts[1:]):
            if p1 - p0 <= 0:
                continue
            mid, half = 0.5 * (p0 + p1), 0.5 * (p1 - p0)
            xs = mid + half * XN
            rho = g_float(xs) - sigma_vec(xs)
            tot += half * float(np.dot(XW, (h - np.abs(xs - peak)) * rho))
        return tot

    if k == 0:
        return S_sing(0, h) + 2.0 * quad(0.0, h, 0.0)
    return S_sing(k, h) + quad((k - 1) * h, (k + 1) * h, k * h)


def stiffness_first_col(n_elem: int, h: float) -> np.ndarray:
    n_q = n_elem - 1
    s = np.array([S(k, h) for k in range(n_elem)])
    q = np.empty(n_q)
    q[0] = (2.0 * s[0] - 2.0 * s[1]) / h**2
    for k in range(1, n_q):
        q[k] = (2.0 * s[k] - s[k + 1] - s[k - 1]) / h**2
    return q


# ----------------------------------------------------------- deficiency solve
_BUILD = {}


def build(a: float, h: float):
    """Q, M, nodes, and exp rhs's for (a, h) — cached."""
    if (a, h) in _BUILD:
        return _BUILD[(a, h)]
    n_elem = int(round(2 * a / h))
    n = n_elem - 1
    q = stiffness_first_col(n_elem, h)
    Q = scipy.linalg.toeplitz(q)
    M = scipy.linalg.toeplitz(np.r_[2 * h / 3, h / 6,
                                    np.zeros(max(n - 2, 0))])
    ii = np.arange(1, n_elem)
    x = -a + ii * h
    xg, wg = leggauss(8)
    b = {}
    for sgn in (+1.0, -1.0):
        mid1 = x - h / 2
        xs1 = mid1[:, None] + (h / 2) * xg[None, :]
        phi1 = (xs1 - (x - h)[:, None]) / h
        mid2 = x + h / 2
        xs2 = mid2[:, None] + (h / 2) * xg[None, :]
        phi2 = ((x + h)[:, None] - xs2) / h
        b[sgn] = ((h / 2) * (np.exp(sgn * xs1) * phi1) @ wg
                  + (h / 2) * (np.exp(sgn * xs2) * phi2) @ wg)
    _BUILD[(a, h)] = (x, h, Q, M, b)
    return _BUILD[(a, h)]


def solve_v(Q, M, b_s, lam, n):
    T = Q - lam * M
    e = np.ones(n)
    B = np.zeros((n + 1, n + 1))
    B[:n, :n] = T
    B[:n, n] = e
    B[n, :n] = e
    sol = scipy.linalg.solve(B, np.r_[b_s, 0.0], assume_a="sym")
    return sol[:n], T


def hat_FT(z: np.ndarray, h: float) -> np.ndarray:
    return h * np.sinc(z * h / (2 * np.pi)) ** 2


def V_of_z(z: np.ndarray, c: np.ndarray, x: np.ndarray,
           h: float) -> np.ndarray:
    return hat_FT(z, h) * (np.exp(1j * np.outer(z, x)) @ c)


def W_real(z: np.ndarray, V: np.ndarray, theta: float):
    """Real gauge of W on the real line; complex for generic theta."""
    return (z - 1j) * V + np.exp(1j * theta) * (z + 1j) * np.conj(V)


def zeros_for(a, h, lam, theta):
    x, hh, Q, M, b = build(a, h)
    n = len(x)
    cp, T = solve_v(Q, M, b[+1.0], lam, n)
    cm, _ = solve_v(Q, M, b[-1.0], lam, n)
    # T-norm normalization (should agree by symmetry)
    np_ = float(cp @ T @ cp)
    nm = float(cm @ T @ cm)
    cm = cm * np.sqrt(np_ / nm)
    zg = np.linspace(0.01, Z_MAX, N_GRID)
    Vp = V_of_z(zg, cp, x, hh)
    if abs(theta) < 1e-15:
        Wf = 2 * (zg * Vp.real + Vp.imag)     # real gauge
    elif abs(theta - np.pi) < 1e-15:
        Wf = 2 * (zg * Vp.imag - Vp.real)     # W/i real gauge
    else:
        Vm = V_of_z(zg, cm, x, hh)
        Wf = np.abs((zg - 1j) * Vp
                    + np.exp(1j * theta) * (zg + 1j) * Vm)
    if theta in (0.0, np.pi):
        def Wdirect(z):
            V = hat_FT(np.array([z]), hh)[0] * complex(
                np.exp(1j * z * x) @ cp)
            if theta == 0.0:
                return 2 * (z * V.real + V.imag)
            return 2 * (z * V.imag - V.real)
        vals = Wf
        idx = np.flatnonzero(np.sign(vals[:-1]) * np.sign(vals[1:]) < 0)
        roots = []
        for i in idx:
            try:
                roots.append(scipy.optimize.brentq(Wdirect, zg[i],
                                                 zg[i + 1], xtol=1e-13,
                                                 rtol=1e-13))
            except ValueError:
                pass
    else:
        # local minima of |W| below threshold
        loc = [zg[i] for i in range(1, len(zg) - 1)
               if Wf[i] < Wf[i - 1] and Wf[i] < Wf[i + 1]]
        roots = []
        for r0 in loc:
            r = scipy.optimize.minimize_scalar(
                lambda z: float(np.abs(W_real(z, hat_FT(np.array([z]), hh)[0]
                                             * complex(np.exp(1j * z * x) @ cp),
                                             theta))),
                bounds=(r0 - 0.01, r0 + 0.01), method="bounded")
            resid = abs(W_real(r.x, hat_FT(np.array([r.x]), hh)[0]
                               * complex(np.exp(1j * r.x * x) @ cp), theta))
            if resid < 1e-3 * max(1.0, float(np.max(np.abs(Wf)))):
                roots.append(float(r.x))
    return roots, np_, nm, x, hh, cp, cm


def main() -> int:
    if CSV.exists() and "--force" not in sys.argv:
        print(f"cached: {CSV} exists (use --force to recompute)"); return 0
    t0 = time.time()
    gam = np.loadtxt(ZEROS_CSV, delimiter=",", skiprows=1, usecols=1)

    rows = []
    info = {}

    # ---- symmetry & gauge checks at a=4, h=0.005, lambda=-1
    x, hh, Q, M, b = build(A_CONV, H_SWEEP)
    n = len(x)
    cp, T = solve_v(Q, M, b[+1.0], -1.0, n)
    cm, _ = solve_v(Q, M, b[-1.0], -1.0, n)
    sym_err = float(np.max(np.abs(cm - cp[::-1]))
                    / np.max(np.abs(cp)))
    np_ = float(cp @ T @ cp)
    nm = float(cm @ T @ cm)
    zt = np.array([10.0, 25.0, 40.0])
    Vt = V_of_z(zt, cp, x, hh)
    Wc = W_real(zt, Vt, 0.0)
    gauge_err = float(np.max(np.abs(Wc.imag)))
    checks = {
        "v_minus_symmetry": bool(sym_err < 1e-6),
        "T_norms_agree": bool(abs(np_ - nm) / abs(np_) < 1e-6),
        "theta0_real_gauge": bool(gauge_err < 1e-8),
    }
    print(f"sym_err={sym_err:.2e} T-norms {np_:.6f}/{nm:.6f} "
          f"gauge_err={gauge_err:.2e}")

    # ---- a-sweep, theta = 0
    for a in A_SWEEP:
        t1 = time.time()
        roots, _, _, _, _, _, _ = zeros_for(a, H_SWEEP, -1.0, 0.0)
        info[f"a={a}"] = roots[:N_ZEROS]
        print(f"a={a}: {len(roots)} zeros in (0,{Z_MAX}): "
              f"{[f'{r:.3f}' for r in roots[:6]]} ({time.time()-t1:.0f}s)")
        for j, r in enumerate(roots[:N_ZEROS], start=1):
            rows.append((a, H_SWEEP, -1.0, 0.0, j, r))

    # ---- h-convergence at a=4
    hconv = {}
    for h in H_CONV:
        roots, _, _, _, _, _, _ = zeros_for(A_CONV, h, -1.0, 0.0)
        hconv[h] = roots
        print(f"a=4 h={h}: first zeros {[f'{r:.4f}' for r in roots[:4]]}")
    if all(len(hconv[h]) >= 1 for h in H_CONV):
        z1 = [hconv[h][0] for h in H_CONV]
        rel = abs(z1[-1] - z1[0]) / abs(z1[-1])
        checks["h_convergence_first_zero"] = bool(rel < 1e-3)
        info["h_conv_first_zero"] = z1

    # ---- lambda-sensitivity at a=4
    lam_roots = {}
    for lam in LAMBDAS:
        roots, _, _, _, _, _, _ = zeros_for(A_CONV, H_SWEEP, lam, 0.0)
        lam_roots[lam] = roots[:4]
        print(f"a=4 lambda={lam}: {[f'{r:.4f}' for r in roots[:4]]}")
    info["lambda_sensitivity"] = {str(k): v for k, v in lam_roots.items()}

    # ---- theta-sensitivity at a=4
    th_roots = {}
    for th in THETAS:
        roots, _, _, _, _, _, _ = zeros_for(A_CONV, H_SWEEP, -1.0, th)
        th_roots[th] = roots[:6]
        print(f"a=4 theta={th:.4f}: {[f'{r:.4f}' for r in roots[:6]]}")
    info["theta_sensitivity"] = {str(k): v for k, v in th_roots.items()}

    DATA.mkdir(parents=True, exist_ok=True)
    with CSV.open("w", encoding="utf-8", newline="") as f:
        f.write("a,h,lambda,theta,j,z_j,gamma_j,z_over_gamma\n")
        for (a, h, lam, th, j, r) in rows:
            g = gam[j - 1]
            f.write(f"{a},{h},{lam},{th},{j},{r:.10f},{g:.10f},"
                    f"{r / g:.6f}\n")

    meta = {
        "experiment": "E-010",
        "task": "3.4c cont. (D-019 §4): zeros of Suzuki W(a,theta;z)",
        "description": "deficiency vectors v_pm from Galerkin resolvent "
                       "(E-009b machinery, bordered mean-zero solve); "
                       "W(a,theta;z) per eq. 1.11; real-gauge zero finding "
                       "on (0,60]; comparison with zeta zero ordinates",
        "script": "research/numerics/scripts/E-010-suzuki-W-function.py",
        "parameters": {"a_sweep": A_SWEEP, "h_sweep": H_SWEEP,
                       "h_conv": H_CONV, "lambdas": LAMBDAS,
                       "thetas": THETAS, "z_max": Z_MAX},
        "checks": checks,
        "key_values": {
            "gamma_1..6": [float(x) for x in gam[:6]],
            **info,
        },
        "caveats": [
            "theta=0 extension has no known zeta-alignment guarantee "
            "(Q-S3: no distinguished theta in the paper)",
            "small a (<=8) — Cor 1.6 concerns a -> infinity; zeros need "
            "not be near gamma_j yet; trend with a is the observable",
        ],
        "elapsed_seconds": round(time.time() - t0, 1),
        "timestamp_utc": datetime.now(timezone.utc).isoformat(),
        "row_count": len(rows),
    }
    JSON.write_text(json.dumps(meta, indent=2), encoding="utf-8")
    ok = all(checks.values())
    print(f"wrote {CSV.name}, {JSON.name} — "
          f"{'ALL CHECKS PASS' if ok else 'CHECK FAILURE: ' + str(checks)}")
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())