#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
blindspot_tests.py  --  Test numerique des consequences de l'entree 045
"L'angle mort de l'univers".

On verifie, avec de vrais calculs, ce que la prose affirme :

  1. L'angle mort est-il GEOMETRIQUE ?  (trace orthogonale au TT,
     fraction du tenseur d'energie visible par le canal TT)
  2. Temperature effective T_eff(L) = hbar c / (2 pi k_B L)  et son
     identite avec la temperature de de Sitter d'une region de taille L.
  3. Decoherence DEPENDANTE DE LA FORME : moment quadrupolaire sans trace
     d'une sphere / d'un disque / d'un haltere / d'un cigare.
  4. Rapport fluctuation-dissipation Gamma/gamma ~ 1/L (test croise).
  5. Energie noire : ordre de grandeur, ce qui est PREDIT vs POSTULE.

Aucune donnee fabriquee. Les seuls intrants sont les constantes physiques
mesurees et la forme du couplage (TT, projecteur transverse-sans-trace).
"""

import numpy as np

# ----------------------------------------------------------------------
# Constantes (SI, CODATA / valeurs cosmologiques standard)
# ----------------------------------------------------------------------
hbar = 1.054571817e-34      # J s
c    = 2.99792458e8         # m/s
kB   = 1.380649e-23         # J/K
G    = 6.67430e-11          # m^3 kg^-1 s^-2
H0   = 2.1927e-18           # s^-1   (67.66 km/s/Mpc, Planck 2018)
rho_c = 3 * H0**2 / (8 * np.pi * G)   # densite critique, kg/m^3
Omega_L = 0.6889            # Planck 2018

def line(t=""):
    print(t)

def header(t):
    print("\n" + "=" * 72)
    print(t)
    print("=" * 72)

# ----------------------------------------------------------------------
# Outils : projecteur transverse-sans-trace (TT) en 3D
# ----------------------------------------------------------------------
def tt_projector(n):
    """Projecteur TT Lambda_{ij,kl}(n) pour une direction d'onde n (unitaire).
       P = I - n n^T ;  Lambda_{ij,kl} = P_ik P_jl - 1/2 P_ij P_kl."""
    n = n / np.linalg.norm(n)
    P = np.eye(3) - np.outer(n, n)
    Lam = (np.einsum('ik,jl->ijkl', P, P)
           - 0.5 * np.einsum('ij,kl->ijkl', P, P))
    return Lam

def project_tt(T, n):
    return np.einsum('ijkl,kl->ij', tt_projector(n), T)

def trace_part(T):
    return np.eye(3) * np.trace(T) / 3.0

def fro(A):
    return float(np.sqrt(np.sum(A * A)))

# ----------------------------------------------------------------------
# TEST 1 -- L'angle mort est geometrique
# ----------------------------------------------------------------------
def test1():
    header("TEST 1  --  L'angle mort est-il geometrique ? (trace vs TT)")
    rng = np.random.default_rng(0)

    # (a) orthogonalite trace / TT, et fraction visible, sur tenseurs aleatoires
    inner_max = 0.0
    visible = []
    for _ in range(20000):
        A = rng.standard_normal((3, 3))
        T = (A + A.T) / 2.0                      # symetrique aleatoire
        n = rng.standard_normal(3)
        Ttt = project_tt(T, n)
        Ttr = trace_part(T)
        inner_max = max(inner_max, abs(np.sum(Ttt * Ttr)))   # <TT, trace>
        visible.append(fro(Ttt)**2 / fro(T)**2)
    visible = np.array(visible)
    line(f"  <T^TT , T^trace>  (max sur 20000 tirages)  = {inner_max:.2e}")
    line(f"     -> trace et TT sont orthogonaux a la precision machine.")
    line(f"  Fraction d'energie du tenseur vue par le canal TT :")
    line(f"     moyenne = {visible.mean()*100:5.1f} %   (ecart-type {visible.std()*100:.1f} %)")

    # (b) un tenseur PUR monopole (pression isotrope) : T_ij = p * delta_ij
    p = 3.7
    Tmono = p * np.eye(3)
    f = [fro(project_tt(Tmono, rng.standard_normal(3)))**2 / fro(Tmono)**2
         for _ in range(1000)]
    line("")
    line(f"  Pression isotrope pure  T_ij = p*delta_ij  (le secteur de Lambda) :")
    line(f"     fraction vue par TT = {max(f):.2e}   (sur 1000 directions)")
    line(f"     -> STRICTEMENT INVISIBLE. Le bain ne la voit jamais, quel que")
    line(f"        soit lambda ou N : c'est orthogonal, pas faible.")

    # (c) decompte des degres de liberte
    line("")
    line("  Decompte des d.o.f. d'un tenseur symetrique 3x3 (6 au total) :")
    line("     trace (monopole, pression/Lambda) ......... 1   [AVEUGLE]")
    line("     TT (quadrupole, cisaillement/marees) ...... 2   [VU]")
    line("     longitudinal/vecteur (jauge) .............. 3   [non couple]")

# ----------------------------------------------------------------------
# TEST 2 -- Temperature effective et lien de Sitter
# ----------------------------------------------------------------------
def test2():
    header("TEST 2  --  T_eff(L) = hbar c / (2 pi k_B L)")
    pref = hbar * c / (2 * np.pi * kB)           # m*K
    line(f"  Prefacteur hbar c / (2 pi k_B) = {pref:.3e}  m.K")
    line("")
    line("    L (taille de region)        T_eff")
    for L, lab in [(1e-9,"1 nm"),(1e-7,"100 nm"),(1e-6,"1 um"),
                   (1e-4,"100 um"),(1e-3,"1 mm"),(1.0,"1 m"),
                   (c/H0,"rayon de Hubble c/H0")]:
        line(f"    {lab:<22}{pref/L:.3e} K")
    # identite de Sitter
    T_dS = hbar * H0 / (2 * np.pi * kB)
    T_eff_H = pref / (c / H0)
    line("")
    line(f"  Au rayon de Hubble, T_eff = {T_eff_H:.3e} K")
    line(f"  Temperature de de Sitter  T_dS = hbar H0/(2 pi k_B) = {T_dS:.3e} K")
    line(f"  Ecart relatif = {abs(T_eff_H-T_dS)/T_dS:.1e}")
    line("  -> IDENTITE. La meme formule qui regle la decoherence en labo")
    line("     reproduit la temperature de l'horizon cosmologique. Le secteur")
    line("     aveugle (Lambda) et le canal visible partagent une seule echelle.")

# ----------------------------------------------------------------------
# TEST 3 -- Decoherence dependante de la forme : quadrupole sans trace
# ----------------------------------------------------------------------
def quadrupole_traceless(points, masses):
    """Q_ij = sum m (3 x_i x_j - r^2 delta_ij), puis on retire la trace
       (elle est deja sans trace par construction). Renvoie ||Q||_F."""
    Q = np.zeros((3, 3))
    for m, x in zip(masses, points):
        r2 = np.dot(x, x)
        Q += m * (3 * np.outer(x, x) - r2 * np.eye(3))
    return Q

def sample_shape(kind, n=4000, R=1.0, rng=None):
    """Nuage de points uniforme d'une forme de meme 'taille' R et meme masse 1."""
    rng = rng or np.random.default_rng(1)
    if kind == "sphere":
        v = rng.standard_normal((n, 3)); v /= np.linalg.norm(v, axis=1, keepdims=True)
        rad = R * rng.random(n) ** (1/3); pts = v * rad[:, None]
    elif kind == "disk":            # aplati (pancake)
        v = rng.standard_normal((n, 3)); v /= np.linalg.norm(v, axis=1, keepdims=True)
        rad = R * rng.random(n) ** (1/3); pts = v * rad[:, None]
        pts[:, 2] *= 0.15
    elif kind == "cigar":           # allonge (prolate)
        v = rng.standard_normal((n, 3)); v /= np.linalg.norm(v, axis=1, keepdims=True)
        rad = R * rng.random(n) ** (1/3); pts = v * rad[:, None]
        pts[:, 2] *= 3.0
    elif kind == "dumbbell":        # deux lobes
        half = n // 2
        a = rng.standard_normal((half, 3)) * 0.15 * R + np.array([0, 0,  0.9 * R])
        b = rng.standard_normal((n - half, 3)) * 0.15 * R + np.array([0, 0, -0.9 * R])
        pts = np.vstack([a, b])
    masses = np.full(len(pts), 1.0 / len(pts))   # masse totale = 1
    # recentrer sur le barycentre
    com = np.average(pts, axis=0, weights=masses)
    pts = pts - com
    return pts, masses

def test3():
    header("TEST 3  --  Decoherence DEPENDANTE DE LA FORME (quadrupole sans trace)")
    line("  Couplage TT -> taux relationnel proportionnel a ||Q||^2 (quadrupole")
    line("  sans trace de la difference de masse entre les deux branches).")
    line("  Meme masse (1) et meme taille (R=1) pour toutes les formes.")
    line("")
    rng = np.random.default_rng(2)
    line(f"    {'forme':<12}{'||Q|| (quadrupole)':<22}{'taux relatif ~||Q||^2'}")
    base = None
    for kind in ["sphere", "disk", "cigar", "dumbbell"]:
        pts, m = sample_shape(kind, rng=rng)
        Q = quadrupole_traceless(pts, m)
        nQ = fro(Q)
        if base is None or kind == "sphere":
            base_sphere = nQ
        line(f"    {kind:<12}{nQ:<22.4e}{nQ**2:.4e}")
    line("")
    line("  -> La SPHERE a un quadrupole nul a la fluctuation d'echantillonnage")
    line("     pres : invisible au bain (elle ne vit que dans le monopole).")
    line("     Disque, cigare, haltere : quadrupole franc -> decoherence visible.")
    line("     PREDICTION : a masse et taille egales, le taux de decoherence")
    line("     relationnel suit ||Q||^2 et s'annule pour une sphere parfaite.")

# ----------------------------------------------------------------------
# TEST 4 -- Rapport FDT Gamma/gamma ~ 1/L
# ----------------------------------------------------------------------
def test4():
    header("TEST 4  --  Test croise fluctuation-dissipation : Gamma/gamma ~ T_eff ~ 1/L")
    line("  Le theoreme fluctuation-dissipation lie le bruit (decoherence Gamma)")
    line("  a la friction (gamma) via la temperature du bain :  Gamma/gamma = ")
    line("  (2 k_B T_eff)/hbar (a un facteur d'ordre 1 pres dependant du couplage).")
    line("  Ici T_eff = hbar c/(2 pi k_B L), donc Gamma/gamma = c/(pi L).")
    line("")
    line(f"    {'L':<14}{'T_eff (K)':<16}{'Gamma/gamma = c/(pi L)  [s^-1]'}")
    pref = hbar * c / (2 * np.pi * kB)
    for L, lab in [(1e-6,"1 um"),(1e-5,"10 um"),(1e-4,"100 um"),(1e-3,"1 mm")]:
        line(f"    {lab:<14}{pref/L:<16.3e}{c/(np.pi*L):.3e}")
    line("")
    line("  -> Signature falsifiable : en variant L sur une decade, le rapport")
    line("     Gamma/gamma doit suivre 1/L. Ni Gamma seul ni gamma seul ne")
    line("     tranchent : c'est leur RAPPORT, mesure dans le MEME mode, qui")
    line("     distingue le bain relationnel d'un bruit thermique ordinaire.")

# ----------------------------------------------------------------------
# TEST 5 -- Energie noire : ce qui est predit vs postule
# ----------------------------------------------------------------------
def test5():
    header("TEST 5  --  Energie noire : ordre de grandeur, predit vs postule")
    rho_L = Omega_L * rho_c                         # kg/m^3
    rho_L_J = rho_L * c**2                           # J/m^3
    Lambda = 3 * Omega_L * H0**2 / c**2              # m^-2
    line(f"  Densite critique      rho_c   = {rho_c:.3e} kg/m^3")
    line(f"  Densite d'energie noire rho_L = {rho_L:.3e} kg/m^3 = {rho_L_J:.3e} J/m^3")
    line(f"  Constante cosmologique  Lambda = {Lambda:.3e} m^-2")
    line(f"  Echelle de longueur     L_Lambda = 1/sqrt(Lambda) = {1/np.sqrt(Lambda):.3e} m")
    line("")
    # le fameux desaccord avec l'energie du vide QFT (coupure de Planck)
    Mpl = np.sqrt(hbar * c / G)                      # masse de Planck (kg)
    lpl = np.sqrt(hbar * G / c**3)                   # longueur de Planck (m)
    rho_planck_J = (c**2/ (lpl**3)) * (Mpl)          # ~ energie de Planck / volume de Planck
    # densite d'energie du vide naive ~ E_planck / l_planck^3
    Epl = np.sqrt(hbar * c**5 / G)                   # energie de Planck (J)
    rho_vac_naive = Epl / lpl**3
    line(f"  Densite de vide QFT naive (coupure Planck) ~ {rho_vac_naive:.3e} J/m^3")
    line(f"  Rapport naive/observe = {rho_vac_naive/rho_L_J:.3e}")
    line(f"     -> le fameux ~10^120. Le cadre ne le RESOUT pas.")
    line("")
    line("  CE QUI EST PREDIT (consequence de l'angle mort) :")
    line("   - Lambda vit dans la trace -> aucun couplage au canal TT du bain.")
    line("     Donc l'energie noire NE doit PAS se decoherer, ni rayonner, ni")
    line("     participer aux marees : elle est 'sombre' par geometrie.")
    line("   - L'echelle 1/sqrt(Lambda) coincide avec le rayon de Hubble c/H0 :")
    L_L = 1/np.sqrt(Lambda); L_H = c/H0
    line(f"        1/sqrt(Lambda) = {L_L:.3e} m   ;   c/H0 = {L_H:.3e} m")
    line(f"        rapport = {L_L/L_H:.3f}  (meme echelle, cf. T_eff de Sitter du test 2).")
    line("")
    line("  CE QUI RESTE POSTULE (honnete) :")
    line("   - La VALEUR de Lambda n'est pas derivee. Le cadre dit pourquoi elle")
    line("     est sombre et decouplee, pas pourquoi elle vaut ce qu'elle vaut.")
    line("   - La reinjection de la trace (Bianchi) reste imposee du dehors.")

if __name__ == "__main__":
    print("\nTESTS NUMERIQUES  --  Entree 045 'L'angle mort de l'univers'")
    print("Framework Bath-TT  --  emergent-gravity.com")
    test1(); test2(); test3(); test4(); test5()
    print("\n" + "=" * 72)
    print("FIN. Calculs reproductibles : python3 blindspot_tests.py")
    print("=" * 72)
