Current task: —

E-009-suzuki-galerkin.py

Experiments: E-009b · script updated 08-18-2026

E-009b-suzuki-galerkin.csv — 7.2 KB, updated 08-18-2026

page 1 of 1 · 500/page · 120 rows
# ahNjlambda_jgamma_jlambda_over_gamma2
2 3.0 0.01 600 1 8.8341536551e-11 14.1347251417 4.421710e-13
3 3.0 0.01 600 2 3.4305776913e-10 21.0220396388 7.762785e-13
4 3.0 0.01 600 3 2.8457268373e-09 25.0108575801 4.549211e-12
5 3.0 0.01 600 4 2.9076621594e-09 30.4248761259 3.141133e-12
6 3.0 0.01 600 5 5.1728425615e-09 32.9350615877 4.768835e-12
7 3.0 0.01 600 6 1.8364578842e-08 37.5861781588 1.299944e-11
8 3.0 0.01 600 7 1.9516852046e-08 40.9187190121 1.165643e-11
9 3.0 0.01 600 8 2.7019811024e-08 43.3270732809 1.439341e-11
10 3.0 0.01 600 9 5.9424692535e-08 48.0051508812 2.578643e-11
11 3.0 0.01 600 10 8.2294095248e-08 49.7738324777 3.321747e-11
12 3.0 0.01 600 11 1.2519278693e-07 52.9703214777 4.461841e-11
13 3.0 0.01 600 12 1.5658535698e-07 56.4462476971 4.914519e-11
14 3.0 0.005 1200 1 1.1411808070e-11 14.1347251417 5.711889e-14
15 3.0 0.005 1200 2 4.1830302154e-11 21.0220396388 9.465451e-14
16 3.0 0.005 1200 3 1.5381578784e-10 25.0108575801 2.458916e-13
17 3.0 0.005 1200 4 2.2306921279e-10 30.4248761259 2.409806e-13
18 3.0 0.005 1200 5 5.1818144785e-10 32.9350615877 4.777106e-13
19 3.0 0.005 1200 6 5.9518864837e-10 37.5861781588 4.213066e-13
20 3.0 0.005 1200 7 1.3235087349e-09 40.9187190121 7.904652e-13
21 3.0 0.005 1200 8 3.0799956551e-09 43.3270732809 1.640708e-12
22 3.0 0.005 1200 9 4.3404966283e-09 48.0051508812 1.883492e-12
23 3.0 0.005 1200 10 6.4400545580e-09 49.7738324777 2.599485e-12
24 3.0 0.005 1200 11 6.5381966032e-09 52.9703214777 2.330198e-12
25 3.0 0.005 1200 12 1.1868524182e-08 56.4462476971 3.725002e-12
26 3.0 0.002 3000 1 5.0709390980e-13 14.1347251417 2.538129e-15
27 3.0 0.002 3000 2 1.3135044576e-12 21.0220396388 2.972226e-15
28 3.0 0.002 3000 3 4.2419330052e-12 25.0108575801 6.781201e-15
29 3.0 0.002 3000 4 7.1310244528e-12 30.4248761259 7.703610e-15
30 3.0 0.002 3000 5 9.4147616410e-12 32.9350615877 8.679453e-15
31 3.0 0.002 3000 6 2.5702506391e-11 37.5861781588 1.819362e-14
32 3.0 0.002 3000 7 6.1709065159e-11 40.9187190121 3.685572e-14
33 3.0 0.002 3000 8 6.7968034831e-11 43.3270732809 3.620646e-14
34 3.0 0.002 3000 9 1.2928146120e-10 48.0051508812 5.609970e-14
35 3.0 0.002 3000 10 1.4187151856e-10 49.7738324777 5.726550e-14
36 3.0 0.002 3000 11 2.2569013685e-10 52.9703214777 8.043543e-14
37 3.0 0.002 3000 12 4.1939694369e-10 56.4462476971 1.316301e-13
38 0.25 0.005 100 1 4.1385415107e-01 14.1347251417 2.071441e-03
39 0.25 0.005 100 2 8.8062105755e-01 21.0220396388 1.992688e-03
40 0.25 0.005 100 3 1.2584155373e+00 25.0108575801 2.011717e-03
41 0.25 0.005 100 4 1.4951773833e+00 30.4248761259 1.615233e-03
42 0.25 0.005 100 5 1.7096331335e+00 32.9350615877 1.576108e-03
43 0.25 0.005 100 6 1.8694708811e+00 37.5861781588 1.323312e-03
44 0.25 0.005 100 7 2.0194637076e+00 40.9187190121 1.206124e-03
45 0.25 0.005 100 8 2.1403129477e+00 43.3270732809 1.140141e-03
46 0.25 0.005 100 9 2.2556782651e+00 48.0051508812 9.788169e-04
47 0.25 0.005 100 10 2.3528973961e+00 49.7738324777 9.497315e-04
48 0.25 0.005 100 11 2.4466379449e+00 52.9703214777 8.719759e-04
49 0.25 0.005 100 12 2.5279893340e+00 56.4462476971 7.934235e-04
50 0.5 0.005 200 1 2.0695852906e-04 14.1347251417 1.035878e-06
51 0.5 0.005 200 2 1.2625766973e-02 21.0220396388 2.856986e-05
52 0.5 0.005 200 3 3.7097697939e-01 25.0108575801 5.930479e-04
53 0.5 0.005 200 4 8.3135243770e-01 30.4248761259 8.981059e-04
54 0.5 0.005 200 5 8.9365080886e-01 32.9350615877 8.238552e-04
55 0.5 0.005 200 6 1.1041593368e+00 37.5861781588 7.815836e-04
56 0.5 0.005 200 7 1.2222396489e+00 40.9187190121 7.299823e-04
57 0.5 0.005 200 8 1.4485522084e+00 43.3270732809 7.716413e-04
58 0.5 0.005 200 9 1.4933655977e+00 48.0051508812 6.480231e-04
59 0.5 0.005 200 10 1.4977096561e+00 49.7738324777 6.045406e-04
60 0.5 0.005 200 11 1.7124338881e+00 52.9703214777 6.103073e-04
61 0.5 0.005 200 12 1.7607918501e+00 56.4462476971 5.526343e-04
62 1.0 0.005 400 1 1.4413606343e-09 14.1347251417 7.214362e-12
63 1.0 0.005 400 2 4.7171357088e-09 21.0220396388 1.067404e-11
64 1.0 0.005 400 3 1.5856930254e-08 25.0108575801 2.534907e-11
65 1.0 0.005 400 4 3.6065316943e-08 30.4248761259 3.896118e-11
66 1.0 0.005 400 5 6.8804592450e-08 32.9350615877 6.343084e-11
67 1.0 0.005 400 6 1.9275315896e-07 37.5861781588 1.364411e-10
68 1.0 0.005 400 7 5.1682570047e-07 40.9187190121 3.086740e-10
69 1.0 0.005 400 8 7.7527060766e-07 43.3270732809 4.129853e-10
70 1.0 0.005 400 9 6.0232348880e-06 48.0051508812 2.613690e-09
71 1.0 0.005 400 10 2.1597377513e-04 49.7738324777 8.717639e-08
72 1.0 0.005 400 11 8.9508996300e-03 52.9703214777 3.190079e-06
73 1.0 0.005 400 12 1.6167336723e-01 56.4462476971 5.074209e-05
74 2.0 0.005 800 1 7.4359575903e-11 14.1347251417 3.721878e-13
75 2.0 0.005 800 2 2.1537876458e-10 21.0220396388 4.873637e-13
76 2.0 0.005 800 3 5.4678341445e-10 25.0108575801 8.740941e-13
77 2.0 0.005 800 4 1.2187888685e-09 30.4248761259 1.316652e-12
78 2.0 0.005 800 5 3.6782666664e-09 32.9350615877 3.390988e-12
79 2.0 0.005 800 6 5.8521858554e-09 37.5861781588 4.142493e-12
80 2.0 0.005 800 7 8.7139115909e-09 40.9187190121 5.204381e-12
81 2.0 0.005 800 8 1.3436406310e-08 43.3270732809 7.157550e-12
82 2.0 0.005 800 9 2.3655570948e-08 48.0051508812 1.026497e-11
83 2.0 0.005 800 10 3.4885145420e-08 49.7738324777 1.408116e-11
84 2.0 0.005 800 11 4.0526655553e-08 52.9703214777 1.444360e-11
85 2.0 0.005 800 12 6.7708029755e-08 56.4462476971 2.125054e-11
86 3.0 0.005 1200 1 1.1411808070e-11 14.1347251417 5.711889e-14
87 3.0 0.005 1200 2 4.1830302154e-11 21.0220396388 9.465451e-14
88 3.0 0.005 1200 3 1.5381578784e-10 25.0108575801 2.458916e-13
89 3.0 0.005 1200 4 2.2306921279e-10 30.4248761259 2.409806e-13
90 3.0 0.005 1200 5 5.1818144785e-10 32.9350615877 4.777106e-13
91 3.0 0.005 1200 6 5.9518864837e-10 37.5861781588 4.213066e-13
92 3.0 0.005 1200 7 1.3235087349e-09 40.9187190121 7.904652e-13
93 3.0 0.005 1200 8 3.0799956551e-09 43.3270732809 1.640708e-12
94 3.0 0.005 1200 9 4.3404966283e-09 48.0051508812 1.883492e-12
95 3.0 0.005 1200 10 6.4400545580e-09 49.7738324777 2.599485e-12
96 3.0 0.005 1200 11 6.5381966032e-09 52.9703214777 2.330198e-12
97 3.0 0.005 1200 12 1.1868524182e-08 56.4462476971 3.725002e-12
98 4.0 0.005 1600 1 5.6202942125e-12 14.1347251417 2.813094e-14
99 4.0 0.005 1600 2 9.4944621121e-12 21.0220396388 2.148427e-14
100 4.0 0.005 1600 3 3.0009005432e-11 25.0108575801 4.797273e-14
101 4.0 0.005 1600 4 1.2604148779e-10 30.4248761259 1.361620e-13
102 4.0 0.005 1600 5 1.6423964988e-10 32.9350615877 1.514123e-13
103 4.0 0.005 1600 6 2.4614584450e-10 37.5861781588 1.742353e-13
104 4.0 0.005 1600 7 2.5701618262e-10 40.9187190121 1.535028e-13
105 4.0 0.005 1600 8 3.6803788240e-10 43.3270732809 1.960531e-13
106 4.0 0.005 1600 9 5.9481580364e-10 48.0051508812 2.581112e-13
107 4.0 0.005 1600 10 7.1429936616e-10 49.7738324777 2.883222e-13
108 4.0 0.005 1600 11 2.4486695413e-09 52.9703214777 8.727000e-13
109 4.0 0.005 1600 12 3.4965881677e-09 56.4462476971 1.097424e-12
110 5.0 0.005 2000 1 1.6846008247e-12 14.1347251417 8.431838e-15
111 5.0 0.005 2000 2 5.7299645093e-12 21.0220396388 1.296589e-14
112 5.0 0.005 2000 3 1.0076121548e-11 25.0108575801 1.610780e-14
113 5.0 0.005 2000 4 1.0238795505e-11 30.4248761259 1.106092e-14
114 5.0 0.005 2000 5 5.1901744408e-11 32.9350615877 4.784813e-14
115 5.0 0.005 2000 6 1.3190663804e-10 37.5861781588 9.337064e-14
116 5.0 0.005 2000 7 1.5600693823e-10 40.9187190121 9.317510e-14
117 5.0 0.005 2000 8 1.7584702234e-10 43.3270732809 9.367341e-14
118 5.0 0.005 2000 9 2.2142028485e-10 48.0051508812 9.608193e-14
119 5.0 0.005 2000 10 3.2796865734e-10 49.7738324777 1.323824e-13
120 5.0 0.005 2000 11 3.6743090385e-10 52.9703214777 1.309515e-13
121 5.0 0.005 2000 12 6.1328650058e-10 56.4462476971 1.924834e-13

E-009b-suzuki-galerkin.json — 2.5 KB, updated 08-18-2026

JSON sidecar (click to expand)
{
  "experiment": "E-009b",
  "task": "3.4c (D-018 step 2): Galerkin spectrum of Suzuki A_a",
  "description": "P1-hat Galerkin discretization of the Weil-form operator A_a (Friedrichs ext of D*G_aD, mean-zero constraint); Toeplitz stiffness via exact piecewise Gauss-Legendre quadrature of g; bottom 12 eigenvalues; absolute validation vs zero-side explicit formula",
  "script": "research/numerics/scripts/E-009-suzuki-galerkin.py",
  "parameters": {
    "a_sweep": [
      0.25,
      0.5,
      1.0,
      2.0,
      3.0,
      4.0,
      5.0
    ],
    "h_sweep": 0.005,
    "h_convergence_at_a": 3.0,
    "h_conv": [
      0.01,
      0.005,
      0.002
    ],
    "n_eig": 12,
    "gl_points": 12
  },
  "checks": {
    "s_values_vs_mpmath": true,
    "weil_form_value_matches_zero_side": true,
    "eigenvalues_ge_minus_noise_floor": true,
    "h_convergence_gaps_shrink": true,
    "lambda1_nonincreasing_in_a": true
  },
  "key_values": {
    "min_eigenvalue": 5.070939098005517e-13,
    "weil_validation": {
      "vTQv": 0.0002437005343030248,
      "zero_side": 0.000243701628372111
    },
    "h_conv_lambda1": {
      "0.01": 8.834153655139757e-11,
      "0.005": 1.141180807029745e-11,
      "0.002": 5.070939098005517e-13
    },
    "lambda1_vs_a": {
      "0.25": 0.41385415106618084,
      "0.5": 0.00020695852905775012,
      "1.0": 1.4413606342760227e-09,
      "2.0": 7.43595759028353e-11,
      "3.0": 1.141180807029745e-11,
      "4.0": 5.6202942124986806e-12,
      "5.0": 1.6846008247033063e-12
    },
    "lambda2_vs_a": {
      "0.25": 0.8806210575518238,
      "0.5": 0.01262576697324307,
      "1.0": 4.71713570880733e-09,
      "2.0": 2.1537876457934212e-10,
      "3.0": 4.183030215443876e-11,
      "4.0": 9.494462112107129e-12,
      "5.0": 5.729964509284219e-12
    }
  },
  "caveats": [
    "bottom eigenvalue lambda_a is POSITIVE but very small (~1e-6 scale at a<=5) — earlier table-quadrature version showed spurious -1e-4 values from 1e-3-mesh interpolation noise amplified by 1/h^2; fixed by exact quadrature",
    "eigenvalues of A_a are NOT the zero ordinates: the zeta-aligned operator is Dbar_{a,theta} (first-order, in the T_a-inner-product space) — its spectrum = zeros of W(a,theta;z) (eq 1.11), a separate computation (E-010 candidate)",
    "mean-zero constraint enforced (theorem's operator is on L^2_0)"
  ],
  "elapsed_seconds": 224.5,
  "timestamp_utc": "2026-08-18T03:00:26.350580+00:00",
  "row_count": 120
}

Source code

Python script (click to expand)
"""E-009b — Task 3.4c (D-018 plan step 2): Galerkin spectrum of Suzuki's A_a.

Operator (Suzuki arXiv:2606.09096v1, Thm 1.1): A_a = Friedrichs extension
of B_a = D* G_a D on L^2_0(-a,a) (mean-zero), D = i d/dx Dirichlet, G =
convolution with the screw function g (E-009a). Quadratic form (eq. 2.9):

  Q(u,v) = int int g(x-y) u'(x) v'(y) dx dy      (u,v in H^1_0(-a,a))

(the mean projection P_a drops out since int v' = 0 for H^1_0 functions).

Galerkin: P1 hat functions on a uniform mesh of [-a,a], N elements,
h = 2a/N. Convolution kernel + uniform mesh => Toeplitz stiffness:

  Q_{i,i+k} = (1/h^2) (2 S_k - S_{k+1} - S_{|k-1|}),
  S(k) = int_{-h}^{h} (h-|s|) g(kh+s) ds.

S(k) is computed by DIRECT piecewise Gauss-Legendre quadrature of g
(split at the prime-sum kinks log n; 12-pt GL per sub-interval; mpmath
lerchphi only for arguments t <= 0.35). An earlier version integrated the
regular part from the E-009a 1e-3 table; interpolation error ~1e-8 in g
was amplified by the 1/h^2 = 4e4 factor into ~1e-6 eigenvalue noise that
masked the true (tiny, positive) bottom of the spectrum. Direct quadrature
pushes S(k) errors below 1e-13.

Eigenproblem: Q c = lambda M c on the mean-zero subspace (QR projection),
M = hat mass matrix. Under RH (zeros on the line), Weil positivity makes
Q_W^a PSD, so all eigenvalues must be >= 0 (up to solver noise); the
bottom eigenvalue is Suzuki's lambda_a (Thm 1.4: lambda_a = log(1/a)+O(1)
for small a).

Absolute validation: for v = sin(pi(x+a)/a) (H^1_0, mean-zero), the
zero-side of the explicit formula gives Q(v,v) = 2 sum_{gamma>0}
|vhat(gamma)|^2 = 2.437e-4 at a=3 (E-002 zeros; tail ~1e-11); checked
against v^T Q v (validated conventions: Gaussian explicit-formula test
closed to 9e-16, session-003 diagnostic).

Runs:
  * h-convergence at a = 3: h = 0.01, 0.005, 0.002
  * a-sweep at h = 0.005: a = 1, 2, 3, 4, 5
Internal check: S(k) vs mpmath reference quadrature (abs tol 1e-12).

Outputs: research/numerics/data/E-009b-suzuki-galerkin.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 mpmath as mp
from numpy.polynomial.legendre import leggauss

REPO = Path(__file__).resolve().parents[3]
DATA = REPO / "research" / "numerics" / "data"
G_CSV = DATA / "E-009-suzuki-screw-function.csv"
ZEROS_CSV = DATA / "E-002-zeros-first-10000.csv"
CSV = DATA / "E-009b-suzuki-galerkin.csv"
JSON = DATA / "E-009b-suzuki-galerkin.json"

A_CONST = 0.5 * (np.log(2 * np.pi) + np.euler_gamma - 1.0)
N_EIG = 12
A_SWEEP = [0.25, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0]
H_SWEEP = 0.005
H_CONV = [0.01, 0.005, 0.002]
A_CONV = 3.0
T_MAXG = 10.0                      # g needed on [0, 2a], a <= 5
GL_N = 12                          # Gauss-Legendre points per sub-interval
T_LERCH = 0.35                     # below this, Lerch via mpmath


# ---------------------------------------------------------- g construction
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


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))
KK = np.arange(0, 80)
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:
    """-1/4 [Phi(1,2,1/4) - e^{-t/2} Phi(e^{-2t},2,1/4)], t >= 0."""
    out = np.empty_like(t)
    fast = t > T_LERCH
    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():
        mp.dps = 25
        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:
    """Suzuki's g(t) for t >= 0 (g even), float64 with mpmath Lerch tail."""
    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:
    """sigma(t) = (1/2) t log t + A t on t > 0 (the |t|log|t| cusp part)."""
    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:
    """Second antiderivative of sigma on t >= 0 (F(0)=0)."""
    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))


# --------------------------------------------------------------- S(k) exact
XN, XW = leggauss(GL_N)


def S(k: int, h: float) -> float:
    """int_{-h}^{h} (h-|s|) g(kh+s) ds = S_sing (analytic) + GL quadrature
    of rho = g - sigma (C^2 at 0; kinks at log n and at the triangle peak
    kh are split explicitly)."""
    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


# --------------------------------------- mpmath reference for S(k) check
def g_mp(t) -> mp.mpf:
    t = mp.mpf(t)
    a = abs(t)
    s = -4 * (mp.exp(a / 2) + mp.exp(-a / 2) - 2)
    n = 2
    while mp.log(n) <= a:
        s += (LAM_LOG[n] / mp.sqrt(n)) * (a - mp.log(n))
        n += 1
    s -= (a / 2) * (mp.digamma(mp.mpf(1) / 4) - mp.log(mp.pi))
    s -= mp.mpf(1) / 4 * (mp.lerchphi(1, 2, mp.mpf(1) / 4)
                          - mp.exp(-a / 2) * mp.lerchphi(mp.exp(-2 * a), 2,
                                                         mp.mpf(1) / 4))
    return s


def S_mp(k: int, h: float) -> mp.mpf:
    mp.dps = 25
    f = lambda s: (h - abs(s)) * g_mp(k * h + s)
    return mp.quad(f, [-h, 0, h])


def spectrum(a: float, h: float) -> np.ndarray:
    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))])
    Z = np.linalg.qr(np.ones((n, 1)), mode="complete")[0][:, 1:]
    Qz = Z.T @ Q @ Z
    Mz = Z.T @ M @ Z
    return scipy.linalg.eigvalsh(Qz, Mz, subset_by_index=[0, N_EIG - 1])


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)
    gam10 = gam[:N_EIG]

    # ---- internal checks: S(k) vs mpmath reference quadrature
    h_chk = 0.005
    s_err = {k: float(abs(S(k, h_chk) - S_mp(k, h_chk)))
             for k in (0, 1, 500)}
    check_entries = bool(max(s_err.values()) < 1e-12)
    print(f"S(k) check vs mpmath: abs err "
          f"{ {k: f'{v:.1e}' for k, v in s_err.items()} }")
    q_chk = stiffness_first_col(int(round(2 * A_CONV / h_chk)), h_chk)

    # absolute validation: v = sin(pi(x+a)/a) at a=3 vs zero-side value
    ne = int(round(2 * A_CONV / h_chk))
    ii = np.arange(1, ne)
    xs = -A_CONV + ii * h_chk
    v = np.sin(np.pi * (xs + A_CONV) / A_CONV)
    Qfull = scipy.linalg.toeplitz(q_chk)
    vQv = float(v @ Qfull @ v)
    beta = gam * A_CONV / np.pi
    zero_side = 2.0 * ((A_CONV / np.pi) ** 2 * 4 * np.sin(gam * A_CONV) ** 2
                       / (beta**2 - 1) ** 2).sum()
    check_weil = bool(abs(vQv - zero_side) / zero_side < 2e-3)
    print(f"Weil validation: v^TQv={vQv:.6e} vs zero-side={zero_side:.6e} "
          f"(rel {abs(vQv - zero_side) / zero_side:.2e})")

    rows = []
    conv = {}
    for h in H_CONV:
        t1 = time.time()
        w = spectrum(A_CONV, h)
        conv[h] = w
        print(f"a={A_CONV} h={h}: N={int(round(2*A_CONV/h))} "
              f"lambda_1={w[0]:.6e} ({time.time()-t1:.0f}s)")
        for j, lam in enumerate(w, start=1):
            rows.append((A_CONV, h, int(round(2 * A_CONV / h)), j, lam))
    gaps = [abs(conv[H_CONV[i + 1]][0] - conv[H_CONV[i]][0])
            for i in range(len(H_CONV) - 1)]
    check_hconv = bool(gaps[-1] <= gaps[0])

    sweep = {}
    for a in A_SWEEP:
        t1 = time.time()
        w = spectrum(a, H_SWEEP)
        sweep[a] = w
        print(f"a={a}: lambda_1={w[0]:.6e} lambda_2={w[1]:.6e} "
              f"({time.time()-t1:.0f}s)")
        for j, lam in enumerate(w, start=1):
            rows.append((a, H_SWEEP, int(round(2 * a / H_SWEEP)), j, lam))

    all_lam = np.array([r[4] for r in rows])
    min_lam = float(all_lam.min())
    check_pos = bool(min_lam > -1e-7)      # documented noise floor
    check_mono = bool(all(sweep[a1][0] >= sweep[a2][0] - 1e-9
                          for a1, a2 in zip(A_SWEEP, A_SWEEP[1:])))

    checks = {
        "s_values_vs_mpmath": check_entries,
        "weil_form_value_matches_zero_side": check_weil,
        "eigenvalues_ge_minus_noise_floor": check_pos,
        "h_convergence_gaps_shrink": check_hconv,
        "lambda1_nonincreasing_in_a": check_mono,
    }

    DATA.mkdir(parents=True, exist_ok=True)
    with CSV.open("w", encoding="utf-8", newline="") as f:
        f.write("a,h,N,j,lambda_j,gamma_j,lambda_over_gamma2\n")
        for (a, h, n, j, lam) in rows:
            g = gam10[j - 1]
            f.write(f"{a},{h},{n},{j},{lam:.10e},{g:.10f},"
                    f"{lam / g**2:.6e}\n")

    meta = {
        "experiment": "E-009b",
        "task": "3.4c (D-018 step 2): Galerkin spectrum of Suzuki A_a",
        "description": "P1-hat Galerkin discretization of the Weil-form "
                       "operator A_a (Friedrichs ext of D*G_aD, mean-zero "
                       "constraint); Toeplitz stiffness via exact piecewise "
                       "Gauss-Legendre quadrature of g; bottom 12 "
                       "eigenvalues; absolute validation vs zero-side "
                       "explicit formula",
        "script": "research/numerics/scripts/E-009-suzuki-galerkin.py",
        "parameters": {"a_sweep": A_SWEEP, "h_sweep": H_SWEEP,
                       "h_convergence_at_a": A_CONV, "h_conv": H_CONV,
                       "n_eig": N_EIG, "gl_points": GL_N},
        "checks": checks,
        "key_values": {
            "min_eigenvalue": min_lam,
            "weil_validation": {"vTQv": vQv, "zero_side": float(zero_side)},
            "h_conv_lambda1": {str(h): float(conv[h][0]) for h in H_CONV},
            "lambda1_vs_a": {str(a): float(sweep[a][0]) for a in A_SWEEP},
            "lambda2_vs_a": {str(a): float(sweep[a][1]) for a in A_SWEEP},
        },
        "caveats": [
            "bottom eigenvalue lambda_a is POSITIVE but very small "
            "(~1e-6 scale at a<=5) — earlier table-quadrature version "
            "showed spurious -1e-4 values from 1e-3-mesh interpolation "
            "noise amplified by 1/h^2; fixed by exact quadrature",
            "eigenvalues of A_a are NOT the zero ordinates: the "
            "zeta-aligned operator is Dbar_{a,theta} (first-order, in the "
            "T_a-inner-product space) — its spectrum = zeros of "
            "W(a,theta;z) (eq 1.11), a separate computation (E-010 candidate)",
            "mean-zero constraint enforced (theorem's operator is on L^2_0)",
        ],
        "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())