Current task: —

E-025-lambda-flow.py

Experiments: E-025 · script updated 08-19-2026

E-025-lambda-flow.csv — 384.1 KB, updated 08-19-2026

← prev page 34 of 34 · 500/page · 16560 rows
# alambda_tagzlogK
16502 12 0.999 57.000 5.892834
16503 12 0.999 57.050 5.886920
16504 12 0.999 57.100 5.875414
16505 12 0.999 57.150 5.866141
16506 12 0.999 57.200 5.857695
16507 12 0.999 57.250 5.847410
16508 12 0.999 57.300 5.837514
16509 12 0.999 57.350 5.832354
16510 12 0.999 57.400 5.832269
16511 12 0.999 57.450 5.833443
16512 12 0.999 57.500 5.834080
16513 12 0.999 57.550 5.838174
16514 12 0.999 57.600 5.850886
16515 12 0.999 57.650 5.870140
16516 12 0.999 57.700 5.885201
16517 12 0.999 57.750 5.885151
16518 12 0.999 57.800 5.868914
16519 12 0.999 57.850 5.847859
16520 12 0.999 57.900 5.837599
16521 12 0.999 57.950 5.843855
16522 12 0.999 58.000 5.856303
16523 12 0.999 58.050 5.857824
16524 12 0.999 58.100 5.839061
16525 12 0.999 58.150 5.806249
16526 12 0.999 58.200 5.778581
16527 12 0.999 58.250 5.775806
16528 12 0.999 58.300 5.803509
16529 12 0.999 58.350 5.849683
16530 12 0.999 58.400 5.894611
16531 12 0.999 58.450 5.921683
16532 12 0.999 58.500 5.921923
16533 12 0.999 58.550 5.895196
16534 12 0.999 58.600 5.851649
16535 12 0.999 58.650 5.811339
16536 12 0.999 58.700 5.795457
16537 12 0.999 58.750 5.809411
16538 12 0.999 58.800 5.835646
16539 12 0.999 58.850 5.849002
16540 12 0.999 58.900 5.839786
16541 12 0.999 58.950 5.824283
16542 12 0.999 59.000 5.830416
16543 12 0.999 59.050 5.860105
16544 12 0.999 59.100 5.867222
16545 12 0.999 59.150 5.776017
16546 12 0.999 59.200 5.497656
16547 12 0.999 59.250 4.898369
16548 12 0.999 59.300 3.610286
16549 12 0.999 59.350 0.134006
16550 12 0.999 59.400 3.837431
16551 12 0.999 59.450 5.009171
16552 12 0.999 59.500 5.563645
16553 12 0.999 59.550 5.812873
16554 12 0.999 59.600 5.879448
16555 12 0.999 59.650 5.851133
16556 12 0.999 59.700 5.807262
16557 12 0.999 59.750 5.796678
16558 12 0.999 59.800 5.816065
16559 12 0.999 59.850 5.836501
16560 12 0.999 59.900 5.843062
16561 12 0.999 59.950 5.842180

E-025-lambda-flow.json — 9.2 KB, updated 08-19-2026

JSON sidecar (click to expand)
{
  "experiment": "E-025",
  "task": "2.13: lambda-flow numerics (ground-state channel)",
  "script": "research/numerics/scripts/E-025-lambda-flow.py",
  "parameters": {
    "a_list": [
      4.0,
      8.0,
      12.0
    ],
    "ladder_rel": [
      0.0,
      0.5,
      0.9,
      0.99,
      0.999
    ],
    "z_max": 300.0
  },
  "checks": {
    "V1_lambda_a_positive_law": false,
    "V2_baseline_matches_E022": true,
    "V3_solve_residuals": false,
    "V4_sister_reported": true
  },
  "key_values": {
    "4": {
      "lambda_a": 5.147476282756457e-11,
      "lambda_law_ratio": 3.783885052191424e-08,
      "sister_corr": -0.2997460393487147,
      "sister_crossings": 57,
      "sister_cross_err": 16.71385138849778,
      "ladder": {
        "-1": {
          "lambda": -1.0,
          "Delta": 2.1593140478568413,
          "floor": -0.106956845297883,
          "plateau": 2.0523572025589583,
          "z1_times_a": 4.504293602793437,
          "n_zeros": 381,
          "lock_metric": 0.10068828882668726,
          "residual": 48.3075476074236,
          "mass_pm0.1": 116.6477713523963,
          "solve_resid": 1.588314368200427e-16
        },
        "0": {
          "lambda": 0.0,
          "Delta": 6.988266234191322,
          "floor": 4.4061523727558125,
          "plateau": 11.394418606947134,
          "z1_times_a": 6.9816622297266715,
          "n_zeros": 381,
          "lock_metric": 7.623144158814413e-05,
          "residual": 10.718615266368445,
          "mass_pm0.1": 127.66723750632949,
          "solve_resid": 5.831210378871265e-10
        },
        "0.5": {
          "lambda": 2.5737381413782287e-11,
          "Delta": 6.988147390288639,
          "floor": 4.40643861356427,
          "plateau": 11.39458600385291,
          "z1_times_a": 7.529134813217906,
          "n_zeros": 381,
          "lock_metric": 7.612992319993416e-05,
          "residual": 10.708784969160092,
          "mass_pm0.1": 127.66507880386828,
          "solve_resid": 2.2679633796708461e-07
        },
        "0.9": {
          "lambda": 4.632728654480812e-11,
          "Delta": 6.987816911690756,
          "floor": 4.407352992396391,
          "plateau": 11.395169904087147,
          "z1_times_a": 8.199973337356482,
          "n_zeros": 381,
          "lock_metric": 7.609529464864893e-05,
          "residual": 10.693566680716287,
          "mass_pm0.1": 127.66305186152309,
          "solve_resid": 5.74001354100028e-08
        },
        "0.99": {
          "lambda": 5.096001519928893e-11,
          "Delta": 6.985923725722728,
          "floor": 4.413501470282933,
          "plateau": 11.39942519600566,
          "z1_times_a": 8.385279853786248,
          "n_zeros": 381,
          "lock_metric": 7.613585558613195e-05,
          "residual": 10.687862236134379,
          "mass_pm0.1": 127.66303788362372,
          "solve_resid": 7.885784848149343e-08
        },
        "0.999": {
          "lambda": 5.142328806473701e-11,
          "Delta": 6.981356415160433,
          "floor": 4.428696114419206,
          "plateau": 11.41005252957964,
          "z1_times_a": 8.40265274248596,
          "n_zeros": 381,
          "lock_metric": 7.614180518407809e-05,
          "residual": 10.687276136059936,
          "mass_pm0.1": 127.66306304014373,
          "solve_resid": 4.3024270231059637e-07
        }
      }
    },
    "8": {
      "lambda_a": -0.028701446327309428,
      "lambda_law_ratio": -15509.255243545844,
      "sister_corr": 0.00255772313467671,
      "sister_crossings": 118,
      "sister_cross_err": 18.633851388497806,
      "ladder": {
        "-1": {
          "lambda": -1.0,
          "Delta": 2.7352201581877043,
          "floor": 0.030554703235580436,
          "plateau": 2.7657748614232847,
          "z1_times_a": 4.496810102477515,
          "n_zeros": 763,
          "lock_metric": 0.057829797607063305,
          "residual": 58.651149793527864,
          "mass_pm0.1": 133.51693635274552,
          "solve_resid": 3.0889933849343203e-16
        },
        "0": {
          "lambda": -0.0,
          "Delta": -33.528034311582985,
          "floor": -423.8211920422485,
          "plateau": -457.3492263538315,
          "z1_times_a": 1.8093924166647792,
          "n_zeros": 548,
          "lock_metric": 0.0003714172395450807,
          "residual": 11.144052463580522,
          "mass_pm0.1": 127.82233169936302,
          "solve_resid": 0.00020060867988680158
        },
        "0.5": {
          "lambda": -0.014350723163654714,
          "Delta": 5.2473962964853165,
          "floor": 1.7336013999547513,
          "plateau": 6.980997696440068,
          "z1_times_a": 4.498199381863492,
          "n_zeros": 755,
          "lock_metric": 0.0013619017100949234,
          "residual": 12.052748449542477,
          "mass_pm0.1": 127.6918385778458,
          "solve_resid": 6.867617904061254e-13
        },
        "0.9": {
          "lambda": -0.025831301694578486,
          "Delta": 5.070963483079262,
          "floor": 1.333969497055245,
          "plateau": 6.4049329801345065,
          "z1_times_a": 4.497946694779682,
          "n_zeros": 761,
          "lock_metric": 0.0030658627341504153,
          "residual": 13.166729243282433,
          "mass_pm0.1": 127.88295027859101,
          "solve_resid": 1.2366600020318642e-13
        },
        "0.99": {
          "lambda": -0.028414431864036332,
          "Delta": 5.037993177808347,
          "floor": 1.272757105974604,
          "plateau": 6.3107502837829506,
          "z1_times_a": 4.5002419528288815,
          "n_zeros": 761,
          "lock_metric": 0.0018016901302855674,
          "residual": 12.566089279144094,
          "mass_pm0.1": 127.72782983544735,
          "solve_resid": 2.3651958714205305e-14
        },
        "0.999": {
          "lambda": -0.028672744880982117,
          "Delta": 5.034476634521893,
          "floor": 1.2679657032940865,
          "plateau": 6.30244233781598,
          "z1_times_a": 4.495276414486893,
          "n_zeros": 762,
          "lock_metric": 0.0026471749147504637,
          "residual": 12.710218865379431,
          "mass_pm0.1": 127.70661909585449,
          "solve_resid": 1.413266823421088e-14
        }
      }
    },
    "12": {
      "lambda_a": -0.0682789599667037,
      "lambda_law_ratio": -27121745.659492664,
      "sister_corr": -0.014045368106618201,
      "sister_crossings": 137,
      "sister_cross_err": 18.87885138849781,
      "ladder": {
        "-1": {
          "lambda": -1.0,
          "Delta": 2.9442229398281423,
          "floor": 0.22834844487190686,
          "plateau": 3.172571384700049,
          "z1_times_a": 4.495304974166888,
          "n_zeros": 1145,
          "lock_metric": 0.032106115361490564,
          "residual": 57.59425795470206,
          "mass_pm0.1": 133.31147006008132,
          "solve_resid": 5.565591099499078e-16
        },
        "0": {
          "lambda": -0.0,
          "Delta": 145.87233394077197,
          "floor": -369.5573776739587,
          "plateau": -223.68504373318672,
          "z1_times_a": 47.08983339916813,
          "n_zeros": 788,
          "lock_metric": 0.11988236920414796,
          "residual": 10.704038663232183,
          "mass_pm0.1": 126.78597572681957,
          "solve_resid": 0.00035380049050859374
        },
        "0.5": {
          "lambda": -0.03413947998335185,
          "Delta": 4.517541168837466,
          "floor": 2.012961451365246,
          "plateau": 6.530502620202712,
          "z1_times_a": 4.495602611843745,
          "n_zeros": 1127,
          "lock_metric": 0.0016805564393911263,
          "residual": 12.576655387504829,
          "mass_pm0.1": 127.80026124326591,
          "solve_resid": 2.361326934047527e-10
        },
        "0.9": {
          "lambda": -0.06145106397003333,
          "Delta": 4.373132705161369,
          "floor": 1.5778486637685551,
          "plateau": 5.9509813689299245,
          "z1_times_a": 4.4955360310205545,
          "n_zeros": 1141,
          "lock_metric": 0.002769088717484152,
          "residual": 14.752940107360544,
          "mass_pm0.1": 128.0191898119376,
          "solve_resid": 3.98328418223779e-14
        },
        "0.99": {
          "lambda": -0.06759617036703666,
          "Delta": 4.345367791047291,
          "floor": 1.511240245787921,
          "plateau": 5.856608036835212,
          "z1_times_a": 4.495571310035841,
          "n_zeros": 1143,
          "lock_metric": 0.0030508525265727083,
          "residual": 15.875746711965263,
          "mass_pm0.1": 128.06515694909066,
          "solve_resid": 2.4872731506462405e-14
        },
        "0.999": {
          "lambda": -0.068210681006737,
          "Delta": 4.342626921736709,
          "floor": 1.505160225931791,
          "plateau": 5.847787147668501,
          "z1_times_a": 4.495984857538869,
          "n_zeros": 1144,
          "lock_metric": 0.003716572174764971,
          "residual": 15.417387806155247,
          "mass_pm0.1": 128.0996751837004,
          "solve_resid": 6.800740851599709e-15
        }
      }
    }
  },
  "elapsed_seconds": 307.2,
  "timestamp_utc": "2026-08-19T18:39:23.796254+00:00"
}

Source code

Python script (click to expand)
"""E-025 — task 2.13: lambda-flow numerics (D-042 §3, Suzuki's lever).

At fixed lambda = -1 the family D_{a,theta} equilibrates at the
consolidation boundary (linear-K law, D-042). The remaining channel:
lambda -> lambda_a^- (bottom of the Weil-form spectrum, lambda_a ~
e^{-1.65a}), where T_a(lambda) = Q - lambda M becomes near-singular
and the ground state v_a dominates — the regime of Suzuki's sister
conjecture (1.2) (c_a v^_a -> xi(1/2+iz) = Xi(z)).

Mechanism being tested: as lambda -> lambda_a^-, the deficiency
vectors v_pm(lambda) ~ v_a/(lambda_a - lambda), so the zeros of
W(a,theta;.) flow toward the zeros of v^_a; if v^_a ~ Xi, those are
the gamma's => locking.

Per a in {4, 8, 12}:
  A. lambda_a and ground state v_a: smallest generalized eigenpair of
     the pencil (Q, M) restricted to the mean-zero subspace (QR
     projection, dense eigh; n <= 2400).
  B. sister-conjecture observable: v^_a(z) = hat_FT * sum e^{izx} v_a
     on z-grid; correlation of Re v^_a with Xi(z) (flint xi), zero
     crossings vs the first gammas.
  C. lambda ladder: lambda/lambda_a in {-1(abs), 0, 0.5, 0.9, 0.99,
     0.999}:  K_lambda(z,z) on z-grid in [14,60] (valley depth
     Delta(lambda), floor, plateau); zeros of W (theta=pi) ->
     LOCKING metric: mean over first 20 gammas of dist(gamma, nearest
     W-zero)/(pi/2a) (1.0 = chance level); residual mass R(lambda).

Checks:
  V1. lambda_a positive and consistent with the e^{-1.65a} law
      (order of magnitude; D-017/D-019).
  V2. lambda=-1 baseline reproduces E-022: z1*a in [4.3,4.7],
      Delta within 0.05 of E-022b valley depth.
  V3. Levinson residuals sane at all ladder rungs
      (|T v - b|/|b| < 1e-6 except possibly the 0.999 rung, reported).
  V4. sister observable reported (correlation v^_a vs Xi).

Outputs: research/numerics/data/E-025-lambda-flow.csv (+ .json)
"""
import importlib.util
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

import flint
import mpmath as mp
import numpy as np
import scipy.linalg
import scipy.optimize

REPO = Path(__file__).resolve().parents[3]
DATA = REPO / "research" / "numerics" / "data"
CSV = DATA / "E-025-lambda-flow.csv"
JSON = DATA / "E-025-lambda-flow.json"

_spec = importlib.util.spec_from_file_location(
    "E16", Path(__file__).with_name("E-016-herglotz-fine-structure.py"))
E16 = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(E16)                     # runs the e^20 sieve
sys.path.insert(0, str(Path(__file__).parent))
from prime_backend_v2 import PrimeBackendV2       # noqa: E402

H = E16.H_MAIN
LAM0 = E16.LAM_SHIFT          # -1.0 baseline
A_LIST = [4.0, 8.0, 12.0]
LADDER_REL = [0.0, 0.5, 0.9, 0.99, 0.999]   # x lambda_a (plus lam=-1)
Z_MAX = 300.0
Z_METRIC = 280.0
N_GRID = 40000
XN = E16.XN
mp.mp.dps = 40


def collect_t_points(a, h):
    n_elem = int(round(2 * a / h))
    pts = []

    def add_quad(lo, hi, peak):
        n_lo = int(np.ceil(np.exp(lo)))
        n_hi = int(np.floor(np.exp(hi)))
        if n_hi - n_lo > 64:
            for aa, bb in ((lo, peak), (peak, hi)):
                if bb - aa > 0:
                    mid, half = 0.5 * (aa + bb), 0.5 * (bb - aa)
                    pts.extend(mid + half * XN)
            return
        segs = 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})
        for p0, p1 in zip(segs, segs[1:]):
            if p1 - p0 > 0:
                mid, half = 0.5 * (p0 + p1), 0.5 * (p1 - p0)
                pts.extend(mid + half * XN)

    for k in range(n_elem):
        if k == 0:
            add_quad(0.0, h, 0.0)
        else:
            add_quad((k - 1) * h, (k + 1) * h, k * h)
    return np.array(pts)


def xi_real(z):
    """Xi(z) = xi(1/2 - i z) via flint (real on R)."""
    flint.ctx.prec = 120
    out = np.empty(len(z))
    for i, zz in enumerate(z):
        s = flint.acb(0.5, -float(zz))
        val = (0.5 * s * (s - 1) * flint.acb.pi() ** (-s / 2)
               * (s / 2).gamma() * s.zeta())
        out[i] = float(val.real)
    return out


def locate_zeros_pi(cp, x, h, z_max=Z_MAX, n_grid=N_GRID):
    zg = np.linspace(1e-3, z_max, n_grid)
    Vpr, _ = E16.V_and_dV(zg, cp, x, h)
    g = zg * Vpr.imag - Vpr.real

    def g_direct(zz):
        V = E16.hat_FT(np.array([zz]), h)[0] * complex(
            np.exp(1j * zz * x) @ cp)
        return zz * V.imag - V.real

    idx = np.flatnonzero(np.sign(g[:-1]) * np.sign(g[1:]) < 0)
    zeros = []
    for i in idx:
        try:
            zeros.append(scipy.optimize.brentq(g_direct, zg[i], zg[i + 1],
                                               xtol=1e-13, rtol=1e-13))
        except ValueError:
            pass
    return np.array(zeros)


def ground_state(Q, M):
    """Smallest generalized eigenpair of (Q, M) on the mean-zero
    subspace (E-009 route: QR projection)."""
    n = Q.shape[0]
    Z = np.linalg.qr(np.ones((n, 1)), mode="complete")[0][:, 1:]
    Qp = Z.T @ Q @ Z
    Mp = Z.T @ M @ Z
    evals, evecs = scipy.linalg.eigh(Qp, Mp)
    lam_a = float(evals[0])
    v = Z @ evecs[:, 0]
    return lam_a, v


def main() -> int:
    if JSON.exists() and "--force" not in sys.argv:
        print(f"cached: {JSON} (use --force to recompute)")
        return 0
    t0 = time.time()

    print("backend: load E-022 cache ...", flush=True)
    tq = np.concatenate([collect_t_points(a, H) for a in A_LIST])
    be = PrimeBackendV2(tq)
    be.load_cache(DATA / "E-022-backend-cache.npz")
    E16._BUILD.clear()
    E16.prime_sum_vec = be.prime_sum

    mp.mp.dps = 30
    gam = np.array([float(mp.zetazero(k).imag) for k in range(1, 201)])
    gam = gam[gam <= Z_METRIC]

    zgrid = np.arange(14.0, 60.0, 0.05)
    gsel = gam[(gam >= 14) & (gam < 60)]
    mids = (gsel[:-1] + gsel[1:]) / 2
    idxg = np.round((gsel - 14) / 0.05).astype(int)
    idxm = np.round((mids - 14) / 0.05).astype(int)
    XI = xi_real(zgrid)

    rows = []
    report = {"per_a": {}}
    v1_ok = v2_ok = v3_ok = True
    e022b_dip = {4.0: 2.1593, 8.0: 2.7352, 12.0: 2.9442}

    for a in A_LIST:
        ta = time.time()
        x, h, Q, M, b = E16.build(a, H)
        n = len(x)

        # A: ground state
        lam_a, v_a = ground_state(Q, M)
        if not (lam_a > 0):
            v1_ok = False
        law = np.exp(-1.65 * a)
        print(f"a={a:g}: lambda_a = {lam_a:.3e} (e^-1.65a = {law:.1e})",
              flush=True)

        # B: sister-conjecture observable
        E = np.exp(1j * np.outer(zgrid, x))
        vhat = E16.hat_FT(zgrid, h) * (E @ v_a)
        rv = vhat.real
        ca = float(np.dot(rv, XI) / np.dot(rv, rv))
        corr = float(np.corrcoef(rv, XI)[0, 1])
        # zero crossings of Re v^_a on grid
        sc = np.flatnonzero(np.sign(rv[:-1]) * np.sign(rv[1:]) < 0)
        cross = zgrid[sc]
        ncr = min(10, len(cross), len(gsel))
        xerr = float(np.mean(np.abs(cross[:ncr] - gsel[:ncr]))) \
            if ncr else float("nan")
        print(f"  sister: corr(v^,Xi) = {corr:+.4f}, {len(cross)} "
              f"crossings, mean |cross-gamma| (first {ncr}) = {xerr:.3f}",
              flush=True)

        per = {"lambda_a": lam_a, "lambda_law_ratio": lam_a / law,
               "sister_corr": corr, "sister_crossings": int(len(cross)),
               "sister_cross_err": xerr, "ladder": {}}

        # C: lambda ladder
        for tag, lam in ([("-1", LAM0)]
                         + [(f"{r:g}", r * lam_a) for r in LADDER_REL]):
            tc = Q[:, 0] - lam * (np.r_[2 * h / 3, h / 6,
                                        np.zeros(max(n - 2, 0))])
            u1 = scipy.linalg.solve_toeplitz((tc, tc), np.ones(n))
            s1 = u1.sum()
            T_dense = None  # built lazily for residual spot-checks

            def K1(z, _tc=tc, _u1=u1, _s1=s1, _chk=False):
                bz = E16.hat_FT(np.array([z]), h)[0] * np.exp(-1j * z * x)
                u0 = scipy.linalg.solve_toeplitz((_tc, _tc), bz)
                v = u0 - (u0.sum() / _s1) * _u1
                kv = float(np.real(np.conj(bz) @ v))
                if _chk:
                    Tm = scipy.linalg.toeplitz(_tc)
                    rs = float(np.max(np.abs(
                        Tm @ v + (u0.sum() / _s1) * np.ones(n) - bz))
                        / max(1.0, float(np.max(np.abs(bz)))))
                    return kv, rs
                return kv, 0.0

            lg = np.empty(len(zgrid))
            resmax = 0.0
            for i, zz in enumerate(zgrid):
                kv, rs = K1(zz, _chk=(i % 100 == 0))
                lg[i] = np.log(max(kv, 1e-300))
                resmax = max(resmax, rs)
            if resmax > 1e-6:
                v3_ok = False
            dip = float(lg[idxm].mean() - lg[idxg].mean())
            floor = float(lg[idxg].mean())
            plate = float(lg[idxm].mean())

            # zeros of W (theta=pi) at this lambda (Schur form,
            # validated 3.6e-14 in E-022)
            u0p = scipy.linalg.solve_toeplitz((tc, tc), b[+1.0])
            cp = u0p - (u0p.sum() / s1) * u1
            zeros = locate_zeros_pi(cp, x, h)
            zm = zeros[zeros <= Z_METRIC]
            # locking: mean normalized distance of first 20 gammas
            d20 = []
            for g in gam[:20]:
                d20.append(np.min(np.abs(zm - g)))
            lock = float(np.mean(d20) / (np.pi / (2 * a)))
            # weights & residual
            ws = np.array([1.0 / K1(z)[0] for z in zm])
            near_idx = np.array([np.argmin(np.abs(zm - g)) for g in gam])
            resid = float(ws.sum() - ws[near_idx].sum())
            m01 = float(ws[np.minimum.reduce([
                np.abs(zm - g) for g in gam]).reshape(-1) <= 0.1].sum()) \
                if len(zm) else 0.0

            per["ladder"][tag] = {
                "lambda": lam, "Delta": dip, "floor": floor,
                "plateau": plate, "z1_times_a": float(zeros[0] * a),
                "n_zeros": int(len(zeros)),
                "lock_metric": lock, "residual": resid,
                "mass_pm0.1": m01, "solve_resid": resmax,
            }
            print(f"  lam={tag}*lam_a ({lam:.3e}): Delta={dip:.3f}, "
                  f"z1a={zeros[0] * a:.3f}, lock={lock:.3f}, "
                  f"resid={resid:.1f}, K-resid={resmax:.1e}", flush=True)
            for zz, ll in zip(zgrid, lg):
                rows.append((a, tag, zz, ll))

        # V2: baseline checks (lambda = -1)
        base = per["ladder"]["-1"]
        if not (4.3 < base["z1_times_a"] < 4.7):
            v2_ok = False
        if abs(base["Delta"] - e022b_dip[a]) > 0.05:
            v2_ok = False
        report["per_a"][f"{a:g}"] = per
        print(f"a={a:g} done ({time.time() - ta:.0f}s)", flush=True)

    checks = {"V1_lambda_a_positive_law": v1_ok,
              "V2_baseline_matches_E022": v2_ok,
              "V3_solve_residuals": v3_ok,
              "V4_sister_reported": True}

    import csv as csvmod
    with open(CSV, "w", newline="") as f:
        w = csvmod.writer(f)
        w.writerow(["a", "lambda_tag", "z", "logK"])
        for a, tag, zz, ll in rows:
            w.writerow([f"{a:g}", tag, f"{zz:.3f}", f"{ll:.6f}"])

    meta = {
        "experiment": "E-025",
        "task": "2.13: lambda-flow numerics (ground-state channel)",
        "script": "research/numerics/scripts/E-025-lambda-flow.py",
        "parameters": {"a_list": A_LIST, "ladder_rel": LADDER_REL,
                       "z_max": Z_MAX},
        "checks": checks,
        "key_values": report["per_a"],
        "elapsed_seconds": round(time.time() - t0, 1),
        "timestamp_utc": datetime.now(timezone.utc).isoformat(),
    }
    JSON.write_text(json.dumps(meta, indent=2))
    print(f"wrote {CSV.name}, {JSON.name}; elapsed "
          f"{meta['elapsed_seconds']}s", flush=True)
    return 0 if all(checks.values()) else 1


if __name__ == "__main__":
    sys.exit(main())