#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
psiqrh_core_bench.py — Benchmark do NÚCLEO GEOMÉTRICO do embedding ΨQRH.

Escopo deliberadamente estreito. Não testamos a "stack" (RiemannSphere +
atenção + weight tying): aquilo é engenharia de arquitetura e já foi medido
alhures com resultado inconclusivo por falta de dados com estrutura. Testamos
aqui apenas o que é matemática verificável:

    f(λ,t) = I0 · sin(ωt + αλ) · cos(ωt − kλ + βλ²) + 0.1·λ

Cinco experimentos, cada um com hipótese e critério de falsificação
declarados ANTES da execução. Nenhum usa dados aleatórios sem estrutura, e
nenhum depende de uma única seed.

  E1  O acoplamento é real e estrutural?     (álgebra, não estatística)
  E2  A curvatura separa este embedding?     (refutação da afirmação do registro)
  E3  A curvatura sobrevive ao treino?       (estabilidade dos parâmetros)
  E4  O embedding ajuda numa tarefa real?    (tarefa sintética COM estrutura)
  E5  O ganho é do acoplamento ou do acaso?  (ablação + 10 seeds, IC 95%)

Reprodutibilidade: seeds fixas, torch determinístico, tudo em CPU/GPU
padronizado. Tempo total ~6 min numa RTX 5060 Ti.

Uso:  python3 psiqrh_core_bench.py            # roda tudo
      python3 psiqrh_core_bench.py --quick    # versão curta
"""
# ─────────────────────────────────────────────────────────────────────
# NOTEBOOK COLAB — ΨQRH benchmark do núcleo geométrico
#
# Rode a célula inteira (Runtime → Run all). Leva ~2-7 min:
#   GPU (T4 ou melhor) ~2 min   |   CPU ~7 min
#
# Saída: psiqrh_core_results.json (baixa automaticamente no fim)
# Também imprime a tabela completa no console.
#
# Este arquivo é autossuficiente: só precisa de torch e numpy, que já
# vêm no Colab. Nada de clonar repositório.
# ─────────────────────────────────────────────────────────────────────
import json, math, os, sys, time
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

QUICK = "--quick" in sys.argv
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
try:
    _here = os.path.dirname(os.path.abspath(__file__))
except NameError:          # Colab / exec(): sem __file__
    _here = os.getcwd()
OUT = os.path.join(_here, "psiqrh_core_results.json")

# ── determinismo ────────────────────────────────────────────────────────
def fixar(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

RESULTADOS = {}
def registrar(exp, **kw):
    RESULTADOS[exp] = kw

def secao(t):
    print("\n" + "=" * 74)
    print(t)
    print("=" * 74)


# ══════════════════════════════════════════════════════════════════════
# E1 — O ACOPLAMENTO É REAL E ESTRUTURAL?
#
# Hipótese: para o embedding ΨQRH, a derivada mista ∂²f/∂λ∂t é não-nula
# em quase todo o domínio; para um embedding separável (wte+wpe) é
# identicamente nula.
#
# Falsificação: se f_λt for nula em mais de 0,1% dos pontos do domínio,
# a hipótese cai.
#
# Método: avaliação analítica (forma fechada) em grade densa. Nenhuma
# diferença finita — foi exatamente isso que o benchmark original errou.
# ══════════════════════════════════════════════════════════════════════
def e1_acoplamento():
    secao("E1 — O acoplamento é real e estrutural?")

    I0, OMEGA, K_W = 0.871, 2.011, 1.635

    def f_lambda_t(lam, t, alpha, beta):
        p1 = OMEGA*t + alpha*lam
        p2 = OMEGA*t - K_W*lam + beta*lam*lam
        return -I0*OMEGA*(alpha - K_W + 2*beta*lam)*np.sin(p1 + p2)

    # grade densa sobre o domínio apresentado no artigo
    lams = np.linspace(-3.0, 3.0, 400)
    ts   = np.linspace(0.0, 6.0, 400)
    L, T = np.meshgrid(lams, ts)

    print(f"  grade: {L.size:,} pontos em λ∈[-3,3], t∈[0,6]")
    print(f"  parâmetros: I0={I0}, ω={OMEGA}, k={K_W}, α=1.423, β=0.348\n")

    saida = {}
    for nome, alpha, beta, separavel in [
        ("ΨQRH (α=1.423, β=0.348)", 1.423, 0.348, False),
        ("ΨQRH (α=0)",              0.000, 0.348, False),
        ("ΨQRH (β=0)",              1.423, 0.000, False),
        ("separável wte+wpe",       0.000, 0.000, True),
    ]:
        g = np.zeros_like(L) if separavel else f_lambda_t(L, T, alpha, beta)
        nz = int(np.count_nonzero(np.abs(g) > 1e-9))
        frac = nz / g.size * 100
        maxabs = float(np.abs(g).max())
        print(f"  {nome:<26} f_λt≠0 em {frac:6.2f}%  |max|={maxabs:.4f}")
        saida[nome] = {"fracao_nao_nula": frac, "max_abs": maxabs}

    # critério declarado
    frac_psiqrh = saida["ΨQRH (α=1.423, β=0.348)"]["fracao_nao_nula"]
    frac_sep = saida["separável wte+wpe"]["fracao_nao_nula"]
    passou = frac_psiqrh > 99.9 and frac_sep < 0.1
    print(f"\n  CRITÉRIO: ΨQRH > 99,9% e separável < 0,1%")
    print(f"  RESULTADO: {'PASSOU' if passou else 'FALHOU'}")
    print(f"  Interpretação: o acoplamento é uma propriedade algébrica do")
    print(f"  embedding, não um artefato de medição. É o resultado mais sólido")
    print(f"  do registro e não depende de treino.")

    registrar("E1_acoplamento", passou=passou, por_config=saida)
    return passou


# ══════════════════════════════════════════════════════════════════════
# E2 — A CURVATURA SEPARA ESTE EMBEDDING?
#
# A seção 6.3 do registro afirma: "K=0 é propriedade de embeddings
# lineares (wte+wpe), não de embeddings paramétricos".
#
# Hipótese alternativa: FALSO. Um embedding separável tem K≠0 sempre que
# L''(λ)·P''(t) ≠ 0, porque f_xy=0 mas f_xx e f_yy não são.
#
# Falsificação da minha hipótese: se o separável tiver K≈0 em todo o
# domínio, o registro estava certo.
# ══════════════════════════════════════════════════════════════════════
def e2_curvatura_nao_separa():
    secao("E2 — A curvatura separa ΨQRH de um embedding linear?")

    I0, OMEGA, K_W = 0.871, 2.011, 1.635

    def K_acoplado(lam, t, alpha, beta):
        p1 = OMEGA*t + alpha*lam
        p2 = OMEGA*t - K_W*lam + beta*lam*lam
        d  = -K_W + 2*beta*lam
        s1, c1 = np.sin(p1), np.cos(p1)
        s2, c2 = np.sin(p2), np.cos(p2)
        sp = np.sin(p1 + p2)
        fx  = I0*(alpha*c1*c2 - d*s1*s2) + 0.1
        fy  = I0*OMEGA*(c1*c2 - s1*s2)
        fxx = I0*(-alpha*alpha*s1*c2 - 2*alpha*d*c1*s2 - d*d*s1*c2 - 2*beta*s1*s2)
        fyy = -2*I0*OMEGA*OMEGA*sp
        fxy = -I0*OMEGA*(alpha - K_W + 2*beta*lam)*sp
        return (fxx*fyy - fxy*fxy) / (1 + fx*fx + fy*fy)**2

    def K_separavel(lam, t, alpha):
        fx  = I0*alpha*np.cos(alpha*lam)
        fy  = I0*OMEGA*np.cos(OMEGA*t)
        fxx = -I0*alpha*alpha*np.sin(alpha*lam)
        fyy = -I0*OMEGA*OMEGA*np.sin(OMEGA*t)
        return (fxx*fyy) / (1 + fx*fx + fy*fy)**2

    lams = np.linspace(-3.0, 3.0, 300)
    ts   = np.linspace(0.0, 6.0, 300)
    L, T = np.meshgrid(lams, ts)

    Ka = K_acoplado(L, T, 1.423, 0.348)
    Ks = K_separavel(L, T, 1.423)

    print(f"  grade: {L.size:,} pontos\n")
    for nome, K in [("ΨQRH acoplado", Ka), ("separável wte+wpe", Ks)]:
        nz = np.count_nonzero(np.abs(K) > 1e-9) / K.size * 100
        print(f"  {nome:<20} K≠0 em {nz:6.2f}%  "
              f"K médio={K.mean():+.4f}  |K| médio={np.abs(K).mean():.4f}  "
              f"faixa=[{K.min():+.3f}, {K.max():+.3f}]")

    nz_sep = np.count_nonzero(np.abs(Ks) > 1e-9) / Ks.size * 100
    # a afirmação do registro exigiria K≈0 no separável
    registro_certo = nz_sep < 1.0
    print(f"\n  Afirmação do registro: 'K=0 é propriedade de embeddings lineares'")
    print(f"  RESULTADO: {'a afirmação se sustenta' if registro_certo else 'A AFIRMAÇÃO É FALSA'}")
    print(f"  O embedding separável tem K≠0 em {nz_sep:.1f}% dos pontos.")
    print(f"  O que se anula num separável é f_λt, não K.")
    print(f"  Consequência: K não distingue os dois embeddings. Acoplamento sim.")

    registrar("E2_curvatura", afirmacao_registro_valida=bool(registro_certo),
              fracao_K_nao_nulo_separavel=float(nz_sep),
              K_medio_acoplado=float(Ka.mean()), K_abs_medio_acoplado=float(np.abs(Ka).mean()),
              K_medio_separavel=float(Ks.mean()), K_abs_medio_separavel=float(np.abs(Ks).mean()))
    return not registro_certo   # passou se refutamos o registro


# ══════════════════════════════════════════════════════════════════════
# E3 — A CURVATURA SOBREVIVE AO TREINO?
#
# Os parâmetros da Tabela 5.6 do registro foram obtidos treinando sobre
# dados SEM estrutura. Se o acoplamento é a propriedade valiosa, ele
# precisa (a) estar presente na inicialização e (b) não colapsar quando
# o modelo é treinado numa tarefa que exige distinguir token e posição.
#
# Tarefa: prever o próximo token numa sequência onde a regra depende
# EXPLICITAMENTE de λ e de t de forma acoplada (ver E4). Se o treino
# empurrar α, β → 0, o acoplamento é decorativo.
#
# Falsificação: se f_λt ≠ 0 em < 50% do domínio após o treino, o
# acoplamento colapsou e não é uma propriedade robusta.
# ══════════════════════════════════════════════════════════════════════
class SurfaceEmbedding(nn.Module):
    """O núcleo geométrico, isolado. Sem atenção, sem RiemannSphere."""
    def __init__(self, vocab, dim=64, max_pos=64, acoplado=True):
        super().__init__()
        self.dim = dim
        self.acoplado = acoplado
        phi = (1 + 5**0.5) / 2
        self.I0    = nn.Parameter(torch.tensor(1.0))
        self.omega = nn.Parameter(torch.tensor(float(phi)))
        self.alpha = nn.Parameter(torch.tensor(1.0/phi))
        self.k     = nn.Parameter(torch.tensor(float(phi)))
        self.beta  = nn.Parameter(torch.tensor(1.0/phi**2))
        self.token = nn.Embedding(vocab, dim)
        self.pos   = nn.Embedding(max_pos, dim)
        nn.init.normal_(self.token.weight, std=0.02)
        nn.init.normal_(self.pos.weight, std=0.02)

    def forward(self, ids, pos):
        lam = self.token(ids)
        t   = self.pos(pos)
        half = self.dim // 2
        lr, li = lam[..., :half], lam[..., half:]
        tr, ti = t[..., :half], t[..., half:]

        if self.acoplado:
            p1r = self.omega*tr + self.alpha*lr
            p1i = self.omega*ti + self.alpha*li
            p2r = self.omega*tr - self.k*lr + self.beta*lr**2
            p2i = self.omega*ti - self.k*li + self.beta*li**2
            h = torch.cat([self.I0*torch.sin(p1r)*torch.cos(p2r),
                           self.I0*torch.sin(p1i)*torch.sin(p2i)], dim=-1)
        else:
            # separável: soma de dois perfis independentes
            h = torch.cat([torch.sin(self.alpha*lr) + torch.sin(self.omega*tr),
                           torch.cos(self.alpha*li) + torch.cos(self.omega*ti)], dim=-1)
        return h + lam * 0.1


def e3_estabilidade(seed=0):
    secao("E3 — O acoplamento sobrevive ao treino numa tarefa acoplada?")

    vocab, dim, T = 40, 64, 32
    fixar(seed)

    emb = SurfaceEmbedding(vocab, dim, T, acoplado=True).to(DEVICE)
    head = nn.Linear(dim, vocab).to(DEVICE)
    opt = torch.optim.AdamW(list(emb.parameters()) + list(head.parameters()), lr=3e-3)

    # Tarefa CAUSAL: prever x_{t+1} quando a sequência obedece
    # x_{t+1} = (x_t + 1) mod V. O modelo precisa distinguir o token λ
    # do deslocamento posicional t — é exatamente isso que a superfície
    # acoplada deveria facilitar. Alvos são causalmente alcançáveis.
    def lote(bs):
        x0 = torch.randint(0, vocab, (bs, 1), device=DEVICE)
        pos = torch.arange(T, device=DEVICE).unsqueeze(0)
        ids = (x0 + pos) % vocab                      # sequência determinística
        pos_e = pos.expand(bs, -1)
        return ids, pos_e, torch.roll(ids, -1, dims=1)

    params_antes = {n: p.detach().clone() for n, p in emb.named_parameters()
                    if n in ("alpha", "beta", "omega", "k", "I0")}

    passos = 150 if QUICK else 600
    emb.train()
    for i in range(passos):
        ids, pos, y = lote(64)
        logits = head(emb(ids, pos))
        loss = F.cross_entropy(logits[:, :-1].reshape(-1, vocab), y[:, 1:].reshape(-1))
        opt.zero_grad(); loss.backward()
        torch.nn.utils.clip_grad_norm_(emb.parameters(), 1.0)
        opt.step()

    emb.eval()
    with torch.no_grad():
        ids, pos, y = lote(256)
        logits = head(emb(ids, pos))
        loss_final = F.cross_entropy(logits[:, :-1].reshape(-1, vocab), y[:, 1:].reshape(-1)).item()
        acc = (logits[:, :-1].argmax(-1) == y[:, 1:]).float().mean().item()
    ppl = math.exp(min(loss_final, 20))
    print(f"  tarefa: y = (token + posição) mod {vocab}")
    print(f"  {passos} passos | loss={loss_final:.4f} | PPL={ppl:.3f} | acurácia={acc*100:.2f}%")
    print(f"  (PPL de um modelo aleatório seria ~{vocab})\n")

    print("  Parâmetros geométricos:")
    print(f"  {'param':<8} {'antes':>10} {'depois':>10} {'razão':>9}")
    razoes = {}
    for n, p in emb.named_parameters():
        if n not in params_antes:
            continue
        a = float(params_antes[n].cpu())
        b = float(p.detach().cpu())
        r = b / a if abs(a) > 1e-12 else float("nan")
        razoes[n] = r
        print(f"  {n:<8} {a:>10.4f} {b:>10.4f} {r:>9.3f}x")

    # o acoplamento persiste?
    I0 = emb.I0.item(); OM = emb.omega.item(); KW = emb.k.item()
    AL = emb.alpha.item(); BE = emb.beta.item()
    lams = np.linspace(-3, 3, 200); ts = np.linspace(0, 6, 200)
    L, TT = np.meshgrid(lams, ts)
    p1 = OM*TT + AL*L; p2 = OM*TT - KW*L + BE*L*L
    fxy = -I0*OM*(AL - KW + 2*BE*L)*np.sin(p1 + p2)
    nz = np.count_nonzero(np.abs(fxy) > 1e-9) / fxy.size * 100
    print(f"\n  f_λt ≠ 0 após treino: {nz:.2f}% do domínio")
    print(f"  CRITÉRIO: > 50%")
    passou = nz > 50.0
    print(f"  RESULTADO: {'PASSOU — o acoplamento não colapsou' if passou else 'FALHOU — colapsou'}")
    if not passou:
        print("  Leitura honesta: o gradiente da tarefa empurrou α,β→0 e o")
        print("  embedding degenerou para o separável. O acoplamento seria decorativo.")

    registrar("E3_estabilidade", seed=seed, passos=passos, loss=loss_final,
              ppl=ppl, acuracia=acc, fracao_acoplamento_pos_treino=float(nz),
              parametros_razao=razoes, passou=bool(passou))
    return passou


# ══════════════════════════════════════════════════════════════════════
# E4 — O EMBEDDING AJUDA NUMA TAREFA REAL?
#
# Diferente do benchmark do registro: dados COM estrutura, treino até
# convergência, múltiplas seeds, e uma tarefa em que o acoplamento entre
# token e posição é a ÚNICA pista disponível.
#
# Tarefa: y_t = (x_t + t) mod V. A regra é linearmente acoplada em λ e t.
# Um embedding que soma token+posição consegue representá-la; um que não
# consegue deveria sofrer. Esta é a pergunta honesta: o acoplamento
# explícito compra algo?
# ══════════════════════════════════════════════════════════════════════
class MiniTransformer(nn.Module):
    def __init__(self, vocab, dim=64, layers=2, heads=4, max_pos=32, acoplado=True):
        super().__init__()
        self.emb = SurfaceEmbedding(vocab, dim, max_pos, acoplado=acoplado)
        self.blocks = nn.ModuleList([
            nn.TransformerEncoderLayer(dim, heads, dim*4, dropout=0.0,
                                       batch_first=True, norm_first=True)
            for _ in range(layers)])
        self.ln = nn.LayerNorm(dim)
        self.head = nn.Linear(dim, vocab)
        self.head.weight = self.emb.token.weight   # weight tying

    def forward(self, ids, pos):
        h = self.emb(ids, pos)
        mask = nn.Transformer.generate_square_subsequent_mask(ids.size(1), device=ids.device)
        for b in self.blocks:
            h = b(h, src_mask=mask, is_causal=True)
        return self.head(self.ln(h))


TAREFAS = {
    # nome: (vocab, T, dim, função de geração, descrição)
    "T1 adicao_facil":  dict(vocab=40,  T=32, dim=64, dificuldade="fácil"),
    "T2 adicao_grande": dict(vocab=200, T=32, dim=64, dificuldade="média"),
    "T3 posicional":    dict(vocab=40,  T=32, dim=64, dificuldade="média"),
}


def gerar(tarefa, bs, vocab, T):
    """Sequências com estrutura determinística e alvo causalmente alcançável."""
    pos = torch.arange(T, device=DEVICE).unsqueeze(0)
    x0 = torch.randint(0, vocab, (bs, 1), device=DEVICE)
    if tarefa == "T1 adicao_facil":
        # x_{t+1} = (x_t + 1) mod V
        ids = (x0 + pos) % vocab
    elif tarefa == "T2 adicao_grande":
        # mesma regra, vocabulário 5x maior — exige mais do embedding
        ids = (x0 + pos) % vocab
    elif tarefa == "T3 posicional":
        # x_{t+1} = (x_t + 1 + (t mod 2)) mod V — alterna a taxa
        ids = (x0 + torch.cumsum(1 + (pos % 2), dim=1)) % vocab
    else:
        raise ValueError(tarefa)
    return ids, pos.expand(bs, -1), torch.roll(ids, -1, dims=1)


def treinar_tarefa(acoplado, seed, tarefa="T1 adicao_facil", passos=None):
    cfg = TAREFAS[tarefa]
    vocab, T, dim = cfg["vocab"], cfg["T"], cfg["dim"]
    fixar(seed)
    passos = passos or (300 if QUICK else 1200)
    model = MiniTransformer(vocab, dim, max_pos=T, acoplado=acoplado).to(DEVICE)
    opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=0.01)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, passos)

    model.train()
    for i in range(passos):
        ids, pos, y = gerar(tarefa, 64, vocab, T)
        logits = model(ids, pos)
        loss = F.cross_entropy(logits[:, :-1].reshape(-1, vocab), y[:, 1:].reshape(-1))
        opt.zero_grad(); loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        opt.step(); sched.step()

    model.eval()
    with torch.no_grad():
        ids, pos, y = gerar(tarefa, 512, vocab, T)
        logits = model(ids, pos)
        loss = F.cross_entropy(logits[:, :-1].reshape(-1, vocab), y[:, 1:].reshape(-1)).item()
        acc = (logits[:, :-1].argmax(-1) == y[:, 1:]).float().mean().item()
    return loss, acc


def e4_tarefa_com_estrutura():
    secao("E4 — O acoplamento ajuda onde a tarefa ainda não saturou?")

    print("  Dados COM estrutura determinística e alvo causalmente alcançável.")
    print("  Três tarefas de dificuldade crescente. Em cada uma, se a")
    print("  arquitetura saturar em 100% de acurácia, a tarefa não separa")
    print("  nada e a linha é marcada como tal.\n")

    seeds = range(2) if QUICK else range(5)
    tabela = {}

    for tarefa in TAREFAS:
        cfg = TAREFAS[tarefa]
        print(f"  ── {tarefa} (V={cfg['vocab']}, {cfg['dificuldade']}) ──")
        linha = {}
        for nome, acop in [("acoplado", True), ("separável", False)]:
            ls, acs = [], []
            for s in seeds:
                l, a = treinar_tarefa(acop, seed=s, tarefa=tarefa)
                ls.append(l); acs.append(a)
            linha[nome] = {"loss": ls, "acc": acs}
            print(f"    {nome:<10} loss={np.mean(ls):.4f}±{np.std(ls):.4f}  "
                  f"acc={np.mean(acs)*100:6.2f}%  PPL={math.exp(min(np.mean(ls),20)):.3f}")
        a_c, s_c = linha["acoplado"], linha["separável"]
        saturado = (np.mean(a_c["acc"]) > 0.995 and np.mean(s_c["acc"]) > 0.995)
        dif = np.mean(s_c["loss"]) - np.mean(a_c["loss"])
        # estimativa do ruído a partir das duas distribuições
        ruido = math.sqrt(np.var(a_c["loss"]) + np.var(s_c["loss"]))
        if saturado:
            print(f"    -> AMBAS SATURARAM. Tarefa não separa arquiteturas.")
        elif abs(dif) <= ruido:
            print(f"    -> diferença {dif:+.4f} dentro do ruído ({ruido:.4f})")
        else:
            print(f"    -> diferença {dif:+.4f} acima do ruído ({ruido:.4f})")
        print()
        tabela[tarefa] = {"saturado": bool(saturado), "diferenca": float(dif),
                          "ruido": float(ruido),
                          "acoplado": {"loss": float(np.mean(a_c["loss"])),
                                       "acc": float(np.mean(a_c["acc"]))},
                          "separavel": {"loss": float(np.mean(s_c["loss"])),
                                        "acc": float(np.mean(s_c["acc"]))}}

    detectaveis = [t for t, v in tabela.items() if not v["saturado"] and abs(v["diferenca"]) > v["ruido"]]
    print(f"  Tarefas em que uma arquitetura se destacou: {len(detectaveis)}")
    if not detectaveis:
        print("  RESULTADO HONESTO: em nenhuma das três tarefas o acoplamento")
        print("  explícito produziu vantagem fora do ruído. As tarefas fáceis")
        print("  saturam; nas médias, a diferença não supera a variância.")
        print("  Isso NÃO refuta a geometria — mostra que estes alvos não")
        print("  exigem a estrutura que ΨQRH oferece. Registrar como limite.")

    registrar("E4_tarefa", seeds=int(len(list(range(2 if QUICK else 5)))), por_tarefa=tabela,
              tarefas_com_efeito=detectaveis)
    return detectaveis


# ══════════════════════════════════════════════════════════════════════
# E5 — O GANHO É DO ACOPLAMENTO OU DO ACASO?
#
# Ablação controlada: mesma arquitetura, mesma contagem de parâmetros,
# mesma tarefa, mesma seed. A única diferença é a forma do embedding.
# 10 seeds por braço, IC 95% bootstrap.
#
# Esta é a resposta ao defeito central do benchmark do registro: uma
# única seed com 3 batches não distingue arquitetura de inicialização.
# ══════════════════════════════════════════════════════════════════════
def bootstrap_ic(vals, n=5000, alpha=0.05):
    vals = np.asarray(vals, dtype=float)
    rng = np.random.default_rng(0)
    medias = np.array([rng.choice(vals, size=len(vals), replace=True).mean()
                       for _ in range(n)])
    return float(np.quantile(medias, alpha/2)), float(np.quantile(medias, 1-alpha/2))


def e5b_diagnostico(seeds=4):
    """CONTROLE — o efeito de E5 é do ACOPLAMENTO ou da capacidade de
    representar o token?

    O embedding separável usa sin/cos, que são periódicos: um lookup de
    200 tokens mapeado por sin(αλ) pode colidir entre si se os valores de
    λ forem grandes. Se for esse o problema, trocar sin/cos por uma
    codificação linear resolve — e o acoplamento deixa de ser a causa.

    Este controle decide entre as duas explicações antes de publicar E5.
    """
    secao("E5b — CONTROLE: é acoplamento ou capacidade de representar o token?")

    tarefa, passos = "T2 adicao_grande", (200 if QUICK else 500)
    cfg = TAREFAS[tarefa]
    vocab, T, dim = cfg["vocab"], cfg["T"], cfg["dim"]

    class SepLinear(nn.Module):
        """Separável, mas SEM periodicidade: soma linear de token e posição.
        Tem os mesmos parâmetros de livre escolha que o acoplado."""
        def __init__(self):
            super().__init__()
            self.wt = nn.Parameter(torch.randn(vocab, dim) * 0.5)
            self.wp = nn.Parameter(torch.randn(T, dim) * 0.5)
        def forward(self, ids, pos):
            return self.wt[ids] + self.wp[pos]

    class SepSeno(nn.Module):
        """Separável com a MESMA codificação senoidal do braço separável,
        mas com pesos de embedding treináveis maiores."""
        def __init__(self):
            super().__init__()
            self.dim = dim
            self.phi = (1 + 5**0.5) / 2
            self.alpha = nn.Parameter(torch.tensor(1.0 / self.phi))
            self.omega = nn.Parameter(torch.tensor(float(self.phi)))
            self.token = nn.Embedding(vocab, dim)
            self.pos = nn.Embedding(T, dim)
            nn.init.normal_(self.token.weight, std=0.5)
            nn.init.normal_(self.pos.weight, std=0.5)
        def forward(self, ids, pos):
            lam = self.token(ids); t = self.pos(pos)
            h = self.dim // 2
            lr, li = lam[..., :h], lam[..., h:]
            tr, ti = t[..., :h], t[..., h:]
            return torch.cat([torch.sin(self.alpha*lr) + torch.sin(self.omega*tr),
                              torch.cos(self.alpha*li) + torch.cos(self.omega*ti)], -1) + lam*0.1

    def rodar(emb_factory, acoplado):
        ls, acs = [], []
        for s in range(seeds):
            fixar(500 + s)
            if acoplado:
                model = MiniTransformer(vocab, dim, max_pos=T, acoplado=True).to(DEVICE)
            else:
                model = MiniTransformer.__new__(MiniTransformer)
                nn.Module.__init__(model)
                model.emb = emb_factory().to(DEVICE)
                model.blocks = nn.ModuleList([
                    nn.TransformerEncoderLayer(dim, 4, dim*4, dropout=0.0,
                                               batch_first=True, norm_first=True)
                    for _ in range(2)]).to(DEVICE)
                model.ln = nn.LayerNorm(dim).to(DEVICE)
                model.head = nn.Linear(dim, vocab, bias=False).to(DEVICE)
            opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=0.01)
            sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, passos)
            model.train()
            for i in range(passos):
                ids, pos, y = gerar(tarefa, 64, vocab, T)
                logits = model(ids, pos)
                loss = F.cross_entropy(logits[:, :-1].reshape(-1, vocab), y[:, 1:].reshape(-1))
                opt.zero_grad(); loss.backward()
                torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
                opt.step(); sched.step()
            model.eval()
            with torch.no_grad():
                ids, pos, y = gerar(tarefa, 512, vocab, T)
                lg = model(ids, pos)
                ls.append(F.cross_entropy(lg[:, :-1].reshape(-1, vocab), y[:, 1:].reshape(-1)).item())
                acs.append((lg[:, :-1].argmax(-1) == y[:, 1:]).float().mean().item())
        return float(np.mean(ls)), float(np.mean(acs))

    print("  três braços, mesma tarefa, mesmo orçamento, mesmas seeds\n")
    l_ac, a_ac = rodar(None, acoplado=True)
    print(f"  acoplado ΨQRH        loss={l_ac:.4f}  acc={a_ac*100:6.2f}%")
    l_sn, a_sn = rodar(SepSeno, acoplado=False)
    print(f"  separável senoidal   loss={l_sn:.4f}  acc={a_sn*100:6.2f}%")
    l_sl, a_sl = rodar(SepLinear, acoplado=False)
    print(f"  separável linear     loss={l_sl:.4f}  acc={a_sl*100:6.2f}%  <- controle de capacidade")

    print()
    if a_sl > 0.9:
        print("  VEREDITO: o separável LINEAR também resolve a tarefa.")
        print("  Isso significa que o efeito de E5 é de CAPACIDADE DE")
        print("  REPRESENTAR O TOKEN, não do acoplamento λ–t. O embedding")
        print("  senoidal colidia por periodicidade, e era isso que quebrava.")
        print("  A vantagem medida em E5 NÃO PODE ser atribuída ao acoplamento.")
        conclusao = "capacidade_de_representacao"
    elif a_ac > a_sl + 0.05:
        print("  VEREDITO: o acoplado supera até o controle linear.")
        print("  O efeito não é explicado por capacidade — o acoplamento")
        print("  λ–t contribui de forma própria nesta tarefa.")
        conclusao = "acoplamento"
    else:
        print("  VEREDITO: inconclusivo entre as duas explicações.")
        conclusao = "indeterminado"

    registrar("E5b_diagnostico", acoplado={"loss": l_ac, "acc": a_ac},
              sep_senoidal={"loss": l_sn, "acc": a_sn},
              sep_linear={"loss": l_sl, "acc": a_sl},
              veredito=conclusao)
    return conclusao


def e5_abacao(seeds=10):
    secao("E5 — Ablação em orçamento apertado: 10 seeds, IC 95% bootstrap")
    print("  T2 (V=200) com pouco treino: o regime onde a capacidade do")
    print("  embedding pode importar. Mesma arquitetura, mesmos parâmetros,")
    print("  mesma tarefa, mesma seed. Única variável: forma do embedding.\n")

    tarefa = "T2 adicao_grande"
    passos = 200 if QUICK else 500

    res = {}
    for nome, acop in [("acoplado", True), ("separável", False)]:
        losses, accs = [], []
        for s in range(seeds):
            l, a = treinar_tarefa(acop, seed=100 + s, tarefa=tarefa, passos=passos)
            losses.append(l); accs.append(a)
            print(f"  {nome:<10} seed {100+s}: loss={l:.4f} acc={a*100:6.2f}%")
        lo, hi = bootstrap_ic(losses)
        res[nome] = {"losses": losses, "accs": accs,
                     "media": float(np.mean(losses)), "std": float(np.std(losses)),
                     "ic95": [lo, hi]}
        print(f"  -> {nome}: loss média={np.mean(losses):.4f} "
              f"IC95=[{lo:.4f}, {hi:.4f}]  acc={np.mean(accs)*100:.2f}%\n")

    a, s = res["acoplado"], res["separável"]
    sobrepoe = not (a["ic95"][1] < s["ic95"][0] or s["ic95"][1] < a["ic95"][0])
    diferenca = s["media"] - a["media"]
    print(f"  IC 95% acoplado  : [{a['ic95'][0]:.4f}, {a['ic95'][1]:.4f}]")
    print(f"  IC 95% separável : [{s['ic95'][0]:.4f}, {s['ic95'][1]:.4f}]")
    print(f"  diferença das médias = {diferenca:+.4f}")
    print(f"  CRITÉRIO (declarado antes): ICs não sobrepostos = efeito real")
    if sobrepoe:
        print(f"  RESULTADO: ICs SE SOBREPÕEM — sem efeito detectável")
        print(f"  Leitura honesta: com {seeds} seeds e {passos} passos, a geometria")
        print(f"  acoplada não produz vantagem distinguível de zero nesta tarefa.")
        print(f"  A hipótese de que o acoplamento ajuda fica NÃO CONFIRMADA.")
    else:
        venc = "acoplado" if diferenca > 0 else "separável"
        print(f"  RESULTADO: ICs NÃO se sobrepõem — efeito real, vence {venc}")
        print(f"  ATENÇÃO: efeito em E5 NÃO implica causa. O controle E5b")
        print(f"  testa se é acoplamento ou apenas capacidade de representar")
        print(f"  o token. Ler os dois juntos antes de qualquer conclusão.")

    registrar("E5_abacao", tarefa=tarefa, passos=passos, seeds=seeds,
              ic_sobrepoe=bool(sobrepoe), diferenca=float(diferenca),
              por_arquitetura=res)
    return not sobrepoe


# ══════════════════════════════════════════════════════════════════════
def main():
    t0 = time.time()
    print("=" * 74)
    print("ΨQRH — BENCHMARK DO NÚCLEO GEOMÉTRICO")
    print(f"device: {DEVICE}" + (f" ({torch.cuda.get_device_name(0)})" if DEVICE=="cuda" else ""))
    print(f"modo: {'rápido' if QUICK else 'completo'}")
    print("=" * 74)
    print("Escopo: apenas f(λ,t) = I0·sin(ωt+αλ)·cos(ωt−kλ+βλ²) + 0.1λ")
    print("Fora de escopo: RiemannSphere, atenção espectral, weight tying.")
    print("Motivo: aquelas são decisões de arquitetura; esta é matemática.")

    fixar(0)
    p1 = e1_acoplamento()
    print(f"\n  [E1 {'PASSOU' if p1 else 'FALHOU'}]")

    p2 = e2_curvatura_nao_separa()
    print(f"\n  [E2 {'refutou o registro' if p2 else 'confirmou o registro'}]")

    p3 = e3_estabilidade(seed=0)
    print(f"\n  [E3 {'PASSOU' if p3 else 'FALHOU'}]")

    e4_tarefa_com_estrutura()
    p5 = e5_abacao(seeds=6 if QUICK else 10)
    print(f"\n  [E5 {'efeito detectado' if p5 else 'sem efeito detectável'}]")

    veredito = e5b_diagnostico(seeds=3 if QUICK else 4)
    print(f"\n  [E5b atribuição do efeito: {veredito}]")

    dt = time.time() - t0
    RESULTADOS["meta"] = {
        "device": DEVICE,
        "gpu": torch.cuda.get_device_name(0) if DEVICE == "cuda" else None,
        "torch": torch.__version__,
        "modo": "quick" if QUICK else "completo",
        "duracao_s": round(dt, 1),
        "escopo": "núcleo geométrico f(λ,t) apenas",
    }

    with open(OUT, "w", encoding="utf-8") as f:
        json.dump(RESULTADOS, f, indent=2, ensure_ascii=False)
    secao(f"CONCLUÍDO em {dt:.1f}s — resultados em {os.path.basename(OUT)}")

    # No Colab, oferece o download do JSON.
    try:
        from google.colab import files as _colab_files
        _colab_files.download(OUT)
    except Exception:
        pass
    print(f"\n  E1 acoplamento é real ........ {'SIM' if p1 else 'NÃO'}")
    print(f"  E2 curvatura separa .......... {'NÃO (registro errado)' if p2 else 'SIM'}")
    print(f"  E3 acoplamento sobrevive ..... {'SIM' if p3 else 'NÃO'}")
    print(f"  E5 efeito na tarefa .......... {'DETECTADO' if p5 else 'NÃO DETECTADO'}")
    print(f"  E5b causa do efeito .......... {veredito}")
    print(f"\n  O que este benchmark NÃO prova: que ΨQRH é melhor que wte+wpe")
    print(f"  em linguagem natural. Para isso seria preciso treinar em corpus")
    print(f"  real com orçamento de compute comparável — fora do escopo aqui.")


if __name__ == "__main__":
    main()
