validate_branch.py — src/21-gravity

The script src/21-gravity/validate_branch.py of the validation package of the paper 21-grav, rendered with an anchor per line. ▶ run the predicates on Google Colab.

21-gravity ledgersrc/21-gravity/validate_branch.py · 4.7 KBvalidate_branch.pyColab
1"""The nonlinear completion (Theorem: no self-sourcing, exponential reading).23Validates the two exact arguments determining the nonlinear completion:45(A) Ward identity / exact Gauss law: in the full SINE-coupled (nonlinear) static6    model on Z^3, the synchronisation flux through every closed surface equals the7    enclosed source demand exactly -- for one source, for two sources, and at8    strong gradients -- and the deviation of the nonlinear potential from the9    linear one scales as the CUBE of the source (field-strength corrections,10    (r_*/r)^4 type), never as the square (no u^2 self-sourcing).1112(B) The reading characterisation (symbolic): isotropic Schwarzschild is the13    exponential reading of the self-sourced potential psi = 2 artanh(U/2)14    (series identity verified), psi is not harmonic (Laplacian ~ (Gm)^3/r^5),15    while the exponential branch reads the harmonic u; and the exponential is16    the unique solution of the composition law R(a+b) = R(a)R(b), which the17    Schwarzschild reading violates.18"""19import numpy as np20import sympy as sp2122ok = True2324# ---------- (A) exact Gauss law in the nonlinear sine model ----------25N = 31                                     # lattice [0,N)^3, sources interior26def solve(sources, sine=True, iters=30000, tol=1e-13):27    """Relax kappa * sum_y sin(u_y - u_x) + m_x = 0 with u = 0 on the boundary."""28    u = np.zeros((N, N, N))29    m = np.zeros((N, N, N))30    for (pos, mass) in sources:31        m[pos] = mass32    for it in range(iters):33        nb = (np.roll(u, 1, 0), np.roll(u, -1, 0), np.roll(u, 1, 1),34              np.roll(u, -1, 1), np.roll(u, 1, 2), np.roll(u, -1, 2))35        if sine:36            drive = sum(np.sin(v - u) for v in nb) + m37        else:38            drive = sum(v - u for v in nb) + m39        drive[0, :, :] = drive[-1, :, :] = 0   # Dirichlet boundary40        drive[:, 0, :] = drive[:, -1, :] = 041        drive[:, :, 0] = drive[:, :, -1] = 042        u += 0.15*drive43        if it % 500 == 0 and np.max(np.abs(drive)) < tol:44            break45    return u, np.max(np.abs(drive))4647def flux_box(u, c, R, sine=True):48    """Outward sine-flux through the surface of the cube of half-size R about c."""49    F = 0.050    lo = [ci - R for ci in c]; hi = [ci + R for ci in c]51    couple = np.sin if sine else (lambda x: x)52    for ax in range(3):53        for face, outw in ((lo[ax], -1), (hi[ax], +1)):54            sl_in = [slice(lo[a], hi[a] + 1) for a in range(3)]55            sl_out = list(sl_in)56            sl_in[ax] = face57            sl_out[ax] = face + outw58            F += np.sum(couple(u[tuple(sl_in)] - u[tuple(sl_out)]))59    return F6061c = (N//2, N//2, N//2)62m0 = 4.0                                    # strong: sin argument ~ 0.55 at first shell63u1, res = solve([(c, m0)])64print(f"  (A) single source m={m0}, solver residual {res:.1e}")65for R in (2, 4, 6, 9):66    F = flux_box(u1, c, R)67    print(f"      flux through box R={R}: {F:.10f}  (demand {m0})")68    if abs(F - m0) > 1e-6: ok = False6970c2 = (N//2 + 5, N//2, N//2)71u2, _ = solve([(c, m0), (c2, 2.0)])72F2 = flux_box(u2, (N//2 + 2, N//2, N//2), 12)73print(f"      two sources, enclosing box: flux = {F2:.10f}  (demand {m0 + 2.0})")74if abs(F2 - (m0 + 2.0)) > 1e-6: ok = False7576# no u^2 self-sourcing: ||u_sine - u_linear|| scales as m^3 (arcsin x = x + x^3/6 + ...)77devs = []78for mm in (1.0, 2.0):79    us, _ = solve([(c, mm)])80    ul, _ = solve([(c, mm)], sine=False)81    devs.append(np.max(np.abs(us - ul)))82p = np.log(devs[1]/devs[0])/np.log(2.0)83print(f"      nonlinear deviation scaling: ||u_sin - u_lin|| ~ m^{p:.2f}  (target 3: cubic, no m^2 term)")84if abs(p - 3) > 0.25: ok = False8586# ---------- (B) symbolic characterisation of the two readings ----------87U = sp.symbols('U', positive=True)88psi = 2*sp.atanh(U/2)89schw = ((1 - U/2)/(1 + U/2))**290ident = sp.simplify(sp.powsimp(sp.expand(sp.exp(-2*psi.rewrite(sp.log)))) - schw)91print(f"  (B) g00(Schwarzschild isotropic) == exp(-2 psi), psi = 2 artanh(U/2): residual {ident}")92if ident != 0: ok = False9394r, Gm = sp.symbols('r Gm', positive=True)95psi_r = (Gm/r) + (Gm/r)**3/12               # psi to the order that decides96lap = sp.simplify(sp.diff(r**2*sp.diff(psi_r, r), r)/r**2)97print(f"      Laplacian(psi) = {lap}  (nonzero: psi is self-sourced; u = Gm/r is harmonic)")98if sp.simplify(lap) == 0: ok = False99100a, b = sp.symbols('a b', positive=True)101expo_ok = sp.simplify(sp.exp(-(a + b)) - sp.exp(-a)*sp.exp(-b)) == 0102f = lambda x: (1 - x/2)/(1 + x/2)103schw_viol = sp.simplify(f(a + b) - f(a)*f(b))104print(f"      composition law: exponential satisfies R(a+b)=R(a)R(b): {expo_ok}; "105      f"Schwarzschild reading violates it (mismatch {sp.expand(schw_viol)} != 0)")106if not expo_ok or sp.simplify(schw_viol) == 0: ok = False107108print("PASS" if ok else "FAIL", "- branch decision (Ward identity + composition law)")109