#!/usr/bin/env python
"""
einstein_residual — audit a learned holographic bulk metric against the equation it is
interpreted in.

For the AdS/DL ansatz (Hashimoto et al. 2018)  ds^2 = -f dt^2 + d eta^2 + g dx^2  with the
probe scalar in a FIXED background, the background must solve vacuum Einstein with negative
cosmological constant. For the combination the network learns, h(eta) = d/d eta ln sqrt(f g^(d-1)),
that equation is the single ODE (their S.34, general d):

        R[h](eta) = -(d/L)^2 + h'(eta) + h(eta)^2 = 0,

whose regular black-brane family is  h(eta) = (d/L) coth(d (eta - eta0) / L)  (the horizon
parameter r0 drops out; only L and the horizon location eta0 remain).

Usage:
    python einstein_residual.py --d 3 --L 1 --eta 1.0 0.9 ... --h 2.9 3.08 ...  [--ref-h ...]
    python einstein_residual.py --json results_R1.json --key A/h_mean --d 3 --L 1
Outputs: residual per slice, RMS residual, distance to the nearest vacuum solution (fit over
L, eta0), and — if a reference metric is given — distance to it. Both distances in the same
units, so "how far from ANY solution" and "how far from the generating solution" can be
compared directly.
"""
import argparse
import json
import sys
import numpy as np


def residual(eta, h, d, L):
    """-(d/L)^2 + h' + h^2 at slice midpoints; h' by finite difference (any ordering of eta)."""
    eta = np.asarray(eta, float); h = np.asarray(h, float)
    order = np.argsort(eta); eta, h = eta[order], h[order]
    hp = np.diff(h) / np.diff(eta)
    hm = 0.5 * (h[1:] + h[:-1])
    em = 0.5 * (eta[1:] + eta[:-1])
    return em, -(d / L) ** 2 + hp + hm ** 2


def family(eta, d, L, eta0):
    x = d * (eta - eta0) / L
    with np.errstate(divide="ignore", invalid="ignore"):
        return (d / L) / np.tanh(x)


def nearest_solution(eta, h, d, L_grid=None, eta0_grid=None):
    """Least-squares nearest member of the vacuum family over (L, eta0)."""
    eta = np.asarray(eta, float); h = np.asarray(h, float)
    L_grid = np.linspace(0.3, 3.0, 271) if L_grid is None else L_grid
    eta0_grid = np.linspace(-1.0, eta.min() - 1e-3, 200) if eta0_grid is None else eta0_grid
    best = (np.inf, None, None)
    for L in L_grid:
        for e0 in eta0_grid:
            hs = family(eta, d, L, e0)
            if not np.all(np.isfinite(hs)):
                continue
            dist = np.sqrt(np.mean((hs - h) ** 2))
            if dist < best[0]:
                best = (dist, L, e0)
    return dict(distance=best[0], L=best[1], eta0=best[2], h_fit=family(eta, d, best[1], best[2]).tolist())


def audit(eta, h, d, L, ref_h=None):
    em, R = residual(eta, h, d, L)
    out = dict(eta_mid=em.tolist(), residual=R.tolist(), rms_residual=float(np.sqrt(np.mean(R ** 2))),
               max_abs_residual=float(np.max(np.abs(R))))
    out["nearest_vacuum_solution"] = nearest_solution(eta, h, d)
    if ref_h is not None:
        ref_h = np.asarray(ref_h, float)
        out["distance_to_reference"] = float(np.sqrt(np.mean((np.asarray(h) - ref_h) ** 2)))
        _, Rref = residual(eta, ref_h, d, L)
        out["reference_rms_residual"] = float(np.sqrt(np.mean(Rref ** 2)))
    return out


def main(argv=None):
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("--d", type=int, default=3, help="boundary spacetime dimension (bulk is d+1)")
    p.add_argument("--L", type=float, default=1.0, help="AdS radius assumed by the paper")
    p.add_argument("--eta", type=float, nargs="*", help="radial slices")
    p.add_argument("--h", type=float, nargs="*", help="learned h at those slices")
    p.add_argument("--ref-h", type=float, nargs="*", help="reference (generating) h at the same slices")
    p.add_argument("--json", help="read h from a results JSON file")
    p.add_argument("--key", help="path/in/json to the h array, e.g. A/h_mean")
    p.add_argument("--eta-default", action="store_true", help="use the AdS/DL grid eta = 1.0, 0.9, ..., 0.2")
    a = p.parse_args(argv)
    if a.json:
        obj = json.load(open(a.json))
        for k in a.key.split("/"):
            obj = obj[k]
        h = np.array(obj, float)
        eta = np.array(a.eta, float) if a.eta else np.round(np.arange(1.0, 0.2 - 1e-9, -0.1), 10)[:len(h)]
    else:
        eta, h = np.array(a.eta, float), np.array(a.h, float)
    out = audit(eta, h, a.d, a.L, a.ref_h)
    print(f"d={a.d} L={a.L}  slices={len(h)}")
    print("eta_mid   residual")
    for e, r in zip(out["eta_mid"], out["residual"]):
        print(f"{e:6.2f}  {r:+8.3f}")
    print(f"RMS residual                 : {out['rms_residual']:.3f}   (max |R| {out['max_abs_residual']:.3f})")
    ns = out["nearest_vacuum_solution"]
    print(f"nearest vacuum solution      : L={ns['L']:.3f} eta0={ns['eta0']:+.3f}  RMS distance {ns['distance']:.3f}")
    if "distance_to_reference" in out:
        print(f"distance to generating metric: {out['distance_to_reference']:.3f}   (its RMS residual {out['reference_rms_residual']:.3f})")
    return out


# --------------------------------------------------------------------------- extra modes
def polynomial_family_audit(learned, exact, z_grid):
    """learned, exact: coefficient lists b_0..b_n of f(z) = sum b_i z^i. Returns per-coefficient
    deviations and the RMS distance of the two functions over z_grid."""
    n = max(len(learned), len(exact))
    lb = np.pad(np.asarray(learned, float), (0, n - len(learned)))
    ex = np.pad(np.asarray(exact, float), (0, n - len(exact)))
    z = np.asarray(z_grid, float)
    fl = sum(c * z ** i for i, c in enumerate(lb))
    fe = sum(c * z ** i for i, c in enumerate(ex))
    return dict(coefficient_deviation=(lb - ex).tolist(),
                rms_distance=float(np.sqrt(np.mean((fl - fe) ** 2))),
                rms_scale=float(np.sqrt(np.mean(fe ** 2))))


def hqcd_h(eta, L, b1, b3, b5):
    """Hashimoto–Ohashi–Sumimoto 2022 ansatz: h(eta) = 1/eta + b1 eta + b3 eta^3 + b5 eta^5."""
    eta = np.asarray(eta, float)
    return 1.0 / eta + b1 * eta + b3 * eta ** 3 + b5 * eta ** 5


def hqcd_vacuum_audit(L, b1, b3, b5, eta=None, d=4):
    """Vacuum AdS_{d+1} residual of an hQCD learned h. NOTE: in that line h includes the dilaton,
    h = d/d eta log sqrt(f g^3 e^{-Phi}), so vacuum Einstein is the WRONG equation by the paper's
    own construction; a large residual here is expected and is the scope demonstration."""
    eta = np.linspace(0.2, 1.0, 9) if eta is None else np.asarray(eta, float)
    h = hqcd_h(eta, L, b1, b3, b5)
    return audit(eta, h, d, L)


if __name__ == "__main__":
    main()
