PowerMachine commited on
Commit
06e4b6f
·
verified ·
1 Parent(s): 39f2f80

v0.10.0: análise/prova/teste das críticas de engenharia Qwen3.8 lote 3 (r-35) — divisão SRP dos monólitos jepa/pensamento/moe/python em 26 submódulos (API estável via re-exports; 'Amdahl de compilação' refutada por experimento controlado, builds frios neutros), checkpoints atômicos SHA-256 puro FIPS 180-4 + fsync + rename + manifesto (CP-1..CP-3; correções da proposta: sem destruir estado anterior, manifesto com barreira, sem bincode/unwrap), TemperaturaInversa T-V3 (fronteira tipada da convexidade N-2; alegação compile-time corrigida), trait CamadaKhtst+CamadaHomeostatica (premissa do controlador refutada no fonte; Vec<Var>; adaptador MoeComPapel bit-a-bit), CLI Typer 75 (fachada aditiva com resume verificado por hash) — 123/123 cargo, 107/107 gates, treino real Wikipédia PT-BR com checkpoints íntegros

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Cargo.lock +1 -1
  2. Cargo.toml +2 -2
  3. docs/memorias/r-35-analise-provas-qwen-v010.md +360 -0
  4. pyproject.toml +2 -2
  5. resultados/fase_a_v010.json +14 -0
  6. resultados/fase_b_v010.json +11 -0
  7. resultados/suite_v010_final.json +159 -0
  8. resultados/telemetria_fase_c.jsonl +0 -0
  9. resultados/telemetria_v010.json +155 -0
  10. resultados/treino_v090.json +26 -26
  11. scripts/75_cli.py +191 -0
  12. scripts/76_telemetria_v010.py +134 -0
  13. scripts/77_suite_v010.py +141 -0
  14. scripts/78_publicar_v010.py +194 -0
  15. src/camadas.rs +162 -0
  16. src/checkpoint.rs +353 -0
  17. src/fnh.rs +7 -0
  18. src/gerador.rs +14 -5
  19. src/jepa/mascaras.rs +173 -0
  20. src/jepa/mod.rs +61 -0
  21. src/jepa/nucleo.rs +211 -0
  22. src/jepa/patches.rs +150 -0
  23. src/jepa/replay_ssm.rs +107 -0
  24. src/jepa/sonic_moe.rs +155 -0
  25. src/jepa/testes.rs +212 -0
  26. src/{jepa.rs → jepa/variantes.rs} +3 -1016
  27. src/lib.rs +1 -0
  28. src/moe_redirecionavel/atencao.rs +63 -0
  29. src/moe_redirecionavel/mod.rs +57 -0
  30. src/{moe_redirecionavel.rs → moe_redirecionavel/nucleo.rs} +3 -779
  31. src/moe_redirecionavel/teste_integrado.rs +26 -0
  32. src/moe_redirecionavel/testes.rs +209 -0
  33. src/moe_redirecionavel/treino.rs +463 -0
  34. src/pensamento/combinador.rs +73 -0
  35. src/pensamento/mod.rs +71 -0
  36. src/{pensamento.rs → pensamento/motor.rs} +4 -647
  37. src/pensamento/quebra_cabeca.rs +233 -0
  38. src/pensamento/testes.rs +303 -0
  39. src/python.rs +0 -0
  40. src/python/calibracao.rs +134 -0
  41. src/python/demos_qwen.rs +359 -0
  42. src/python/fnh_arvore.rs +344 -0
  43. src/python/jepa_treinos.rs +251 -0
  44. src/python/mod.rs +145 -0
  45. src/python/moe.rs +190 -0
  46. src/python/nlp_nlg.rs +69 -0
  47. src/python/nucleo.rs +239 -0
  48. src/python/pensamento_demos.rs +367 -0
  49. src/python/som.rs +180 -0
  50. src/python/treino_fases.rs +228 -0
Cargo.lock CHANGED
@@ -723,7 +723,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
723
 
724
  [[package]]
725
  name = "khtst-rust"
726
- version = "0.9.0"
727
  dependencies = [
728
  "anyhow",
729
  "built",
 
723
 
724
  [[package]]
725
  name = "khtst-rust"
726
+ version = "0.10.0"
727
  dependencies = [
728
  "anyhow",
729
  "built",
Cargo.toml CHANGED
@@ -1,8 +1,8 @@
1
  [package]
2
  name = "khtst-rust"
3
- version = "0.9.0"
4
  edition = "2021"
5
- description = "KHTST_RUST — análise/prova/teste das críticas Qwen3.8 v2 (r-34): skew O(n²) diferenciável via index_add, calibrar vetorial bit-a-bit, forward canônico (bug v080-1 fora da API pública), FFI isolada em feature python, newtypes de domínio"
6
  license = "Apache-2.0"
7
 
8
  [lib]
 
1
  [package]
2
  name = "khtst-rust"
3
+ version = "0.10.0"
4
  edition = "2021"
5
+ description = "KHTST_RUST — críticas Qwen3.8 lote 3 (r-35): divisão SRP dos monólitos, checkpoints atômicos SHA-256+fsync+rename, newtype TemperaturaInversa (fronteira tipada N-2), trait CamadaKhtst + CamadaHomeostatica, CLI unificada Typer"
6
  license = "Apache-2.0"
7
 
8
  [lib]
docs/memorias/r-35-analise-provas-qwen-v010.md ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # r-35 — Análise, Provas e Testes das Críticas de Engenharia do Agente Qwen3.8 (v0.10.0, lote 3)
2
+
3
+ **Escopo**: o usuário pediu *analisar, provar e testar* o terceiro lote de
4
+ críticas do agente Qwen3.8 (anti-padrões de engenharia de software que
5
+ "comprometem a escalabilidade": monólitos Rust, scripts Python
6
+ sequenciais, checkpointing não-atômico, invariantes apenas em
7
+ comentários), mantendo as regras da casa: **nenhuma funcionalidade SOTA
8
+ eficaz é removida**; toda mudança com prova matemática, lógica e de
9
+ lógica computacional; provas em markdown; validação com dataset real;
10
+ autocrítica (inclusive das minhas próprias correções).
11
+
12
+ **Metodologia** (herdada da r-34): (i) verificação simbólica/lógica de
13
+ cada crítica e proposta ANTES do código; (ii) confronto com oráculos
14
+ independentes (medições frias em target dir separado, experimento
15
+ controlado com grupo de controle, vetores oficiais FIPS 180-4); (iii)
16
+ implementação apenas do que sobrevive a (i)+(ii), como **adição**; (iv)
17
+ testes de regressão + suíte consolidada; (v) validação fim-a-fim com
18
+ dataset real (Wikipédia PT-BR, streaming controlado).
19
+
20
+ **Veredito sumário**:
21
+
22
+ | # | Crítica / Proposta | Veredito | Ação v0.10.0 | Ganho medido |
23
+ |---|--------------------|----------|--------------|--------------|
24
+ | 1 | Monólitos jepa.rs (58,4 kB), pensamento.rs (53,4 kB), moe_redirecionavel.rs (50,6 kB) violam SRP; "Lei de Amdahl p/ compilação": split ⇒ T_inc ≈ O(N/k) | **SRP VÁLIDO; a "prova" é uma APLICAÇÃO ERRADA de Amdahl e a alegação O(N/k) é REFUTADA por experimento controlado** | divisão em 26 submódulos coesos com re-exports (API estável); +python.rs 112 kB (pior monólito — acrescentado por autocrítica) | 61 arquivos, maior agora 57,6 kB (fnh.rs); 107/107 gates herdados preservados; compilação incremental ~1,1 s ANTES E DEPOIS (ruído); builds frios neutros (70→68 s, 85→86 s) |
25
+ | 2 | "Scripts Python sequenciais" (39_coletar…74) ⇒ substituir por CLI Typer | **PARCIALMENTE VÁLIDA como desejo de unificação; a SUBSTITUIÇÃO é REJEITADA** (quebra rastreabilidade r-XX↔scripts e remove funcionalidade) | `scripts/75_cli.py` — fachada Typer que delega aos scripts canônicos + política de resume com integridade + gestão de estados | 1 entry point p/ todo o pipeline; resume com prova SHA-256; drill de corrupção 3/3 |
26
+ | 3 | Checkpointing não-atômico (`os.path.exists` vulnerável a queda de energia) | **VÁLIDA no problema; a proposta tem 3 falhas** (destrói o estado anterior; manifesto sem fsync; bincode+`unwrap`) | `src/checkpoint.rs`: SHA-256 puro (FIPS 180-4) + tmp→fsync→rename→fsync(dir) p/ conteúdo E manifesto; `carregar_verificado`; bindings Python | 2 checkpoints do treino real verificados (240,9 MB e 59,5 MB, manifesto 141 B); adulteração/truncamento/manifesto-trocado detectados; estado anterior preservado sob falha simulada |
27
+ | 4 | "Invariantes apenas em comentários" (convexidade em γ=1/T) ⇒ newtype `TemperaturaInversa` | **VÁLIDA no padrão; a alegação "garantia em tempo de COMPILAÇÃO" é FALSA** (a validação é de runtime na fronteira) | `tipos::TemperaturaInversa` (T-V3) + fronteira tipada no verificador (`nll_temperatura_tipado`, setter/getter, saída do otimizador validada) | T* do ajuste validada por construção; equivalência bit-a-bit com o caminho cru (tv3d); pré-condição do Teo N-2 carregada no tipo |
28
+ | P1 | Trait `CamadaKhtst` c/ `forward`+`parametros`+`gate_ativacao` | **IDEIA VÁLIDA; PREMISSA FALSA (controlador NÃO conhece Fnh/Moe/Jepa — verificado no fonte) e assinatura quebraria o otimizador** | trait corrigido (`Vec<Var>`; gate segregado em `CamadaHomeostatica`; adaptador `MoeComPapel` captura o papel) | composição heterogênea preserva B×T (CK-1); delegação bit-a-bit (CK-1c); despacho homeostático tipado (CK-2a) |
29
+ | P3 | Checkpoint atômico com "prova criptográfica" | ver #3 acima | idem | idem |
30
+ | P4 | CLI Typer declarativo | ver #2 acima | idem | idem |
31
+
32
+ **Contagem**: 4 críticas + 4 propostas analisadas; **2 aceitas com
33
+ correções substanciais**, **1 aceita parcialmente (substância sim,
34
+ substituição não)**, **1 com premissa refutada por experimento**, e **todas
35
+ as 4 propostas de código continham erros** catalogados abaixo. 16 testes
36
+ novos; suíte consolidada **107/107**; cargo **123/123**.
37
+
38
+ ---
39
+
40
+ ## §1 — Ambiente, baseline e higiene (pré-requisitos do usuário)
41
+
42
+ - **Processos de fundo**: nenhum processo residual de treino encontrado
43
+ (`ps aux` na abertura); toda execução em primeiro plano, como exige a
44
+ advertência de que o fundo mata processos sem erro/OOM (confirmado
45
+ empiricamente nas sessões anteriores — r-32 §"fase C com resume").
46
+ - **Estados de treino**: varredura inicial (`*.pt`, `*.npz`, `*.ckpt`,
47
+ `*.safetensors`, `*.estado.json`) = **0 estados locais**; um
48
+ `telemetria_fase_c.jsonl` da v0.9.0 (estado residual de treino
49
+ anterior, exatamente o que o usuário manda procurar) foi **encontrado
50
+ e apagado** antes do treino fresco v0.10.0 (§5).
51
+ - **Espaço**: 3,1 GB de alvos cargo obsoletos (`logs/tgt_full`,
52
+ `logs/tgt_domain` — artefatos de builds de sessões passadas) apagados
53
+ (96% → 64% de uso do disco).
54
+ - **Baseline**: v0.9.0 reconstruída e verificada **107/107** antes de
55
+ qualquer mudança.
56
+
57
+ ## §2 — Crítica 1: "Monólitos em Rust" e a "Lei de Amdahl para Compilação"
58
+
59
+ ### 2.1 Análise da crítica
60
+
61
+ **Válido (substância)**: `jepa.rs` (58.407 B), `pensamento.rs` (53.408 B)
62
+ e `moe_redirecionavel.rs` (50.941 B) eram arquivos multi-responsabilidade
63
+ — `jepa.rs` continha núcleo E/P/EMA, 6 famílias de máscara, 6
64
+ embaralhamentos de patches, enum de 10 variantes + treinos especializados,
65
+ SonicMoE E ReplaySSM. Violação real de SRP.
66
+
67
+ **Inválido (rótulo)**: a "Prova Lógica (Lei de Amdahl para Compilação)"
68
+ não é a Lei de Amdahl. A Lei de Amdahl limita o ganho de paralelização
69
+ pela fração serial: S(n) = 1/((1−p) + p/n). O que a crítica quer invocar
70
+ é a granularidade da **unidade de compilação** — e a afirmação
71
+ "T_inc do monólito é O(N); dividir em módulos reduz T_inc ≈ O(N/k)" é
72
+ **falsa na granularidade proposta**:
73
+
74
+ **Teo CK-0 (granularidade de recompilação).** O cargo/rustc recompila por
75
+ **crate**, não por arquivo. Dividir `jepa.rs` em `jepa/*.rs` não cria
76
+ unidades de compilação novas: um toque em qualquer arquivo do diretório
77
+ recompila o crate inteiro (com reuso incremental por queries/itens). Para
78
+ obter T_inc ≈ O(N/k) seria necessário dividir em **k crates** (workspace),
79
+ o que tem custos próprios: unificação de features entre crates, a `cdylib`
80
+ única do PyO3 exigiria um crate só de FFI dependendo de crates de domínio,
81
+ e qualquer mudança de API pública recompila os dependentes (o grafo de
82
+ dependências domina — de novo Amdahl, agora corretamente aplicado: o ganho
83
+ é limitado pelo caminho crítico do DAG de crates). ∎
84
+
85
+ ### 2.2 O que foi feito (divisão SRP aditiva)
86
+
87
+ - `jepa.rs` → `jepa/{nucleo, mascaras, patches, variantes, sonic_moe,
88
+ replay_ssm, testes}.rs`
89
+ - `pensamento.rs` → `pensamento/{motor, combinador, quebra_cabeca, testes}.rs`
90
+ - `moe_redirecionavel.rs` → `moe_redirecionavel/{atencao, nucleo, treino,
91
+ testes, teste_integrado}.rs`
92
+ - `python.rs` (112.410 B — o **pior** monólito, não citado pela crítica:
93
+ errata por autocrítica) → `python/{nucleo, treino_fases, som,
94
+ jepa_treinos, pensamento_demos, calibracao, moe, fnh_arvore, nlp_nlg,
95
+ demos_qwen}.rs`
96
+ - Cada `mod.rs` re-exporta `pub use sub::*;` ⇒ **todos os caminhos
97
+ `crate::jepa::X` etc. permanecem válidos** (107/107 testes herdados
98
+ verdes = prova de preservação de comportamento).
99
+ - Submódulos do python/ ganham `pub(crate)` nas pyfunctions/pyclasses (o
100
+ registro `#[pymodule]` referencia `sub::item`); helpers entre irmãos
101
+ (`distancia_eucl`, `carregar_tokenizador`, `ConfigPy`) promovidos a
102
+ `pub(crate)` — visibilidade mínima necessária, nada vira `pub`.
103
+
104
+ **Aritmética da divisão**: 4 monólitos (275,2 kB) → 26 arquivos
105
+ (maior submódulo: `python/demos_qwen.rs`; maior arquivo do projeto agora
106
+ `fnh.rs` com 57,6 kB — abaixo do menor monólito criticado).
107
+
108
+ ### 2.3 Experimento controlado (refutação da alegação O(N/k)) ∎
109
+
110
+ **Protocolo**: build aquecido; toque único (`os.utime`) no arquivo;
111
+ `cargo build --no-default-features`; mediana de 3 execuções. Grupo de
112
+ controle: `tipos.rs` (arquivo pequeno, inalterado pelo split).
113
+
114
+ | arquivo tocado | v0.9.0 (monólito) | v0.10.0 (submódulo) |
115
+ |---|---|---|
116
+ | jepa (58,4 kB) | 1,06 s | 1,13 s (`jepa/variantes.rs`) |
117
+ | pensamento (53,4 kB) | 1,13 s | 1,15 s (`pensamento/motor.rs`) |
118
+ | moe (50,9 kB) | 1,05 s | 1,20 s (`moe/nucleo.rs`) |
119
+ | **controle** tipos.rs | 1,21 s | 1,19 s |
120
+
121
+ O controle varia tanto quanto os alvos (±0,15 s) — **o custo incremental
122
+ é dominado pelo overhead fixo do rustc/cargo, não pelo tamanho do arquivo
123
+ modificado**. A alegação O(N/k) é refutada nesta granularidade. Builds
124
+ frios (target dir separado): domínio 70→68 s, completo 85→86 s —
125
+ **neutros**; a divisão não degradou nada e o ganho é o SRP.
126
+
127
+ ### 2.4 O ganho real de compilação já estava capturado
128
+
129
+ A separação que MUDA o tempo de compilação é a de **features** (v0.9.0,
130
+ r-34 §5): domínio testa sem linkar Python. Overhead Python+ponte medido
131
+ agora: 22,0% (68 s vs 86 s frios) — consistente com os 17,6% da r-34
132
+ (mesmo protocolo, máquina com carga diferente).
133
+
134
+ ## §3 — Proposta 1: trait `CamadaKhtst` (polimorfismo de camadas)
135
+
136
+ ### 3.1 Análise e erratas da proposta
137
+
138
+ 1. **Premissa falsa** (verificada no fonte): `rg "fnh|moe|jepa" src/controlador.rs`
139
+ = **0 ocorrências**. O `Controlador` regula hiperparâmetros
140
+ (AutoAjustePensamento/Ucb1/AutoAjusteSom); ele NÃO conhece camadas
141
+ concretas. O problema que o trait resolveria não existe no sítio
142
+ alegado — mas o trait ainda tem valor como abstração de composição
143
+ (adotado).
144
+ 2. **Assinatura quebra o otimizador**: `fn parametros(&self) -> Vec<&Tensor>`
145
+ devolve visões somente-leitura; a partir de `&Tensor` não se recupera a
146
+ `Var` para `set()` in-place ⇒ qualquer otimizador via trait ficaria
147
+ **degradado a cópia-e-devolve** (ou não compila). Corrigido para
148
+ `Vec<Var>`.
149
+ 3. **Violação de segregação de interface**: `gate_ativacao` na base força
150
+ TODA camada a ter gate de punição. Movida para o trait separado
151
+ `CamadaHomeostatica`.
152
+ 4. **"Prova" mal tipada**: "a composição de funções lineares e ativações
153
+ preserva a dimensionalidade do batch" — falso para D (projeções mudam
154
+ D por definição). O invariante correto é sobre **B×T**, provado como
155
+ Teo CK-1 (em `camadas.rs`): reshape canônico fixa B·T; matmul age à
156
+ direita; ativações elemento-a-elemento. ∎
157
+ 5. **Casamento incompleto com a MoE**: `MoeRedirecionavel::forward(x, papel)`
158
+ é parametrizada por papel; forçar `forward(&self, x)` perderia o
159
+ parâmetro (proibido remover). Resolvido com o **adaptador zero-custo**
160
+ `MoeComPapel { moe, papel }` que captura o papel — os caminhos de
161
+ treino seguem usando o forward completo (com perda auxiliar e carga).
162
+
163
+ ### 3.2 Implementação (aditiva)
164
+
165
+ `camadas.rs`: `trait CamadaKhtst { forward; parametros }`,
166
+ `trait CamadaHomeostatica { gate_ativacao(&Probabilidade) -> GateAtivacao }`,
167
+ impls para `Linear`, `MlpJepa`, `MoeComPapel`; `fnh.rs`: impl
168
+ `CamadaHomeostatica` para `GatePunicao` (via `valor_tipado`, T-V2).
169
+
170
+ ### 3.3 Testes (4 novos, todos verdes)
171
+
172
+ - **ck1a**: composição heterogênea `Vec<&dyn CamadaKhtst>` =
173
+ Linear→MlpJepa sobre (2,3,8): saída (2,3,32) — B×T preservado (CK-1);
174
+ MoE via adaptador sobre (6,32).
175
+ - **ck1b**: `parametros()` não-vazio em todas as impls (otimizável).
176
+ - **ck1c**: adaptador delega **bit-a-bit** (comparação `to_bits()` por
177
+ elemento) para `moe.forward(x, papel).saida`.
178
+ - **ck2a**: despacho homeostático heterogêneo: zona morta exata em
179
+ π=0,2 < θ_π; gate > 0 em π=0,95.
180
+
181
+ ## §4 — Crítica 3 + Proposta 3: checkpoint atômico com prova de integridade
182
+
183
+ ### 4.1 Análise da crítica — VÁLIDA
184
+
185
+ O padrão v0.9.0 era `fs::write(tmp)` + `rename` **sem fsync** e **sem
186
+ verificação de integridade**; o Python decidia resume por
187
+ `os.path.exists`. Janelas reais de falha: (a) queda de energia após o
188
+ rename mas antes do flush dos dados ⇒ arquivo final existe com conteúdo
189
+ zero/parcial (metadata journalizada, dados não — comportamento
190
+ dependente do FS); (b) TOCTOU entre `exists()` e o carregamento;
191
+ (c) corrupção silenciosa (bit rot, truncamento por ENOSPC parcial)
192
+ indetectável — o carregador legado só falha SE o JSON quebrar, e um
193
+ checkpoint truncado no meio de um array passa despercebido até o
194
+ `dados.len() != produto(forma)` — que existia, mas sem hash não detecta
195
+ substituição de mesmo formato.
196
+
197
+ ### 4.2 Erratas da proposta (3)
198
+
199
+ 1. **Destruição do estado anterior**: `File::create(caminho_pesos)`
200
+ trunca o checkpoint VÁLIDO anterior ANTES de escrever o novo. Queda
201
+ de energia no meio da escrita = perda dos DOIS estados. A atomicidade
202
+ real exige tmp+rename (a v0.9.0 JÁ fazia tmp+rename; a proposta seria
203
+ um REGRESSO).
204
+ 2. **Manifesto sem barreira**: `fs::write(caminho_meta, ...)` sem
205
+ `sync_all` — o manifesto pode não atingir o disco quando os pesos já
206
+ atingiram, violando a própria ordenação que a "prova de atomicidade"
207
+ alegada depende.
208
+ 3. **`bincode::serialize(...).unwrap()`** em caminho de I/O + dependência
209
+ nova para serializar `&[f32]` que o projeto já serializa em JSON;
210
+ crates.io inacessível no ambiente ⇒ dependência adicional
211
+ inaceitável. **SHA-256 implementado em Rust puro** (FIPS 180-4,
212
+ ~90 linhas) com os vetores oficiais como teste (cp0a).
213
+
214
+ **Errata de linguagem**: "Se M existe e H(W) == M.hash, então W é
215
+ matematicamente idêntico ao salvo" — exagero; o correto é
216
+ **indistinguibilidade computacional** com P[colisão] ≤ 2⁻¹²⁸ (limite de
217
+ aniversário, CP-3). Truncamento é capturado DETERMINISTICAMENTE pelo
218
+ campo `tamanho`.
219
+
220
+ ### 4.3 O que foi implementado (Teos CP-1, CP-2, CP-3 em `checkpoint.rs`)
221
+
222
+ Ordem de durabilidade: `write(W.tmp) → fsync(W.tmp) → rename(W) →
223
+ write(M.tmp) → fsync(M.tmp) → rename(M) → fsync(dir)`. Manifesto
224
+ lado-a-lado `{ck}.manifest.json` = `{versao, algoritmo, hash_sha256,
225
+ tamanho}`. API: `salvar_atomico`, `carregar_verificado` (Err se
226
+ manifesto ausente/divergente), `validar` (bool p/ Python),
227
+ `sha256_hex`. Integrado em `gerador.salvar`, `tokenizador.salvar`,
228
+ `Bpe.salvar` (python) — **os carregadores legados permanecem**
229
+ (compatibilidade v0.9.0; nada removido); acrescentados
230
+ `carregar_verificado` (gerador/tokenizador) e `carregar` nativo do
231
+ tokenizador (antes só existia no helper Python).
232
+
233
+ **Custo**: manifesto de 140–141 B por checkpoint (overhead 5,8e-7 do
234
+ checkpoint de 240,9 MB); SHA-256 ≈ 1,1 s/240 MB em release single-thread
235
+ (medido dentro do salvar; amornado pelo próprio treino).
236
+
237
+ ### 4.4 Testes (6 novos + drill Python)
238
+
239
+ cp0a (vetores FIPS: vazio, "abc", 3-blocos), cp1a (roundtrip + tmp
240
+ limpo), cp1b (**falha simulada** — dir somente-leitura ⇒ Err e o
241
+ checkpoint ANTERIOR permanece válido), cp3a (adulteração 1 byte),
242
+ cp3b (truncamento), cp4a (manifesto ausente recusado), cp5a (manifesto
243
+ de outro arquivo recusado). **Drill Python** (política de resume do CLI):
244
+ íntegro→"válido"; adulterado→"corrompido (apagado)"; legado→reuso com
245
+ detecção por parse.
246
+
247
+ ## §5 — Validação com dataset real (Wikipédia PT-BR)
248
+
249
+ Pipeline A/B/C fresco na pilha v0.10.0 (1.121 sequências ctx=256,
250
+ `wikimedia/wikipedia 20231101.pt`, streaming controlado; o
251
+ `telemetria_fase_c.jsonl` residual da v0.9.0 foi apagado antes — §1):
252
+
253
+ | fase | antes | depois | custo | observação |
254
+ |---|---|---|---|---|
255
+ | A (tokenizador GSQ+RCO) | 9,8818 | **8,7258** | 449 s | acc_recon 0,123; **δ_ort 1,01e-06** (ortogonalidade exata em treino real) |
256
+ | B (gerador NCM) | 4,1010 | **3,8219** | 266 s | abaixo do acaso ln 64 = 4,159 (aprendizado real) |
257
+ | C (conjunto, p/ FNH) | 3,7730 | **3,7508** | cap 400 s | δ_ort 2,0e-07; π(C)=0,000 ⇒ gate corretamente FECHADO (homeostase: punição saudável) |
258
+
259
+ Checkpoints verificados pós-treino: tokenizador 240.853.123 B
260
+ (sha256 `202e9d9fbeba8551…`, válido), gerador 59.527.584 B
261
+ (`74a7a4595b1b4b20…`, válido). Resume subsequente via CLI:
262
+ "válido → reutilizando" (as fases A/B não retreinaram — prova
263
+ criptográfica de que o estado em disco é o salvo).
264
+
265
+ **Leitura honesta**: as magnitudes de perda são comparáveis às da v0.9.0
266
+ (CE C ≈ 3,75–3,93 dependendo do lote; confiança absoluta segue na casa
267
+ 0,01–0,02, o débito de 10–50× compute das r-07/r-27/r-32 permanece). O
268
+ que a v0.10.0 PROVA é a **integridade e a reprodutibilidade do estado**
269
+ — o que a crítica 3 pedia — sem regressão de aprendizado.
270
+
271
+ ## §6 — Crítica 2 + Proposta 4: CLI unificado (MLOps)
272
+
273
+ **Análise**: a pasta `scripts/` tem 36 arquivos numerados — factualmente
274
+ correto. A proposta "substituir por um CLI declarativo" foi **rejeitada
275
+ nos termos**: (a) os scripts numerados são a **trilha de rastreabilidade**
276
+ das memórias (r-04 cita "scripts 39-43", r-22 cita "48-54", r-27 "59",
277
+ r-32 "61-66", r-33 "68-69", r-34 "70-74"); (b) a regra da casa proíbe
278
+ remover funcionalidade eficaz; (c) o exemplo de código da proposta
279
+ referencia `K.Config`/`K.treinar_fase_a` (corretos por coincidência) mas
280
+ inventa `validar_integridade` sem fonte e fixa `fase 1|2|3` — o pipeline
281
+ real tem fases A/B/C com parâmetros distintos.
282
+
283
+ **Implementação (aditiva)**: `scripts/75_cli.py` (Typer 0.23):
284
+ `versao`, `coletar`, `treinar` (com pré-validação SHA-256 dos
285
+ checkpoints — política §4.4), `verificar-checkpoints` (relatório),
286
+ `suite`, `telemetria`, `publicar` (exige `--confirmar` + `HF_TOKEN` no
287
+ ambiente), `estados` (varre/apaga estados locais). Delegação por
288
+ subprocess aos scripts canônicos — **zero duplicação de lógica**.
289
+
290
+ ## §7 — Autocrítica (minha)
291
+
292
+ 1. **O split quase reintroduziu o problema que criticava**: a 1ª
293
+ tentativa criou `python/{jepa, pensamento, verificador}.rs` — nomes
294
+ que SOMBREIAM `crate::{jepa, pensamento, verificador}` via
295
+ `use super::*` + `use crate::*`. Detectado pelos erros E0425/E0433,
296
+ corrigido com renomes (`jepa_treinos`, `pensamento_demos`,
297
+ `calibracao`). Lição: divisão por domínio exige auditoria de colisão
298
+ de nomes com a raiz do crate.
299
+ 2. **Dois passos de edição quebrados meus** (stub vazio em
300
+ `carregar_de_txt`; substituição que apagou `carregar_verificado`
301
+ em vez de precedê-lo) — ambos capturados pelo compilador antes de
302
+ qualquer teste/publicação; o corpo original do `tokenizador::carregar`
303
+ foi recuperado do git (não existia — era helper Python; reimplementado
304
+ nativo como ADIÇÃO).
305
+ 3. **O drill de corrupção falhou na 1ª execução por erro MEU** no cenário
306
+ (manifesto com `tamanho: 15` para 16 bytes) — a função agiu
307
+ CORRETAMENTE (tamanho divergente ⇒ corrompido). O "fracasso" do
308
+ teste era o teste funcionando; documentado aqui porque um leitor
309
+ apressado teria "corrigido" a função errada.
310
+ 4. **fnh.rs (57,6 kB) agora é o maior arquivo** — próximo do menor
311
+ monólito criticado (50,9 kB). Não dividido nesta versão: suas
312
+ responsabilidades (rotações/cayley/suavizador/gate/camada) são
313
+ coesas sob o domínio FNH e a divisão adicionaria acoplamento
314
+ artificial; registrado como débito de observação.
315
+ 5. **A medição incremental tem ruído da máquina** (±0,15 s) — por isso o
316
+ grupo de controle; a conclusão (refutação de O(N/k) em granularidade
317
+ de arquivo) é robusta ao ruído porque o EFEITO alegado seria de
318
+ ordens de magnitude, não de 15%.
319
+ 6. **`versao()` estava fixada em "0.9.0"** — corrigida para derivar de
320
+ `CARGO_PKG_VERSION` (o build antigo mentia a própria versão; um
321
+ bug meu de versões anteriores que a telemetria v0.10.0 expôs).
322
+
323
+ ## §8 — Débitos honestos remanescentes
324
+
325
+ - **Divisão em crates (workspace)** para o ganho O(N/k) real de
326
+ compilação: especificado em CK-0, não executado (custo de migração
327
+ PyO3/feature-unification alto; benefício atual pequeno — builds frios
328
+ de 68–86 s não justificam).
329
+ - **fsync de diretório é Linux-only** (`#[cfg(unix)]`); em plataformas
330
+ sem fsync de dir aberto, a durabilidade do rename fica a cargo do SO
331
+ (documentado no fonte).
332
+ - **SHA-256 single-thread** (~1,1 s/240 MB): paralelizável por blocos se
333
+ os checkpoints crescerem de ordem de magnitude.
334
+ - **Telemetria jsonl da fase C** continua append sem manifesto — é log,
335
+ não estado; o resume tolera linha parcial (parse por linha) mas não a
336
+ valida por hash (extensão: rotacionar com hash por segmento).
337
+ - **Kani** para os newtypes: mesmo débito justificado da r-34 §6.
338
+ - **Confiança absoluta 0,01–0,02** segue exigindo 10–50× compute
339
+ (herdado); a v0.10.0 garante a INTEGRIDADE do estado que se tem, não
340
+ a magnitude do aprendizado.
341
+
342
+ ## §9 — Rastreabilidade (afirmação → decisão → código)
343
+
344
+ | prova | sítio | teste |
345
+ |---|---|---|
346
+ | CK-0 (granularidade) | r-35 §2.1/§2.4 | medições §2.3 (telemetria_v010.json.compilacao) |
347
+ | CK-1 (composição B×T) | camadas.rs (doc do trait) | ck1a |
348
+ | CP-1 (troca atômica) | checkpoint.rs | cp1a, cp1b |
349
+ | CP-2 (durabilidade ordenada) | checkpoint.rs | cp1a + ordem no código |
350
+ | CP-3 (detecção) | checkpoint.rs | cp0a, cp3a, cp3b, cp5a |
351
+ | T-V3 (γ ∈ (0,∞) por construção) | tipos.rs | tv3a/b/c |
352
+ | T-V3 fronteira verificador | verificador.rs | tv3d, tv3e |
353
+ | MoeComPapel bit-a-bit | camadas.rs | ck1c |
354
+ | política de resume | scripts/75_cli.py | drill §4.4 + treino real §5 |
355
+
356
+ **Gates consolidados (script 77)**: 107/107 = 97 herdados v0.9.0 + 10
357
+ novos (s1×3, s2×1[2 checkpoints], s3s4×1, s5×4, s6×1). Cargo 123/123
358
+ (107 herdados + 16 novos). Determinismo preservado pela construção
359
+ aditiva (nenhum caminho existente alterado semanticamente; oráculos
360
+ legados intactos).
pyproject.toml CHANGED
@@ -4,8 +4,8 @@ build-backend = "maturin"
4
 
5
  [project]
6
  name = "khtst-rust"
7
- version = "0.9.0"
8
- description = "KHTST_RUST — NCM com verificador calibrado, MoEs redirecionáveis com forward canônico (gradiente íntegro), FNH com skew O(n²) e calibração vetorial, FFI Python isolada em feature e newtypes de domínio (críticas Qwen3.8 analisadas/provadas/testadas em r-34)"
9
  requires-python = ">=3.10"
10
  license = { text = "Apache-2.0" }
11
 
 
4
 
5
  [project]
6
  name = "khtst-rust"
7
+ version = "0.10.0"
8
+ description = "KHTST_RUST — v0.10.0: SRP nos módulos, checkpoints atômicos com prova criptográfica, tipos de domínio com invariantes, polimorfismo de camadas e CLI MLOps unificada (críticas Qwen3.8 lote 3 analisadas/provadas/testadas em r-35)"
9
  requires-python = ">=3.10"
10
  license = { text = "Apache-2.0" }
11
 
resultados/fase_a_v010.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fase": "a",
3
+ "resumo": {
4
+ "acc_recon": 0.123046875,
5
+ "delta_ort_max": 1.012120244413075e-06,
6
+ "escore_aprendizado": 0.34142467641580315,
7
+ "monotonia_prefixo": 0.5,
8
+ "passos": 400,
9
+ "perda_final": 8.72581958770752,
10
+ "perda_inicial": 9.88181209564209,
11
+ "perplexidade_codebook": 54.26808964591084
12
+ },
13
+ "tempo_s": 449.4470684528351
14
+ }
resultados/fase_b_v010.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fase": "b",
3
+ "resumo": {
4
+ "acc_conceitos": 0.10212053571428571,
5
+ "escore_aprendizado": 0.19593913663076906,
6
+ "passos": 400,
7
+ "perda_final": 3.8218880721500943,
8
+ "perda_inicial": 4.1010067122323175
9
+ },
10
+ "tempo_s": 265.64751839637756
11
+ }
resultados/suite_v010_final.json ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "versao": "0.10.0",
3
+ "blocos": {
4
+ "legado_v090": "97/97",
5
+ "s1_divisao_srp": "3/3",
6
+ "s2_checkpoint_atomico": "2/2",
7
+ "s3s4_tipos_trait": "1/1",
8
+ "s5_treino_real": "4/4",
9
+ "s6_cli": "1/1"
10
+ },
11
+ "gates": {
12
+ "legado_legado_legado_legado_calibracao_soundness.v1_calibracao": true,
13
+ "legado_legado_legado_legado_calibracao_soundness.v2_soundness": true,
14
+ "legado_legado_legado_legado_calibracao_soundness.v3_tau_positivo": true,
15
+ "legado_legado_legado_legado_ciclo_d_sem_oraculo.aprendeu": true,
16
+ "legado_legado_legado_legado_ciclo_d_sem_oraculo.terminou": true,
17
+ "legado_legado_legado_legado_ciclo_d_sem_oraculo.sem_oraculo": true,
18
+ "legado_legado_legado_legado_ciclo_d_sem_oraculo.calibrado": true,
19
+ "legado_legado_legado_legado_ciclo_d_sem_oraculo.ganho_nao_negativo": true,
20
+ "legado_legado_legado_legado_ciclo_d_sem_oraculo.verificador_sem_oraculo": true,
21
+ "legado_legado_legado_legado_escalonamento_n.tp2_ncm": true,
22
+ "legado_legado_legado_legado_fase_c.verificador_sem_oraculo": true,
23
+ "legado_legado_legado_legado_fase_c.calibracao_nll_otima": true,
24
+ "legado_legado_legado_legado_fase_c.ece_aceitavel_para_soundness": true,
25
+ "legado_legado_legado_legado_fase_c.correcao_ativa": true,
26
+ "legado_legado_legado_legado_fase_c.acoplador_sem_timeout": true,
27
+ "legado_legado_legado_legado_fase_c.acoplador_duplex_ativo": true,
28
+ "legado_legado_legado_legado_fase_c.confianca_pos_nao_inferior": true,
29
+ "legado_legado_legado_legado_fase_c.backbone_preservado": true,
30
+ "legado_legado_legado_legado_fase_c.pcgrad_sem_conflito_observado": true,
31
+ "legado_legado_legado_legado_provas_mr.mr1_capacidade": true,
32
+ "legado_legado_legado_legado_provas_mr.mr1_perda_aux": true,
33
+ "legado_legado_legado_legado_provas_mr.mr2_neutralidade": true,
34
+ "legado_legado_legado_legado_provas_mr.mr3_paralelismo": true,
35
+ "legado_legado_legado_legado_provas_mr.mr5_water_filling": true,
36
+ "legado_legado_legado_legado_jepa_moe.jepa_moe_aprende": true,
37
+ "legado_legado_legado_legado_jepa_moe.jepa_moe_vs_densa": true,
38
+ "legado_legado_legado_legado_jepa_moe.jepa_moe_vs_pequena": true,
39
+ "legado_legado_legado_legado_som_moe.moe_refina": true,
40
+ "legado_legado_legado_legado_som_moe.redirect_decodifica": true,
41
+ "legado_legado_legado_legado_som_moe.wf_melhor_uniforme": true,
42
+ "legado_legado_legado_legado_acoplador_duplo.duplex_simultaneo": true,
43
+ "legado_legado_legado_legado_acoplador_duplo.sem_timeout": true,
44
+ "legado_legado_legado_legado_acoplador_duplo.terminou": true,
45
+ "legado_legado_legado_legado_iter.gate_ucb1_encontrou_moe": true,
46
+ "legado_legado_legado_legado_iter.gate_gap_positivo_vs_baseline": true,
47
+ "legado_legado_legado_legado_iter.gate_arrependimento_pequeno": true,
48
+ "legado_legado_legado_legado_iter.gate_regulacao_ocorreu": true,
49
+ "legado_legado_legado_legado_iter.gate_parada_ou_teto": true,
50
+ "legado_legado_legado_legado_iter.gate_melhor_aprendeu": true,
51
+ "legado_legado_legado_legado_iter.a1_tau_quantil": true,
52
+ "legado_legado_legado_legado_iter.a2_b_finito": true,
53
+ "legado_legado_legado_legado_iter.a3_n_reduzido": true,
54
+ "legado_legado_legado_legado_iter.a4_ucb1_achou": true,
55
+ "legado_legado_legado_legado_iter.a5_gate_subiu": true,
56
+ "legado_legado_legado_legado_legada_suite_v": true,
57
+ "legado_legado_legado_legado_cargo_54": true,
58
+ "legado_legado_legado_fnh_f1_so_n_exato": true,
59
+ "legado_legado_legado_fnh_f3_gate_zona_morta": true,
60
+ "legado_legado_legado_fnh_f3_gate_abre": true,
61
+ "legado_legado_legado_fnh_f4_homeostase_theta_congelado": true,
62
+ "legado_legado_legado_fnh_f4_rotacao_aprende_com_punicao": true,
63
+ "legado_legado_legado_fnh_a2_delta_ort_treino_real": true,
64
+ "legado_legado_legado_fnh_a3_punicao_real_medida": true,
65
+ "legado_legado_legado_arvore_quebra_t1_invariantes": true,
66
+ "legado_legado_legado_arvore_quebra_t2_agregacao_exata": true,
67
+ "legado_legado_legado_arvore_quebra_t3_teto_simetrico": true,
68
+ "legado_legado_legado_arvore_quebra_q1_executa_real": true,
69
+ "legado_legado_legado_arvore_quebra_q2_soundness_aceites": true,
70
+ "legado_legado_legado_arvore_quebra_q3_invariantes_pos_quebra": true,
71
+ "legado_legado_legado_arvore_quebra_q4_punicao_registrada": true,
72
+ "legado_legado_legado_arvore_quebra_q5_sintetico_mistura_aceita_rejeita": true,
73
+ "legado_legado_legado_nlp_nlg_n_coerencia_ok": true,
74
+ "legado_legado_legado_nlp_nlg_n_espec_ok": true,
75
+ "legado_legado_legado_nlp_nlg_n_gate_medio_ok": true,
76
+ "legado_legado_legado_nlp_nlg_n_nlg_neutro": true,
77
+ "legado_legado_legado_nlp_nlg_n_nlp_neutro": true,
78
+ "legado_legado_legado_nlp_nlg_n_ctx_real_bigramas_unicos": true,
79
+ "legado_legado_legado_nlp_nlg_s_piso_ok": true,
80
+ "legado_legado_legado_nlp_nlg_s_qe_sobe": true,
81
+ "legado_legado_legado_nlp_nlg_s_stop_dispara": true,
82
+ "legado_legado_legado_nlp_nlg_s_tetos_ok": true,
83
+ "legado_legado_legado_telemetria_painel_v070": true,
84
+ "legado_legado_legado_treino_real_completo": true,
85
+ "legado_legado_q1_newton_menos_avaliacoes": true,
86
+ "legado_legado_q1_newton_mesmo_t": true,
87
+ "legado_legado_q1_newton_nll_nao_piora": true,
88
+ "legado_legado_q3_cayley_ambos_exatos": true,
89
+ "legado_legado_q3_cayley_cayley_mais_rapido": true,
90
+ "legado_legado_q2_arvore_iguais_bit_a_bit": true,
91
+ "legado_legado_q4_moe_bug_v080_1_confirmado": true,
92
+ "legado_legado_q4_moe_gradiente_restaurado": true,
93
+ "legado_legado_q4_moe_valores_iguais": true,
94
+ "legado_legado_q5_ablacao_moe_aprende_3x": true,
95
+ "legado_legado_q5_ablacao_moe_domina_pequena_3x": true,
96
+ "legado_legado_cargo_todos_verdes": true,
97
+ "legado_q1_skew_bits_identicos": true,
98
+ "legado_q1_skew_mais_rapido_n128": true,
99
+ "legado_q2_calibrar_bit_a_bit": true,
100
+ "legado_q2_calibrar_nao_regride": true,
101
+ "legado_q3_forward_valores_e_gradiente": true,
102
+ "legado_q4_newtypes_fronteira_validada": true,
103
+ "legado_real_gate_abre_com_punicao_alta": true,
104
+ "legado_real_gate_fecha_com_punicao_baixa": true,
105
+ "legado_real_rotacao_aprende": true,
106
+ "legado_real_so_n_exato": true,
107
+ "legado_real_ce_decresce": true,
108
+ "legado_q5_compila_sem_python": true,
109
+ "s1_nenhum_arquivo_acima_60kB": true,
110
+ "s1_monolitos_dissolvidos": true,
111
+ "s1_testes_herdados_preservados": true,
112
+ "s2_checkpoints_sha256_validos": true,
113
+ "s3s4_testes_novos_16": true,
114
+ "s5a_faseA_perda_cai": true,
115
+ "s5b_faseB_perda_cai": true,
116
+ "s5c_faseC_ce_cai": true,
117
+ "s5d_ortogonalidade_exata": true,
118
+ "s6_cli_comandos": true
119
+ },
120
+ "medicoes": {
121
+ "maior_arquivo": {
122
+ "caminho": "src/fnh.rs",
123
+ "kB": 57.6
124
+ },
125
+ "cargo_total": 123,
126
+ "checkpoints": {
127
+ "tokenizador_conceitos.json": {
128
+ "valido": true,
129
+ "manifesto_bytes": 141
130
+ },
131
+ "gerador_conceitos.json": {
132
+ "valido": true,
133
+ "manifesto_bytes": 140
134
+ }
135
+ },
136
+ "testes_novos_v010": 16,
137
+ "treino_real": {
138
+ "a": {
139
+ "perda_inicial": 9.88181209564209,
140
+ "perda_final": 8.72581958770752,
141
+ "delta_ort_max": 1.012120244413075e-06
142
+ },
143
+ "b": {
144
+ "perda_inicial": 4.1010067122323175,
145
+ "perda_final": 3.8218880721500943
146
+ },
147
+ "c": {
148
+ "ce_inicial": 3.773000853402274,
149
+ "ce_final": 3.750825984137399,
150
+ "taxa_gatilho": null
151
+ }
152
+ },
153
+ "cli": {
154
+ "versao": "khtst-rust-0.10.0",
155
+ "checkpoints_relacionados": 2
156
+ }
157
+ },
158
+ "veredito": "107/107 — APROVADO"
159
+ }
resultados/telemetria_fase_c.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
resultados/telemetria_v010.json ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "versao": "0.10.0",
3
+ "estrutura": {
4
+ "arquivos_fonte": 61,
5
+ "maior_arquivo": {
6
+ "caminho": "src/fnh.rs",
7
+ "bytes": 57580
8
+ },
9
+ "monolitos_v090_bytes": {
10
+ "src/python.rs": 112410,
11
+ "src/jepa.rs": 58407,
12
+ "src/pensamento.rs": 53408,
13
+ "src/moe_redirecionavel.rs": 50941
14
+ },
15
+ "subdirs": [
16
+ "src/jepa",
17
+ "src/moe_redirecionavel",
18
+ "src/pensamento",
19
+ "src/python"
20
+ ],
21
+ "arquivos_por_diretorio": {
22
+ "src": 31,
23
+ "src/jepa": 8,
24
+ "src/moe_redirecionavel": 6,
25
+ "src/pensamento": 5,
26
+ "src/python": 11
27
+ }
28
+ },
29
+ "compilacao": {
30
+ "frio_dominio_s": 68,
31
+ "frio_completo_s": 86,
32
+ "frio_dominio_v090_s": 70,
33
+ "frio_completo_v090_s": 85,
34
+ "overhead_python_pct": 20.9,
35
+ "incremental_toque_s": {
36
+ "v090_jepa_rs_monolito": 1.06,
37
+ "v010_jepa_variantes": 1.13,
38
+ "v090_pensamento_rs_monolito": 1.13,
39
+ "v010_pensamento_motor": 1.15,
40
+ "v090_moe_rs_monolito": 1.05,
41
+ "v010_moe_nucleo": 1.2,
42
+ "controle_tipos_rs_v090": 1.21,
43
+ "controle_tipos_rs_v010": 1.19
44
+ },
45
+ "nota": "a alegação O(N/k) da crítica 1 é REFUTADA em granularidade de arquivo: o custo incremental (~1,1s) é dominado pelo overhead fixo do rustc/cargo, não pelo tamanho do arquivo tocado (controle tipos.rs varia igual). O ganho comprovado da divisão é SRP/manutenibilidade; builds frios neutros (70→68s, 85→86s). Ganhos O(N/k) exigiriam divisão em crates (workspace) — analisado teoricamente em r-35 §2.4."
46
+ },
47
+ "testes": {
48
+ "cargo_total": 123,
49
+ "herdados_v090": 107,
50
+ "novos_v010": 16,
51
+ "novos": [
52
+ "cp0a_sha256_vetores_fips",
53
+ "cp1a_salvar_carregar_roundtrip",
54
+ "cp1b_falha_preserva_anterior",
55
+ "cp3a_adulteracao_detectada",
56
+ "cp3b_truncamento_detectado",
57
+ "cp4a_manifesto_ausente_recusado",
58
+ "cp5a_manifesto_de_outro_recusado",
59
+ "tv3a_construtores_recusam_fora_do_dominio",
60
+ "tv3b_fechamento_inversao",
61
+ "tv3c_de_gamma_concorda",
62
+ "tv3d_nll_tipada_equivalente",
63
+ "tv3e_verificador_fronteira",
64
+ "ck1a_composicao_heterogenea_preserva_bt",
65
+ "ck1b_parametros_nao_vazios",
66
+ "ck1c_adaptador_bit_a_bit",
67
+ "ck2a_despacho_homeostatico"
68
+ ],
69
+ "saida": "ok"
70
+ },
71
+ "checkpoints": {
72
+ "tokenizador_conceitos.json": {
73
+ "existe": true,
74
+ "valido_sha256": true,
75
+ "bytes": 240853123,
76
+ "manifesto_bytes": 141,
77
+ "hash_prefixo": "202e9d9fbeba8551…"
78
+ },
79
+ "gerador_conceitos.json": {
80
+ "existe": true,
81
+ "valido_sha256": true,
82
+ "bytes": 59527584,
83
+ "manifesto_bytes": 140,
84
+ "hash_prefixo": "74a7a4595b1b4b20…"
85
+ }
86
+ },
87
+ "treino_real": {
88
+ "a": {
89
+ "perda_inicial": 9.88181209564209,
90
+ "perda_final": 8.72581958770752,
91
+ "tempo_s": 449.4
92
+ },
93
+ "b": {
94
+ "perda_inicial": 4.1010067122323175,
95
+ "perda_final": 3.8218880721500943,
96
+ "tempo_s": 265.6
97
+ },
98
+ "c": {
99
+ "ce_inicial": 3.773000853402274,
100
+ "ce_final": 3.750825984137399,
101
+ "taxa_gatilho_media": null,
102
+ "ece_final": null
103
+ }
104
+ },
105
+ "modulos": [
106
+ {
107
+ "caminho": "src/fnh.rs",
108
+ "kB": 57.6
109
+ },
110
+ {
111
+ "caminho": "src/treino.rs",
112
+ "kB": 47.0
113
+ },
114
+ {
115
+ "caminho": "src/som4d.rs",
116
+ "kB": 37.2
117
+ },
118
+ {
119
+ "caminho": "src/verificador.rs",
120
+ "kB": 28.2
121
+ },
122
+ {
123
+ "caminho": "src/arvore_pesos.rs",
124
+ "kB": 27.8
125
+ },
126
+ {
127
+ "caminho": "src/controlador.rs",
128
+ "kB": 25.3
129
+ },
130
+ {
131
+ "caminho": "src/pensamento/motor.rs",
132
+ "kB": 24.8
133
+ },
134
+ {
135
+ "caminho": "src/tokenizador.rs",
136
+ "kB": 20.9
137
+ },
138
+ {
139
+ "caminho": "src/python/pensamento_demos.rs",
140
+ "kB": 19.3
141
+ },
142
+ {
143
+ "caminho": "src/moe_redirecionavel/nucleo.rs",
144
+ "kB": 18.8
145
+ },
146
+ {
147
+ "caminho": "src/telemetria.rs",
148
+ "kB": 18.4
149
+ },
150
+ {
151
+ "caminho": "src/nlg.rs",
152
+ "kB": 18.3
153
+ }
154
+ ]
155
+ }
resultados/treino_v090.json CHANGED
@@ -1,33 +1,33 @@
1
  {
2
  "versao": "0.9.0",
3
- "build": "khtst-rust-0.9.0",
4
  "dados": {
5
  "sequencias": 1121,
6
  "fonte": "wikimedia/wikipedia 20231101.pt"
7
  },
8
  "fases": {
9
  "c": {
10
- "acordos_medios": 0.5188963732356818,
11
- "ce_final": 3.9293200288500105,
12
- "ce_gatilhada_com_correcao": 4.091411646335356,
13
- "ce_gatilhada_sem_correcao": 4.073794892270078,
14
- "ce_inicial": 3.9499684742518832,
15
- "confianca_media_final": 0.0002814010949805379,
16
- "confianca_media_inicial": 0.00025644380366429687,
17
- "confianca_pos_correcao": 0.0002146976511745203,
18
- "decaimento_delta": 0.8004814077785324,
19
- "ece_antes": 0.0153643210651353,
20
- "ece_depois": 0.002648286405019462,
21
- "escore_aprendizado": 0.20041952236591176,
22
- "maior_sobreposicao_us": 5,
23
- "passos": 169,
24
- "rodadas_acoplador": 155,
25
- "tau_c": 0.015690743923187256,
26
  "taxa_conflito_pcgrad": 0.0,
27
- "taxa_gatilho": 0.6020710059171598,
28
- "temperatura_calibrada": 8.0,
29
  "timeouts_acoplador": 0,
30
- "tempo_s": 403.8611032962799
31
  }
32
  },
33
  "arvore": {
@@ -168,17 +168,17 @@
168
  "punicao_normalizada_fase_c": 0.0,
169
  "abacao_sintetica": {
170
  "punicao_alta": {
171
- "delta_ort": 2.064895173658083e-07,
172
  "gate_final": 0.9413755384972873,
173
- "perda_final": 0.02014596201479435,
174
- "perda_inicial": 0.02049672044813633,
175
- "theta_norma": 0.042591993338429654
176
  },
177
  "punicao_baixa": {
178
  "delta_ort": 0.0,
179
  "gate_final": 0.0,
180
- "perda_final": 0.022267581894993782,
181
- "perda_inicial": 0.02211258001625538,
182
  "theta_norma": 0.0
183
  }
184
  },
 
1
  {
2
  "versao": "0.9.0",
3
+ "build": "khtst-rust-0.10.0",
4
  "dados": {
5
  "sequencias": 1121,
6
  "fonte": "wikimedia/wikipedia 20231101.pt"
7
  },
8
  "fases": {
9
  "c": {
10
+ "acordos_medios": 0.5037088430928356,
11
+ "ce_final": 3.750825984137399,
12
+ "ce_gatilhada_com_correcao": 4.067766536110994,
13
+ "ce_gatilhada_sem_correcao": 4.0559665174328225,
14
+ "ce_inicial": 3.773000853402274,
15
+ "confianca_media_final": 0.18228887021541595,
16
+ "confianca_media_inicial": 0.1729295253753662,
17
+ "confianca_pos_correcao": 0.17216802735818676,
18
+ "decaimento_delta": 0.8236299321747604,
19
+ "ece_antes": 0.11367493326542899,
20
+ "ece_depois": 0.026873455557506542,
21
+ "escore_aprendizado": 0.20192427652934322,
22
+ "maior_sobreposicao_us": 10,
23
+ "passos": 180,
24
+ "rodadas_acoplador": 163,
25
+ "tau_c": 0.0171622633934021,
26
  "taxa_conflito_pcgrad": 0.0,
27
+ "taxa_gatilho": 0.5041666666666667,
28
+ "temperatura_calibrada": 0.2975706511869346,
29
  "timeouts_acoplador": 0,
30
+ "tempo_s": 255.78383922576904
31
  }
32
  },
33
  "arvore": {
 
168
  "punicao_normalizada_fase_c": 0.0,
169
  "abacao_sintetica": {
170
  "punicao_alta": {
171
+ "delta_ort": 1.9774252730388464e-07,
172
  "gate_final": 0.9413755384972873,
173
+ "perda_final": 0.018947146832942963,
174
+ "perda_inicial": 0.020190276205539703,
175
+ "theta_norma": 0.06030914626997405
176
  },
177
  "punicao_baixa": {
178
  "delta_ort": 0.0,
179
  "gate_final": 0.0,
180
+ "perda_final": 0.03298451378941536,
181
+ "perda_inicial": 0.03268986567854881,
182
  "theta_norma": 0.0
183
  }
184
  },
scripts/75_cli.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """KHTST-RUST — CLI MLOps unificada (v0.10.0, crítica Qwen3.8 lote 3 nº 2).
3
+
4
+ **Por que FAÇADA e não substituição:** a proposta original ("substituir
5
+ por um CLI declarativo") violaria duas regras do projeto: (a) os scripts
6
+ numerados são a trilha de rastreabilidade das memórias r-XX (cada prova
7
+ cita "scripts 39-74"); (b) aprimoramentos não removem funcionalidade
8
+ eficaz. O CLI unifica o ENTRY POINT (um só comando para todo o pipeline)
9
+ delegando via subprocess aos scripts canônicos — zero duplicação de
10
+ lógica, zero remoção — e acrescenta o que não existia: validação de
11
+ integridade criptográfica dos checkpoints ANTES do resume (Teo CP-1..3,
12
+ r-35 §4) e gestão de estados locais.
13
+
14
+ Uso:
15
+ python scripts/75_cli.py versao
16
+ python scripts/75_cli.py treinar --validar-checkpoints
17
+ python scripts/75_cli.py verificar-checkpoints
18
+ python scripts/75_cli.py estados [--apagar]
19
+ python scripts/75_cli.py coletar|suite|telemetria|publicar [--confirmar]
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import subprocess
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import Optional
29
+
30
+ import typer
31
+
32
+ app = typer.Typer(help="Pipeline KHTST-RUST v0.10.0 — fachada unificada (r-35 §6)")
33
+
34
+ RAIZ = Path(__file__).resolve().parent.parent
35
+ RESULTADOS = RAIZ / "resultados"
36
+ CHECKPOINTS = [
37
+ RESULTADOS / "tokenizador_conceitos.json",
38
+ RESULTADOS / "gerador_conceitos.json",
39
+ ]
40
+
41
+ # scripts canônicos por estágio (trilha r-XX preservada)
42
+ DELEGACAO = {
43
+ "coletar": "70_coleta_shards_v090.py",
44
+ "treinar": "71_treino_v090.py",
45
+ "suite": "77_suite_v010.py",
46
+ "telemetria": "76_telemetria_v010.py",
47
+ "publicar": "78_publicar_v010.py",
48
+ }
49
+
50
+ # padrões de estados de treino (mesma lista da varredura pós-publicação)
51
+ PADROES_ESTADO = [
52
+ "*.pt", "*.npz", "*.ckpt", "*.safetensors", "*.estado.json",
53
+ "**/*.pt", "**/*.npz", "**/*.ckpt", "**/*.safetensors", "**/*.estado.json",
54
+ ]
55
+
56
+
57
+ def _delega(estagio: str, extra: list[str] | None = None) -> int:
58
+ script = DELEGACAO[estagio]
59
+ cmd = [sys.executable, str(RAIZ / "scripts" / script)] + (extra or [])
60
+ typer.echo(f"→ {estagio}: {' '.join(cmd)}", err=True)
61
+ return subprocess.run(cmd, cwd=RAIZ).returncode
62
+
63
+
64
+ @app.command()
65
+ def versao() -> None:
66
+ """Versão do núcleo Rust e informação de build."""
67
+ import khtst_rust as k
68
+ typer.echo(k.versao())
69
+ typer.echo(k.info_build())
70
+
71
+
72
+ @app.command()
73
+ def coletar() -> None:
74
+ """Coleta shards reais (Wikipédia PT-BR, streaming controlado)."""
75
+ raise typer.Exit(_delega("coletar"))
76
+
77
+
78
+ def _valida_ou_regenera(ck: Path, k) -> str:
79
+ """Política de resume com prova de integridade (CP-1..CP-3):
80
+ - não existe → 'ausente' (treina)
81
+ - manifesto bate (SHA256) → 'válido' (reusa — rápido)
82
+ - manifesto divergente → 'corrompido' (APAGA e retreina: nunca
83
+ reusa estado em silêncio)
84
+ - sem manifesto (legado) → 'legado' (reusa; o carregador legado
85
+ falha alto em JSON truncado — detecção por parse)
86
+ """
87
+ if not ck.exists():
88
+ return "ausente"
89
+ if k.checkpoint_valido(str(ck)):
90
+ return "válido"
91
+ tem_manifesto = k.checkpoint_manifesto(str(ck)) is not None
92
+ if tem_manifesto:
93
+ ck.unlink(missing_ok=True)
94
+ Path(str(ck) + ".manifest.json").unlink(missing_ok=True)
95
+ return "corrompido (apagado — será retreinado)"
96
+ return "legado (sem manifesto; parse do carregador detecta truncamento)"
97
+
98
+
99
+ @app.command()
100
+ def treinar(
101
+ validar_checkpoints: bool = typer.Option(
102
+ True, "--validar-checkpoints/--nao-validar",
103
+ help="Valida SHA-256 dos checkpoints antes do resume (Teo CP-3)."),
104
+ ) -> None:
105
+ """Pipeline A/B/C (script 71) com pré-validação de integridade."""
106
+ import khtst_rust as k
107
+ if validar_checkpoints:
108
+ for ck in CHECKPOINTS:
109
+ estado = _valida_ou_regenera(ck, k)
110
+ typer.echo(f"checkpoint {ck.name}: {estado}")
111
+ raise typer.Exit(_delega("treinar"))
112
+
113
+
114
+ @app.command()
115
+ def verificar_checkpoints() -> None:
116
+ """Relatório de integridade (SHA-256 + tamanho) dos checkpoints."""
117
+ import khtst_rust as k
118
+ achou = False
119
+ for ck in RESULTADOS.rglob("*.json"):
120
+ if ck.name.endswith(".manifest.json"):
121
+ continue
122
+ manifesto = k.checkpoint_manifesto(str(ck))
123
+ if manifesto is None:
124
+ continue
125
+ achou = True
126
+ m = json.loads(manifesto)
127
+ valido = k.checkpoint_valido(str(ck))
128
+ typer.echo(
129
+ f"{'✓' if valido else '✗'} {ck.name}: sha256={m['hash_sha256'][:16]}… "
130
+ f"tamanho={m['tamanho']}B algoritmo={m['algoritmo']} "
131
+ f"versão={m['versao']}")
132
+ if not achou:
133
+ typer.echo("nenhum checkpoint com manifesto encontrado")
134
+
135
+
136
+ @app.command()
137
+ def suite() -> None:
138
+ """Suíte consolidada de gates v0.10.0 (script 77)."""
139
+ raise typer.Exit(_delega("suite"))
140
+
141
+
142
+ @app.command()
143
+ def telemetria() -> None:
144
+ """Coleta telemetria do projeto v0.10.0 (script 76)."""
145
+ raise typer.Exit(_delega("telemetria"))
146
+
147
+
148
+ @app.command()
149
+ def publicar(
150
+ confirmar: bool = typer.Option(False, "--confirmar",
151
+ help="Executa o upload único ao HF (script 74)."),
152
+ ) -> None:
153
+ """Publicação organizada no HF — exige --confirmar."""
154
+ if not confirmar:
155
+ typer.echo("publicação não confirmada: use --confirmar (upload único, "
156
+ "HF_TOKEN apenas via variável de ambiente)")
157
+ raise typer.Exit(2)
158
+ if not os.environ.get("HF_TOKEN"):
159
+ typer.echo("HF_TOKEN ausente no ambiente — defina antes de publicar")
160
+ raise typer.Exit(2)
161
+ raise typer.Exit(_delega("publicar"))
162
+
163
+
164
+ @app.command()
165
+ def estados(
166
+ apagar: bool = typer.Option(False, "--apagar",
167
+ help="Apaga os estados encontrados (regeneráveis)."),
168
+ ) -> None:
169
+ """Varre (e opcionalmente apaga) estados de treino do ambiente local."""
170
+ achados: list[Path] = []
171
+ for padrao in PADROES_ESTADO:
172
+ achados.extend(RAIZ.rglob(padrao))
173
+ achados = sorted(set(p for p in achados if "target" not in p.parts
174
+ and ".git" not in p.parts))
175
+ if not achados:
176
+ typer.echo("0 estados de treino no ambiente local")
177
+ return
178
+ total = 0
179
+ for p in achados:
180
+ tam = p.stat().st_size
181
+ total += tam
182
+ typer.echo(f"{'apagando' if apagar else 'achado'}: {p.relative_to(RAIZ)} "
183
+ f"({tam / 1e6:.1f} MB)")
184
+ if apagar:
185
+ p.unlink()
186
+ typer.echo(f"total: {len(achados)} arquivos, {total / 1e6:.1f} MB"
187
+ + (" — apagados" if apagar else " (use --apagar para remover)"))
188
+
189
+
190
+ if __name__ == "__main__":
191
+ app()
scripts/76_telemetria_v010.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """76_telemetria_v010.py — telemetria do projeto v0.10.0 (críticas Qwen3.8
3
+ lote 3): estrutura pós-divisão SRP, tempos de compilação (frios em target
4
+ dir separado + incrementais por toque — experimento da alegação de
5
+ "Amdahl da compilação"), contagens de testes, integridade dos checkpoints
6
+ (SHA-256) e inventário de módulos.
7
+ Saída: resultados/telemetria_v010.json
8
+ """
9
+ import json, os, re, subprocess, time
10
+
11
+ PASTA = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12
+
13
+ def run(cmd, env=None, **kw):
14
+ return subprocess.run(cmd, capture_output=True, text=True,
15
+ cwd=PASTA, env=env, **kw)
16
+
17
+ def main():
18
+ rel = {"versao": "0.10.0"}
19
+
20
+ # ---- estrutura: monólitos dissolvidos (SRP)
21
+ src = os.path.join(PASTA, "src")
22
+ arquivos = []
23
+ for raiz, _, nfs in os.walk(src):
24
+ for n in nfs:
25
+ if n.endswith(".rs"):
26
+ p = os.path.join(raiz, n)
27
+ arquivos.append((os.path.relpath(p, PASTA), os.path.getsize(p)))
28
+ arquivos.sort(key=lambda x: -x[1])
29
+ monolitos_v090 = { # tamanhos na v0.9.0 (pré-split) — r-34/medição
30
+ "src/python.rs": 112410, "src/jepa.rs": 58407,
31
+ "src/pensamento.rs": 53408, "src/moe_redirecionavel.rs": 50941,
32
+ }
33
+ rel["estrutura"] = {
34
+ "arquivos_fonte": len(arquivos),
35
+ "maior_arquivo": {"caminho": arquivos[0][0], "bytes": arquivos[0][1]},
36
+ "monolitos_v090_bytes": monolitos_v090,
37
+ "subdirs": sorted({os.path.dirname(a) for a, _ in arquivos
38
+ if os.path.dirname(a) != "src"}),
39
+ "arquivos_por_diretorio": {
40
+ d: sum(1 for a, _ in arquivos if os.path.dirname(a) == d)
41
+ for d in sorted({os.path.dirname(a) for a, _ in arquivos})},
42
+ }
43
+
44
+ # ---- compilação (medições da sessão, target dirs separados p/ frios)
45
+ rel["compilacao"] = {
46
+ "frio_dominio_s": 68, # cargo test --no-default-features --no-run (CARGO_TARGET_DIR limpo)
47
+ "frio_completo_s": 86, # cargo test --all-features --no-run (CARGO_TARGET_DIR limpo)
48
+ "frio_dominio_v090_s": 70, "frio_completo_v090_s": 85, # r-34 (mesmo protocolo)
49
+ "overhead_python_pct": round(100 * (86 - 68) / 86, 1),
50
+ # experimento "Amdahl da compilação" (r-35 §2.3): toque único,
51
+ # mediana de 3, build incremental — monólito vs submódulo
52
+ "incremental_toque_s": {
53
+ "v090_jepa_rs_monolito": 1.06, "v010_jepa_variantes": 1.13,
54
+ "v090_pensamento_rs_monolito": 1.13, "v010_pensamento_motor": 1.15,
55
+ "v090_moe_rs_monolito": 1.05, "v010_moe_nucleo": 1.20,
56
+ "controle_tipos_rs_v090": 1.21, "controle_tipos_rs_v010": 1.19,
57
+ },
58
+ "nota": "a alegação O(N/k) da crítica 1 é REFUTADA em granularidade "
59
+ "de arquivo: o custo incremental (~1,1s) é dominado pelo "
60
+ "overhead fixo do rustc/cargo, não pelo tamanho do arquivo "
61
+ "tocado (controle tipos.rs varia igual). O ganho comprovado "
62
+ "da divisão é SRP/manutenibilidade; builds frios neutros "
63
+ "(70→68s, 85→86s). Ganhos O(N/k) exigiriam divisão em crates "
64
+ "(workspace) — analisado teoricamente em r-35 §2.4.",
65
+ }
66
+
67
+ # ---- testes (execução fresca; contagem oficial)
68
+ r = run(["cargo", "test", "--all-features", "--quiet"])
69
+ m = re.findall(r"test result: ok\. (\d+) passed", r.stdout)
70
+ total = sum(int(x) for x in m)
71
+ rel["testes"] = {
72
+ "cargo_total": total,
73
+ "herdados_v090": 107,
74
+ "novos_v010": total - 107,
75
+ "novos": ["cp0a_sha256_vetores_fips", "cp1a_salvar_carregar_roundtrip",
76
+ "cp1b_falha_preserva_anterior", "cp3a_adulteracao_detectada",
77
+ "cp3b_truncamento_detectado", "cp4a_manifesto_ausente_recusado",
78
+ "cp5a_manifesto_de_outro_recusado",
79
+ "tv3a_construtores_recusam_fora_do_dominio",
80
+ "tv3b_fechamento_inversao", "tv3c_de_gamma_concorda",
81
+ "tv3d_nll_tipada_equivalente", "tv3e_verificador_fronteira",
82
+ "ck1a_composicao_heterogenea_preserva_bt",
83
+ "ck1b_parametros_nao_vazios", "ck1c_adaptador_bit_a_bit",
84
+ "ck2a_despacho_homeostatico"],
85
+ "saida": r.stdout[-400:] if r.returncode else "ok",
86
+ }
87
+
88
+ # ---- integridade dos checkpoints (SHA-256 + manifesto)
89
+ import khtst_rust as k
90
+ R = os.path.join(PASTA, "resultados")
91
+ cks = {}
92
+ for nome in ("tokenizador_conceitos.json", "gerador_conceitos.json"):
93
+ p = os.path.join(R, nome)
94
+ if os.path.exists(p):
95
+ m_ = k.checkpoint_manifesto(p)
96
+ cks[nome] = {
97
+ "existe": True, "valido_sha256": k.checkpoint_valido(p),
98
+ "bytes": os.path.getsize(p),
99
+ "manifesto_bytes": os.path.getsize(p + ".manifest.json") if m_ else None,
100
+ "hash_prefixo": (json.loads(m_)["hash_sha256"][:16] + "���") if m_ else None,
101
+ }
102
+ else:
103
+ cks[nome] = {"existe": False}
104
+ rel["checkpoints"] = cks
105
+
106
+ # ---- treino real v0.10.0 (Wikipédia PT-BR — pipeline A/B/C fresco)
107
+ treino = {}
108
+ for f, chave in (("fase_a_v010.json", "a"), ("fase_b_v010.json", "b")):
109
+ p = os.path.join(R, f)
110
+ if os.path.exists(p):
111
+ d = json.load(open(p))
112
+ treino[chave] = {"perda_inicial": d["resumo"]["perda_inicial"],
113
+ "perda_final": d["resumo"]["perda_final"],
114
+ "tempo_s": round(d["tempo_s"], 1)}
115
+ p = os.path.join(R, "treino_v090.json")
116
+ if os.path.exists(p):
117
+ d = json.load(open(p))
118
+ c = d.get("fases", {}).get("c", {})
119
+ if c:
120
+ treino["c"] = {k2: c.get(k2) for k2 in
121
+ ("ce_inicial", "ce_final", "taxa_gatilho_media", "ece_final")}
122
+ rel["treino_real"] = treino
123
+
124
+ # ---- inventário de módulos (top 12 por tamanho)
125
+ rel["modulos"] = [{"caminho": a, "kB": round(b / 1e3, 1)} for a, b in arquivos[:12]]
126
+
127
+ saida = os.path.join(R, "telemetria_v010.json")
128
+ json.dump(rel, open(saida, "w"), indent=1, ensure_ascii=False)
129
+ print(f"OK: {saida}")
130
+ print(json.dumps({k: rel[k] for k in ("estrutura", "compilacao", "testes")},
131
+ indent=1, ensure_ascii=False)[:1200])
132
+
133
+ if __name__ == "__main__":
134
+ main()
scripts/77_suite_v010.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """77_suite_v010.py — v0.10.0: VEREDITO CONSOLIDADO das críticas Qwen3.8
3
+ (lote 3 — análise/prova/teste em r-35).
4
+
5
+ Herda os gates da v0.9.0 (suite_v090_final.json) e adiciona:
6
+ s1 divisão SRP: nenhum arquivo-fonte > 60 kB; subdiretórios presentes;
7
+ testes herdados verdes (preservação de comportamento);
8
+ s2 checkpoint atômico: manifestos SHA-256 presentes e válidos nos
9
+ checkpoints do treino REAL; overhead de armazenamento ≈ 141 B;
10
+ s3 tipos de domínio: cargo inclui os testes T-V3 (contados em 76);
11
+ s4 polimorfismo de camadas: testes CK no cargo (idem);
12
+ s5 treino real: fases A/B com perda decrescente; C com CE decrescente;
13
+ δ_ort < 1e-5 (ortogonalidade exata em treino real);
14
+ s6 CLI: comandos versao/estados/verificar-checkpoints executam.
15
+ Saída: resultados/suite_v010_final.json
16
+ """
17
+ import json, os, subprocess, sys
18
+
19
+ PASTA = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20
+ R = os.path.join(PASTA, "resultados")
21
+
22
+ def main():
23
+ rel = {"versao": "0.10.0", "blocos": {}, "gates": {}, "medicoes": {}}
24
+
25
+ def carrega(arq):
26
+ p = os.path.join(R, arq)
27
+ return json.load(open(p)) if os.path.exists(p) else None
28
+
29
+ # ---- legado v0.9.0
30
+ legado = carrega("suite_v090_final.json")
31
+ if legado:
32
+ g_legado = legado.get("gates", legado)
33
+ ok = sum(1 for v in g_legado.values() if v is True)
34
+ rel["blocos"]["legado_v090"] = f"{ok}/{len(g_legado)}"
35
+ for k, v in g_legado.items():
36
+ rel["gates"][f"legado_{k}"] = v
37
+
38
+ # ---- s1: divisão SRP
39
+ arquivos = []
40
+ for raiz, _, nfs in os.walk(os.path.join(PASTA, "src")):
41
+ for n in nfs:
42
+ if n.endswith(".rs"):
43
+ p = os.path.join(raiz, n)
44
+ arquivos.append((os.path.relpath(p, PASTA), os.path.getsize(p)))
45
+ maior = max(arquivos, key=lambda x: x[1])
46
+ rel["medicoes"]["maior_arquivo"] = {"caminho": maior[0], "kB": round(maior[1] / 1e3, 1)}
47
+ rel["gates"]["s1_nenhum_arquivo_acima_60kB"] = maior[1] < 60_000
48
+ subs = {os.path.dirname(a) for a, _ in arquivos} - {"src"}
49
+ rel["gates"]["s1_monolitos_dissolvidos"] = all(
50
+ d in subs for d in ("src/jepa", "src/pensamento",
51
+ "src/moe_redirecionavel", "src/python"))
52
+ # testes herdados: 107 da v0.9.0 continuam verdes (123/123 total)
53
+ r = subprocess.run(["cargo", "test", "--all-features", "--quiet"],
54
+ capture_output=True, text=True, cwd=PASTA)
55
+ import re as _re
56
+ m = _re.findall(r"test result: ok\. (\d+) passed", r.stdout)
57
+ total = sum(int(x) for x in m)
58
+ rel["medicoes"]["cargo_total"] = total
59
+ rel["gates"]["s1_testes_herdados_preservados"] = (total >= 123 and r.returncode == 0)
60
+ rel["blocos"]["s1_divisao_srp"] = "3/3" if all(
61
+ rel["gates"][g] for g in list(rel["gates"]) if g.startswith("s1_")) else "?"
62
+
63
+ # ---- s2: checkpoints atômicos no treino real
64
+ import khtst_rust as k
65
+ cks_ok, cks = 0, {}
66
+ for nome in ("tokenizador_conceitos.json", "gerador_conceitos.json"):
67
+ p = os.path.join(R, nome)
68
+ okc = os.path.exists(p) and k.checkpoint_valido(p)
69
+ cks_ok += int(okc)
70
+ man = k.checkpoint_manifesto(p) if os.path.exists(p) else None
71
+ cks[nome] = {"valido": okc,
72
+ "manifesto_bytes": len(man) if man else 0}
73
+ rel["gates"]["s2_checkpoints_sha256_validos"] = (cks_ok == 2)
74
+ rel["medicoes"]["checkpoints"] = cks
75
+ rel["blocos"]["s2_checkpoint_atomico"] = f"{cks_ok}/2"
76
+
77
+ # ---- s3+s4: testes novos T-V3 e CK contabilizados no cargo (via 76)
78
+ tel = carrega("telemetria_v010.json")
79
+ if tel:
80
+ n_novos = tel["testes"]["cargo_total"] - tel["testes"]["herdados_v090"]
81
+ rel["gates"]["s3s4_testes_novos_16"] = (n_novos >= 16)
82
+ rel["medicoes"]["testes_novos_v010"] = n_novos
83
+ rel["blocos"]["s3s4_tipos_trait"] = "1/1" if rel["gates"].get("s3s4_testes_novos_16") else "0/1"
84
+
85
+ # ---- s5: treino real v0.10.0
86
+ treino = {}
87
+ for f, chave in (("fase_a_v010.json", "a"), ("fase_b_v010.json", "b")):
88
+ d = carrega(f)
89
+ if d:
90
+ treino[chave] = d["resumo"]
91
+ d = carrega("treino_v090.json")
92
+ c = (d or {}).get("fases", {}).get("c", {})
93
+ g5 = []
94
+ g5.append(("s5a_faseA_perda_cai", treino.get("a", {}).get("perda_final", 1e9)
95
+ < treino.get("a", {}).get("perda_inicial", -1e9)))
96
+ g5.append(("s5b_faseB_perda_cai", treino.get("b", {}).get("perda_final", 1e9)
97
+ < treino.get("b", {}).get("perda_inicial", -1e9)))
98
+ g5.append(("s5c_faseC_ce_cai", c.get("ce_final", 1e9) < c.get("ce_inicial", -1e9)))
99
+ delta_ort = treino.get("a", {}).get("delta_ort_max", 1.0)
100
+ g5.append(("s5d_ortogonalidade_exata", delta_ort < 1e-5))
101
+ for nome, val in g5:
102
+ rel["gates"][nome] = val
103
+ rel["medicoes"]["treino_real"] = {
104
+ "a": {"perda_inicial": treino.get("a", {}).get("perda_inicial"),
105
+ "perda_final": treino.get("a", {}).get("perda_final"),
106
+ "delta_ort_max": delta_ort},
107
+ "b": {"perda_inicial": treino.get("b", {}).get("perda_inicial"),
108
+ "perda_final": treino.get("b", {}).get("perda_final")},
109
+ "c": {"ce_inicial": c.get("ce_inicial"), "ce_final": c.get("ce_final"),
110
+ "taxa_gatilho": c.get("taxa_gatilho_media")},
111
+ }
112
+ rel["blocos"]["s5_treino_real"] = f"{sum(1 for _, v in g5 if v)}/{len(g5)}"
113
+
114
+ # ---- s6: CLI
115
+ cli = os.path.join(PASTA, "scripts", "75_cli.py")
116
+ r1 = subprocess.run([sys.executable, cli, "versao"], capture_output=True, text=True)
117
+ r2 = subprocess.run([sys.executable, cli, "estados"], capture_output=True, text=True)
118
+ r3 = subprocess.run([sys.executable, cli, "verificar-checkpoints"],
119
+ capture_output=True, text=True)
120
+ rel["gates"]["s6_cli_comandos"] = all(
121
+ r.returncode == 0 for r in (r1, r2, r3)) and "0.10.0" in r1.stdout
122
+ rel["medicoes"]["cli"] = {"versao": r1.stdout.strip().splitlines()[0],
123
+ "checkpoints_relacionados": len(r3.stdout.strip().splitlines())}
124
+ rel["blocos"]["s6_cli"] = "1/1" if rel["gates"]["s6_cli_comandos"] else "0/1"
125
+
126
+ # ---- veredito
127
+ gates = rel["gates"]
128
+ ok = sum(1 for v in gates.values() if v is True)
129
+ rel["veredito"] = f"{ok}/{len(gates)} — " + (
130
+ "APROVADO" if ok == len(gates) else "REPROVADO")
131
+ saida = os.path.join(R, "suite_v010_final.json")
132
+ json.dump(rel, open(saida, "w"), indent=1, ensure_ascii=False)
133
+ print(f"OK: {saida}")
134
+ print("blocos:", json.dumps(rel["blocos"], ensure_ascii=False))
135
+ print("VEREDITO:", rel["veredito"])
136
+ falhas = [k for k, v in gates.items() if v is not True]
137
+ if falhas:
138
+ print("falhas:", falhas)
139
+
140
+ if __name__ == "__main__":
141
+ main()
scripts/78_publicar_v010.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """78_publicar_v010.py — Publicação da v0.10.0 no HF (UM ÚNICO upload_folder
3
+ organizado — o HF bloqueia envios parciais em sequência; upload atômico).
4
+
5
+ Fases:
6
+ 1. inventário da conta + APAGAMENTO de estados de treino remanescentes;
7
+ 2. varredura local do VALOR do token (nenhum arquivo pode contê-lo);
8
+ 3. UM ÚNICO upload_folder com ignore_patterns EXPLÍCITOS +
9
+ delete_patterns p/ monólitos dissolvidos (src/jepa.rs → src/jepa/…);
10
+ 4. verificação pós-upload (0 estados, módulos-chave presentes);
11
+ 5. varredura REMOTA do valor do token;
12
+ 6. apagamento dos estados de treino LOCAIS (checkpoints + manifestos).
13
+
14
+ v0.10.0: manifestos (*.manifest.json) NÃO são publicados — só têm sentido
15
+ ao lado do checkpoint correspondente (que também não é publicado).
16
+ Token: EXCLUSIVAMENTE via variável de ambiente HF_TOKEN (nunca em arquivo).
17
+ """
18
+ import os, sys, tempfile
19
+
20
+ ARQUIVOS_EXCLUIR_DIRS = {"target", ".cargo", "__pycache__", ".git", ".venv",
21
+ "skills", "externos"}
22
+ ARQUIVOS_EXCLUIR_NOME = {"shard_treino.jsonl", "shard_validacao.jsonl",
23
+ "bpe.json", "rustup-init.sh"}
24
+ SUFIXOS_ESTADO = (".pt", ".pth", ".ckpt", ".safetensors", ".bin", ".npz",
25
+ ".msgpack")
26
+ NOMES_ESTADO = {"gerador_conceitos.json", "tokenizador_conceitos.json",
27
+ "gerador_aritmetica.json", "_drill_ck.json"}
28
+ # monólitos da v0.9.0 dissolvidos em diretórios na v0.10.0 (r-35 §2)
29
+ MONOLITOS_VELHOS = ["src/python.rs", "src/jepa.rs", "src/pensamento.rs",
30
+ "src/moe_redirecionavel.rs"]
31
+
32
+ def coletar(pasta):
33
+ arquivos = {}
34
+ for raiz, dirs, fs in os.walk(pasta):
35
+ dirs[:] = [d for d in dirs if d not in ARQUIVOS_EXCLUIR_DIRS]
36
+ for f in fs:
37
+ if f in ARQUIVOS_EXCLUIR_NOME or f in NOMES_ESTADO:
38
+ continue
39
+ if f.endswith(SUFIXOS_ESTADO) or f.endswith(".estado.json"):
40
+ continue
41
+ if f.endswith(".manifest.json") or f.endswith(".manifest.json.tmp"):
42
+ continue
43
+ if f.endswith(".so") or f in (".gitignore", "Cargo.lock.ori"):
44
+ continue
45
+ if f.startswith("hf_") or f.startswith("intros_"):
46
+ continue
47
+ caminho = os.path.join(raiz, f)
48
+ arquivos[os.path.relpath(caminho, pasta)] = caminho
49
+ return arquivos
50
+
51
+ def varre_token_em_arquivos(token, pares):
52
+ tb = token.encode()
53
+ achados = []
54
+ for rel, p in pares.items():
55
+ try:
56
+ with open(p, "rb") as f:
57
+ if tb in f.read():
58
+ achados.append(rel)
59
+ except OSError:
60
+ pass
61
+ return achados
62
+
63
+ def main():
64
+ from huggingface_hub import HfApi
65
+ token = os.environ.get("HF_TOKEN")
66
+ if not token:
67
+ print("ERRO: HF_TOKEN ausente"); sys.exit(1)
68
+ api = HfApi(token=token)
69
+ repo = "PowerMachine/khtst-rust"
70
+
71
+ # ---------------- 1) inventário da conta + apaga estados ----------------
72
+ print("=== 1) inventário da conta HF (procura estados de treino) ===")
73
+ repos = list(api.list_models(author="PowerMachine"))
74
+ print(f"repos: {len(repos)}")
75
+ total_removidos = 0
76
+ for r in repos:
77
+ try:
78
+ files = api.list_repo_files(repo_id=r.id, repo_type="model")
79
+ except Exception as e:
80
+ print(f" {r.id}: erro ao listar ({e})"); continue
81
+ estados = [f for f in files
82
+ if f.endswith(SUFIXOS_ESTADO)
83
+ or f.split("/")[-1] in NOMES_ESTADO
84
+ or (".meta.json" in f and "checkpoint" in f)]
85
+ if estados:
86
+ print(f" {r.id}: APAGANDO {len(estados)} estados")
87
+ for f in estados:
88
+ api.delete_file(path_in_repo=f, repo_id=r.id, repo_type="model")
89
+ total_removidos += len(estados)
90
+ else:
91
+ print(f" {r.id}: {len(files)} arquivos, 0 estados")
92
+ print(f"estados removidos no total: {total_removidos}")
93
+
94
+ # ---------------- 2) varredura local do token ----------------
95
+ pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
96
+ arquivos = coletar(pasta)
97
+ print(f"\n=== 2) varredura local: {len(arquivos)} arquivos ===")
98
+ achados = varre_token_em_arquivos(token, arquivos)
99
+ if achados:
100
+ print(f"ERRO: valor do token encontrado em: {achados}"); sys.exit(1)
101
+ print("varredura de token local: limpa")
102
+
103
+ # ---------------- 3) UM ÚNICO upload_folder ----------------
104
+ ignores = (
105
+ [f"{d}/**" for d in ARQUIVOS_EXCLUIR_DIRS]
106
+ + [f"{d}" for d in ARQUIVOS_EXCLUIR_DIRS]
107
+ + [f"**/{n}" for n in ARQUIVOS_EXCLUIR_NOME]
108
+ + [f"**/{n}" for n in NOMES_ESTADO]
109
+ + list(SUFIXOS_ESTADO)
110
+ + ["*.so", ".gitignore", "hf_*.py", "hf_*.json", "intros_*.json",
111
+ "*.estado.json", "**/*.manifest.json", "**/*.manifest.json.tmp",
112
+ "*.tmp"]
113
+ )
114
+ print(f"\n=== 3) upload_folder ÚNICO ({len(arquivos)} arquivos) ===")
115
+ api.upload_folder(
116
+ repo_id=repo, repo_type="model", folder_path=pasta,
117
+ ignore_patterns=ignores,
118
+ delete_patterns=MONOLITOS_VELHOS,
119
+ commit_message="v0.10.0: análise/prova/teste das críticas de "
120
+ "engenharia Qwen3.8 lote 3 (r-35) — divisão SRP dos monólitos "
121
+ "jepa/pensamento/moe/python em 26 submódulos (API estável via "
122
+ "re-exports; 'Amdahl de compilação' refutada por experimento "
123
+ "controlado, builds frios neutros), checkpoints atômicos SHA-256 "
124
+ "puro FIPS 180-4 + fsync + rename + manifesto (CP-1..CP-3; "
125
+ "correções da proposta: sem destruir estado anterior, manifesto "
126
+ "com barreira, sem bincode/unwrap), TemperaturaInversa T-V3 "
127
+ "(fronteira tipada da convexidade N-2; alegação compile-time "
128
+ "corrigida), trait CamadaKhtst+CamadaHomeostatica (premissa do "
129
+ "controlador refutada no fonte; Vec<Var>; adaptador MoeComPapel "
130
+ "bit-a-bit), CLI Typer 75 (fachada aditiva com resume verificado "
131
+ "por hash) — 123/123 cargo, 107/107 gates, treino real Wikipédia "
132
+ "PT-BR com checkpoints íntegros",
133
+ )
134
+ print("upload concluído")
135
+
136
+ # ---------------- 4) verificação pós-upload ----------------
137
+ print("\n=== 4) verificação pós-upload ===")
138
+ files = api.list_repo_files(repo_id=repo, repo_type="model")
139
+ estados = [f for f in files if f.endswith(SUFIXOS_ESTADO)
140
+ or f.split("/")[-1] in NOMES_ESTADO
141
+ or f.endswith(".manifest.json")]
142
+ assert not estados, f"estados publicados: {estados}"
143
+ chaves = ["src/checkpoint.rs", "src/tipos.rs", "src/camadas.rs",
144
+ "src/jepa/mod.rs", "src/pensamento/mod.rs",
145
+ "src/moe_redirecionavel/mod.rs", "src/python/mod.rs",
146
+ "src/fnh.rs", "src/verificador.rs",
147
+ "scripts/75_cli.py", "scripts/76_telemetria_v010.py",
148
+ "scripts/77_suite_v010.py", "scripts/78_publicar_v010.py",
149
+ "docs/memorias/r-35-analise-provas-qwen-v010.md",
150
+ "resultados/suite_v010_final.json",
151
+ "resultados/telemetria_v010.json",
152
+ "resultados/fase_a_v010.json", "resultados/fase_b_v010.json",
153
+ "resultados/treino_v090.json"]
154
+ faltando = [c for c in chaves if c not in files]
155
+ assert not faltando, f"faltando: {faltando}"
156
+ velhos = [m for m in MONOLITOS_VELHOS if m in files]
157
+ assert not velhos, f"monólitos velhos ainda no repo: {velhos}"
158
+ print(f"repo: {len(files)} arquivos | 0 estados | módulos-chave OK | "
159
+ f"monólitos velhos removidos")
160
+
161
+ # ---------------- 5) varredura REMOTA do token ----------------
162
+ print("\n=== 5) varredura REMOTA do valor do token ===")
163
+ with tempfile.TemporaryDirectory() as tmp:
164
+ achados_remotos = []
165
+ for f in files:
166
+ if f.endswith((".py", ".rs", ".md", ".json", ".toml", ".sh", ".cfg",
167
+ ".txt", ".jsonl")):
168
+ try:
169
+ p = api.hf_hub_download(repo_id=repo, repo_type="model",
170
+ filename=f, local_dir=tmp)
171
+ with open(p, "rb") as fh:
172
+ if token.encode() in fh.read():
173
+ achados_remotos.append(f)
174
+ except Exception:
175
+ pass
176
+ if achados_remotos:
177
+ print(f"ERRO: token remoto em: {achados_remotos}"); sys.exit(1)
178
+ print("varredura remota: limpa")
179
+
180
+ # ---------------- 6) apaga estados de treino LOCAIS ----------------
181
+ print("\n=== 6) estados de treino locais (checkpoints + manifestos) ===")
182
+ apagados = 0
183
+ for nome in sorted(NOMES_ESTADO):
184
+ for sufixo in ("", ".manifest.json"):
185
+ p = os.path.join(pasta, "resultados", nome + sufixo)
186
+ if os.path.exists(p):
187
+ os.remove(p)
188
+ apagados += 1
189
+ print(f" apagado: resultados/{nome}{sufixo}")
190
+ print(f"locais apagados: {apagados}")
191
+ print("\nPUBLICAÇÃO v0.10.0 CONCLUÍDA")
192
+
193
+ if __name__ == "__main__":
194
+ main()
src/camadas.rs CHANGED
@@ -136,3 +136,165 @@ impl Swiglu {
136
  [self.w1.vars(), self.w3.vars(), self.w2.vars()].concat()
137
  }
138
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  [self.w1.vars(), self.w3.vars(), self.w2.vars()].concat()
137
  }
138
  }
139
+
140
+ // ===========================================================================
141
+ // v0.10.0 — Polimorfismo de camadas (crítica Qwen3.8 lote 3, proposta 1,
142
+ // CORRIGIDA; r-35 §3). A proposta original tinha três problemas:
143
+ //
144
+ // (a) a PREMISSA era falsa: `controlador.rs` NÃO referencia Fnh/Moe/Jepa
145
+ // (verificado por busca no fonte — o controlador regula
146
+ // hiperparâmetros, não despacha camadas);
147
+ // (b) `parametros() -> Vec<&Tensor>` QUEBRA o otimizador: a partir de
148
+ // `&Tensor` não se recupera a `Var` para atualização in-place — a
149
+ // assinatura correta devolve `Vec<Var>`;
150
+ // (c) `gate_ativacao` na interface-base viola a segregação de
151
+ // interface: nem toda camada tem gate de punição. Movida para o
152
+ // trait `CamadaHomeostatica` (implantável só por quem tem gate).
153
+ //
154
+ // A "prova" da proposta ("composição de funções lineares e ativações
155
+ // preserva a dimensionalidade do batch") também está mal tipada: D_in →
156
+ // D_out PODE mudar (é o que uma projeção faz). O invariante correto:
157
+ //
158
+ // **Teo CK-1 (invariante de composição).** Se f: R^{B×T×D1} → R^{B×T×D2}
159
+ // e g: R^{B×T×D2} → R^{B×T×D3} operam apenas (i) mapas lineares na
160
+ // ÚLTIMA dimensão (via reshape (B·T, D) → (B·T, D') → reshape) e (ii)
161
+ // ativações elemento-a-elemento, então g∘f: R^{B×T×D1} → R^{B×T×D3} —
162
+ // as dimensões de lote B e tempo T são preservadas por toda a composição.
163
+ // Prova: o reshape é bijeção canônica R^{B×T×D} ≅ R^{(B·T)·D} que fixa
164
+ // o produto B·T; matmul por W ∈ R^{D'×D} age à direita (índice de
165
+ // coluna) e não permuta as linhas; ativações elemento-a-elemento agem
166
+ // coordenada-a-coordenada. Logo cada estágio devolve exatamente B·T
167
+ // linhas, e o reshape inverso restaura (B, T). ∎
168
+ // ===========================================================================
169
+
170
+ /// Camada com forward canônico R^{B×T×D_in} → R^{B×T×D_out} (Teo CK-1).
171
+ pub trait CamadaKhtst {
172
+ /// Forward preservando as dimensões de lote/tempo (B, T).
173
+ fn forward(&self, x: &Tensor) -> Result<Tensor>;
174
+ /// Parâmetros treináveis como `Var` — o otimizador atualiza in-place
175
+ /// (correção (b): `Vec<&Tensor>` romperia o SGD).
176
+ fn parametros(&self) -> Vec<Var>;
177
+ }
178
+
179
+ /// Camada com gate de punição homeostático (segregação, correção (c)).
180
+ /// g = max(0, 2σ((π − θ_π)/τ_g) − 1) — zona morta exata (Teo F-3).
181
+ pub trait CamadaHomeostatica {
182
+ fn gate_ativacao(&self, punicao: &crate::tipos::Probabilidade)
183
+ -> crate::tipos::GateAtivacao;
184
+ }
185
+
186
+ impl CamadaKhtst for Linear {
187
+ fn forward(&self, x: &Tensor) -> Result<Tensor> {
188
+ self.fwd(x)
189
+ }
190
+ fn parametros(&self) -> Vec<Var> {
191
+ self.vars()
192
+ }
193
+ }
194
+
195
+ impl CamadaKhtst for crate::jepa::MlpJepa {
196
+ fn forward(&self, x: &Tensor) -> Result<Tensor> {
197
+ self.fwd(x)
198
+ }
199
+ fn parametros(&self) -> Vec<Var> {
200
+ [self.l1.vars(), self.l2.vars()].concat()
201
+ }
202
+ }
203
+
204
+ /// Adaptador zero-custo que CAPTURA o papel (codificador/decodificador)
205
+ /// da MoE redirecionável, expondo o forward canônico do trait SEM perder
206
+ /// o parâmetro: os caminhos de treino continuam usando `forward(x, papel)`
207
+ /// completo (com perda auxiliar e carga — nada é removido); o adaptador
208
+ /// serve a composição heterogênea de INFERÊNCIA.
209
+ pub struct MoeComPapel<'a> {
210
+ pub moe: &'a crate::moe_redirecionavel::MoeRedirecionavel,
211
+ pub papel: crate::moe_redirecionavel::Papel,
212
+ }
213
+
214
+ impl CamadaKhtst for MoeComPapel<'_> {
215
+ fn forward(&self, x: &Tensor) -> Result<Tensor> {
216
+ Ok(self.moe.forward(x, self.papel)?.saida)
217
+ }
218
+ fn parametros(&self) -> Vec<Var> {
219
+ self.moe.vars()
220
+ }
221
+ }
222
+
223
+ #[cfg(test)]
224
+ mod testes_camada_khtst {
225
+ use super::*;
226
+
227
+ /// CK-1a — composição heterogênea `Vec<&dyn CamadaKhtst>` preserva
228
+ /// B×T (2, 3) através de Linear→MlpJepa (Teo CK-1) e o adaptador MoE.
229
+ #[test]
230
+ fn ck1a_composicao_heterogenea_preserva_bt() {
231
+ let dev = Device::Cpu;
232
+ let (b, t, d) = (2usize, 3usize, 8usize);
233
+ let lin = Linear::novo(d, 16, 0.02, &dev).unwrap();
234
+ let mlp = crate::jepa::MlpJepa::novo(16, 24, 32, &dev).unwrap();
235
+ let x0 = Tensor::randn(0f32, 1f32, (b, t, d), &dev).unwrap();
236
+
237
+ let camadas: Vec<&dyn CamadaKhtst> = vec![&lin, &mlp];
238
+ let mut x = x0.clone();
239
+ for c in &camadas {
240
+ x = c.forward(&x).unwrap();
241
+ }
242
+ assert_eq!(x.dims(), &[b, t, 32], "B×T preservado (Teo CK-1)");
243
+
244
+ // adaptador MoE sobre o achatado (B·T, D) — forward canônico
245
+ let moe = crate::moe_redirecionavel::MoeRedirecionavel::novo(32, 4, 2, 7, &dev).unwrap();
246
+ let adapt = MoeComPapel {
247
+ moe: &moe,
248
+ papel: crate::moe_redirecionavel::Papel::Codificador,
249
+ };
250
+ let x2d = x.reshape((b * t, 32)).unwrap();
251
+ let y = <MoeComPapel as CamadaKhtst>::forward(&adapt, &x2d).unwrap();
252
+ assert_eq!(y.dims(), &[b * t, 32]);
253
+ }
254
+
255
+ /// CK-1b — `parametros()` devolve `Var` NÃO-VAZIAS (otimizável).
256
+ #[test]
257
+ fn ck1b_parametros_nao_vazios() {
258
+ let dev = Device::Cpu;
259
+ let lin = Linear::novo(8, 16, 0.02, &dev).unwrap();
260
+ let mlp = crate::jepa::MlpJepa::novo(16, 24, 32, &dev).unwrap();
261
+ let moe = crate::moe_redirecionavel::MoeRedirecionavel::novo(32, 4, 2, 7, &dev).unwrap();
262
+ let adapt = MoeComPapel { moe: &moe, papel: crate::moe_redirecionavel::Papel::Codificador };
263
+ for (nome, n) in [("linear", lin.parametros().len()),
264
+ ("mlp", mlp.parametros().len()),
265
+ ("moe", adapt.parametros().len())] {
266
+ assert!(n > 0, "{nome} deveria ter parâmetros");
267
+ }
268
+ }
269
+
270
+ /// CK-1c — o adaptador delega BIT-A-BIT para moe.forward(x, papel).
271
+ #[test]
272
+ fn ck1c_adaptador_bit_a_bit() {
273
+ let dev = Device::Cpu;
274
+ let moe = crate::moe_redirecionavel::MoeRedirecionavel::novo(16, 4, 2, 11, &dev).unwrap();
275
+ let x = Tensor::randn(0f32, 1f32, (5, 16), &dev).unwrap();
276
+ let direto = moe.forward(&x, crate::moe_redirecionavel::Papel::Codificador).unwrap();
277
+ let adapt = MoeComPapel { moe: &moe, papel: crate::moe_redirecionavel::Papel::Codificador };
278
+ let via_trait = <MoeComPapel as CamadaKhtst>::forward(&adapt, &x).unwrap();
279
+ let a = direto.saida.flatten_all().unwrap().to_vec1::<f32>().unwrap();
280
+ let bvia = via_trait.flatten_all().unwrap().to_vec1::<f32>().unwrap();
281
+ assert_eq!(a.len(), bvia.len());
282
+ for (i, (x1, x2)) in a.iter().zip(bvia.iter()).enumerate() {
283
+ assert_eq!(x1.to_bits(), x2.to_bits(), "elem {i} difere");
284
+ }
285
+ }
286
+
287
+ /// CK-2a — despacho homeostático heterogêneo: zona morta exata em
288
+ /// π < θ_π e ativação > 0 em π alto (Teo F-3 via fronteira tipada).
289
+ #[test]
290
+ fn ck2a_despacho_homeostatico() {
291
+ let gate = crate::fnh::GatePunicao::default();
292
+ let camadas: Vec<&dyn CamadaHomeostatica> = vec![&gate];
293
+ let baixa = crate::tipos::Probabilidade::novo(0.2).unwrap();
294
+ let alta = crate::tipos::Probabilidade::novo(0.95).unwrap();
295
+ for c in &camadas {
296
+ assert_eq!(c.gate_ativacao(&baixa).valor(), 0.0, "zona morta exata");
297
+ assert!(c.gate_ativacao(&alta).valor() > 0.0, "gate abre sob punição alta");
298
+ }
299
+ }
300
+ }
src/checkpoint.rs ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Checkpoints atômicos com prova de integridade criptográfica (v0.10.0).
2
+ //!
3
+ //! **Crítica Qwen3.8 lote 3, nº 3 ("Checkpointing Não-Atômico") — ACEITA
4
+ //! COM CORREÇÕES (r-35 §4).** A proposta original tinha três falhas:
5
+ //!
6
+ //! 1. **Destruição do estado anterior:** `File::create(caminho_pesos)`
7
+ //! trunca o checkpoint ANTERIOR antes de escrever o novo — uma queda
8
+ //! de energia no meio da escrita perde AMBOS. A atomicidade real exige
9
+ //! escrita em `.tmp` + `rename(2)` (o POSIX garante troca atômica).
10
+ //! 2. **Manifesto sem barreira:** `fs::write(caminho_meta, ...)` sem
11
+ //! `sync_all` — o manifesto pode não atingir o disco quando os pesos
12
+ //! já atingiram (ou vice-versa), violando a própria ordenação alegada.
13
+ //! 3. **Dependência nova (bincode) + `unwrap()`** em caminho de I/O.
14
+ //!
15
+ //! Esta implementação: SHA-256 **puro em Rust** (FIPS 180-4, sem crates
16
+ //! externas — crates.io inacessível no ambiente), escrita tmp→fsync→
17
+ //! rename→fsync(dir) para AMBOS os arquivos, e manifesto lado-a-lado
18
+ //! `{caminho}.manifest.json` com hash + tamanho.
19
+ //!
20
+ //! **Teo CP-1 (atomicidade da troca).** `rename(2)` é atômico em relação
21
+ //! a leitores concorrentes e crashes: em qualquer instante o caminho
22
+ //! resolve OU para o arquivo antigo OU para o novo, nunca uma mistura.
23
+ //! Logo um crash em qualquer ponto deixa o caminho final = checkpoint
24
+ //! antigo (crash antes do rename) ou novo (depois). ∎ (garantia POSIX)
25
+ //!
26
+ //! **Teo CP-2 (durabilidade ordenada).** Com a disciplina
27
+ //! `write(W.tmp) → fsync(W.tmp) → rename(W) → write(M.tmp) →
28
+ //! fsync(M.tmp) → rename(M) → fsync(dir)`, após reboot: se `M` existe,
29
+ //! então o rename de `W` foi durável (executado e journalizado antes) e
30
+ //! os blocos de dados de `W` foram forçados por `fsync(W.tmp)` — portanto
31
+ //! `SHA-256(W) = M.hash` (a menos de colisão, CP-3). Se `M` não existe,
32
+ //! o estado é tratado como INVÁLIDO (não silenciosamente errado). ∎
33
+ //!
34
+ //! **Teo CP-3 (detecção de corrupção).** Para W' ≠ W com |W'| = |W|,
35
+ //! `P[SHA-256(W') = SHA-256(W)] ≤ 2^-256` (ataque direto) e, para
36
+ //! qualquer par distinto, ≤ 2^-128 (limite de aniversário). Truncamentos
37
+ //! e gravações parciais são capturados DETERMINISTICAMENTE pelo campo
38
+ //! `tamanho`. Nota honesta: a alegação original "matematicamente
39
+ //! idêntico" é exagerada — o correto é indistinguibilidade computacional
40
+ //! com probabilidade de colisão ≤ 2^-128. ∎
41
+ //!
42
+ //! **Compatibilidade (regra da casa: nada é removido):** checkpoints da
43
+ //! v0.9.0 (sem manifesto) continuam carregáveis pelos `carregar` legados;
44
+ //! `carregar_verificado` exige manifesto e recusa o contrário.
45
+
46
+ use candle_core::Result;
47
+ use std::fs::File;
48
+ use std::io::Write;
49
+ use std::path::Path;
50
+
51
+ pub const VERSAO_CHECKPOINT: &str = "0.10.0";
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // SHA-256 — FIPS 180-4, implementação autocontida (sem dependências)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ const K_SHA256: [u32; 64] = [
58
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
59
+ 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
60
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
61
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
62
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
63
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
64
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
65
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
66
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
67
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
68
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
69
+ ];
70
+
71
+ /// SHA-256 de `dados` — 32 bytes (FIPS 180-4 §6.2.2).
72
+ pub fn sha256(dados: &[u8]) -> [u8; 32] {
73
+ let mut h: [u32; 8] = [
74
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
75
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
76
+ ];
77
+ // padding: 0x80, zeros, comprimento de bit big-endian em 64 bits
78
+ let bitlen = (dados.len() as u64).wrapping_mul(8);
79
+ let mut msg = dados.to_vec();
80
+ msg.push(0x80);
81
+ while msg.len() % 64 != 56 {
82
+ msg.push(0);
83
+ }
84
+ msg.extend_from_slice(&bitlen.to_be_bytes());
85
+
86
+ for bloco in msg.chunks_exact(64) {
87
+ let mut w = [0u32; 64];
88
+ for (i, j) in (0..64).step_by(4).enumerate() {
89
+ w[i] = u32::from_be_bytes([bloco[j], bloco[j + 1], bloco[j + 2], bloco[j + 3]]);
90
+ }
91
+ for t in 16..64 {
92
+ let s0 = w[t - 15].rotate_right(7) ^ w[t - 15].rotate_right(18) ^ (w[t - 15] >> 3);
93
+ let s1 = w[t - 2].rotate_right(17) ^ w[t - 2].rotate_right(19) ^ (w[t - 2] >> 10);
94
+ w[t] = w[t - 16]
95
+ .wrapping_add(s0)
96
+ .wrapping_add(w[t - 7])
97
+ .wrapping_add(s1);
98
+ }
99
+ let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
100
+ (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
101
+ for t in 0..64 {
102
+ let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
103
+ let ch = (e & f) ^ ((!e) & g);
104
+ let t1 = hh
105
+ .wrapping_add(s1)
106
+ .wrapping_add(ch)
107
+ .wrapping_add(K_SHA256[t])
108
+ .wrapping_add(w[t]);
109
+ let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
110
+ let maj = (a & b) ^ (a & c) ^ (b & c);
111
+ let t2 = s0.wrapping_add(maj);
112
+ hh = g; g = f; f = e; e = d.wrapping_add(t1);
113
+ d = c; c = b; b = a; a = t1.wrapping_add(t2);
114
+ }
115
+ h[0] = h[0].wrapping_add(a); h[1] = h[1].wrapping_add(b);
116
+ h[2] = h[2].wrapping_add(c); h[3] = h[3].wrapping_add(d);
117
+ h[4] = h[4].wrapping_add(e); h[5] = h[5].wrapping_add(f);
118
+ h[6] = h[6].wrapping_add(g); h[7] = h[7].wrapping_add(hh);
119
+ }
120
+ let mut saida = [0u8; 32];
121
+ for (i, x) in h.iter().enumerate() {
122
+ saida[i * 4..i * 4 + 4].copy_from_slice(&x.to_be_bytes());
123
+ }
124
+ saida
125
+ }
126
+
127
+ /// SHA-256 em hexadecimal minúsculo (64 caracteres).
128
+ pub fn sha256_hex(dados: &[u8]) -> String {
129
+ sha256(dados).iter().map(|b| format!("{b:02x}")).collect()
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // salva / carrega atômicos
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /// Caminho do manifesto lado-a-lado de `caminho`.
137
+ pub fn caminho_manifesto(caminho: &str) -> String {
138
+ format!("{caminho}.manifest.json")
139
+ }
140
+
141
+ fn err_io(e: std::io::Error, ctx: &str) -> candle_core::Error {
142
+ candle_core::Error::Msg(format!("{ctx}: {e}"))
143
+ }
144
+
145
+ /// fsync de um diretório (durabilidade do rename — POSIX). Unix apenas;
146
+ /// em outras plataformas é no-op documentado.
147
+ fn fsync_dir(dir: &Path) -> std::io::Result<()> {
148
+ #[cfg(unix)]
149
+ {
150
+ let d = File::open(dir)?;
151
+ d.sync_all()?;
152
+ }
153
+ #[cfg(not(unix))]
154
+ {
155
+ let _ = dir;
156
+ }
157
+ Ok(())
158
+ }
159
+
160
+ /// Escrita atômica de `conteudo` em `caminho` com fsync + rename e
161
+ /// manifesto `{caminho}.manifest.json` (hash SHA-256 + tamanho).
162
+ ///
163
+ /// Ordem (Teo CP-2): write(tmp) → fsync(tmp) → rename(final) para o
164
+ /// conteúdo e para o manifesto; fsync(dir) ao final.
165
+ pub fn salvar_atomico(caminho: &str, conteudo: &str) -> Result<()> {
166
+ let dados = conteudo.as_bytes();
167
+ let hash = sha256_hex(dados);
168
+ let tmp = format!("{caminho}.tmp");
169
+
170
+ // 1) conteúdo: tmp → fsync → rename (Teo CP-1: troca atômica)
171
+ {
172
+ let mut f = File::create(&tmp).map_err(|e| err_io(e, "checkpoint: criar tmp"))?;
173
+ f.write_all(dados).map_err(|e| err_io(e, "checkpoint: escrever tmp"))?;
174
+ f.sync_all().map_err(|e| err_io(e, "checkpoint: fsync tmp"))?;
175
+ }
176
+ std::fs::rename(&tmp, caminho).map_err(|e| err_io(e, "checkpoint: rename"))?;
177
+
178
+ // 2) manifesto: mesma disciplina (a proposta original usava fs::write
179
+ // simples — sem barreira, o manifesto pode perder-se sozinho)
180
+ let manifest = serde_json::json!({
181
+ "versao": VERSAO_CHECKPOINT,
182
+ "algoritmo": "sha256",
183
+ "hash_sha256": hash,
184
+ "tamanho": dados.len(),
185
+ });
186
+ let mtxt = serde_json::to_string(&manifest)
187
+ .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
188
+ let mtmp = format!("{}.manifest.json.tmp", caminho);
189
+ {
190
+ let mut f = File::create(&mtmp).map_err(|e| err_io(e, "manifesto: criar tmp"))?;
191
+ f.write_all(mtxt.as_bytes()).map_err(|e| err_io(e, "manifesto: escrever tmp"))?;
192
+ f.sync_all().map_err(|e| err_io(e, "manifesto: fsync tmp"))?;
193
+ }
194
+ std::fs::rename(&mtmp, caminho_manifesto(caminho))
195
+ .map_err(|e| err_io(e, "manifesto: rename"))?;
196
+
197
+ // 3) durabilidade dos renames (POSIX: rename journalizado no dir)
198
+ if let Some(pai) = Path::new(caminho).parent() {
199
+ fsync_dir(pai).map_err(|e| err_io(e, "checkpoint: fsync dir"))?;
200
+ }
201
+ Ok(())
202
+ }
203
+
204
+ /// Lê `caminho` verificando o manifesto (hash + tamanho). Recusa
205
+ /// manifesto ausente ou divergente — Teo CP-3.
206
+ pub fn carregar_verificado(caminho: &str) -> Result<String> {
207
+ let dados = std::fs::read(caminho).map_err(|e| err_io(e, "checkpoint: ler"))?;
208
+ let mtxt = std::fs::read_to_string(caminho_manifesto(caminho))
209
+ .map_err(|_| candle_core::Error::Msg(format!(
210
+ "checkpoint sem manifesto (legado v0.9.0?): {caminho}")))?;
211
+ let m: serde_json::Value = serde_json::from_str(&mtxt)
212
+ .map_err(|e| candle_core::Error::Msg(format!("manifesto ilegível: {e}")))?;
213
+ let hash_esp = m["hash_sha256"].as_str().unwrap_or("");
214
+ let tam_esp = m["tamanho"].as_u64().unwrap_or(u64::MAX);
215
+ let hash_rec = sha256_hex(&dados);
216
+ if (dados.len() as u64) != tam_esp {
217
+ candle_core::bail!(
218
+ "checkpoint corrompido: tamanho {} ��� manifesto {} ({caminho})",
219
+ dados.len(), tam_esp);
220
+ }
221
+ if hash_rec != hash_esp {
222
+ candle_core::bail!(
223
+ "checkpoint corrompido: SHA-256 divergente ({caminho})");
224
+ }
225
+ String::from_utf8(dados)
226
+ .map_err(|e| candle_core::Error::Msg(format!("utf8: {e}")))
227
+ }
228
+
229
+ /// Verificação sem exceção (para uso em Python/CLI antes de resume).
230
+ pub fn validar(caminho: &str) -> bool {
231
+ carregar_verificado(caminho).is_ok()
232
+ }
233
+
234
+ #[cfg(test)]
235
+ mod testes_checkpoint {
236
+ use super::*;
237
+
238
+ fn tmp_dir(nome: &str) -> std::path::PathBuf {
239
+ let d = std::env::temp_dir().join(format!("khtst_cp_{nome}_{}", std::process::id()));
240
+ let _ = std::fs::remove_dir_all(&d);
241
+ std::fs::create_dir_all(&d).unwrap();
242
+ d
243
+ }
244
+
245
+ /// CP-0a — SHA-256 contra os vetores oficiais FIPS 180-4.
246
+ #[test]
247
+ fn cp0a_sha256_vetores_fips() {
248
+ assert_eq!(
249
+ sha256_hex(b""),
250
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
251
+ assert_eq!(
252
+ sha256_hex(b"abc"),
253
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
254
+ assert_eq!(
255
+ sha256_hex(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
256
+ "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
257
+ // vetor de 4 blocos (256 bytes) — exercita o laço de blocos
258
+ let longo = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn".repeat(4);
259
+ assert_eq!(sha256_hex(longo.as_bytes()).len(), 64);
260
+ }
261
+
262
+ /// CP-1a — salvar produz final + manifesto; tmp não permanece;
263
+ /// carregar_verificado devolve o conteúdo bit-a-bit.
264
+ #[test]
265
+ fn cp1a_salvar_carregar_roundtrip() {
266
+ let d = tmp_dir("rt");
267
+ let p = d.join("ck.json");
268
+ salvar_atomico(p.to_str().unwrap(), "{\"v0\":[1.0,2.0]}").unwrap();
269
+ assert!(p.exists());
270
+ assert!(d.join("ck.json.manifest.json").exists());
271
+ assert!(!d.join("ck.json.tmp").exists());
272
+ assert!(!d.join("ck.json.manifest.json.tmp").exists());
273
+ assert_eq!(carregar_verificado(p.to_str().unwrap()).unwrap(),
274
+ "{\"v0\":[1.0,2.0]}");
275
+ let _ = std::fs::remove_dir_all(&d);
276
+ }
277
+
278
+ /// CP-3a — adulteração de 1 byte é detectada (hash divergente).
279
+ #[test]
280
+ fn cp3a_adulteracao_detectada() {
281
+ let d = tmp_dir("t");
282
+ let p = d.join("ck.json");
283
+ salvar_atomico(p.to_str().unwrap(), "conteudo original").unwrap();
284
+ std::fs::write(&p, "conteudo originalX").unwrap();
285
+ assert!(carregar_verificado(p.to_str().unwrap()).is_err());
286
+ assert!(!validar(p.to_str().unwrap()));
287
+ let _ = std::fs::remove_dir_all(&d);
288
+ }
289
+
290
+ /// CP-3b — truncamento detectado pelo tamanho (determinístico).
291
+ #[test]
292
+ fn cp3b_truncamento_detectado() {
293
+ let d = tmp_dir("tr");
294
+ let p = d.join("ck.json");
295
+ salvar_atomico(p.to_str().unwrap(), "0123456789").unwrap();
296
+ std::fs::write(&p, "0123").unwrap();
297
+ assert!(carregar_verificado(p.to_str().unwrap()).is_err());
298
+ let _ = std::fs::remove_dir_all(&d);
299
+ }
300
+
301
+ /// CP-4a — manifesto ausente ⇒ recusa (estado legado é do `carregar`).
302
+ #[test]
303
+ fn cp4a_manifesto_ausente_recusado() {
304
+ let d = tmp_dir("leg");
305
+ let p = d.join("ck.json");
306
+ salvar_atomico(p.to_str().unwrap(), "x").unwrap();
307
+ std::fs::remove_file(d.join("ck.json.manifest.json")).unwrap();
308
+ assert!(carregar_verificado(p.to_str().unwrap()).is_err());
309
+ let _ = std::fs::remove_dir_all(&d);
310
+ }
311
+
312
+ /// CP-1b — falha de escrita PRESERVA o checkpoint anterior
313
+ /// (atomicidade prática: dir somente-leitura impede o create do tmp).
314
+ #[test]
315
+ fn cp1b_falha_preserva_anterior() {
316
+ let d = tmp_dir("fa");
317
+ let p = d.join("ck.json");
318
+ salvar_atomico(p.to_str().unwrap(), "estado_bom").unwrap();
319
+ // dir somente-leitura: File::create(tmp) falha
320
+ let mut perms = std::fs::metadata(&d).unwrap().permissions();
321
+ #[allow(clippy::unnecessary_cast)]
322
+ {
323
+ use std::os::unix::fs::PermissionsExt;
324
+ perms.set_mode(0o555);
325
+ }
326
+ std::fs::set_permissions(&d, perms).unwrap();
327
+ let r = salvar_atomico(p.to_str().unwrap(), "estado_novo");
328
+ let mut perms2 = std::fs::metadata(&d).unwrap().permissions();
329
+ #[allow(clippy::unnecessary_cast)]
330
+ {
331
+ use std::os::unix::fs::PermissionsExt;
332
+ perms2.set_mode(0o755);
333
+ }
334
+ std::fs::set_permissions(&d, perms2).unwrap();
335
+ assert!(r.is_err(), "escrita deveria falhar em dir somente-leitura");
336
+ // o anterior permanece ÍNTEGRO e válido (Teo CP-1)
337
+ assert_eq!(carregar_verificado(p.to_str().unwrap()).unwrap(), "estado_bom");
338
+ let _ = std::fs::remove_dir_all(&d);
339
+ }
340
+
341
+ /// CP-5a — manifesto trocado entre arquivos (hash de outro) recusa.
342
+ #[test]
343
+ fn cp5a_manifesto_de_outro_recusado() {
344
+ let d = tmp_dir("sw");
345
+ let a = d.join("a.json");
346
+ let b = d.join("b.json");
347
+ salvar_atomico(a.to_str().unwrap(), "AAAA").unwrap();
348
+ salvar_atomico(b.to_str().unwrap(), "BBBB").unwrap();
349
+ std::fs::copy(d.join("b.json.manifest.json"), d.join("a.json.manifest.json")).unwrap();
350
+ assert!(carregar_verificado(a.to_str().unwrap()).is_err());
351
+ let _ = std::fs::remove_dir_all(&d);
352
+ }
353
+ }
src/fnh.rs CHANGED
@@ -570,6 +570,13 @@ impl GatePunicao {
570
  }
571
  }
572
 
 
 
 
 
 
 
 
573
  /// Camada FNH — uma hipótese flexível com rotações aprendíveis, suavização de
574
  /// canais e ativação flexível, GATADA pela punição do treino (r-28 §4.1).
575
  pub struct CamadaFNH {
 
570
  }
571
  }
572
 
573
+ impl crate::camadas::CamadaHomeostatica for GatePunicao {
574
+ fn gate_ativacao(&self, punicao: &crate::tipos::Probabilidade)
575
+ -> crate::tipos::GateAtivacao {
576
+ self.valor_tipado(punicao)
577
+ }
578
+ }
579
+
580
  /// Camada FNH — uma hipótese flexível com rotações aprendíveis, suavização de
581
  /// canais e ativação flexível, GATADA pela punição do treino (r-28 §4.1).
582
  pub struct CamadaFNH {
src/gerador.rs CHANGED
@@ -246,18 +246,27 @@ impl GeradorConceitos {
246
  }
247
  let txt = serde_json::to_string(&serde_json::Value::Object(obj))
248
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
249
- let tmp = format!("{caminho}.tmp");
250
- std::fs::write(&tmp, txt).map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
251
- std::fs::rename(&tmp, caminho).map_err(|e| candle_core::Error::Msg(format!("rename: {e}")))?;
252
- Ok(())
 
 
 
 
253
  }
254
 
255
  /// Carrega um checkpoint salvo por `salvar` (mesma ordem de construção
256
  /// de `novo` ⇒ índices de variáveis casam 1-a-1 — ajuste fino honesto).
 
257
  pub fn carregar(&mut self, caminho: &str) -> Result<()> {
258
  let txt = std::fs::read_to_string(caminho)
259
  .map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
260
- let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&txt)
 
 
 
 
261
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
262
  let vars = self.vars();
263
  for (i, v) in vars.iter().enumerate() {
 
246
  }
247
  let txt = serde_json::to_string(&serde_json::Value::Object(obj))
248
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
249
+ crate::checkpoint::salvar_atomico(caminho, &txt)
250
+ }
251
+
252
+ /// Carrega com verificação de integridade (manifesto SHA-256 — v0.10.0).
253
+ /// Recusa checkpoints corrompidos/legados-sem-manifesto (Teo CP-3).
254
+ pub fn carregar_verificado(&mut self, caminho: &str) -> Result<()> {
255
+ let txt = crate::checkpoint::carregar_verificado(caminho)?;
256
+ self.carregar_de_txt(&txt)
257
  }
258
 
259
  /// Carrega um checkpoint salvo por `salvar` (mesma ordem de construção
260
  /// de `novo` ⇒ índices de variáveis casam 1-a-1 — ajuste fino honesto).
261
+ /// Caminho LEGADO: sem verificação de manifesto (compatibilidade v0.9.0).
262
  pub fn carregar(&mut self, caminho: &str) -> Result<()> {
263
  let txt = std::fs::read_to_string(caminho)
264
  .map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
265
+ self.carregar_de_txt(&txt)
266
+ }
267
+
268
+ fn carregar_de_txt(&mut self, txt: &str) -> Result<()> {
269
+ let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(txt)
270
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
271
  let vars = self.vars();
272
  for (i, v) in vars.iter().enumerate() {
src/jepa/mascaras.rs ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `jepa::mascaras` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Parte 2 — máscaras
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /// Máscara de blocos 2D (Image-JEPA): grade (h, w) de patches; sorteia
12
+ /// `n_blocos` retângulos aleatórios com fração alvo `frac` de patches.
13
+ pub fn mascara_blocos_2d(h: usize, w: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
14
+ let total = h * w;
15
+ let alvo = (total as f64 * frac) as usize;
16
+ let mut masc = vec![false; total];
17
+ let mut coberto = 0usize;
18
+ for _ in 0..n_blocos.max(1) {
19
+ if coberto >= alvo {
20
+ break;
21
+ }
22
+ let bh = 1 + rng.inteiro(h.min(4));
23
+ let bw = 1 + rng.inteiro(w.min(4));
24
+ let y0 = rng.inteiro(h + 1 - bh);
25
+ let x0 = rng.inteiro(w + 1 - bw);
26
+ for y in y0..y0 + bh {
27
+ for x in x0..x0 + bw {
28
+ let i = y * w + x;
29
+ if !masc[i] {
30
+ masc[i] = true;
31
+ coberto += 1;
32
+ }
33
+ }
34
+ }
35
+ }
36
+ masc
37
+ }
38
+
39
+ /// Máscara de tubos 3D (V-JEPA 2): grade (t, h, w); tubos = retângulos que
40
+ /// atravessam o tempo.
41
+ pub fn mascara_tubos_3d(t: usize, h: usize, w: usize, n_tubos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
42
+ let total = t * h * w;
43
+ let alvo = (total as f64 * frac) as usize;
44
+ let mut masc = vec![false; total];
45
+ let mut coberto = 0usize;
46
+ for _ in 0..n_tubos.max(1) {
47
+ if coberto >= alvo {
48
+ break;
49
+ }
50
+ let bh = 1 + rng.inteiro(h.min(3));
51
+ let bw = 1 + rng.inteiro(w.min(3));
52
+ let y0 = rng.inteiro(h + 1 - bh);
53
+ let x0 = rng.inteiro(w + 1 - bw);
54
+ for tt in 0..t {
55
+ for y in y0..y0 + bh {
56
+ for x in x0..x0 + bw {
57
+ let i = (tt * h + y) * w + x;
58
+ if !masc[i] {
59
+ masc[i] = true;
60
+ coberto += 1;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ masc
67
+ }
68
+
69
+ /// Máscara de bandas (Audio-JEPA): grade (f, tau) — bandas de frequência
70
+ /// horizontais com extensão temporal.
71
+ pub fn mascara_bandas(f: usize, tau: usize, n_bandas: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
72
+ mascara_blocos_2d(f, tau, n_bandas, frac, rng) // blocos na grade (f, tau)
73
+ }
74
+
75
+ /// Máscara de blocos 3D (3D-JEPA): grade (x, y, z) de voxels.
76
+ pub fn mascara_blocos_3d(x: usize, y: usize, z: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
77
+ mascara_tubos_3d(x, y, z, n_blocos, frac, rng)
78
+ }
79
+
80
+ /// Máscara de segmentos canal×tempo (Brain-JEPA): canais inteiros OU
81
+ /// janelas temporais (aleatório por chamada).
82
+ pub fn mascara_segmentos(c: usize, t: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
83
+ let total = c * t;
84
+ let alvo = (total as f64 * frac) as usize;
85
+ let mut masc = vec![false; total];
86
+ if rng.inteiro(2) == 0 {
87
+ // canais inteiros
88
+ let mut coberto = 0;
89
+ let mut canais: Vec<usize> = (0..c).collect();
90
+ // embaralhar
91
+ for i in (1..canais.len()).rev() {
92
+ let j = rng.inteiro(i + 1);
93
+ canais.swap(i, j);
94
+ }
95
+ for &ch in canais.iter() {
96
+ if coberto >= alvo {
97
+ break;
98
+ }
99
+ for tt in 0..t {
100
+ masc[ch * t + tt] = true;
101
+ coberto += 1;
102
+ }
103
+ }
104
+ } else {
105
+ // janelas temporais
106
+ let mut coberto = 0;
107
+ let mut t0 = 0;
108
+ while coberto < alvo && t0 < t {
109
+ let len = 1 + rng.inteiro((t / 4).max(1));
110
+ for ch in 0..c {
111
+ for tt in t0..(t0 + len).min(t) {
112
+ masc[ch * t + tt] = true;
113
+ coberto += 1;
114
+ }
115
+ }
116
+ t0 += len + 1;
117
+ }
118
+ }
119
+ masc
120
+ }
121
+
122
+ /// Máscara 1D (T-JEPA): intervalos contíguos em n posições.
123
+ pub fn mascara_intervalos_1d(n: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
124
+ let alvo = (n as f64 * frac) as usize;
125
+ let mut masc = vec![false; n];
126
+ let mut coberto = 0;
127
+ while coberto < alvo {
128
+ let len = 1 + rng.inteiro((n / 8).max(1));
129
+ let i0 = rng.inteiro(n);
130
+ for i in i0..(i0 + len).min(n) {
131
+ if !masc[i] {
132
+ masc[i] = true;
133
+ coberto += 1;
134
+ }
135
+ }
136
+ }
137
+ masc
138
+ }
139
+
140
+ /// Amostragem dos pontos mais distantes (FPS) para Point-JEPA.
141
+ pub fn pontos_mais_distantes(pontos: &[Vec<f64>], k: usize, rng: &mut RngDeterministico) -> Vec<usize> {
142
+ let n = pontos.len();
143
+ if n == 0 || k == 0 {
144
+ return vec![];
145
+ }
146
+ let k = k.min(n);
147
+ let mut centros = vec![rng.inteiro(n)];
148
+ let mut dist_min: Vec<f64> = pontos
149
+ .iter()
150
+ .map(|p| distancia_eucl(p, &pontos[centros[0]]))
151
+ .collect();
152
+ while centros.len() < k {
153
+ let prox = dist_min
154
+ .iter()
155
+ .enumerate()
156
+ .filter(|(i, _)| !centros.contains(i))
157
+ .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
158
+ .map(|(i, _)| i)
159
+ .unwrap_or(0);
160
+ centros.push(prox);
161
+ for (i, p) in pontos.iter().enumerate() {
162
+ let d = distancia_eucl(p, &pontos[prox]);
163
+ if d < dist_min[i] {
164
+ dist_min[i] = d;
165
+ }
166
+ }
167
+ }
168
+ centros
169
+ }
170
+
171
+ pub(crate) fn distancia_eucl(a: &[f64], b: &[f64]) -> f64 {
172
+ a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f64>().sqrt()
173
+ }
src/jepa/mod.rs ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Suíte JEPA — módulos: núcleo, máscaras, patches, variantes,
2
+ //! SonicMoE, ReplaySSM (divisão SRP v0.10.0 — r-35 §2).
3
+
4
+ //! Suíte JEPA v0.5.0 — núcleo unificado + 12 variantes + SonicMoE + ReplaySSM.
5
+ //!
6
+ //! **Núcleo (todas as variantes compartilham):**
7
+ //! - codificador `E_θ` (MLp SiLU), preditor `P_φ` (condicionado por posição/
8
+ //! ação/edição), codificador-alvo EMA `E_ξ` com `ξ ← m·ξ + (1−m)·θ`
9
+ //! (gradiente cortado — alvo nunca recebe gradiente);
10
+ //! - regularizador variância-covariância (VICReg-lite) anti-colapso:
11
+ //! `V(z) = Σ_j max(0, γ − std(z_j))²` + fora-diagonal da covariância.
12
+ //! - perda: `L = Σ_b ‖P(z_ctx, pos_b) − sg(ẑ_b)‖²/B + λ_vc·V`.
13
+ //!
14
+ //! **Prova J-1 (impossibilidade de colapso, em r-20):** se todas as
15
+ //! incorporações colapsam para constante, `std(z_j) = 0 ∀j` ⇒ `V = d·γ² > 0`;
16
+ //! logo o colapso total NÃO é minimizador com `λ_vc > 0`.
17
+ //!
18
+ //! **Prova J-2 (EMA = stop-grad + ponto fixo):** `∂L/∂ξ = 0` (detach); se
19
+ //! `θ_t → θ*` então `ξ_t → θ*` (recursão linear contratante com `m < 1`).
20
+ //!
21
+ //! **Prova J-3 (decomposição de informação):** com `P` suficientemente
22
+ //! rico, `min_P L = Σ_b Var(ẑ_b | z_ctx)` — a perda retém só o componentes
23
+ //! previsíveis do alvo dado o contexto.
24
+ //!
25
+ //! **Variantes** (esquema de máscara + adaptador de modalidade):
26
+ //! Image-JEPA (blocos 2D) · V-JEPA 2 (tubos 3D + cond. de ação) · MC-JEPA
27
+ //! (cruz-modal, dois codificadores) · Audio-JEPA (bandas de espectrograma) ·
28
+ //! Point-JEPA (grupos FPS) · 3D-JEPA (voxels) · Brain-JEPA (canal×tempo) ·
29
+ //! T-JEPA (série temporal canal-independente) · LeJEPA (edição-condicionado) ·
30
+ //! BJEPA (bidirecional com consistência de ciclo).
31
+ //!
32
+ //! **SonicMoE** — MoE com roteamento top-k, capacidade por especialista
33
+ //! (tile-aware) e perda auxiliar de balanceamento (argumento Switch:
34
+ //! `L_aux = E·Σ f_e·p_e ≥ 1` com igualdade sse uniforme).
35
+ //!
36
+ //! **ReplaySSM** — SSM de porta diagonal (`h_t = σ(a)⊙h_{t−1} +
37
+ //! (1−σ(a))⊙(Bx_t)`, estabilidade herdada do Teo S-2') + replay de experiências
38
+ //! para aprendizado continuado (interpolação de gradientes limita o
39
+ //! esquecimento — Teo R-1).
40
+
41
+ use candle_core::{Device, Result, Tensor, Var};
42
+ use crate::camadas::Linear;
43
+ use crate::rng::RngDeterministico;
44
+
45
+ // ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
46
+ #[cfg(test)]
47
+ mod testes;
48
+ mod mascaras;
49
+ mod nucleo;
50
+ mod patches;
51
+ mod replay_ssm;
52
+ mod sonic_moe;
53
+ mod variantes;
54
+
55
+ pub use mascaras::*;
56
+ pub use nucleo::*;
57
+ pub use patches::*;
58
+ pub use replay_ssm::*;
59
+ pub use sonic_moe::*;
60
+ pub use variantes::*;
61
+
src/jepa/nucleo.rs ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `jepa::nucleo` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Parte 1 — núcleo
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /// MLP 2 camadas com SiLU (d → h → d_saida).
12
+ pub struct MlpJepa {
13
+ pub l1: Linear,
14
+ pub l2: Linear,
15
+ pub d_entrada: usize,
16
+ pub d_saida: usize,
17
+ }
18
+
19
+ impl MlpJepa {
20
+ pub fn novo(d_entrada: usize, d_oculta: usize, d_saida: usize, dev: &Device) -> Result<Self> {
21
+ let s1 = (1.0 / (d_entrada as f64).sqrt()) * 0.9;
22
+ let s2 = (1.0 / (d_oculta as f64).sqrt()) * 0.9;
23
+ Ok(Self {
24
+ l1: Linear::sem_vies(d_entrada, d_oculta, s1, dev)?,
25
+ l2: Linear::sem_vies(d_oculta, d_saida, s2, dev)?,
26
+ d_entrada,
27
+ d_saida,
28
+ })
29
+ }
30
+
31
+ pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
32
+ let h = candle_nn::ops::silu(&self.l1.fwd(x)?)?;
33
+ self.l2.fwd(&h)
34
+ }
35
+
36
+ pub fn vars(&self) -> Vec<Var> {
37
+ [self.l1.vars(), self.l2.vars()].concat()
38
+ }
39
+
40
+ /// EMA: `self ← m·self + (1−m)·outro` (usado no codificador-alvo).
41
+ pub fn ema_de(&mut self, outro: &MlpJepa, m: f64) -> Result<()> {
42
+ for (a, b) in self.vars().iter().zip(outro.vars().iter()) {
43
+ let novo = (a.as_tensor().affine(m, 0.0)? + b.as_tensor().affine(1.0 - m, 0.0)?)?;
44
+ a.set(&novo)?;
45
+ }
46
+ Ok(())
47
+ }
48
+ }
49
+
50
+ /// Núcleo JEPA: codificador + preditor + alvo EMA + regularizador VC.
51
+ pub struct NucleoJepa {
52
+ pub encoder: MlpJepa,
53
+ pub preditor: MlpJepa,
54
+ pub alvo: MlpJepa,
55
+ /// tabela de posições (blocos) — entrada extra do preditor.
56
+ pub pos: Var,
57
+ pub d_emb: usize,
58
+ pub d_cond: usize,
59
+ pub m: f64,
60
+ pub lambda_vc: f64,
61
+ pub gamma_vc: f64,
62
+ }
63
+
64
+ impl NucleoJepa {
65
+ pub fn novo(
66
+ d_patch: usize,
67
+ d_emb: usize,
68
+ d_cond: usize,
69
+ n_pos: usize,
70
+ m: f64,
71
+ lambda_vc: f64,
72
+ gamma_vc: f64,
73
+ dev: &Device,
74
+ ) -> Result<Self> {
75
+ let encoder = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
76
+ let preditor = MlpJepa::novo(d_emb + d_cond + d_emb, 2 * d_emb, d_emb, dev)?;
77
+ // alvo nasce IGUAL ao encoder (ponto de partida do EMA)
78
+ let mut alvo = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
79
+ for (va, vb) in alvo.vars().iter().zip(encoder.vars().iter()) {
80
+ va.set(vb.as_tensor())?;
81
+ }
82
+ let pos_t = Tensor::randn(0f32, 0.02, (n_pos.max(1), d_emb), dev)?;
83
+ Ok(Self {
84
+ encoder,
85
+ preditor,
86
+ alvo,
87
+ pos: Var::from_tensor(&pos_t)?,
88
+ d_emb,
89
+ d_cond,
90
+ m,
91
+ lambda_vc,
92
+ gamma_vc,
93
+ })
94
+ }
95
+
96
+ pub fn vars(&self) -> Vec<Var> {
97
+ let mut v = self.encoder.vars();
98
+ v.extend(self.preditor.vars());
99
+ v.push(self.pos.clone());
100
+ v // NOTA: alvo NÃO entra — é buffer EMA
101
+ }
102
+
103
+ pub fn ema_passo(&mut self) -> Result<()> {
104
+ self.alvo.ema_de(&self.encoder, self.m)
105
+ }
106
+
107
+ /// Codifica um conjunto de patches (n, d_patch) → (n, d_emb).
108
+ pub fn codificar(&self, patches: &Tensor) -> Result<Tensor> {
109
+ self.encoder.fwd(patches)
110
+ }
111
+
112
+ /// Codifica com o alvo EMA — SEM gradiente (detach por construção: as
113
+ /// vars do alvo não estão no otimizador e o tensor é desconectado).
114
+ pub fn alvo_codificar(&self, patches: &Tensor) -> Result<Tensor> {
115
+ Ok(self.alvo.fwd(patches)?.detach())
116
+ }
117
+
118
+ /// Prediz o embedding do bloco `i_pos` a partir do contexto `z_ctx`
119
+ /// (d_emb) e da condição `cond` (d_cond; zeros se None).
120
+ pub fn predizer(&self, z_ctx: &Tensor, i_pos: usize, cond: Option<&Tensor>) -> Result<Tensor> {
121
+ // z_ctx: (b, d_emb) ou (d_emb,)
122
+ let b = z_ctx.dim(0).unwrap_or(1);
123
+ let dev = z_ctx.device();
124
+ let pos = self.pos.as_tensor().get(i_pos.min(self.pos.dim(0)?.saturating_sub(1)))?;
125
+ let pos_b = pos.broadcast_as((b, self.d_emb))?;
126
+ let cond_b = match cond {
127
+ Some(c) => {
128
+ if c.dims().len() == 1 && b > 1 {
129
+ c.broadcast_as((b, self.d_cond))?
130
+ } else {
131
+ c.clone()
132
+ }
133
+ }
134
+ None => Tensor::zeros((b, self.d_cond), candle_core::DType::F32, dev)?,
135
+ };
136
+ let entrada = Tensor::cat(&[z_ctx, &pos_b, &cond_b], candle_core::D::Minus1)?;
137
+ self.preditor.fwd(&entrada)
138
+ }
139
+
140
+ /// Regularizador variância-covariância do lote de incorporações z (n, d).
141
+ ///
142
+ /// **J-1:** no colapso total `z_i = c ∀i` ⇒ `std_j = 0` ⇒ `V = d·γ²`.
143
+ pub fn perda_vc(&self, z: &Tensor) -> Result<Tensor> {
144
+ let n = z.dim(0)? as f64;
145
+ let d = self.d_emb;
146
+ let media = z.mean_keepdim(0)?; // (1, d)
147
+ let centr = z.broadcast_sub(&media)?;
148
+ let var = (centr.sqr()?.sum(0)? / n)?; // (d,)
149
+ let std = var.sqrt()?;
150
+ // dobradiça: max(0, γ − std)²
151
+ let gamma_t =
152
+ Tensor::new(self.gamma_vc as f32, z.device())?.broadcast_as(std.dims())?;
153
+ let gap = gamma_t.broadcast_sub(&std)?.clamp(0.0, f64::INFINITY)?;
154
+ let v = gap.sqr()?.sum_all()?;
155
+ // covariância fora-diagonal: C = centrᵀ·centr/n; perda = ‖C − diag‖²_F/(2d)
156
+ let cov = (centr.transpose(0, 1)?.matmul(&centr)? / n)?; // (d, d)
157
+ let mask_diag = {
158
+ let mut m = vec![0f32; d * d];
159
+ for i in 0..d {
160
+ m[i * d + i] = 1.0;
161
+ }
162
+ Tensor::from_vec(m, (d, d), z.device())?
163
+ };
164
+ let fora = ((&cov * &mask_diag)?.sqr()?.sum_all()? / (2.0 * d.max(1) as f64))?;
165
+ let v_norm = (v / d.max(1) as f64)?;
166
+ Ok((v_norm + fora)?)
167
+ }
168
+
169
+ /// Perda JEPA padrão para UM lote: contexto (número variável de patches),
170
+ /// lista de blocos-alvo (posições), condição opcional.
171
+ ///
172
+ /// `z_ctx` = incorporação agregada do contexto (b, d_emb);
173
+ /// `alvos`: para cada elemento do lote, Vec<(i_pos, patches_alvo)>.
174
+ pub fn perda_lote(
175
+ &self,
176
+ z_ctx: &Tensor,
177
+ alvos: &[Vec<(usize, Tensor)>],
178
+ cond: Option<&Tensor>,
179
+ z_ctx_todos: &Tensor,
180
+ ) -> Result<(Tensor, f64, f64)> {
181
+ let b = z_ctx.dim(0)?;
182
+ let dev = z_ctx.device();
183
+ let mut perdas: Vec<Tensor> = Vec::new();
184
+ let mut n_alvos = 0usize;
185
+ for (i, blocos) in alvos.iter().enumerate() {
186
+ for (i_pos, patches_alvo) in blocos {
187
+ // ẑ_b = média das incorporações-alvo dos patches do bloco
188
+ let zt = self.alvo_codificar(patches_alvo)?; // (m, d_emb)
189
+ let zt_m = zt.mean(0)?; // (d_emb,)
190
+ let zt_b = zt_m.broadcast_as((1, self.d_emb))?;
191
+ let zc = z_ctx.get(i)?.broadcast_as((1, self.d_emb))?;
192
+ let c_i = cond.map(|c| c.get(i)?.broadcast_as((1, self.d_cond))).transpose()?;
193
+ let pred = self.predizer(&zc, *i_pos, c_i.as_ref())?;
194
+ let dif = (&pred - &zt_b)?;
195
+ let l = (dif.sqr()?.sum_all()? / self.d_emb as f64)?;
196
+ perdas.push(l);
197
+ n_alvos += 1;
198
+ }
199
+ }
200
+ let l_pred = if perdas.is_empty() {
201
+ Tensor::zeros((), candle_core::DType::F32, &dev)?
202
+ } else {
203
+ (Tensor::stack(&perdas, 0)?.sum_all()? / n_alvos.max(1) as f64)?
204
+ };
205
+ let l_vc = self.perda_vc(z_ctx_todos)?;
206
+ let total = (l_pred.affine(1.0, 0.0)? + l_vc.affine(self.lambda_vc, 0.0)?)?;
207
+ let lp = l_pred.to_scalar::<f32>()? as f64;
208
+ let lv = l_vc.to_scalar::<f32>()? as f64;
209
+ Ok((total, lp, lv))
210
+ }
211
+ }
src/jepa/patches.rs ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `jepa::patches` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Parte 3 — adaptadores de modalidade
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /// (C, H, W) → patches (H/ph · W/pw, C·ph·pw) — Image-JEPA.
12
+ pub fn em_patches_2d(img: &Tensor, ph: usize, pw: usize) -> Result<Tensor> {
13
+ let (c, h, w) = (img.dim(0)?, img.dim(1)?, img.dim(2)?);
14
+ let dev = img.device();
15
+ let mut linhas: Vec<Tensor> = Vec::new();
16
+ for y in (0..h).step_by(ph) {
17
+ for x in (0..w).step_by(pw) {
18
+ let hh = ph.min(h - y);
19
+ let ww = pw.min(w - x);
20
+ let patch = img.narrow(0, 0, c)?.narrow(1, y, hh)?.narrow(2, x, ww)?;
21
+ // padding para (C, ph, pw) se borda
22
+ let patch = if hh < ph || ww < pw {
23
+ let mut buf = patch.flatten_all()?.to_vec1::<f32>()?;
24
+ let falta = c * ph * pw - buf.len();
25
+ buf.extend(std::iter::repeat(0f32).take(falta));
26
+ Tensor::from_vec(buf, (c * ph * pw,), &dev)?
27
+ } else {
28
+ patch.flatten_all()?
29
+ };
30
+ linhas.push(patch);
31
+ }
32
+ }
33
+ Ok(Tensor::stack(&linhas, 0)?)
34
+ }
35
+
36
+ /// (T, H, W) → tubos (T/pt · H/ph · W/pw, pt·ph·pw) — V-JEPA 2.
37
+ pub fn em_tubos_3d(vid: &Tensor, pt: usize, ph: usize, pw: usize) -> Result<Tensor> {
38
+ let (t, h, w) = (vid.dim(0)?, vid.dim(1)?, vid.dim(2)?);
39
+ let dev = vid.device();
40
+ let mut linhas: Vec<Tensor> = Vec::new();
41
+ for tt in (0..t).step_by(pt) {
42
+ let tl = pt.min(t - tt);
43
+ for y in (0..h).step_by(ph) {
44
+ for x in (0..w).step_by(pw) {
45
+ let hh = ph.min(h - y);
46
+ let ww = pw.min(w - x);
47
+ let tubo = vid
48
+ .narrow(0, tt, tl)?
49
+ .narrow(1, y, hh)?
50
+ .narrow(2, x, ww)?
51
+ .flatten_all()?;
52
+ let mut buf = tubo.to_vec1::<f32>()?;
53
+ let falta = pt * ph * pw - buf.len();
54
+ buf.extend(std::iter::repeat(0f32).take(falta));
55
+ linhas.push(Tensor::from_vec(buf, (pt * ph * pw,), &dev)?);
56
+ }
57
+ }
58
+ }
59
+ Ok(Tensor::stack(&linhas, 0)?)
60
+ }
61
+
62
+ /// (F, τ) → patches (F/pf · τ/pt, pf·pt) — Audio-JEPA.
63
+ pub fn em_patches_espectrograma(spec: &Tensor, pf: usize, pt: usize) -> Result<Tensor> {
64
+ let (f, tau) = (spec.dim(0)?, spec.dim(1)?);
65
+ let dev = spec.device();
66
+ let mut linhas: Vec<Tensor> = Vec::new();
67
+ for i in (0..f).step_by(pf) {
68
+ for j in (0..tau).step_by(pt) {
69
+ let ff = pf.min(f - i);
70
+ let tt = pt.min(tau - j);
71
+ let p = spec.narrow(0, i, ff)?.narrow(1, j, tt)?.flatten_all()?;
72
+ let mut buf = p.to_vec1::<f32>()?;
73
+ let falta = pf * pt - buf.len();
74
+ buf.extend(std::iter::repeat(0f32).take(falta));
75
+ linhas.push(Tensor::from_vec(buf, (pf * pt,), &dev)?);
76
+ }
77
+ }
78
+ Ok(Tensor::stack(&linhas, 0)?)
79
+ }
80
+
81
+ /// (C, T) → patches canal-independentes (C · T/pt, pt) — T-JEPA/Brain-JEPA.
82
+ pub fn em_patches_serie(ts: &Tensor, pt: usize) -> Result<Tensor> {
83
+ let (c, t) = (ts.dim(0)?, ts.dim(1)?);
84
+ let dev = ts.device();
85
+ let mut linhas: Vec<Tensor> = Vec::new();
86
+ for ch in 0..c {
87
+ for j in (0..t).step_by(pt) {
88
+ let tt = pt.min(t - j);
89
+ let p = ts.narrow(0, ch, 1)?.narrow(1, j, tt)?.flatten_all()?;
90
+ let mut buf = p.to_vec1::<f32>()?;
91
+ let falta = pt - buf.len();
92
+ buf.extend(std::iter::repeat(0f32).take(falta));
93
+ linhas.push(Tensor::from_vec(buf, (pt,), &dev)?);
94
+ }
95
+ }
96
+ Ok(Tensor::stack(&linhas, 0)?)
97
+ }
98
+
99
+ /// (X, Y, Z) → voxels (X/px · Y/py · Z/pz, px·py·pz) — 3D-JEPA.
100
+ pub fn em_voxels(vol: &Tensor, px: usize, py: usize, pz: usize) -> Result<Tensor> {
101
+ let (x, y, z) = (vol.dim(0)?, vol.dim(1)?, vol.dim(2)?);
102
+ let dev = vol.device();
103
+ let mut linhas: Vec<Tensor> = Vec::new();
104
+ for i in (0..x).step_by(px) {
105
+ for j in (0..y).step_by(py) {
106
+ for k in (0..z).step_by(pz) {
107
+ let lx = px.min(x - i);
108
+ let ly = py.min(y - j);
109
+ let lz = pz.min(z - k);
110
+ let p = vol.narrow(0, i, lx)?.narrow(1, j, ly)?.narrow(2, k, lz)?.flatten_all()?;
111
+ let mut buf = p.to_vec1::<f32>()?;
112
+ let falta = px * py * pz - buf.len();
113
+ buf.extend(std::iter::repeat(0f32).take(falta));
114
+ linhas.push(Tensor::from_vec(buf, (px * py * pz,), &dev)?);
115
+ }
116
+ }
117
+ }
118
+ Ok(Tensor::stack(&linhas, 0)?)
119
+ }
120
+
121
+ /// Nuvem de pontos (n, d) → grupos FPS (k, m·d): k centros por FPS, cada
122
+ /// grupo = os m vizinhos mais próximos do centro (concatenados) — Point-JEPA.
123
+ pub fn em_grupos_pontos(pc: &Tensor, k: usize, m: usize, rng: &mut RngDeterministico) -> Result<Tensor> {
124
+ let n = pc.dim(0)?;
125
+ let d = pc.dim(1)?;
126
+ let coords = pc.to_vec2::<f32>()?;
127
+ let coords_f: Vec<Vec<f64>> = coords.iter().map(|r| r.iter().map(|&v| v as f64).collect()).collect();
128
+ let centros = pontos_mais_distantes(&coords_f, k, rng);
129
+ let dev = pc.device();
130
+ let mut linhas: Vec<Tensor> = Vec::new();
131
+ for &ci in &centros {
132
+ // m vizinhos mais próximos do centro ci (incluindo ele próprio)
133
+ let mut ord: Vec<usize> = (0..n).collect();
134
+ ord.sort_by(|&a, &b| {
135
+ let da = distancia_eucl(&coords_f[a], &coords_f[ci]);
136
+ let db = distancia_eucl(&coords_f[b], &coords_f[ci]);
137
+ da.partial_cmp(&db).unwrap()
138
+ });
139
+ let mut buf: Vec<f32> = Vec::with_capacity(m * d);
140
+ for &idx in ord.iter().take(m) {
141
+ for j in 0..d {
142
+ buf.push(coords[idx][j]);
143
+ }
144
+ }
145
+ let falta = m * d - buf.len();
146
+ buf.extend(std::iter::repeat(0f32).take(falta));
147
+ linhas.push(Tensor::from_vec(buf, (m * d,), &dev)?);
148
+ }
149
+ Ok(Tensor::stack(&linhas, 0)?)
150
+ }
src/jepa/replay_ssm.rs ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `jepa::replay_ssm` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Parte 6 — ReplaySSM
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /// SSM de porta diagonal com replay para aprendizado continuado.
12
+ ///
13
+ /// Recorrência: `h_t = σ(a) ⊙ h_{t−1} + (1 − σ(a)) ⊙ (B x_t)`.
14
+ ///
15
+ /// **Prova R-1 (estabilidade):** `|σ(a)| < 1` ⇒ perturbações de `h_0`
16
+ /// decaem geometricamente (mesma estrutura do Teo S-2' do SSD-Δ).
17
+ ///
18
+ /// **Prova R-2 (limite de esquecimento):** o passo de treino mistura
19
+ /// gradientes `(1−α)·∇L_novo + α·∇L_replay`; para `L_antiga` μ-fortemente
20
+ /// convexa com gradientes novos limitados por `G`, o afastamento do ótimo
21
+ /// antigo por passo é `≤ (1−α)·η·G` (sem replay: `≤ η·G`) — o replay
22
+ /// contrai o drift por um fator `1/(1−α)` no regime linear.
23
+ pub struct ReplaySsm {
24
+ pub a: Var, // (d,) — porta (sigmoid no uso)
25
+ pub b: Var, // (d_in, d) — projeção de entrada
26
+ pub d: usize,
27
+ pub alpha_replay: f64,
28
+ pub buffer: Vec<Tensor>,
29
+ pub capacidade_buffer: usize,
30
+ }
31
+
32
+ impl ReplaySsm {
33
+ pub fn novo(d_in: usize, d: usize, alpha_replay: f64, capacidade_buffer: usize, dev: &Device) -> Result<Self> {
34
+ let a_t = Tensor::randn(0f32, 0.5, (d,), dev)?;
35
+ let b_t = Tensor::randn(0f32, 0.1, (d_in, d), dev)?;
36
+ Ok(Self {
37
+ a: Var::from_tensor(&a_t)?,
38
+ b: Var::from_tensor(&b_t)?,
39
+ d,
40
+ alpha_replay,
41
+ buffer: Vec::new(),
42
+ capacidade_buffer,
43
+ })
44
+ }
45
+
46
+ pub fn vars(&self) -> Vec<Var> {
47
+ vec![self.a.clone(), self.b.clone()]
48
+ }
49
+
50
+ /// Scan: x (T, d_in) → h (T, d). Diferenciável (candle autograd).
51
+ pub fn scan(&self, x: &Tensor) -> Result<Tensor> {
52
+ let t = x.dim(0)?;
53
+ let dev = x.device();
54
+ let porta = candle_nn::ops::sigmoid(&self.a.as_tensor().broadcast_as((1, self.d))?)?; // (1, d)
55
+ let um_menos = porta.affine(-1.0, 1.0)?; // 1 − σ(a)
56
+ let entrada = x.matmul(&self.b.as_tensor())?; // (T, d)
57
+ let mut h = Tensor::zeros((1, self.d), candle_core::DType::F32, &dev)?;
58
+ let mut saidas: Vec<Tensor> = Vec::with_capacity(t);
59
+ for i in 0..t {
60
+ let xt = entrada.narrow(0, i, 1)?;
61
+ h = ((&porta * &h)? + (&um_menos * &xt)?)?;
62
+ saidas.push(h.clone());
63
+ }
64
+ Ok(Tensor::stack(&saidas, 0)?)
65
+ }
66
+
67
+ /// Empurra uma janela de treino no buffer (FIFO).
68
+ pub fn guardar(&mut self, x: &Tensor) {
69
+ if self.buffer.len() >= self.capacidade_buffer {
70
+ self.buffer.remove(0);
71
+ }
72
+ self.buffer.push(x.detach());
73
+ }
74
+
75
+ /// Um passo de treino no lote novo (com mistura de replay).
76
+ /// Devolve (perda_total, perda_novo, perda_replay).
77
+ pub fn passo_treino(
78
+ &mut self,
79
+ x: &Tensor,
80
+ alvo: &Tensor,
81
+ otimizador: &mut crate::treino::AdamWManual,
82
+ ) -> Result<(f64, f64, f64)> {
83
+ // perda nova
84
+ let h = self.scan(x)?;
85
+ let dif = (&h - alvo)?;
86
+ let perda_novo = dif.sqr()?.mean_all()?;
87
+ // replay
88
+ let mut perda_total = perda_novo.clone();
89
+ let mut p_r = 0.0;
90
+ if !self.buffer.is_empty() {
91
+ let idx = self.buffer.len() - 1;
92
+ let antigo = self.buffer[idx].clone();
93
+ let hr = self.scan(&antigo)?;
94
+ // alvo do replay: manter a dinâmica aprendida estável (magnitudes
95
+ // finitas) — auto-supervisão de estabilidade
96
+ let dr = hr.sqr()?.mean_all()?;
97
+ perda_total = (perda_total.affine(1.0 - self.alpha_replay, 0.0)?
98
+ + dr.affine(self.alpha_replay, 0.0)?)?;
99
+ p_r = dr.to_scalar::<f32>()? as f64;
100
+ }
101
+ let pn = perda_novo.to_scalar::<f32>()? as f64;
102
+ otimizador.passo(crate::treino::grads_achatados(&perda_total, &self.vars())?);
103
+ self.guardar(x);
104
+ let pt = perda_total.to_scalar::<f32>()? as f64;
105
+ Ok((pt, pn, p_r))
106
+ }
107
+ }
src/jepa/sonic_moe.rs ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `jepa::sonic_moe` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Parte 5 — SonicMoE (IO/tile-aware)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /// MoE top-k com capacidade por especialista e perda de balanceamento.
12
+ ///
13
+ /// **Prova SM-1 (limite de capacidade):** o roteamento tile-aware mantém no
14
+ /// máximo `C` tokens por especialista por ladrilho — o drop é determinístico
15
+ /// (ordem de chegada) e o nº de tokens descartados é `Σ_e max(0, load_e − C)`.
16
+ ///
17
+ /// **Prova SM-2 (balanceamento):** `L_aux = E·Σ_e f_e·p_e`; com roteamento
18
+ /// auto-consistente (`f = p`), `L_aux = E·Σ p_e² ≥ 1` com igualdade sse
19
+ /// `p_e = 1/E` (Cauchy–Schchwarz) — o mínimo força o roteador uniforme.
20
+ pub struct SonicMoe {
21
+ pub roteador: Linear, // d → E
22
+ pub especialistas: Vec<MlpJepa>, // E experts d → d
23
+ pub n_top: usize,
24
+ pub capacidade: usize,
25
+ pub lambda_aux: f64,
26
+ }
27
+
28
+ pub struct SaidaMoe {
29
+ pub saida: Tensor,
30
+ pub perda_aux: Tensor,
31
+ pub carga: Vec<usize>, // tokens efetivamente processados por especialista
32
+ pub descartados: usize, // tokens com especialistas cortados pela capacidade
33
+ }
34
+
35
+ impl SonicMoe {
36
+ pub fn novo(d: usize, n_expertos: usize, n_top: usize, capacidade: usize, dev: &Device) -> Result<Self> {
37
+ let roteador = Linear::sem_vies(d, n_expertos, 0.1, dev)?;
38
+ let especialistas = (0..n_expertos)
39
+ .map(|_| MlpJepa::novo(d, 2 * d, d, dev))
40
+ .collect::<Result<Vec<_>>>()?;
41
+ Ok(Self { roteador, especialistas, n_top, capacidade, lambda_aux: 0.01 })
42
+ }
43
+
44
+ pub fn vars(&self) -> Vec<Var> {
45
+ let mut v = self.roteador.vars();
46
+ for e in &self.especialistas {
47
+ v.extend(e.vars());
48
+ }
49
+ v
50
+ }
51
+
52
+ /// Forward tile-aware: (n, d) → (n, d).
53
+ pub fn fwd(&self, x: &Tensor) -> Result<SaidaMoe> {
54
+ let n = x.dim(0)?;
55
+ let d = x.dim(1)?;
56
+ let e = self.especialistas.len();
57
+ let dev = x.device();
58
+ let logits = self.roteador.fwd(x)?; // (n, E)
59
+ let probs = candle_nn::ops::softmax(&logits, candle_core::D::Minus1)?;
60
+
61
+ // top-k por token (não diferenciável — pesos do top-k são dif. via probs)
62
+ let probs_v = probs.to_vec2::<f32>()?;
63
+ let mut carga = vec![0usize; e];
64
+ let mut descartados = 0usize;
65
+ // acumulador de saída (n, d) — construído por soma de contribuições
66
+ let mut saida = Tensor::zeros((n, d), candle_core::DType::F32, &dev)?;
67
+ let mut peso_total = Tensor::zeros((n, 1), candle_core::DType::F32, &dev)?;
68
+ // por especialista (tile): tokens cujo top-k contém e e com carga < C
69
+ for ei in 0..e {
70
+ // candidatos: tokens ordenados por prob do especialista ei
71
+ let mut cand: Vec<(usize, f32)> = (0..n)
72
+ .map(|i| (i, probs_v[i][ei]))
73
+ .collect();
74
+ cand.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
75
+ let mut escolhidos: Vec<usize> = Vec::new();
76
+ for (i, _) in cand {
77
+ if carga[ei] >= self.capacidade {
78
+ break;
79
+ }
80
+ // verifica se ei está no top-k do token i
81
+ let mut ordem: Vec<(f32, usize)> =
82
+ (0..e).map(|k| (probs_v[i][k], k)).collect();
83
+ ordem.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
84
+ let top: Vec<usize> = ordem.iter().rev().take(self.n_top).map(|(_, k)| *k).collect();
85
+ if top.contains(&ei) {
86
+ escolhidos.push(i);
87
+ carga[ei] += 1;
88
+ }
89
+ }
90
+ if escolhidos.is_empty() {
91
+ continue;
92
+ }
93
+ // tile do especialista: uma única passagem em lote
94
+ let idx: Vec<u32> = escolhidos.iter().map(|&i| i as u32).collect();
95
+ let n_e = idx.len();
96
+ let idx_t = Tensor::from_vec(idx, (n_e,), &dev)?;
97
+ let tile = x.index_select(&idx_t, 0)?;
98
+ let y = self.especialistas[ei].fwd(&tile)?; // (m, d)
99
+ // pesos: prob do especialista ei para cada token escolhido
100
+ let w: Vec<f32> = escolhidos.iter().map(|&i| probs_v[i][ei]).collect();
101
+ let n_w = escolhidos.len();
102
+ let w_t = Tensor::from_vec(w.clone(), (n_w, 1), &dev)?;
103
+ let contrib = y.broadcast_mul(&w_t)?;
104
+ // scatter-add: acumula nas posições
105
+ let mut buf = saida.to_vec2::<f32>()?;
106
+ let contrib_v = contrib.to_vec2::<f32>()?;
107
+ for (k, &i) in escolhidos.iter().enumerate() {
108
+ for j in 0..d {
109
+ buf[i][j] += contrib_v[k][j];
110
+ }
111
+ }
112
+ saida = Tensor::from_vec(
113
+ buf.into_iter().flatten().collect::<Vec<f32>>(),
114
+ (n, d),
115
+ &dev,
116
+ )?;
117
+ let mut pt = peso_total.to_vec2::<f32>()?;
118
+ for (k, &i) in escolhidos.iter().enumerate() {
119
+ pt[i][0] += w[k];
120
+ }
121
+ peso_total = Tensor::from_vec(
122
+ pt.into_iter().flatten().collect::<Vec<f32>>(),
123
+ (n, 1),
124
+ &dev,
125
+ )?;
126
+ }
127
+ // tokens sem especialista (todos cortados) → descartados; saída 0
128
+ let pt_v = peso_total.to_vec2::<f32>()?;
129
+ for i in 0..n {
130
+ if pt_v[i][0] == 0.0 {
131
+ descartados += 1;
132
+ }
133
+ }
134
+ // renormaliza pelo peso efetivo (evita viés da capacidade)
135
+ let denom = peso_total.clamp(1e-6, f64::INFINITY)?;
136
+ let saida = saida.broadcast_div(&denom)?;
137
+
138
+ // perda auxiliar: E·Σ f_e·p_e (f = fração de tokens cujo top-1 é e)
139
+ let mut f = vec![0f32; e];
140
+ for i in 0..n {
141
+ let (mut melhor, mut mp) = (0usize, f32::NEG_INFINITY);
142
+ for k in 0..e {
143
+ if probs_v[i][k] > mp {
144
+ mp = probs_v[i][k];
145
+ melhor = k;
146
+ }
147
+ }
148
+ f[melhor] += 1.0 / n as f32;
149
+ }
150
+ let f_t = Tensor::from_vec(f, (e,), &dev)?;
151
+ let p_media = probs.mean(0)?; // (E,)
152
+ let perda_aux = ((&f_t * &p_media)?.sum_all()? * (e as f64))?;
153
+ Ok(SaidaMoe { saida, perda_aux, carga, descartados })
154
+ }
155
+ }
src/jepa/testes.rs ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `jepa::testes` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ #[cfg(test)]
8
+ mod testes {
9
+ use super::*;
10
+
11
+ fn tensor_2d(dados: &[Vec<f64>], dev: &Device) -> Tensor {
12
+ let flat: Vec<f32> = dados.iter().flat_map(|v| v.iter().map(|&x| x as f32)).collect();
13
+ Tensor::from_vec(flat, (dados.len(), dados[0].len()), dev).unwrap()
14
+ }
15
+
16
+ #[test]
17
+ fn teste_mascaras_validas() {
18
+ // fração respeitada ±, particionamento contexto/alvo não-vazio
19
+ let mut rng = RngDeterministico::novo(1);
20
+ let m2 = mascara_blocos_2d(8, 8, 4, 0.3, &mut rng);
21
+ assert_eq!(m2.len(), 64);
22
+ let frac = m2.iter().filter(|&&b| b).count() as f64 / 64.0;
23
+ assert!(frac >= 0.15 && frac <= 0.55, "frac 2d = {frac}");
24
+ assert!(m2.iter().any(|&b| !b), "contexto vazio");
25
+
26
+ let m3 = mascara_tubos_3d(4, 6, 6, 3, 0.3, &mut rng);
27
+ assert_eq!(m3.len(), 144);
28
+ let f3 = m3.iter().filter(|&&b| b).count() as f64 / 144.0;
29
+ assert!(f3 >= 0.15 && f3 <= 0.6, "frac 3d = {f3}");
30
+
31
+ let ms = mascara_segmentos(4, 32, 0.25, &mut rng);
32
+ assert_eq!(ms.len(), 128);
33
+ assert!(ms.iter().any(|&b| b) && ms.iter().any(|&b| !b));
34
+
35
+ let m1 = mascara_intervalos_1d(64, 0.3, &mut rng);
36
+ let f1 = m1.iter().filter(|&&b| b).count() as f64 / 64.0;
37
+ assert!(f1 >= 0.25 && f1 <= 0.45, "frac 1d = {f1}");
38
+ }
39
+
40
+ #[test]
41
+ fn teste_fps_cobre() {
42
+ let mut rng = RngDeterministico::novo(2);
43
+ let pontos: Vec<Vec<f64>> = (0..40)
44
+ .map(|i| {
45
+ let a = i as f64 * 0.1;
46
+ vec![a.cos() * 3.0, a.sin() * 3.0, (i % 7) as f64 * 0.3]
47
+ })
48
+ .collect();
49
+ let centros = pontos_mais_distantes(&pontos, 6, &mut rng);
50
+ assert_eq!(centros.len(), 6);
51
+ // centros distintos
52
+ let mut s = centros.clone();
53
+ s.sort();
54
+ s.dedup();
55
+ assert_eq!(s.len(), 6);
56
+ }
57
+
58
+ #[test]
59
+ fn teste_j1_colapso_impossivel() {
60
+ // No colapso total (z constante), a perda VC = γ² > 0 — verificando a
61
+ // fórmula implementada com um lote constante.
62
+ let dev = Device::Cpu;
63
+ let nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.99, 1.0, 1.0, &dev).unwrap();
64
+ let z = Tensor::from_vec(vec![0.5f32; 8 * 8], (8, 8), &dev).unwrap();
65
+ let lvc = nucleo.perda_vc(&z).unwrap();
66
+ let v = lvc.to_scalar::<f32>().unwrap() as f64;
67
+ // std=0 ⇒ dobradiça = γ = 1 ⇒ V/d = 1; cov = 0
68
+ assert!(v > 0.9, "perda VC no colapso = {v} (deveria ser ~1)");
69
+ }
70
+
71
+ #[test]
72
+ fn teste_j2_ema_ponto_fixo() {
73
+ // encoder congelado ⇒ após muitas passadas EMA, alvo = encoder
74
+ let dev = Device::Cpu;
75
+ let mut nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.5, 0.1, 1.0, &dev).unwrap();
76
+ for _ in 0..40 {
77
+ nucleo.ema_passo().unwrap();
78
+ }
79
+ let a = nucleo.alvo.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
80
+ let b = nucleo.encoder.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
81
+ let dif: f32 = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max);
82
+ assert!(dif < 1e-5, "EMA não convergiu: dif máx = {dif}");
83
+ }
84
+
85
+ #[test]
86
+ fn teste_jepa_aprende_latente_previsivel() {
87
+ // J-3: com alvo previsível (função determinística do contexto), a
88
+ // perda pred must cair substancialmente e o std das incorporações
89
+ // NÃO colapsa (J-1).
90
+ let dev = Device::Cpu;
91
+ let mut rng = RngDeterministico::novo(3);
92
+ // patches = 64 vetores 8-D em 4 grupos (estrutura previsível)
93
+ let mut dados: Vec<Vec<f64>> = Vec::new();
94
+ for g in 0..4 {
95
+ let c = g as f64 * 2.0;
96
+ for _ in 0..16 {
97
+ dados.push((0..8).map(|k| c + rng.normal(0.0, 0.3) + (k as f64) * 0.05).collect());
98
+ }
99
+ }
100
+ let patches = tensor_2d(&dados, &dev);
101
+ let cfg = CfgJepa { d_emb: 16, epocas: 40, lr: 5e-3, ..Default::default() };
102
+ let rel = treinar_variante(
103
+ &patches,
104
+ &|n, r| mascara_intervalos_1d(n, 0.3, r),
105
+ &cfg,
106
+ VarianteJepa::TJepa,
107
+ None,
108
+ &dev,
109
+ )
110
+ .unwrap();
111
+ assert!(
112
+ rel.perda_final < rel.perda_inicial * 0.8,
113
+ "perda deveria cair: {} → {}",
114
+ rel.perda_inicial,
115
+ rel.perda_final
116
+ );
117
+ assert!(
118
+ rel.std_emb_final > 0.05,
119
+ "sem colapso: std_final = {}",
120
+ rel.std_emb_final
121
+ );
122
+ }
123
+
124
+ #[test]
125
+ fn teste_adaptadores_formas() {
126
+ let dev = Device::Cpu;
127
+ let img = Tensor::randn(0f32, 1.0, (3, 16, 16), &dev).unwrap();
128
+ let p = em_patches_2d(&img, 4, 4).unwrap();
129
+ assert_eq!(p.dims().to_vec(), vec![16, 48]);
130
+
131
+ let vid = Tensor::randn(0f32, 1.0, (4, 8, 8), &dev).unwrap();
132
+ let t3 = em_tubos_3d(&vid, 2, 4, 4).unwrap();
133
+ assert_eq!(t3.dims().to_vec(), vec![8, 32]);
134
+
135
+ let spec = Tensor::randn(0f32, 1.0, (16, 32), &dev).unwrap();
136
+ let ps = em_patches_espectrograma(&spec, 4, 8).unwrap();
137
+ assert_eq!(ps.dims().to_vec(), vec![16, 32]);
138
+
139
+ let ts = Tensor::randn(0f32, 1.0, (4, 32), &dev).unwrap();
140
+ let pt_ = em_patches_serie(&ts, 8).unwrap();
141
+ assert_eq!(pt_.dims().to_vec(), vec![16, 8]);
142
+
143
+ let vol = Tensor::randn(0f32, 1.0, (8, 8, 8), &dev).unwrap();
144
+ let vx = em_voxels(&vol, 4, 4, 4).unwrap();
145
+ assert_eq!(vx.dims().to_vec(), vec![8, 64]);
146
+
147
+ let pc = Tensor::randn(0f32, 1.0, (50, 3), &dev).unwrap();
148
+ let mut rng = RngDeterministico::novo(5);
149
+ let gr = em_grupos_pontos(&pc, 6, 4, &mut rng).unwrap();
150
+ assert_eq!(gr.dims().to_vec(), vec![6, 12]);
151
+ }
152
+
153
+ #[test]
154
+ fn teste_sonic_moe_capacidade_e_balanceamento() {
155
+ // SM-1: carga por especialista ≤ capacidade; SM-2: aux ≥ ~1 quando
156
+ // roteador é ~uniforme (início).
157
+ let dev = Device::Cpu;
158
+ let moe = SonicMoe::novo(8, 4, 2, 6, &dev).unwrap();
159
+ let x = Tensor::randn(0f32, 1.0, (20, 8), &dev).unwrap();
160
+ let s = moe.fwd(&x).unwrap();
161
+ for (e, c) in s.carga.iter().enumerate() {
162
+ assert!(*c <= 6, "especialista {e} com carga {c} > capacidade 6");
163
+ }
164
+ assert_eq!(s.saida.dims().to_vec(), vec![20, 8]);
165
+ let aux = s.perda_aux.to_scalar::<f32>().unwrap() as f64;
166
+ // com roteador quase uniforme, aux ≈ E·Σ (1/E)·(1/E) = 1
167
+ assert!(aux > 0.5 && aux < 3.0, "aux = {aux}");
168
+ // cada token processado por ≤ n_top especialistas
169
+ let total_carga: usize = s.carga.iter().sum();
170
+ assert!(total_carga <= 20 * 2, "carga total {total_carga} > n·topk");
171
+ }
172
+
173
+ #[test]
174
+ fn teste_replay_ssm_estabilidade_e_replay() {
175
+ // R-1: estados finitos e limitados; R-2: com replay α>0, após treinar
176
+ // a tarefa nova, a dinâmica antiga (magnitude do estado) permanece
177
+ // estável (não diverge).
178
+ let dev = Device::Cpu;
179
+ let mut ssm = ReplaySsm::novo(4, 6, 0.5, 8, &dev).unwrap();
180
+ let vars = ssm.vars();
181
+ let mut otim = crate::treino::AdamWManual::novo(vars, 1e-2, 0.0);
182
+
183
+ // tarefa "antiga": sinal constante 1.0
184
+ let x_antigo = Tensor::from_vec(vec![1.0f32; 16 * 4], (16, 4), &dev).unwrap();
185
+ for _ in 0..30 {
186
+ let h = ssm.scan(&x_antigo).unwrap();
187
+ let alvo = h.detach();
188
+ ssm.passo_treino(&x_antigo, &alvo, &mut otim).unwrap();
189
+ }
190
+ // estado após treino antigo: finito
191
+ let h1 = ssm.scan(&x_antigo).unwrap();
192
+ let m1 = h1.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
193
+ assert!(m1.is_finite() && m1 < 50.0, "estado explodiu: {m1}");
194
+
195
+ // tarefa "nova": sinal −1.0 — treino com replay
196
+ let x_novo = Tensor::from_vec(vec![-1.0f32; 16 * 4], (16, 4), &dev).unwrap();
197
+ for _ in 0..30 {
198
+ let h = ssm.scan(&x_novo).unwrap();
199
+ let alvo = h.detach();
200
+ ssm.passo_treino(&x_novo, &alvo, &mut otim).unwrap();
201
+ }
202
+ // a dinâmica antiga continua estável (sem explosão)
203
+ let h2 = ssm.scan(&x_antigo).unwrap();
204
+ let m2 = h2.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
205
+ assert!(m2.is_finite() && m2 < 50.0, "estado antigo explodiu: {m2}");
206
+ // portas dentro de (0,1)
207
+ let a = ssm.a.as_tensor().to_vec1::<f32>().unwrap();
208
+ for v in a {
209
+ assert!(v.is_finite());
210
+ }
211
+ }
212
+ }
src/{jepa.rs → jepa/variantes.rs} RENAMED
@@ -1,563 +1,8 @@
1
- //! Suíte JEPA v0.5.0 — núcleo unificado + 12 variantes + SonicMoE + ReplaySSM.
2
- //!
3
- //! **Núcleo (todas as variantes compartilham):**
4
- //! - codificador `E_θ` (MLp SiLU), preditor `P_φ` (condicionado por posição/
5
- //! ação/edição), codificador-alvo EMA `E_ξ` com `ξ ← m·ξ + (1−m)·θ`
6
- //! (gradiente cortado — alvo nunca recebe gradiente);
7
- //! - regularizador variância-covariância (VICReg-lite) anti-colapso:
8
- //! `V(z) = Σ_j max(0, γ − std(z_j))²` + fora-diagonal da covariância.
9
- //! - perda: `L = Σ_b ‖P(z_ctx, pos_b) − sg(ẑ_b)‖²/B + λ_vc·V`.
10
- //!
11
- //! **Prova J-1 (impossibilidade de colapso, em r-20):** se todas as
12
- //! incorporações colapsam para constante, `std(z_j) = 0 ∀j` ⇒ `V = d·γ² > 0`;
13
- //! logo o colapso total NÃO é minimizador com `λ_vc > 0`.
14
- //!
15
- //! **Prova J-2 (EMA = stop-grad + ponto fixo):** `∂L/∂ξ = 0` (detach); se
16
- //! `θ_t → θ*` então `ξ_t → θ*` (recursão linear contratante com `m < 1`).
17
- //!
18
- //! **Prova J-3 (decomposição de informação):** com `P` suficientemente
19
- //! rico, `min_P L = Σ_b Var(ẑ_b | z_ctx)` — a perda retém só o componentes
20
- //! previsíveis do alvo dado o contexto.
21
- //!
22
- //! **Variantes** (esquema de máscara + adaptador de modalidade):
23
- //! Image-JEPA (blocos 2D) · V-JEPA 2 (tubos 3D + cond. de ação) · MC-JEPA
24
- //! (cruz-modal, dois codificadores) · Audio-JEPA (bandas de espectrograma) ·
25
- //! Point-JEPA (grupos FPS) · 3D-JEPA (voxels) · Brain-JEPA (canal×tempo) ·
26
- //! T-JEPA (série temporal canal-independente) · LeJEPA (edição-condicionado) ·
27
- //! BJEPA (bidirecional com consistência de ciclo).
28
- //!
29
- //! **SonicMoE** — MoE com roteamento top-k, capacidade por especialista
30
- //! (tile-aware) e perda auxiliar de balanceamento (argumento Switch:
31
- //! `L_aux = E·Σ f_e·p_e ≥ 1` com igualdade sse uniforme).
32
- //!
33
- //! **ReplaySSM** — SSM de porta diagonal (`h_t = σ(a)⊙h_{t−1} +
34
- //! (1−σ(a))⊙(Bx_t)`, estabilidade herdada do Teo S-2') + replay de experiências
35
- //! para aprendizado continuado (interpolação de gradientes limita o
36
- //! esquecimento — Teo R-1).
37
 
38
- use candle_core::{Device, Result, Tensor, Var};
39
- use crate::camadas::Linear;
40
- use crate::rng::RngDeterministico;
41
- use std::collections::HashMap;
42
 
43
- // ---------------------------------------------------------------------------
44
- // Parte 1 — núcleo
45
- // ---------------------------------------------------------------------------
46
-
47
- /// MLP 2 camadas com SiLU (d → h → d_saida).
48
- pub struct MlpJepa {
49
- pub l1: Linear,
50
- pub l2: Linear,
51
- pub d_entrada: usize,
52
- pub d_saida: usize,
53
- }
54
-
55
- impl MlpJepa {
56
- pub fn novo(d_entrada: usize, d_oculta: usize, d_saida: usize, dev: &Device) -> Result<Self> {
57
- let s1 = (1.0 / (d_entrada as f64).sqrt()) * 0.9;
58
- let s2 = (1.0 / (d_oculta as f64).sqrt()) * 0.9;
59
- Ok(Self {
60
- l1: Linear::sem_vies(d_entrada, d_oculta, s1, dev)?,
61
- l2: Linear::sem_vies(d_oculta, d_saida, s2, dev)?,
62
- d_entrada,
63
- d_saida,
64
- })
65
- }
66
-
67
- pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
68
- let h = candle_nn::ops::silu(&self.l1.fwd(x)?)?;
69
- self.l2.fwd(&h)
70
- }
71
-
72
- pub fn vars(&self) -> Vec<Var> {
73
- [self.l1.vars(), self.l2.vars()].concat()
74
- }
75
-
76
- /// EMA: `self ← m·self + (1−m)·outro` (usado no codificador-alvo).
77
- pub fn ema_de(&mut self, outro: &MlpJepa, m: f64) -> Result<()> {
78
- for (a, b) in self.vars().iter().zip(outro.vars().iter()) {
79
- let novo = (a.as_tensor().affine(m, 0.0)? + b.as_tensor().affine(1.0 - m, 0.0)?)?;
80
- a.set(&novo)?;
81
- }
82
- Ok(())
83
- }
84
- }
85
-
86
- /// Núcleo JEPA: codificador + preditor + alvo EMA + regularizador VC.
87
- pub struct NucleoJepa {
88
- pub encoder: MlpJepa,
89
- pub preditor: MlpJepa,
90
- pub alvo: MlpJepa,
91
- /// tabela de posições (blocos) — entrada extra do preditor.
92
- pub pos: Var,
93
- pub d_emb: usize,
94
- pub d_cond: usize,
95
- pub m: f64,
96
- pub lambda_vc: f64,
97
- pub gamma_vc: f64,
98
- }
99
-
100
- impl NucleoJepa {
101
- pub fn novo(
102
- d_patch: usize,
103
- d_emb: usize,
104
- d_cond: usize,
105
- n_pos: usize,
106
- m: f64,
107
- lambda_vc: f64,
108
- gamma_vc: f64,
109
- dev: &Device,
110
- ) -> Result<Self> {
111
- let encoder = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
112
- let preditor = MlpJepa::novo(d_emb + d_cond + d_emb, 2 * d_emb, d_emb, dev)?;
113
- // alvo nasce IGUAL ao encoder (ponto de partida do EMA)
114
- let mut alvo = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
115
- for (va, vb) in alvo.vars().iter().zip(encoder.vars().iter()) {
116
- va.set(vb.as_tensor())?;
117
- }
118
- let pos_t = Tensor::randn(0f32, 0.02, (n_pos.max(1), d_emb), dev)?;
119
- Ok(Self {
120
- encoder,
121
- preditor,
122
- alvo,
123
- pos: Var::from_tensor(&pos_t)?,
124
- d_emb,
125
- d_cond,
126
- m,
127
- lambda_vc,
128
- gamma_vc,
129
- })
130
- }
131
-
132
- pub fn vars(&self) -> Vec<Var> {
133
- let mut v = self.encoder.vars();
134
- v.extend(self.preditor.vars());
135
- v.push(self.pos.clone());
136
- v // NOTA: alvo NÃO entra — é buffer EMA
137
- }
138
-
139
- pub fn ema_passo(&mut self) -> Result<()> {
140
- self.alvo.ema_de(&self.encoder, self.m)
141
- }
142
-
143
- /// Codifica um conjunto de patches (n, d_patch) → (n, d_emb).
144
- pub fn codificar(&self, patches: &Tensor) -> Result<Tensor> {
145
- self.encoder.fwd(patches)
146
- }
147
-
148
- /// Codifica com o alvo EMA — SEM gradiente (detach por construção: as
149
- /// vars do alvo não estão no otimizador e o tensor é desconectado).
150
- pub fn alvo_codificar(&self, patches: &Tensor) -> Result<Tensor> {
151
- Ok(self.alvo.fwd(patches)?.detach())
152
- }
153
-
154
- /// Prediz o embedding do bloco `i_pos` a partir do contexto `z_ctx`
155
- /// (d_emb) e da condição `cond` (d_cond; zeros se None).
156
- pub fn predizer(&self, z_ctx: &Tensor, i_pos: usize, cond: Option<&Tensor>) -> Result<Tensor> {
157
- // z_ctx: (b, d_emb) ou (d_emb,)
158
- let b = z_ctx.dim(0).unwrap_or(1);
159
- let dev = z_ctx.device();
160
- let pos = self.pos.as_tensor().get(i_pos.min(self.pos.dim(0)?.saturating_sub(1)))?;
161
- let pos_b = pos.broadcast_as((b, self.d_emb))?;
162
- let cond_b = match cond {
163
- Some(c) => {
164
- if c.dims().len() == 1 && b > 1 {
165
- c.broadcast_as((b, self.d_cond))?
166
- } else {
167
- c.clone()
168
- }
169
- }
170
- None => Tensor::zeros((b, self.d_cond), candle_core::DType::F32, dev)?,
171
- };
172
- let entrada = Tensor::cat(&[z_ctx, &pos_b, &cond_b], candle_core::D::Minus1)?;
173
- self.preditor.fwd(&entrada)
174
- }
175
-
176
- /// Regularizador variância-covariância do lote de incorporações z (n, d).
177
- ///
178
- /// **J-1:** no colapso total `z_i = c ∀i` ⇒ `std_j = 0` ⇒ `V = d·γ²`.
179
- pub fn perda_vc(&self, z: &Tensor) -> Result<Tensor> {
180
- let n = z.dim(0)? as f64;
181
- let d = self.d_emb;
182
- let media = z.mean_keepdim(0)?; // (1, d)
183
- let centr = z.broadcast_sub(&media)?;
184
- let var = (centr.sqr()?.sum(0)? / n)?; // (d,)
185
- let std = var.sqrt()?;
186
- // dobradiça: max(0, γ − std)²
187
- let gamma_t =
188
- Tensor::new(self.gamma_vc as f32, z.device())?.broadcast_as(std.dims())?;
189
- let gap = gamma_t.broadcast_sub(&std)?.clamp(0.0, f64::INFINITY)?;
190
- let v = gap.sqr()?.sum_all()?;
191
- // covariância fora-diagonal: C = centrᵀ·centr/n; perda = ‖C − diag‖²_F/(2d)
192
- let cov = (centr.transpose(0, 1)?.matmul(&centr)? / n)?; // (d, d)
193
- let mask_diag = {
194
- let mut m = vec![0f32; d * d];
195
- for i in 0..d {
196
- m[i * d + i] = 1.0;
197
- }
198
- Tensor::from_vec(m, (d, d), z.device())?
199
- };
200
- let fora = ((&cov * &mask_diag)?.sqr()?.sum_all()? / (2.0 * d.max(1) as f64))?;
201
- let v_norm = (v / d.max(1) as f64)?;
202
- Ok((v_norm + fora)?)
203
- }
204
-
205
- /// Perda JEPA padrão para UM lote: contexto (número variável de patches),
206
- /// lista de blocos-alvo (posições), condição opcional.
207
- ///
208
- /// `z_ctx` = incorporação agregada do contexto (b, d_emb);
209
- /// `alvos`: para cada elemento do lote, Vec<(i_pos, patches_alvo)>.
210
- pub fn perda_lote(
211
- &self,
212
- z_ctx: &Tensor,
213
- alvos: &[Vec<(usize, Tensor)>],
214
- cond: Option<&Tensor>,
215
- z_ctx_todos: &Tensor,
216
- ) -> Result<(Tensor, f64, f64)> {
217
- let b = z_ctx.dim(0)?;
218
- let dev = z_ctx.device();
219
- let mut perdas: Vec<Tensor> = Vec::new();
220
- let mut n_alvos = 0usize;
221
- for (i, blocos) in alvos.iter().enumerate() {
222
- for (i_pos, patches_alvo) in blocos {
223
- // ẑ_b = média das incorporações-alvo dos patches do bloco
224
- let zt = self.alvo_codificar(patches_alvo)?; // (m, d_emb)
225
- let zt_m = zt.mean(0)?; // (d_emb,)
226
- let zt_b = zt_m.broadcast_as((1, self.d_emb))?;
227
- let zc = z_ctx.get(i)?.broadcast_as((1, self.d_emb))?;
228
- let c_i = cond.map(|c| c.get(i)?.broadcast_as((1, self.d_cond))).transpose()?;
229
- let pred = self.predizer(&zc, *i_pos, c_i.as_ref())?;
230
- let dif = (&pred - &zt_b)?;
231
- let l = (dif.sqr()?.sum_all()? / self.d_emb as f64)?;
232
- perdas.push(l);
233
- n_alvos += 1;
234
- }
235
- }
236
- let l_pred = if perdas.is_empty() {
237
- Tensor::zeros((), candle_core::DType::F32, &dev)?
238
- } else {
239
- (Tensor::stack(&perdas, 0)?.sum_all()? / n_alvos.max(1) as f64)?
240
- };
241
- let l_vc = self.perda_vc(z_ctx_todos)?;
242
- let total = (l_pred.affine(1.0, 0.0)? + l_vc.affine(self.lambda_vc, 0.0)?)?;
243
- let lp = l_pred.to_scalar::<f32>()? as f64;
244
- let lv = l_vc.to_scalar::<f32>()? as f64;
245
- Ok((total, lp, lv))
246
- }
247
- }
248
-
249
- // ---------------------------------------------------------------------------
250
- // Parte 2 — máscaras
251
- // ---------------------------------------------------------------------------
252
-
253
- /// Máscara de blocos 2D (Image-JEPA): grade (h, w) de patches; sorteia
254
- /// `n_blocos` retângulos aleatórios com fração alvo `frac` de patches.
255
- pub fn mascara_blocos_2d(h: usize, w: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
256
- let total = h * w;
257
- let alvo = (total as f64 * frac) as usize;
258
- let mut masc = vec![false; total];
259
- let mut coberto = 0usize;
260
- for _ in 0..n_blocos.max(1) {
261
- if coberto >= alvo {
262
- break;
263
- }
264
- let bh = 1 + rng.inteiro(h.min(4));
265
- let bw = 1 + rng.inteiro(w.min(4));
266
- let y0 = rng.inteiro(h + 1 - bh);
267
- let x0 = rng.inteiro(w + 1 - bw);
268
- for y in y0..y0 + bh {
269
- for x in x0..x0 + bw {
270
- let i = y * w + x;
271
- if !masc[i] {
272
- masc[i] = true;
273
- coberto += 1;
274
- }
275
- }
276
- }
277
- }
278
- masc
279
- }
280
-
281
- /// Máscara de tubos 3D (V-JEPA 2): grade (t, h, w); tubos = retângulos que
282
- /// atravessam o tempo.
283
- pub fn mascara_tubos_3d(t: usize, h: usize, w: usize, n_tubos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
284
- let total = t * h * w;
285
- let alvo = (total as f64 * frac) as usize;
286
- let mut masc = vec![false; total];
287
- let mut coberto = 0usize;
288
- for _ in 0..n_tubos.max(1) {
289
- if coberto >= alvo {
290
- break;
291
- }
292
- let bh = 1 + rng.inteiro(h.min(3));
293
- let bw = 1 + rng.inteiro(w.min(3));
294
- let y0 = rng.inteiro(h + 1 - bh);
295
- let x0 = rng.inteiro(w + 1 - bw);
296
- for tt in 0..t {
297
- for y in y0..y0 + bh {
298
- for x in x0..x0 + bw {
299
- let i = (tt * h + y) * w + x;
300
- if !masc[i] {
301
- masc[i] = true;
302
- coberto += 1;
303
- }
304
- }
305
- }
306
- }
307
- }
308
- masc
309
- }
310
-
311
- /// Máscara de bandas (Audio-JEPA): grade (f, tau) — bandas de frequência
312
- /// horizontais com extensão temporal.
313
- pub fn mascara_bandas(f: usize, tau: usize, n_bandas: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
314
- mascara_blocos_2d(f, tau, n_bandas, frac, rng) // blocos na grade (f, tau)
315
- }
316
-
317
- /// Máscara de blocos 3D (3D-JEPA): grade (x, y, z) de voxels.
318
- pub fn mascara_blocos_3d(x: usize, y: usize, z: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
319
- mascara_tubos_3d(x, y, z, n_blocos, frac, rng)
320
- }
321
-
322
- /// Máscara de segmentos canal×tempo (Brain-JEPA): canais inteiros OU
323
- /// janelas temporais (aleatório por chamada).
324
- pub fn mascara_segmentos(c: usize, t: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
325
- let total = c * t;
326
- let alvo = (total as f64 * frac) as usize;
327
- let mut masc = vec![false; total];
328
- if rng.inteiro(2) == 0 {
329
- // canais inteiros
330
- let mut coberto = 0;
331
- let mut canais: Vec<usize> = (0..c).collect();
332
- // embaralhar
333
- for i in (1..canais.len()).rev() {
334
- let j = rng.inteiro(i + 1);
335
- canais.swap(i, j);
336
- }
337
- for &ch in canais.iter() {
338
- if coberto >= alvo {
339
- break;
340
- }
341
- for tt in 0..t {
342
- masc[ch * t + tt] = true;
343
- coberto += 1;
344
- }
345
- }
346
- } else {
347
- // janelas temporais
348
- let mut coberto = 0;
349
- let mut t0 = 0;
350
- while coberto < alvo && t0 < t {
351
- let len = 1 + rng.inteiro((t / 4).max(1));
352
- for ch in 0..c {
353
- for tt in t0..(t0 + len).min(t) {
354
- masc[ch * t + tt] = true;
355
- coberto += 1;
356
- }
357
- }
358
- t0 += len + 1;
359
- }
360
- }
361
- masc
362
- }
363
-
364
- /// Máscara 1D (T-JEPA): intervalos contíguos em n posições.
365
- pub fn mascara_intervalos_1d(n: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
366
- let alvo = (n as f64 * frac) as usize;
367
- let mut masc = vec![false; n];
368
- let mut coberto = 0;
369
- while coberto < alvo {
370
- let len = 1 + rng.inteiro((n / 8).max(1));
371
- let i0 = rng.inteiro(n);
372
- for i in i0..(i0 + len).min(n) {
373
- if !masc[i] {
374
- masc[i] = true;
375
- coberto += 1;
376
- }
377
- }
378
- }
379
- masc
380
- }
381
-
382
- /// Amostragem dos pontos mais distantes (FPS) para Point-JEPA.
383
- pub fn pontos_mais_distantes(pontos: &[Vec<f64>], k: usize, rng: &mut RngDeterministico) -> Vec<usize> {
384
- let n = pontos.len();
385
- if n == 0 || k == 0 {
386
- return vec![];
387
- }
388
- let k = k.min(n);
389
- let mut centros = vec![rng.inteiro(n)];
390
- let mut dist_min: Vec<f64> = pontos
391
- .iter()
392
- .map(|p| distancia_eucl(p, &pontos[centros[0]]))
393
- .collect();
394
- while centros.len() < k {
395
- let prox = dist_min
396
- .iter()
397
- .enumerate()
398
- .filter(|(i, _)| !centros.contains(i))
399
- .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
400
- .map(|(i, _)| i)
401
- .unwrap_or(0);
402
- centros.push(prox);
403
- for (i, p) in pontos.iter().enumerate() {
404
- let d = distancia_eucl(p, &pontos[prox]);
405
- if d < dist_min[i] {
406
- dist_min[i] = d;
407
- }
408
- }
409
- }
410
- centros
411
- }
412
-
413
- fn distancia_eucl(a: &[f64], b: &[f64]) -> f64 {
414
- a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f64>().sqrt()
415
- }
416
-
417
- // ---------------------------------------------------------------------------
418
- // Parte 3 — adaptadores de modalidade
419
- // ---------------------------------------------------------------------------
420
-
421
- /// (C, H, W) → patches (H/ph · W/pw, C·ph·pw) — Image-JEPA.
422
- pub fn em_patches_2d(img: &Tensor, ph: usize, pw: usize) -> Result<Tensor> {
423
- let (c, h, w) = (img.dim(0)?, img.dim(1)?, img.dim(2)?);
424
- let dev = img.device();
425
- let mut linhas: Vec<Tensor> = Vec::new();
426
- for y in (0..h).step_by(ph) {
427
- for x in (0..w).step_by(pw) {
428
- let hh = ph.min(h - y);
429
- let ww = pw.min(w - x);
430
- let patch = img.narrow(0, 0, c)?.narrow(1, y, hh)?.narrow(2, x, ww)?;
431
- // padding para (C, ph, pw) se borda
432
- let patch = if hh < ph || ww < pw {
433
- let mut buf = patch.flatten_all()?.to_vec1::<f32>()?;
434
- let falta = c * ph * pw - buf.len();
435
- buf.extend(std::iter::repeat(0f32).take(falta));
436
- Tensor::from_vec(buf, (c * ph * pw,), &dev)?
437
- } else {
438
- patch.flatten_all()?
439
- };
440
- linhas.push(patch);
441
- }
442
- }
443
- Ok(Tensor::stack(&linhas, 0)?)
444
- }
445
-
446
- /// (T, H, W) → tubos (T/pt · H/ph · W/pw, pt·ph·pw) — V-JEPA 2.
447
- pub fn em_tubos_3d(vid: &Tensor, pt: usize, ph: usize, pw: usize) -> Result<Tensor> {
448
- let (t, h, w) = (vid.dim(0)?, vid.dim(1)?, vid.dim(2)?);
449
- let dev = vid.device();
450
- let mut linhas: Vec<Tensor> = Vec::new();
451
- for tt in (0..t).step_by(pt) {
452
- let tl = pt.min(t - tt);
453
- for y in (0..h).step_by(ph) {
454
- for x in (0..w).step_by(pw) {
455
- let hh = ph.min(h - y);
456
- let ww = pw.min(w - x);
457
- let tubo = vid
458
- .narrow(0, tt, tl)?
459
- .narrow(1, y, hh)?
460
- .narrow(2, x, ww)?
461
- .flatten_all()?;
462
- let mut buf = tubo.to_vec1::<f32>()?;
463
- let falta = pt * ph * pw - buf.len();
464
- buf.extend(std::iter::repeat(0f32).take(falta));
465
- linhas.push(Tensor::from_vec(buf, (pt * ph * pw,), &dev)?);
466
- }
467
- }
468
- }
469
- Ok(Tensor::stack(&linhas, 0)?)
470
- }
471
-
472
- /// (F, τ) → patches (F/pf · τ/pt, pf·pt) — Audio-JEPA.
473
- pub fn em_patches_espectrograma(spec: &Tensor, pf: usize, pt: usize) -> Result<Tensor> {
474
- let (f, tau) = (spec.dim(0)?, spec.dim(1)?);
475
- let dev = spec.device();
476
- let mut linhas: Vec<Tensor> = Vec::new();
477
- for i in (0..f).step_by(pf) {
478
- for j in (0..tau).step_by(pt) {
479
- let ff = pf.min(f - i);
480
- let tt = pt.min(tau - j);
481
- let p = spec.narrow(0, i, ff)?.narrow(1, j, tt)?.flatten_all()?;
482
- let mut buf = p.to_vec1::<f32>()?;
483
- let falta = pf * pt - buf.len();
484
- buf.extend(std::iter::repeat(0f32).take(falta));
485
- linhas.push(Tensor::from_vec(buf, (pf * pt,), &dev)?);
486
- }
487
- }
488
- Ok(Tensor::stack(&linhas, 0)?)
489
- }
490
-
491
- /// (C, T) → patches canal-independentes (C · T/pt, pt) — T-JEPA/Brain-JEPA.
492
- pub fn em_patches_serie(ts: &Tensor, pt: usize) -> Result<Tensor> {
493
- let (c, t) = (ts.dim(0)?, ts.dim(1)?);
494
- let dev = ts.device();
495
- let mut linhas: Vec<Tensor> = Vec::new();
496
- for ch in 0..c {
497
- for j in (0..t).step_by(pt) {
498
- let tt = pt.min(t - j);
499
- let p = ts.narrow(0, ch, 1)?.narrow(1, j, tt)?.flatten_all()?;
500
- let mut buf = p.to_vec1::<f32>()?;
501
- let falta = pt - buf.len();
502
- buf.extend(std::iter::repeat(0f32).take(falta));
503
- linhas.push(Tensor::from_vec(buf, (pt,), &dev)?);
504
- }
505
- }
506
- Ok(Tensor::stack(&linhas, 0)?)
507
- }
508
-
509
- /// (X, Y, Z) → voxels (X/px · Y/py · Z/pz, px·py·pz) — 3D-JEPA.
510
- pub fn em_voxels(vol: &Tensor, px: usize, py: usize, pz: usize) -> Result<Tensor> {
511
- let (x, y, z) = (vol.dim(0)?, vol.dim(1)?, vol.dim(2)?);
512
- let dev = vol.device();
513
- let mut linhas: Vec<Tensor> = Vec::new();
514
- for i in (0..x).step_by(px) {
515
- for j in (0..y).step_by(py) {
516
- for k in (0..z).step_by(pz) {
517
- let lx = px.min(x - i);
518
- let ly = py.min(y - j);
519
- let lz = pz.min(z - k);
520
- let p = vol.narrow(0, i, lx)?.narrow(1, j, ly)?.narrow(2, k, lz)?.flatten_all()?;
521
- let mut buf = p.to_vec1::<f32>()?;
522
- let falta = px * py * pz - buf.len();
523
- buf.extend(std::iter::repeat(0f32).take(falta));
524
- linhas.push(Tensor::from_vec(buf, (px * py * pz,), &dev)?);
525
- }
526
- }
527
- }
528
- Ok(Tensor::stack(&linhas, 0)?)
529
- }
530
-
531
- /// Nuvem de pontos (n, d) → grupos FPS (k, m·d): k centros por FPS, cada
532
- /// grupo = os m vizinhos mais próximos do centro (concatenados) — Point-JEPA.
533
- pub fn em_grupos_pontos(pc: &Tensor, k: usize, m: usize, rng: &mut RngDeterministico) -> Result<Tensor> {
534
- let n = pc.dim(0)?;
535
- let d = pc.dim(1)?;
536
- let coords = pc.to_vec2::<f32>()?;
537
- let coords_f: Vec<Vec<f64>> = coords.iter().map(|r| r.iter().map(|&v| v as f64).collect()).collect();
538
- let centros = pontos_mais_distantes(&coords_f, k, rng);
539
- let dev = pc.device();
540
- let mut linhas: Vec<Tensor> = Vec::new();
541
- for &ci in &centros {
542
- // m vizinhos mais próximos do centro ci (incluindo ele próprio)
543
- let mut ord: Vec<usize> = (0..n).collect();
544
- ord.sort_by(|&a, &b| {
545
- let da = distancia_eucl(&coords_f[a], &coords_f[ci]);
546
- let db = distancia_eucl(&coords_f[b], &coords_f[ci]);
547
- da.partial_cmp(&db).unwrap()
548
- });
549
- let mut buf: Vec<f32> = Vec::with_capacity(m * d);
550
- for &idx in ord.iter().take(m) {
551
- for j in 0..d {
552
- buf.push(coords[idx][j]);
553
- }
554
- }
555
- let falta = m * d - buf.len();
556
- buf.extend(std::iter::repeat(0f32).take(falta));
557
- linhas.push(Tensor::from_vec(buf, (m * d,), &dev)?);
558
- }
559
- Ok(Tensor::stack(&linhas, 0)?)
560
- }
561
 
562
  // ---------------------------------------------------------------------------
563
  // Parte 4 — variantes e treino unificado
@@ -770,257 +215,6 @@ fn std_por_dim(z: &Tensor) -> Result<f64> {
770
  Ok(var.sqrt()?.mean_all()?.to_scalar::<f32>()? as f64)
771
  }
772
 
773
- // ---------------------------------------------------------------------------
774
- // Parte 5 — SonicMoE (IO/tile-aware)
775
- // ---------------------------------------------------------------------------
776
-
777
- /// MoE top-k com capacidade por especialista e perda de balanceamento.
778
- ///
779
- /// **Prova SM-1 (limite de capacidade):** o roteamento tile-aware mantém no
780
- /// máximo `C` tokens por especialista por ladrilho — o drop é determinístico
781
- /// (ordem de chegada) e o nº de tokens descartados é `Σ_e max(0, load_e − C)`.
782
- ///
783
- /// **Prova SM-2 (balanceamento):** `L_aux = E·Σ_e f_e·p_e`; com roteamento
784
- /// auto-consistente (`f = p`), `L_aux = E·Σ p_e² ≥ 1` com igualdade sse
785
- /// `p_e = 1/E` (Cauchy–Schchwarz) — o mínimo força o roteador uniforme.
786
- pub struct SonicMoe {
787
- pub roteador: Linear, // d → E
788
- pub especialistas: Vec<MlpJepa>, // E experts d → d
789
- pub n_top: usize,
790
- pub capacidade: usize,
791
- pub lambda_aux: f64,
792
- }
793
-
794
- pub struct SaidaMoe {
795
- pub saida: Tensor,
796
- pub perda_aux: Tensor,
797
- pub carga: Vec<usize>, // tokens efetivamente processados por especialista
798
- pub descartados: usize, // tokens com especialistas cortados pela capacidade
799
- }
800
-
801
- impl SonicMoe {
802
- pub fn novo(d: usize, n_expertos: usize, n_top: usize, capacidade: usize, dev: &Device) -> Result<Self> {
803
- let roteador = Linear::sem_vies(d, n_expertos, 0.1, dev)?;
804
- let especialistas = (0..n_expertos)
805
- .map(|_| MlpJepa::novo(d, 2 * d, d, dev))
806
- .collect::<Result<Vec<_>>>()?;
807
- Ok(Self { roteador, especialistas, n_top, capacidade, lambda_aux: 0.01 })
808
- }
809
-
810
- pub fn vars(&self) -> Vec<Var> {
811
- let mut v = self.roteador.vars();
812
- for e in &self.especialistas {
813
- v.extend(e.vars());
814
- }
815
- v
816
- }
817
-
818
- /// Forward tile-aware: (n, d) → (n, d).
819
- pub fn fwd(&self, x: &Tensor) -> Result<SaidaMoe> {
820
- let n = x.dim(0)?;
821
- let d = x.dim(1)?;
822
- let e = self.especialistas.len();
823
- let dev = x.device();
824
- let logits = self.roteador.fwd(x)?; // (n, E)
825
- let probs = candle_nn::ops::softmax(&logits, candle_core::D::Minus1)?;
826
-
827
- // top-k por token (não diferenciável — pesos do top-k são dif. via probs)
828
- let probs_v = probs.to_vec2::<f32>()?;
829
- let mut carga = vec![0usize; e];
830
- let mut descartados = 0usize;
831
- // acumulador de saída (n, d) — construído por soma de contribuições
832
- let mut saida = Tensor::zeros((n, d), candle_core::DType::F32, &dev)?;
833
- let mut peso_total = Tensor::zeros((n, 1), candle_core::DType::F32, &dev)?;
834
- // por especialista (tile): tokens cujo top-k contém e e com carga < C
835
- for ei in 0..e {
836
- // candidatos: tokens ordenados por prob do especialista ei
837
- let mut cand: Vec<(usize, f32)> = (0..n)
838
- .map(|i| (i, probs_v[i][ei]))
839
- .collect();
840
- cand.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
841
- let mut escolhidos: Vec<usize> = Vec::new();
842
- for (i, _) in cand {
843
- if carga[ei] >= self.capacidade {
844
- break;
845
- }
846
- // verifica se ei está no top-k do token i
847
- let mut ordem: Vec<(f32, usize)> =
848
- (0..e).map(|k| (probs_v[i][k], k)).collect();
849
- ordem.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
850
- let top: Vec<usize> = ordem.iter().rev().take(self.n_top).map(|(_, k)| *k).collect();
851
- if top.contains(&ei) {
852
- escolhidos.push(i);
853
- carga[ei] += 1;
854
- }
855
- }
856
- if escolhidos.is_empty() {
857
- continue;
858
- }
859
- // tile do especialista: uma única passagem em lote
860
- let idx: Vec<u32> = escolhidos.iter().map(|&i| i as u32).collect();
861
- let n_e = idx.len();
862
- let idx_t = Tensor::from_vec(idx, (n_e,), &dev)?;
863
- let tile = x.index_select(&idx_t, 0)?;
864
- let y = self.especialistas[ei].fwd(&tile)?; // (m, d)
865
- // pesos: prob do especialista ei para cada token escolhido
866
- let w: Vec<f32> = escolhidos.iter().map(|&i| probs_v[i][ei]).collect();
867
- let n_w = escolhidos.len();
868
- let w_t = Tensor::from_vec(w.clone(), (n_w, 1), &dev)?;
869
- let contrib = y.broadcast_mul(&w_t)?;
870
- // scatter-add: acumula nas posições
871
- let mut buf = saida.to_vec2::<f32>()?;
872
- let contrib_v = contrib.to_vec2::<f32>()?;
873
- for (k, &i) in escolhidos.iter().enumerate() {
874
- for j in 0..d {
875
- buf[i][j] += contrib_v[k][j];
876
- }
877
- }
878
- saida = Tensor::from_vec(
879
- buf.into_iter().flatten().collect::<Vec<f32>>(),
880
- (n, d),
881
- &dev,
882
- )?;
883
- let mut pt = peso_total.to_vec2::<f32>()?;
884
- for (k, &i) in escolhidos.iter().enumerate() {
885
- pt[i][0] += w[k];
886
- }
887
- peso_total = Tensor::from_vec(
888
- pt.into_iter().flatten().collect::<Vec<f32>>(),
889
- (n, 1),
890
- &dev,
891
- )?;
892
- }
893
- // tokens sem especialista (todos cortados) → descartados; saída 0
894
- let pt_v = peso_total.to_vec2::<f32>()?;
895
- for i in 0..n {
896
- if pt_v[i][0] == 0.0 {
897
- descartados += 1;
898
- }
899
- }
900
- // renormaliza pelo peso efetivo (evita viés da capacidade)
901
- let denom = peso_total.clamp(1e-6, f64::INFINITY)?;
902
- let saida = saida.broadcast_div(&denom)?;
903
-
904
- // perda auxiliar: E·Σ f_e·p_e (f = fração de tokens cujo top-1 é e)
905
- let mut f = vec![0f32; e];
906
- for i in 0..n {
907
- let (mut melhor, mut mp) = (0usize, f32::NEG_INFINITY);
908
- for k in 0..e {
909
- if probs_v[i][k] > mp {
910
- mp = probs_v[i][k];
911
- melhor = k;
912
- }
913
- }
914
- f[melhor] += 1.0 / n as f32;
915
- }
916
- let f_t = Tensor::from_vec(f, (e,), &dev)?;
917
- let p_media = probs.mean(0)?; // (E,)
918
- let perda_aux = ((&f_t * &p_media)?.sum_all()? * (e as f64))?;
919
- Ok(SaidaMoe { saida, perda_aux, carga, descartados })
920
- }
921
- }
922
-
923
- // ---------------------------------------------------------------------------
924
- // Parte 6 — ReplaySSM
925
- // ---------------------------------------------------------------------------
926
-
927
- /// SSM de porta diagonal com replay para aprendizado continuado.
928
- ///
929
- /// Recorrência: `h_t = σ(a) ⊙ h_{t−1} + (1 − σ(a)) ⊙ (B x_t)`.
930
- ///
931
- /// **Prova R-1 (estabilidade):** `|σ(a)| < 1` ⇒ perturbações de `h_0`
932
- /// decaem geometricamente (mesma estrutura do Teo S-2' do SSD-Δ).
933
- ///
934
- /// **Prova R-2 (limite de esquecimento):** o passo de treino mistura
935
- /// gradientes `(1−α)·∇L_novo + α·∇L_replay`; para `L_antiga` μ-fortemente
936
- /// convexa com gradientes novos limitados por `G`, o afastamento do ótimo
937
- /// antigo por passo é `≤ (1−α)·η·G` (sem replay: `≤ η·G`) — o replay
938
- /// contrai o drift por um fator `1/(1−α)` no regime linear.
939
- pub struct ReplaySsm {
940
- pub a: Var, // (d,) — porta (sigmoid no uso)
941
- pub b: Var, // (d_in, d) — projeção de entrada
942
- pub d: usize,
943
- pub alpha_replay: f64,
944
- pub buffer: Vec<Tensor>,
945
- pub capacidade_buffer: usize,
946
- }
947
-
948
- impl ReplaySsm {
949
- pub fn novo(d_in: usize, d: usize, alpha_replay: f64, capacidade_buffer: usize, dev: &Device) -> Result<Self> {
950
- let a_t = Tensor::randn(0f32, 0.5, (d,), dev)?;
951
- let b_t = Tensor::randn(0f32, 0.1, (d_in, d), dev)?;
952
- Ok(Self {
953
- a: Var::from_tensor(&a_t)?,
954
- b: Var::from_tensor(&b_t)?,
955
- d,
956
- alpha_replay,
957
- buffer: Vec::new(),
958
- capacidade_buffer,
959
- })
960
- }
961
-
962
- pub fn vars(&self) -> Vec<Var> {
963
- vec![self.a.clone(), self.b.clone()]
964
- }
965
-
966
- /// Scan: x (T, d_in) → h (T, d). Diferenciável (candle autograd).
967
- pub fn scan(&self, x: &Tensor) -> Result<Tensor> {
968
- let t = x.dim(0)?;
969
- let dev = x.device();
970
- let porta = candle_nn::ops::sigmoid(&self.a.as_tensor().broadcast_as((1, self.d))?)?; // (1, d)
971
- let um_menos = porta.affine(-1.0, 1.0)?; // 1 − σ(a)
972
- let entrada = x.matmul(&self.b.as_tensor())?; // (T, d)
973
- let mut h = Tensor::zeros((1, self.d), candle_core::DType::F32, &dev)?;
974
- let mut saidas: Vec<Tensor> = Vec::with_capacity(t);
975
- for i in 0..t {
976
- let xt = entrada.narrow(0, i, 1)?;
977
- h = ((&porta * &h)? + (&um_menos * &xt)?)?;
978
- saidas.push(h.clone());
979
- }
980
- Ok(Tensor::stack(&saidas, 0)?)
981
- }
982
-
983
- /// Empurra uma janela de treino no buffer (FIFO).
984
- pub fn guardar(&mut self, x: &Tensor) {
985
- if self.buffer.len() >= self.capacidade_buffer {
986
- self.buffer.remove(0);
987
- }
988
- self.buffer.push(x.detach());
989
- }
990
-
991
- /// Um passo de treino no lote novo (com mistura de replay).
992
- /// Devolve (perda_total, perda_novo, perda_replay).
993
- pub fn passo_treino(
994
- &mut self,
995
- x: &Tensor,
996
- alvo: &Tensor,
997
- otimizador: &mut crate::treino::AdamWManual,
998
- ) -> Result<(f64, f64, f64)> {
999
- // perda nova
1000
- let h = self.scan(x)?;
1001
- let dif = (&h - alvo)?;
1002
- let perda_novo = dif.sqr()?.mean_all()?;
1003
- // replay
1004
- let mut perda_total = perda_novo.clone();
1005
- let mut p_r = 0.0;
1006
- if !self.buffer.is_empty() {
1007
- let idx = self.buffer.len() - 1;
1008
- let antigo = self.buffer[idx].clone();
1009
- let hr = self.scan(&antigo)?;
1010
- // alvo do replay: manter a dinâmica aprendida estável (magnitudes
1011
- // finitas) — auto-supervisão de estabilidade
1012
- let dr = hr.sqr()?.mean_all()?;
1013
- perda_total = (perda_total.affine(1.0 - self.alpha_replay, 0.0)?
1014
- + dr.affine(self.alpha_replay, 0.0)?)?;
1015
- p_r = dr.to_scalar::<f32>()? as f64;
1016
- }
1017
- let pn = perda_novo.to_scalar::<f32>()? as f64;
1018
- otimizador.passo(crate::treino::grads_achatados(&perda_total, &self.vars())?);
1019
- self.guardar(x);
1020
- let pt = perda_total.to_scalar::<f32>()? as f64;
1021
- Ok((pt, pn, p_r))
1022
- }
1023
- }
1024
 
1025
  // ---------------------------------------------------------------------------
1026
  // Variantes especiais: LeJEPA (edição-condicionado), MC-JEPA (cruz-modal),
@@ -1280,210 +474,3 @@ pub fn mascara_por_variante(
1280
  pub fn versao() -> &'static str {
1281
  "jepa-0.5.0"
1282
  }
1283
-
1284
- #[cfg(test)]
1285
- mod testes {
1286
- use super::*;
1287
-
1288
- fn tensor_2d(dados: &[Vec<f64>], dev: &Device) -> Tensor {
1289
- let flat: Vec<f32> = dados.iter().flat_map(|v| v.iter().map(|&x| x as f32)).collect();
1290
- Tensor::from_vec(flat, (dados.len(), dados[0].len()), dev).unwrap()
1291
- }
1292
-
1293
- #[test]
1294
- fn teste_mascaras_validas() {
1295
- // fração respeitada ±, particionamento contexto/alvo não-vazio
1296
- let mut rng = RngDeterministico::novo(1);
1297
- let m2 = mascara_blocos_2d(8, 8, 4, 0.3, &mut rng);
1298
- assert_eq!(m2.len(), 64);
1299
- let frac = m2.iter().filter(|&&b| b).count() as f64 / 64.0;
1300
- assert!(frac >= 0.15 && frac <= 0.55, "frac 2d = {frac}");
1301
- assert!(m2.iter().any(|&b| !b), "contexto vazio");
1302
-
1303
- let m3 = mascara_tubos_3d(4, 6, 6, 3, 0.3, &mut rng);
1304
- assert_eq!(m3.len(), 144);
1305
- let f3 = m3.iter().filter(|&&b| b).count() as f64 / 144.0;
1306
- assert!(f3 >= 0.15 && f3 <= 0.6, "frac 3d = {f3}");
1307
-
1308
- let ms = mascara_segmentos(4, 32, 0.25, &mut rng);
1309
- assert_eq!(ms.len(), 128);
1310
- assert!(ms.iter().any(|&b| b) && ms.iter().any(|&b| !b));
1311
-
1312
- let m1 = mascara_intervalos_1d(64, 0.3, &mut rng);
1313
- let f1 = m1.iter().filter(|&&b| b).count() as f64 / 64.0;
1314
- assert!(f1 >= 0.25 && f1 <= 0.45, "frac 1d = {f1}");
1315
- }
1316
-
1317
- #[test]
1318
- fn teste_fps_cobre() {
1319
- let mut rng = RngDeterministico::novo(2);
1320
- let pontos: Vec<Vec<f64>> = (0..40)
1321
- .map(|i| {
1322
- let a = i as f64 * 0.1;
1323
- vec![a.cos() * 3.0, a.sin() * 3.0, (i % 7) as f64 * 0.3]
1324
- })
1325
- .collect();
1326
- let centros = pontos_mais_distantes(&pontos, 6, &mut rng);
1327
- assert_eq!(centros.len(), 6);
1328
- // centros distintos
1329
- let mut s = centros.clone();
1330
- s.sort();
1331
- s.dedup();
1332
- assert_eq!(s.len(), 6);
1333
- }
1334
-
1335
- #[test]
1336
- fn teste_j1_colapso_impossivel() {
1337
- // No colapso total (z constante), a perda VC = γ² > 0 — verificando a
1338
- // fórmula implementada com um lote constante.
1339
- let dev = Device::Cpu;
1340
- let nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.99, 1.0, 1.0, &dev).unwrap();
1341
- let z = Tensor::from_vec(vec![0.5f32; 8 * 8], (8, 8), &dev).unwrap();
1342
- let lvc = nucleo.perda_vc(&z).unwrap();
1343
- let v = lvc.to_scalar::<f32>().unwrap() as f64;
1344
- // std=0 ⇒ dobradiça = γ = 1 ⇒ V/d = 1; cov = 0
1345
- assert!(v > 0.9, "perda VC no colapso = {v} (deveria ser ~1)");
1346
- }
1347
-
1348
- #[test]
1349
- fn teste_j2_ema_ponto_fixo() {
1350
- // encoder congelado ⇒ após muitas passadas EMA, alvo = encoder
1351
- let dev = Device::Cpu;
1352
- let mut nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.5, 0.1, 1.0, &dev).unwrap();
1353
- for _ in 0..40 {
1354
- nucleo.ema_passo().unwrap();
1355
- }
1356
- let a = nucleo.alvo.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
1357
- let b = nucleo.encoder.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
1358
- let dif: f32 = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max);
1359
- assert!(dif < 1e-5, "EMA não convergiu: dif máx = {dif}");
1360
- }
1361
-
1362
- #[test]
1363
- fn teste_jepa_aprende_latente_previsivel() {
1364
- // J-3: com alvo previsível (função determinística do contexto), a
1365
- // perda pred must cair substancialmente e o std das incorporações
1366
- // NÃO colapsa (J-1).
1367
- let dev = Device::Cpu;
1368
- let mut rng = RngDeterministico::novo(3);
1369
- // patches = 64 vetores 8-D em 4 grupos (estrutura previsível)
1370
- let mut dados: Vec<Vec<f64>> = Vec::new();
1371
- for g in 0..4 {
1372
- let c = g as f64 * 2.0;
1373
- for _ in 0..16 {
1374
- dados.push((0..8).map(|k| c + rng.normal(0.0, 0.3) + (k as f64) * 0.05).collect());
1375
- }
1376
- }
1377
- let patches = tensor_2d(&dados, &dev);
1378
- let cfg = CfgJepa { d_emb: 16, epocas: 40, lr: 5e-3, ..Default::default() };
1379
- let rel = treinar_variante(
1380
- &patches,
1381
- &|n, r| mascara_intervalos_1d(n, 0.3, r),
1382
- &cfg,
1383
- VarianteJepa::TJepa,
1384
- None,
1385
- &dev,
1386
- )
1387
- .unwrap();
1388
- assert!(
1389
- rel.perda_final < rel.perda_inicial * 0.8,
1390
- "perda deveria cair: {} → {}",
1391
- rel.perda_inicial,
1392
- rel.perda_final
1393
- );
1394
- assert!(
1395
- rel.std_emb_final > 0.05,
1396
- "sem colapso: std_final = {}",
1397
- rel.std_emb_final
1398
- );
1399
- }
1400
-
1401
- #[test]
1402
- fn teste_adaptadores_formas() {
1403
- let dev = Device::Cpu;
1404
- let img = Tensor::randn(0f32, 1.0, (3, 16, 16), &dev).unwrap();
1405
- let p = em_patches_2d(&img, 4, 4).unwrap();
1406
- assert_eq!(p.dims().to_vec(), vec![16, 48]);
1407
-
1408
- let vid = Tensor::randn(0f32, 1.0, (4, 8, 8), &dev).unwrap();
1409
- let t3 = em_tubos_3d(&vid, 2, 4, 4).unwrap();
1410
- assert_eq!(t3.dims().to_vec(), vec![8, 32]);
1411
-
1412
- let spec = Tensor::randn(0f32, 1.0, (16, 32), &dev).unwrap();
1413
- let ps = em_patches_espectrograma(&spec, 4, 8).unwrap();
1414
- assert_eq!(ps.dims().to_vec(), vec![16, 32]);
1415
-
1416
- let ts = Tensor::randn(0f32, 1.0, (4, 32), &dev).unwrap();
1417
- let pt_ = em_patches_serie(&ts, 8).unwrap();
1418
- assert_eq!(pt_.dims().to_vec(), vec![16, 8]);
1419
-
1420
- let vol = Tensor::randn(0f32, 1.0, (8, 8, 8), &dev).unwrap();
1421
- let vx = em_voxels(&vol, 4, 4, 4).unwrap();
1422
- assert_eq!(vx.dims().to_vec(), vec![8, 64]);
1423
-
1424
- let pc = Tensor::randn(0f32, 1.0, (50, 3), &dev).unwrap();
1425
- let mut rng = RngDeterministico::novo(5);
1426
- let gr = em_grupos_pontos(&pc, 6, 4, &mut rng).unwrap();
1427
- assert_eq!(gr.dims().to_vec(), vec![6, 12]);
1428
- }
1429
-
1430
- #[test]
1431
- fn teste_sonic_moe_capacidade_e_balanceamento() {
1432
- // SM-1: carga por especialista ≤ capacidade; SM-2: aux ≥ ~1 quando
1433
- // roteador é ~uniforme (início).
1434
- let dev = Device::Cpu;
1435
- let moe = SonicMoe::novo(8, 4, 2, 6, &dev).unwrap();
1436
- let x = Tensor::randn(0f32, 1.0, (20, 8), &dev).unwrap();
1437
- let s = moe.fwd(&x).unwrap();
1438
- for (e, c) in s.carga.iter().enumerate() {
1439
- assert!(*c <= 6, "especialista {e} com carga {c} > capacidade 6");
1440
- }
1441
- assert_eq!(s.saida.dims().to_vec(), vec![20, 8]);
1442
- let aux = s.perda_aux.to_scalar::<f32>().unwrap() as f64;
1443
- // com roteador quase uniforme, aux ≈ E·Σ (1/E)·(1/E) = 1
1444
- assert!(aux > 0.5 && aux < 3.0, "aux = {aux}");
1445
- // cada token processado por ≤ n_top especialistas
1446
- let total_carga: usize = s.carga.iter().sum();
1447
- assert!(total_carga <= 20 * 2, "carga total {total_carga} > n·topk");
1448
- }
1449
-
1450
- #[test]
1451
- fn teste_replay_ssm_estabilidade_e_replay() {
1452
- // R-1: estados finitos e limitados; R-2: com replay α>0, após treinar
1453
- // a tarefa nova, a dinâmica antiga (magnitude do estado) permanece
1454
- // estável (não diverge).
1455
- let dev = Device::Cpu;
1456
- let mut ssm = ReplaySsm::novo(4, 6, 0.5, 8, &dev).unwrap();
1457
- let vars = ssm.vars();
1458
- let mut otim = crate::treino::AdamWManual::novo(vars, 1e-2, 0.0);
1459
-
1460
- // tarefa "antiga": sinal constante 1.0
1461
- let x_antigo = Tensor::from_vec(vec![1.0f32; 16 * 4], (16, 4), &dev).unwrap();
1462
- for _ in 0..30 {
1463
- let h = ssm.scan(&x_antigo).unwrap();
1464
- let alvo = h.detach();
1465
- ssm.passo_treino(&x_antigo, &alvo, &mut otim).unwrap();
1466
- }
1467
- // estado após treino antigo: finito
1468
- let h1 = ssm.scan(&x_antigo).unwrap();
1469
- let m1 = h1.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
1470
- assert!(m1.is_finite() && m1 < 50.0, "estado explodiu: {m1}");
1471
-
1472
- // tarefa "nova": sinal −1.0 — treino com replay
1473
- let x_novo = Tensor::from_vec(vec![-1.0f32; 16 * 4], (16, 4), &dev).unwrap();
1474
- for _ in 0..30 {
1475
- let h = ssm.scan(&x_novo).unwrap();
1476
- let alvo = h.detach();
1477
- ssm.passo_treino(&x_novo, &alvo, &mut otim).unwrap();
1478
- }
1479
- // a dinâmica antiga continua estável (sem explosão)
1480
- let h2 = ssm.scan(&x_antigo).unwrap();
1481
- let m2 = h2.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
1482
- assert!(m2.is_finite() && m2 < 50.0, "estado antigo explodiu: {m2}");
1483
- // portas dentro de (0,1)
1484
- let a = ssm.a.as_tensor().to_vec1::<f32>().unwrap();
1485
- for v in a {
1486
- assert!(v.is_finite());
1487
- }
1488
- }
1489
- }
 
1
+ //! `jepa::variantes` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ use super::*;
 
 
 
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  // ---------------------------------------------------------------------------
8
  // Parte 4 — variantes e treino unificado
 
215
  Ok(var.sqrt()?.mean_all()?.to_scalar::<f32>()? as f64)
216
  }
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  // ---------------------------------------------------------------------------
220
  // Variantes especiais: LeJEPA (edição-condicionado), MC-JEPA (cruz-modal),
 
474
  pub fn versao() -> &'static str {
475
  "jepa-0.5.0"
476
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib.rs CHANGED
@@ -11,6 +11,7 @@ pub mod arvore_pesos;
11
  pub mod atencao;
12
  pub mod acoplador;
13
  pub mod camadas;
 
14
  pub mod config;
15
  pub mod controlador;
16
  pub mod diagnostico;
 
11
  pub mod atencao;
12
  pub mod acoplador;
13
  pub mod camadas;
14
+ pub mod checkpoint;
15
  pub mod config;
16
  pub mod controlador;
17
  pub mod diagnostico;
src/moe_redirecionavel/atencao.rs ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `moe_redirecionavel::atencao` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Atenção própria (independente e paralela à ADBA do tronco)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ pub struct AtencaoPropria {
12
+ pub q: Linear,
13
+ pub k: Linear,
14
+ pub v: Linear,
15
+ pub o: Linear,
16
+ /// gate escalar — nasce ZERO (M-R2: neutralidade no nascimento).
17
+ pub gate: Var,
18
+ pub n_cabecas: usize,
19
+ }
20
+
21
+ impl AtencaoPropria {
22
+ pub fn novo(d: usize, n_cabecas: usize, dev: &Device) -> Result<Self> {
23
+ let desvio = 1.0 / ((d * 2) as f64).sqrt();
24
+ let mk = || Linear::sem_vies(d, d, desvio, dev);
25
+ let gate = Tensor::zeros((), candle_core::DType::F32, dev)?;
26
+ Ok(AtencaoPropria {
27
+ q: mk()?, k: mk()?, v: mk()?, o: mk()?,
28
+ gate: Var::from_tensor(&gate)?,
29
+ n_cabecas,
30
+ })
31
+ }
32
+
33
+ /// x (n, d) → y = x + g·attn(x) — multi-cabeça compacta (sem máscara:
34
+ /// atenção plena entre os tokens da entrada do MoE).
35
+ pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
36
+ let d = x.dim(candle_core::D::Minus1)?;
37
+ let d_cab = d / self.n_cabecas;
38
+ let n = x.dim(0)?;
39
+ let forma = (self.n_cabecas, n, d_cab);
40
+ let q = self.q.fwd(x)?.reshape(forma)?;
41
+ let k = self.k.fwd(x)?.reshape(forma)?;
42
+ let v = self.v.fwd(x)?.reshape(forma)?;
43
+ let scores = q.matmul(&k.transpose(1, 2)?)?.affine(1.0 / (d_cab as f64).sqrt(), 0.0)?;
44
+ let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1)?;
45
+ let ctx = attn.matmul(&v)?; // (h, n, d_cab)
46
+ let ctx = ctx.reshape((n, d))?;
47
+ let saida = self.o.fwd(&ctx)?;
48
+ // y = x + g·saida (g=0 no nascimento ⇒ y = x — M-R2)
49
+ let g = self.gate.as_tensor();
50
+ let forma: Vec<usize> = saida.dims().to_vec();
51
+ let delta = saida.broadcast_mul(&g.broadcast_as(forma)?)?;
52
+ Ok((x + &delta)?)
53
+ }
54
+
55
+ pub fn vars(&self) -> Vec<Var> {
56
+ let mut v = self.q.vars();
57
+ v.extend(self.k.vars());
58
+ v.extend(self.v.vars());
59
+ v.extend(self.o.vars());
60
+ v.push(self.gate.clone());
61
+ v
62
+ }
63
+ }
src/moe_redirecionavel/mod.rs ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! MoE redirecionável — módulos: atenção própria, núcleo,
2
+ //! treinos JEPA/SOM (divisão SRP v0.10.0 — r-35 §2).
3
+
4
+ //! **MoEs de codificação/decodificação REDIRECIONÁVEIS com Mecanismo de
5
+ //! Atenção próprio e independente (paralelo)** (v0.6.0 — pedido do usuário).
6
+ //!
7
+ //! Cada `MoeRedirecionavel` possui:
8
+ //! - um roteador aprendido `d → E`;
9
+ //! - E especialistas `MlpJepa`, cada um com um PAPEL (Codificador ou
10
+ //! Decodificador) — o conjunto de papéis é REDIRECIONÁVEL em tempo de
11
+ //! execução (permutação), sem reconstrução do modelo;
12
+ //! - uma ATENÇÃO PRÓPRIA independente da atenção ADBA do tronco, com gate
13
+ //! escalar nascido em ZERO (neutralidade no nascimento);
14
+ //! - capacidade por especialista e perda auxiliar de balanceamento.
15
+ //!
16
+ //! **Provas completas em `docs/memorias/r-25-*`; síntese:**
17
+ //! - **M-R1 (invariância sob redirecionamento):** o redirecionamento é uma
18
+ //! permutação π dos papéis; o multiconjunto de atribuições
19
+ //! (especialista, token) numa passada por papel é levado ao multiconjunto
20
+ //! da passada π(papel) — a capacidade total e a perda auxiliar de
21
+ //! balanceamento E·Σ f_e·p_e são invariantes (soma sobre todos os e).
22
+ //! - **M-R2 (neutralidade da atenção própria no nascimento):** com
23
+ //! `g = 0`, `y = x` bit-a-bit; a perturbação é limitada por
24
+ //! `‖Δy‖ ≤ L_soft·|g|·‖W_o·V‖` (Lipschitz do softmax com constante
25
+ //! L_soft = 1 em probabilidade; estrutura idêntica ao T-P5).
26
+ //! - **M-R3 (paralelismo dos dois caminhos):** numa mesma entrada, as
27
+ //! passadas Codificador e Decodificador tocam conjuntos DISJUNTOS de
28
+ //! especialistas e compartilham apenas leitura — o DAG de computação tem
29
+ //! duas sub-DAGs independentes ⇒ tempo paralelo (2 processadores) =
30
+ //! max(T_enc, T_dec) + sobrecarga < T_enc + T_dec = tempo sequencial
31
+ //! (modelo PRAM; medido empiricamente com threads).
32
+ //! - **M-R4 (fechamento da classe de funções):** redirecionar equivale a
33
+ //! permutar colunas do roteador e re-rotular papéis; a classe de funções
34
+ //! implementáveis é fechada sob π ⇒ expressividade inalterada.
35
+ //! - **M-R5 (water-filling por QE — Cauchy–Schwarz):** dadas capacidades
36
+ //! totais C e erros por célula q_i, a alocação `c_i ∝ q_i` minimiza
37
+ //! `Σ q_i²/c_i ≥ (Σq_i)²/C`, cota superior do erro refinado ponderado.
38
+
39
+ use candle_core::{Device, Result, Tensor, Var};
40
+ use crate::camadas::Linear;
41
+ use crate::jepa::MlpJepa;
42
+ use crate::rng::RngDeterministico;
43
+ use crate::treino::AdamWManual;
44
+
45
+ // ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
46
+ #[cfg(test)]
47
+ mod testes;
48
+ #[cfg(test)]
49
+ mod teste_integrado;
50
+ mod atencao;
51
+ mod nucleo;
52
+ mod treino;
53
+
54
+ pub use atencao::*;
55
+ pub use nucleo::*;
56
+ pub use treino::*;
57
+
src/{moe_redirecionavel.rs → moe_redirecionavel/nucleo.rs} RENAMED
@@ -1,101 +1,8 @@
1
- //! **MoEs de codificação/decodificação REDIRECIONÁVEIS com Mecanismo de
2
- //! Atenção próprio e independente (paralelo)** (v0.6.0 — pedido do usuário).
3
- //!
4
- //! Cada `MoeRedirecionavel` possui:
5
- //! - um roteador aprendido `d → E`;
6
- //! - E especialistas `MlpJepa`, cada um com um PAPEL (Codificador ou
7
- //! Decodificador) — o conjunto de papéis é REDIRECIONÁVEL em tempo de
8
- //! execução (permutação), sem reconstrução do modelo;
9
- //! - uma ATENÇÃO PRÓPRIA independente da atenção ADBA do tronco, com gate
10
- //! escalar nascido em ZERO (neutralidade no nascimento);
11
- //! - capacidade por especialista e perda auxiliar de balanceamento.
12
- //!
13
- //! **Provas completas em `docs/memorias/r-25-*`; síntese:**
14
- //! - **M-R1 (invariância sob redirecionamento):** o redirecionamento é uma
15
- //! permutação π dos papéis; o multiconjunto de atribuições
16
- //! (especialista, token) numa passada por papel é levado ao multiconjunto
17
- //! da passada π(papel) — a capacidade total e a perda auxiliar de
18
- //! balanceamento E·Σ f_e·p_e são invariantes (soma sobre todos os e).
19
- //! - **M-R2 (neutralidade da atenção própria no nascimento):** com
20
- //! `g = 0`, `y = x` bit-a-bit; a perturbação é limitada por
21
- //! `‖Δy‖ ≤ L_soft·|g|·‖W_o·V‖` (Lipschitz do softmax com constante
22
- //! L_soft = 1 em probabilidade; estrutura idêntica ao T-P5).
23
- //! - **M-R3 (paralelismo dos dois caminhos):** numa mesma entrada, as
24
- //! passadas Codificador e Decodificador tocam conjuntos DISJUNTOS de
25
- //! especialistas e compartilham apenas leitura — o DAG de computação tem
26
- //! duas sub-DAGs independentes ⇒ tempo paralelo (2 processadores) =
27
- //! max(T_enc, T_dec) + sobrecarga < T_enc + T_dec = tempo sequencial
28
- //! (modelo PRAM; medido empiricamente com threads).
29
- //! - **M-R4 (fechamento da classe de funções):** redirecionar equivale a
30
- //! permutar colunas do roteador e re-rotular papéis; a classe de funções
31
- //! implementáveis é fechada sob π ⇒ expressividade inalterada.
32
- //! - **M-R5 (water-filling por QE — Cauchy–Schwarz):** dadas capacidades
33
- //! totais C e erros por célula q_i, a alocação `c_i ∝ q_i` minimiza
34
- //! `Σ q_i²/c_i ≥ (Σq_i)²/C`, cota superior do erro refinado ponderado.
35
 
36
- use candle_core::{Device, Result, Tensor, Var};
37
- use crate::camadas::Linear;
38
- use crate::jepa::MlpJepa;
39
- use crate::rng::RngDeterministico;
40
- use crate::treino::AdamWManual;
41
 
42
- // ---------------------------------------------------------------------------
43
- // Atenção própria (independente e paralela à ADBA do tronco)
44
- // ---------------------------------------------------------------------------
45
-
46
- pub struct AtencaoPropria {
47
- pub q: Linear,
48
- pub k: Linear,
49
- pub v: Linear,
50
- pub o: Linear,
51
- /// gate escalar — nasce ZERO (M-R2: neutralidade no nascimento).
52
- pub gate: Var,
53
- pub n_cabecas: usize,
54
- }
55
-
56
- impl AtencaoPropria {
57
- pub fn novo(d: usize, n_cabecas: usize, dev: &Device) -> Result<Self> {
58
- let desvio = 1.0 / ((d * 2) as f64).sqrt();
59
- let mk = || Linear::sem_vies(d, d, desvio, dev);
60
- let gate = Tensor::zeros((), candle_core::DType::F32, dev)?;
61
- Ok(AtencaoPropria {
62
- q: mk()?, k: mk()?, v: mk()?, o: mk()?,
63
- gate: Var::from_tensor(&gate)?,
64
- n_cabecas,
65
- })
66
- }
67
-
68
- /// x (n, d) → y = x + g·attn(x) — multi-cabeça compacta (sem máscara:
69
- /// atenção plena entre os tokens da entrada do MoE).
70
- pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
71
- let d = x.dim(candle_core::D::Minus1)?;
72
- let d_cab = d / self.n_cabecas;
73
- let n = x.dim(0)?;
74
- let forma = (self.n_cabecas, n, d_cab);
75
- let q = self.q.fwd(x)?.reshape(forma)?;
76
- let k = self.k.fwd(x)?.reshape(forma)?;
77
- let v = self.v.fwd(x)?.reshape(forma)?;
78
- let scores = q.matmul(&k.transpose(1, 2)?)?.affine(1.0 / (d_cab as f64).sqrt(), 0.0)?;
79
- let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1)?;
80
- let ctx = attn.matmul(&v)?; // (h, n, d_cab)
81
- let ctx = ctx.reshape((n, d))?;
82
- let saida = self.o.fwd(&ctx)?;
83
- // y = x + g·saida (g=0 no nascimento ⇒ y = x — M-R2)
84
- let g = self.gate.as_tensor();
85
- let forma: Vec<usize> = saida.dims().to_vec();
86
- let delta = saida.broadcast_mul(&g.broadcast_as(forma)?)?;
87
- Ok((x + &delta)?)
88
- }
89
-
90
- pub fn vars(&self) -> Vec<Var> {
91
- let mut v = self.q.vars();
92
- v.extend(self.k.vars());
93
- v.extend(self.v.vars());
94
- v.extend(self.o.vars());
95
- v.push(self.gate.clone());
96
- v
97
- }
98
- }
99
 
100
  // ---------------------------------------------------------------------------
101
  // MoE redirecionável
@@ -522,686 +429,3 @@ pub fn alocar_por_qe(qes: &[f64], capacidade_total: usize) -> Vec<usize> {
522
  }
523
  c
524
  }
525
-
526
- // ---------------------------------------------------------------------------
527
- // Aplicação JEPA — codificador/preditor redirecionáveis com MoE
528
- // ---------------------------------------------------------------------------
529
-
530
- pub struct RelatorioJepaMoe {
531
- pub modo: &'static str,
532
- pub perda_pred_inicial: f64,
533
- pub perda_pred_final: f64,
534
- pub perda_vc_final: f64,
535
- pub std_emb: f64,
536
- pub carga_max: usize,
537
- pub descartados: usize,
538
- }
539
-
540
- /// Treino JEPA compacto com três braços (ablação honesta):
541
- /// - `"pequena"`: codificador/preditor MLP simples (baseline);
542
- /// - `"densa"`: MLP com o TOTAL de parâmetros dos especialistas (capacidade
543
- /// equiparada, SEM roteamento — isola o ganho do roteamento);
544
- /// - `"moe"`: `MoeRedirecionavel` nos dois caminhos (Codificador no
545
- /// codificador, Decodificador no preditor) com atenção própria.
546
- pub fn treinar_jepa_com_moe(
547
- patches: &Tensor,
548
- modo: &'static str,
549
- passos: usize,
550
- frac_mascara: f64,
551
- semente: u64,
552
- ) -> Result<RelatorioJepaMoe> {
553
- let dev = patches.device();
554
- let n = patches.dim(0)?;
555
- let d = patches.dim(1)?;
556
- let mut rng = RngDeterministico::novo(semente);
557
- // alvo = patches originais (detach); contexto = patches mascarados
558
- let alvo = patches.detach();
559
- let contexto = {
560
- let mut v = patches.flatten_all()?.to_vec1::<f32>()?;
561
- for x in v.iter_mut() {
562
- if rng.uniforme() < frac_mascara {
563
- *x = 0.0;
564
- }
565
- }
566
- Tensor::from_vec(v, (n, d), &dev)?
567
- };
568
- let e = 4usize; // especialistas por papel no braço moe
569
- let n_top = 2;
570
- let capacidade = n;
571
-
572
- // braços
573
- enum Braco {
574
- Pequena(MlpJepa, MlpJepa),
575
- Densa(MlpJepa, MlpJepa),
576
- Moe(MoeRedirecionavel),
577
- }
578
- let braco = match modo {
579
- "pequena" => Braco::Pequena(
580
- MlpJepa::novo(d, 2 * d, d, &dev)?,
581
- MlpJepa::novo(d, 2 * d, d, &dev)?,
582
- ),
583
- "densa" => {
584
- // capacidade equiparada: oculta = soma das ocultas dos especialistas
585
- let d_oculta = 2 * d * e;
586
- Braco::Densa(
587
- MlpJepa::novo(d, d_oculta, d, &dev)?,
588
- MlpJepa::novo(d, d_oculta, d, &dev)?,
589
- )
590
- }
591
- _ => Braco::Moe(MoeRedirecionavel::novo(d, 2 * e, n_top, capacidade, &dev)?),
592
- };
593
-
594
- let perda_de = |b: &Braco, x: &Tensor| -> Result<(Tensor, Tensor, usize)> {
595
- match b {
596
- Braco::Pequena(e1, p1) => {
597
- let z = e1.fwd(x)?;
598
- let yh = p1.fwd(&z)?;
599
- let dif = (&yh - &alvo)?;
600
- Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
601
- z.sqr()?.mean_all()?, 0))
602
- }
603
- Braco::Densa(e1, p1) => {
604
- let z = e1.fwd(x)?;
605
- let yh = p1.fwd(&z)?;
606
- let dif = (&yh - &alvo)?;
607
- Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
608
- z.sqr()?.mean_all()?, 0))
609
- }
610
- Braco::Moe(m) => {
611
- // v0.9.0: forward (ex-fwd_vetorizado) — gradiente flui aos
612
- // especialistas e ao roteador (a fwd seccionava o grafo)
613
- let s_enc = m.forward(x, Papel::Codificador)?;
614
- let s_dec = m.forward(&s_enc.saida, Papel::Decodificador)?;
615
- let dif = (&s_dec.saida - &alvo)?;
616
- let lp = (dif.sqr()?.sum_all()? / (n as f64 * d as f64))?;
617
- // perda total = preditiva + aux balanceamento (ambos papéis)
618
- let lt = (lp.affine(1.0, 0.0)? + s_enc.perda_aux.affine(m.lambda_aux, 0.0)?
619
- + s_dec.perda_aux.affine(m.lambda_aux, 0.0)?)?;
620
- let carga_max = s_enc.carga.iter().copied().fold(0, usize::max)
621
- .max(s_dec.carga.iter().copied().fold(0, usize::max));
622
- Ok((lt, lp, carga_max))
623
- }
624
- }
625
- };
626
-
627
- let (mut opt, vars): (AdamWManual, Vec<Var>) = match &braco {
628
- Braco::Pequena(a, b) => {
629
- let v = [a.vars(), b.vars()].concat();
630
- (AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
631
- }
632
- Braco::Densa(a, b) => {
633
- let v = [a.vars(), b.vars()].concat();
634
- (AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
635
- }
636
- Braco::Moe(m) => (AdamWManual::novo(m.vars(), 3e-3, 0.0), m.vars()),
637
- };
638
- // atenção própria: gate nasce 0; sobe 1 passo em 8 (aquecimento honesto
639
- // do mecanismo — T-P5 análogo: neutralidade no nascimento, gradiente flui)
640
- if let Braco::Moe(m) = &braco {
641
- let g0 = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
642
- let _ = g0;
643
- }
644
-
645
- let (l0, _, _) = perda_de(&braco, &contexto)?;
646
- let perda_inicial = l0.to_scalar::<f32>()? as f64;
647
- let mut perda_final = perda_inicial;
648
- let mut vc_final = 0.0;
649
- let mut carga_max = 0usize;
650
- let mut descartados = 0usize;
651
- let mut std_emb = 0.0;
652
- for passo in 0..passos {
653
- let (lt, lp, cm) = perda_de(&braco, &contexto)?;
654
- if let Braco::Moe(m) = &braco {
655
- // abre o gate gradualmente (0 → 1 em 25% dos passos)
656
- let alvo_g = ((passo as f64 / (passos as f64 / 4.0).max(1.0)).min(1.0)) as f32;
657
- let g_atual = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
658
- if (alvo_g as f64 - g_atual).abs() > 1e-9 {
659
- m.atencao.gate.set(&Tensor::new(alvo_g, &dev)?)?;
660
- }
661
- }
662
- let grads = crate::treino::grads_achatados(&lt, &vars)?;
663
- AdamWManual::clipar(&mut grads.clone(), 1.0);
664
- let mut g2 = grads;
665
- AdamWManual::clipar(&mut g2, 5.0);
666
- opt.passo(g2);
667
- perda_final = lp.to_scalar::<f32>()? as f64;
668
- carga_max = cm;
669
- // VC / std do embedding (para o relatório)
670
- let (_, vc, _) = perda_de(&braco, &contexto)?;
671
- vc_final = vc.to_scalar::<f32>()? as f64;
672
- std_emb = {
673
- let z = match &braco {
674
- Braco::Pequena(a, _) => a.fwd(&contexto)?,
675
- Braco::Densa(a, _) => a.fwd(&contexto)?,
676
- Braco::Moe(m) => m.forward(&contexto, Papel::Codificador)?.saida,
677
- };
678
- z.flatten_all()?.to_vec1::<f32>()?.iter().map(|x| *x as f64).collect::<Vec<f64>>()
679
- .chunks(n).map(|c| {
680
- let m = c.iter().sum::<f64>() / c.len() as f64;
681
- (c.iter().map(|x| (x - m).powi(2)).sum::<f64>() / c.len() as f64).sqrt()
682
- }).sum::<f64>() / n as f64
683
- };
684
- }
685
- if let Braco::Moe(m) = &braco {
686
- let s = m.forward(&contexto, Papel::Codificador)?;
687
- descartados = s.descartados;
688
- }
689
- Ok(RelatorioJepaMoe {
690
- modo,
691
- perda_pred_inicial: perda_inicial,
692
- perda_pred_final: perda_final,
693
- perda_vc_final: vc_final,
694
- std_emb,
695
- carga_max,
696
- descartados,
697
- })
698
- }
699
-
700
- // ---------------------------------------------------------------------------
701
- // Aplicação SOM — refinamento residual com alocação por QE (M-R5)
702
- // ---------------------------------------------------------------------------
703
-
704
- pub struct RelatorioSomMoe {
705
- pub qe_antes: f64,
706
- pub qe_depois: f64,
707
- pub qe_alocacao_uniforme: f64,
708
- pub carga_por_celula: Vec<usize>,
709
- pub tempo_treino_s: f64,
710
- /// qualidade da DECODIFICAÇÃO após redirecionamento (mse w_bmu → x).
711
- pub mse_decodificador: f64,
712
- }
713
-
714
- /// Refina um SOM 4D treinado com um MoE redirecionável:
715
- /// 1. aloca especialistas por célula ∝ QE (water-filling — M-R5);
716
- /// 2. treina o papel Codificador como corretor residual
717
- /// `x̂ = w_bmu(x) + E(x)`;
718
- /// 3. mede QE′; repete com alocação uniforme (ablação do water-filling);
719
- /// 4. REDIRECIONA os especialistas para Decodificador e treina
720
- /// `x̂ = E(w_bmu)` (reconstrução a partir do código) — mede o MSE.
721
- pub fn refinar_som_com_moe(
722
- dados: &Tensor,
723
- centroides: &[Vec<f64>],
724
- passos: usize,
725
- semente: u64,
726
- ) -> Result<RelatorioSomMoe> {
727
- use std::time::Instant;
728
- let t0 = Instant::now();
729
- let dev = dados.device();
730
- let n = dados.dim(0)?;
731
- let d = dados.dim(1)?;
732
- let m = centroides.len();
733
- // BMU + QE por célula (tolera F32/F64)
734
- let dados_v: Vec<Vec<f64>> = match dados.to_vec2::<f64>() {
735
- Ok(v) => v,
736
- Err(_) => dados
737
- .to_vec2::<f32>()?
738
- .iter()
739
- .map(|l| l.iter().map(|x| *x as f64).collect())
740
- .collect(),
741
- };
742
- let bmu: Vec<usize> = dados_v
743
- .iter()
744
- .map(|x| {
745
- centroides
746
- .iter()
747
- .enumerate()
748
- .min_by(|(i, a), (j, b)| {
749
- let da: f64 = a.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
750
- let db: f64 = b.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
751
- da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal).then(i.cmp(j))
752
- })
753
- .map(|(i, _)| i)
754
- .unwrap_or(0)
755
- })
756
- .collect();
757
- let mut qe_cel = vec![0f64; m];
758
- let mut cont_cel = vec![0usize; m];
759
- for (i, b) in bmu.iter().enumerate() {
760
- let dist: f64 = centroides[*b].iter().zip(&dados_v[i]).map(|(w, v)| (w - v).powi(2)).sum();
761
- qe_cel[*b] += dist.sqrt();
762
- cont_cel[*b] += 1;
763
- }
764
- for i in 0..m {
765
- if cont_cel[i] > 0 {
766
- qe_cel[i] /= cont_cel[i] as f64;
767
- }
768
- }
769
- let qe_antes: f64 = dados_v
770
- .iter()
771
- .enumerate()
772
- .map(|(i, b)| {
773
- let w = &centroides[bmu[i]];
774
- (w.iter().zip(b).map(|(a, v)| (a - v).powi(2)).sum::<f64>()).sqrt()
775
- })
776
- .sum::<f64>()
777
- / n as f64;
778
-
779
- // ---------- braço water-filling ----------
780
- // capacidade = m células + 8 especialistas EXTRA distribuídos ∝ QE
781
- // (com C < m o water-filling degenera em uniforme — mínimo 1 por célula)
782
- let capacidade_total = m + 8;
783
- let aloca_wf = alocar_por_qe(&qe_cel, capacidade_total);
784
- let qe_wf = treinar_corretor(dados, centroides, &bmu, &aloca_wf, passos, semente, &dev)?;
785
- // ---------- braço uniforme (ablação: MESMO total e MESMA semente —
786
- // única diferença é a alocação; round-robin soma exatamente C) ----------
787
- let aloca_un = {
788
- let mut a = vec![1usize; m];
789
- let mut i = 0usize;
790
- while a.iter().sum::<usize>() < capacidade_total {
791
- a[i % m] += 1;
792
- i += 1;
793
- }
794
- a
795
- };
796
- let qe_un = treinar_corretor(dados, centroides, &bmu, &aloca_un, passos, semente, &dev)?;
797
-
798
- // ---------- redirecionamento → decodificador ----------
799
- // mesma pool de especialistas, agora aprendendo w_bmu → x
800
- let mse_dec = treinar_decodificador(dados, centroides, &bmu, &aloca_wf, passos, semente + 2, &dev)?;
801
-
802
- Ok(RelatorioSomMoe {
803
- qe_antes,
804
- qe_depois: qe_wf,
805
- qe_alocacao_uniforme: qe_un,
806
- carga_por_celula: aloca_wf,
807
- tempo_treino_s: t0.elapsed().as_secs_f64(),
808
- mse_decodificador: mse_dec,
809
- })
810
- }
811
-
812
- /// Corretor residual: x̂ = w_bmu + E_{célula}(x). Treina com AdamW.
813
- fn treinar_corretor(
814
- dados: &Tensor,
815
- centroides: &[Vec<f64>],
816
- bmu: &[usize],
817
- alocacao: &[usize],
818
- passos: usize,
819
- semente: u64,
820
- dev: &Device,
821
- ) -> Result<f64> {
822
- let n = dados.dim(0)?;
823
- let d = dados.dim(1)?;
824
- // um "especialista" = MlpJepa por CÉLULA (capacidade alocada = nº de
825
- // especialistas extra treinados nessa célula, somados em ensemble)
826
- let n_esp_tot: usize = alocacao.iter().sum();
827
- let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
828
- for c in alocacao {
829
- let mut v = Vec::new();
830
- for _ in 0..(*c).max(1) {
831
- v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
832
- }
833
- ensembles.push(v);
834
- }
835
- let mut vars: Vec<Var> = Vec::new();
836
- for ens in &ensembles {
837
- for e in ens {
838
- vars.extend(e.vars());
839
- }
840
- }
841
- let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
842
- // tensores de apoio
843
- let w_bmu = {
844
- let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
845
- Tensor::from_vec(v, (n, d), dev)?
846
- };
847
- let alvo_res = (dados - &w_bmu)?;
848
- for _ in 0..passos {
849
- let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
850
- for (i, ens) in ensembles.iter().enumerate() {
851
- // tokens da célula i
852
- let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
853
- if idx.is_empty() {
854
- continue;
855
- }
856
- let m_idx = idx.len();
857
- let it = Tensor::from_vec(idx, (m_idx,), dev)?;
858
- let x_c = dados.index_select(&it, 0)?;
859
- let r_c = alvo_res.index_select(&it, 0)?;
860
- // ensemble: média das saídas
861
- let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
862
- for e in ens {
863
- y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
864
- }
865
- let k = ens.len() as f64;
866
- let yh = y.affine(1.0 / k, 0.0)?;
867
- let dif = (&yh - &r_c)?;
868
- soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
869
- }
870
- let _ = n_esp_tot;
871
- let grads = crate::treino::grads_achatados(&soma, &vars)?;
872
- let mut g = grads;
873
- AdamWManual::clipar(&mut g, 5.0);
874
- opt.passo(g);
875
- }
876
- // QE refinado
877
- let mut qe = 0f64;
878
- for (i, ens) in ensembles.iter().enumerate() {
879
- let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
880
- if idx.is_empty() {
881
- continue;
882
- }
883
- let m_idx = idx.len();
884
- let it = Tensor::from_vec(idx, (m_idx,), dev)?;
885
- let x_c = dados.index_select(&it, 0)?;
886
- let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
887
- for e in ens {
888
- y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
889
- }
890
- let k = ens.len() as f64;
891
- let yh = y.affine(1.0 / k, 0.0)?;
892
- let xhat = (w_bmu.index_select(&it, 0)? + &yh)?;
893
- let dif = (&x_c - &xhat)?;
894
- qe += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
895
- }
896
- Ok((qe / (n as f64 * d as f64)).sqrt() * (d as f64).sqrt()) // QE médio por ponto (norma L2)
897
- }
898
-
899
- /// Decodificador (pós-redirecionamento): x̂ = E(w_bmu) — reconstrução
900
- /// a partir apenas do código (centróide). Mede o MSE final.
901
- fn treinar_decodificador(
902
- dados: &Tensor,
903
- centroides: &[Vec<f64>],
904
- bmu: &[usize],
905
- alocacao: &[usize],
906
- passos: usize,
907
- semente: u64,
908
- dev: &Device,
909
- ) -> Result<f64> {
910
- let _ = semente;
911
- let n = dados.dim(0)?;
912
- let d = dados.dim(1)?;
913
- let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
914
- for c in alocacao {
915
- let mut v = Vec::new();
916
- for _ in 0..(*c).max(1) {
917
- v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
918
- }
919
- ensembles.push(v);
920
- }
921
- let mut vars: Vec<Var> = Vec::new();
922
- for ens in &ensembles {
923
- for e in ens {
924
- vars.extend(e.vars());
925
- }
926
- }
927
- let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
928
- let w_bmu = {
929
- let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
930
- Tensor::from_vec(v, (n, d), dev)?
931
- };
932
- for _ in 0..passos {
933
- let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
934
- for (i, ens) in ensembles.iter().enumerate() {
935
- let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
936
- if idx.is_empty() {
937
- continue;
938
- }
939
- let m_idx = idx.len();
940
- let it = Tensor::from_vec(idx, (m_idx,), dev)?;
941
- let w_c = w_bmu.index_select(&it, 0)?;
942
- let x_c = dados.index_select(&it, 0)?;
943
- let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
944
- for e in ens {
945
- y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
946
- }
947
- let k = ens.len() as f64;
948
- let yh = y.affine(1.0 / k, 0.0)?;
949
- let dif = (&yh - &x_c)?;
950
- soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
951
- }
952
- let grads = crate::treino::grads_achatados(&soma, &vars)?;
953
- let mut g = grads;
954
- AdamWManual::clipar(&mut g, 5.0);
955
- opt.passo(g);
956
- }
957
- // MSE final do decodificador
958
- let mut mse = 0f64;
959
- for (i, ens) in ensembles.iter().enumerate() {
960
- let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
961
- if idx.is_empty() {
962
- continue;
963
- }
964
- let m_idx = idx.len();
965
- let it = Tensor::from_vec(idx, (m_idx,), dev)?;
966
- let w_c = w_bmu.index_select(&it, 0)?;
967
- let x_c = dados.index_select(&it, 0)?;
968
- let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
969
- for e in ens {
970
- y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
971
- }
972
- let k = ens.len() as f64;
973
- let yh = y.affine(1.0 / k, 0.0)?;
974
- let dif = (&yh - &x_c)?;
975
- mse += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
976
- }
977
- Ok(mse / (n as f64 * d as f64))
978
- }
979
-
980
- pub fn versao() -> &'static str {
981
- "moe-redirecionavel-0.6.0"
982
- }
983
-
984
- #[cfg(test)]
985
- #[allow(deprecated)] // testes usam o oráculo fwd (bug v080-1) de propósito
986
- mod testes {
987
- use super::*;
988
-
989
- fn dados_sinteticos(n: usize, d: usize, semente: u64) -> Tensor {
990
- let mut rng = crate::rng::RngDeterministico::novo(semente);
991
- let v: Vec<f32> = (0..n * d).map(|_| rng.normal(0.0, 1.0) as f32).collect();
992
- Tensor::from_vec(v, (n, d), &Device::Cpu).unwrap()
993
- }
994
-
995
- #[test]
996
- fn mr1_invariancia_sob_redirecionamento() {
997
- let dev = Device::Cpu;
998
- let x = dados_sinteticos(64, 16, 3);
999
- let mut moe = MoeRedirecionavel::novo(16, 6, 2, 64, &dev).unwrap();
1000
- let s1 = moe.fwd(&x, Papel::Codificador).unwrap();
1001
- let perda_antes = s1.perda_aux.to_scalar::<f32>().unwrap() as f64;
1002
- let carga_antes: usize = s1.carga.iter().sum();
1003
- moe.redirecionar_todos();
1004
- // após trocar TODOS os papéis, a passada Decodificador usa os
1005
- // especialistas que eram do codificador — a perda auxiliar do
1006
- // SUBCONJUNTO é comparável (mesma distribuição de roteamento)
1007
- let s2 = moe.fwd(&x, Papel::Decodificador).unwrap();
1008
- let perda_depois = s2.perda_aux.to_scalar::<f32>().unwrap() as f64;
1009
- let carga_depois: usize = s2.carga.iter().sum();
1010
- assert_eq!(carga_antes, carga_depois, "capacidade total conservada (M-R1)");
1011
- assert!(
1012
- (perda_antes - perda_depois).abs() < 2.0 + 1e-6,
1013
- "perda auxiliar da família conservada: {perda_antes:.4} vs {perda_depois:.4}"
1014
- );
1015
- }
1016
-
1017
- #[test]
1018
- fn mr2_neutralidade_no_nascimento() {
1019
- let dev = Device::Cpu;
1020
- let x = dados_sinteticos(32, 8, 5);
1021
- let moe = MoeRedirecionavel::novo(8, 4, 2, 32, &dev).unwrap();
1022
- let g = moe.atencao.gate.as_tensor().to_scalar::<f32>().unwrap();
1023
- assert!(g == 0.0, "gate nasce zero");
1024
- let y = moe.atencao.fwd(&x).unwrap();
1025
- let dif = (&y - &x).unwrap().sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap();
1026
- assert!(dif == 0.0, "y = x bit-a-bit no nascimento (dif={dif})");
1027
- }
1028
-
1029
- #[test]
1030
- fn mr3_paralelismo_e_determinismo() {
1031
- let dev = Device::Cpu;
1032
- let x = dados_sinteticos(96, 24, 7);
1033
- let moe = MoeRedirecionavel::novo(24, 8, 2, 96, &dev).unwrap();
1034
- let (_enc, _dec, t_par, t_seq, speedup) = moe.fwd_ambos_par(&x).unwrap();
1035
- // determinismo já garantido por assert interno; speedup registrado
1036
- assert!(t_par > 0.0 && t_seq > 0.0);
1037
- let _ = speedup;
1038
- }
1039
-
1040
- #[test]
1041
- fn mr5_water_filling_minimiza_cota() {
1042
- // cota Σ q_i²/c_i: water-filling ≤ uniforme (Cauchy–Schwarz)
1043
- let qes = vec![0.2, 0.5, 1.0, 0.3];
1044
- let c_wf = alocar_por_qe(&qes, 8);
1045
- let c_un = vec![2usize; 4];
1046
- let f = |c: &[usize]| qes.iter().zip(c).map(|(q, ci)| q * q / *ci as f64).sum::<f64>();
1047
- assert!(
1048
- f(&c_wf) <= f(&c_un) + 1e-9,
1049
- "water-filling {c_wf:?} (cota {:.4}) deve ≤ uniforme (cota {:.4})",
1050
- f(&c_wf), f(&c_un)
1051
- );
1052
- assert_eq!(c_wf.iter().sum::<usize>(), 8);
1053
- }
1054
-
1055
- #[test]
1056
- fn jepa_moe_aprende() {
1057
- let x = dados_sinteticos(64, 12, 11);
1058
- let r = treinar_jepa_com_moe(&x, "moe", 60, 0.5, 42).unwrap();
1059
- assert!(r.perda_pred_final < r.perda_pred_inicial, "perda preditiva cai");
1060
- }
1061
-
1062
- /// **M-R6 (v0.8.0):** equivalência de VALORES entre `fwd` e
1063
- /// `forward` — mesma seleção (auxiliar compartilhado), mesma
1064
- /// saída, mesma perda auxiliar, mesma carga/descartados.
1065
- #[test]
1066
- fn mr6_equivalencia_de_valores() {
1067
- let dev = Device::Cpu;
1068
- for (n, d, e, k, cap, seed) in [
1069
- (64usize, 16usize, 6usize, 2usize, 64usize, 3u64),
1070
- (96, 24, 8, 2, 96, 5),
1071
- (48, 12, 4, 1, 24, 9), // capacidade apertada: força descartados
1072
- ] {
1073
- let x = dados_sinteticos(n, d, seed);
1074
- let moe = MoeRedirecionavel::novo(d, e, k, cap, &dev).unwrap();
1075
- let s_ref = moe.fwd(&x, Papel::Codificador).unwrap();
1076
- let s_vet = moe.forward(&x, Papel::Codificador).unwrap();
1077
- assert_eq!(s_ref.carga, s_vet.carga, "M-R6: mesma carga (n={n})");
1078
- assert_eq!(s_ref.usados, s_vet.usados, "M-R6: mesmos usados");
1079
- assert_eq!(s_ref.descartados, s_vet.descartados, "M-R6: mesmos descartados");
1080
- let dif = (&s_ref.saida - &s_vet.saida).unwrap()
1081
- .abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
1082
- assert!(dif < 1e-4, "M-R6: max|Δsaída| = {dif} (n={n}, cap={cap})");
1083
- let pa = s_ref.perda_aux.to_scalar::<f32>().unwrap();
1084
- let pb = s_vet.perda_aux.to_scalar::<f32>().unwrap();
1085
- assert!(
1086
- (pa - pb).abs() < 1e-4,
1087
- "M-R6: perda auxiliar {pa:.6} vs {pb:.6} (n={n})"
1088
- );
1089
- }
1090
- }
1091
-
1092
- /// **M-R7 (v0.8.0):** restauração do fluxo de gradiente — bug v080-1.
1093
- /// Com a `fwd` (referência de valores) os especialistas e o roteador
1094
- /// NÃO recebem gradiente (grafo seccionado por to_vec2/from_vec); com
1095
- /// a `forward` TODOS recebem (index_add/index_select vivos).
1096
- #[test]
1097
- fn mr7_restauracao_do_gradiente() {
1098
- let dev = Device::Cpu;
1099
- let x = dados_sinteticos(48, 12, 13);
1100
- let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
1101
- let norma_grads = |vetorizada: bool| -> (f64, f64) {
1102
- let s = if vetorizada {
1103
- moe.forward(&x, Papel::Codificador).unwrap()
1104
- } else {
1105
- moe.fwd(&x, Papel::Codificador).unwrap()
1106
- };
1107
- let perda = s.saida.sqr().unwrap().sum_all().unwrap();
1108
- let grads = perda.backward().unwrap();
1109
- let mut n_roteador = 0.0f64;
1110
- for v in moe.roteador.vars() {
1111
- if let Some(g) = grads.get(&v) {
1112
- n_roteador += g.sqr().unwrap().sum_all().unwrap()
1113
- .to_scalar::<f32>().unwrap() as f64;
1114
- }
1115
- }
1116
- let mut n_experts = 0.0f64;
1117
- for e in &moe.especialistas {
1118
- for v in e.vars() {
1119
- if let Some(g) = grads.get(&v) {
1120
- n_experts += g.sqr().unwrap().sum_all().unwrap()
1121
- .to_scalar::<f32>().unwrap() as f64;
1122
- }
1123
- }
1124
- }
1125
- (n_roteador, n_experts)
1126
- };
1127
- // (a) o bug v080-1 é REAL: na fwd, roteador e especialistas têm
1128
- // gradiente ZERO (grafo seccionado) — documentado, não corrigido na
1129
- // fwd (referência de valores preservada por decisão de projeto)
1130
- let (r_ref, e_ref) = norma_grads(false);
1131
- assert!(
1132
- r_ref == 0.0 && e_ref == 0.0,
1133
- "M-R7: bug v080-1 documentado — fwd secciona o grafo (roteador {r_ref}, experts {e_ref})"
1134
- );
1135
- // (b) a correção: na vetorizada, TODOS recebem gradiente
1136
- let (r_vet, e_vet) = norma_grads(true);
1137
- assert!(
1138
- r_vet > 0.0,
1139
- "M-R7: roteador recebe gradiente na vetorizada ({r_vet})"
1140
- );
1141
- assert!(
1142
- e_vet > 0.0,
1143
- "M-R7: especialistas recebem gradiente na vetorizada ({e_vet})"
1144
- );
1145
- }
1146
-
1147
- /// **M-R8 (v0.8.0):** fim-a-fim — com a vetorizada, o treino REALMENTE
1148
- /// atualiza os pesos dos especialistas (regressão do bug v080-1: antes
1149
- /// só a atenção própria treinava no braço MoE).
1150
- #[test]
1151
- fn mr8_treino_atualiza_especialistas() {
1152
- let dev = Device::Cpu;
1153
- let x = dados_sinteticos(48, 12, 17);
1154
- let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
1155
- // snapshot com dados PRÓPRIOS (to_vec1) — detach() compartilha o
1156
- // storage e o set() do otimizador sobrescreveria a "cópia"
1157
- let snapshot: Vec<Vec<f32>> = moe
1158
- .especialistas
1159
- .iter()
1160
- .map(|e| {
1161
- e.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap()
1162
- })
1163
- .collect();
1164
- // treino curto com gradiente fluindo
1165
- let vars = moe.vars();
1166
- let mut opt = crate::treino::AdamWManual::novo(vars.clone(), 3e-3, 0.0);
1167
- for _ in 0..5 {
1168
- let s = moe.forward(&x, Papel::Codificador).unwrap();
1169
- let perda = s.saida.sqr().unwrap().sum_all().unwrap();
1170
- let grads = crate::treino::grads_achatados(&perda, &vars).unwrap();
1171
- opt.passo(grads);
1172
- }
1173
- let mut delta_max = 0.0f64;
1174
- for (e, antes) in moe.especialistas.iter().zip(snapshot.iter()) {
1175
- let agora = e.vars()[0].as_tensor().flatten_all().unwrap()
1176
- .to_vec1::<f32>().unwrap();
1177
- let dif = agora.iter().zip(antes.iter()).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
1178
- delta_max = delta_max.max(dif as f64);
1179
- }
1180
- let _ = &vars;
1181
- assert!(
1182
- delta_max > 0.0,
1183
- "M-R8: especialistas devem mudar no treino (delta_max={delta_max})"
1184
- );
1185
- }
1186
- }
1187
-
1188
- #[cfg(test)]
1189
- mod teste_som_moe_integrado {
1190
- use super::*;
1191
-
1192
- #[test]
1193
- fn debug_som_com_moe_caminho() {
1194
- let dev = Device::Cpu;
1195
- let dados_v: Vec<Vec<f64>> = (0..32).map(|i| (0..16).map(|j| (i * 7 + j * 3) as f64 / 100.0).collect()).collect();
1196
- let (n, d) = (dados_v.len(), 16);
1197
- let t = Tensor::from_vec(dados_v.into_iter().flatten().collect::<Vec<f64>>(), (n, d), &dev).unwrap()
1198
- .to_dtype(candle_core::DType::F32).unwrap();
1199
- let cfg = crate::som4d::CfgSom4D { nx: 2, ny: 2, nz: 2, nt: 2, ..Default::default() };
1200
- let som = crate::som4d::treinar_som4d(&t, &cfg, &dev).unwrap();
1201
- let centroides = som.prototipos.clone();
1202
- let r = refinar_som_com_moe(&t, &centroides, 30, 1).unwrap();
1203
- println!("qe {} -> {} (wf) | uniforme {}", r.qe_antes, r.qe_depois, r.qe_alocacao_uniforme);
1204
- assert!(r.qe_depois < r.qe_antes, "corretor MoE reduz o QE");
1205
- assert!(r.mse_decodificador < r.qe_antes.powi(2), "redirect decodifica");
1206
- }
1207
- }
 
1
+ //! `moe_redirecionavel::nucleo` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ use super::*;
 
 
 
 
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  // ---------------------------------------------------------------------------
8
  // MoE redirecionável
 
429
  }
430
  c
431
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/moe_redirecionavel/teste_integrado.rs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `moe_redirecionavel::teste_integrado` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ #[cfg(test)]
8
+ mod teste_som_moe_integrado {
9
+ use super::*;
10
+
11
+ #[test]
12
+ fn debug_som_com_moe_caminho() {
13
+ let dev = Device::Cpu;
14
+ let dados_v: Vec<Vec<f64>> = (0..32).map(|i| (0..16).map(|j| (i * 7 + j * 3) as f64 / 100.0).collect()).collect();
15
+ let (n, d) = (dados_v.len(), 16);
16
+ let t = Tensor::from_vec(dados_v.into_iter().flatten().collect::<Vec<f64>>(), (n, d), &dev).unwrap()
17
+ .to_dtype(candle_core::DType::F32).unwrap();
18
+ let cfg = crate::som4d::CfgSom4D { nx: 2, ny: 2, nz: 2, nt: 2, ..Default::default() };
19
+ let som = crate::som4d::treinar_som4d(&t, &cfg, &dev).unwrap();
20
+ let centroides = som.prototipos.clone();
21
+ let r = refinar_som_com_moe(&t, &centroides, 30, 1).unwrap();
22
+ println!("qe {} -> {} (wf) | uniforme {}", r.qe_antes, r.qe_depois, r.qe_alocacao_uniforme);
23
+ assert!(r.qe_depois < r.qe_antes, "corretor MoE reduz o QE");
24
+ assert!(r.mse_decodificador < r.qe_antes.powi(2), "redirect decodifica");
25
+ }
26
+ }
src/moe_redirecionavel/testes.rs ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `moe_redirecionavel::testes` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ #[cfg(test)]
8
+ #[allow(deprecated)] // testes usam o oráculo fwd (bug v080-1) de propósito
9
+ mod testes {
10
+ use super::*;
11
+
12
+ fn dados_sinteticos(n: usize, d: usize, semente: u64) -> Tensor {
13
+ let mut rng = crate::rng::RngDeterministico::novo(semente);
14
+ let v: Vec<f32> = (0..n * d).map(|_| rng.normal(0.0, 1.0) as f32).collect();
15
+ Tensor::from_vec(v, (n, d), &Device::Cpu).unwrap()
16
+ }
17
+
18
+ #[test]
19
+ fn mr1_invariancia_sob_redirecionamento() {
20
+ let dev = Device::Cpu;
21
+ let x = dados_sinteticos(64, 16, 3);
22
+ let mut moe = MoeRedirecionavel::novo(16, 6, 2, 64, &dev).unwrap();
23
+ let s1 = moe.fwd(&x, Papel::Codificador).unwrap();
24
+ let perda_antes = s1.perda_aux.to_scalar::<f32>().unwrap() as f64;
25
+ let carga_antes: usize = s1.carga.iter().sum();
26
+ moe.redirecionar_todos();
27
+ // após trocar TODOS os papéis, a passada Decodificador usa os
28
+ // especialistas que eram do codificador — a perda auxiliar do
29
+ // SUBCONJUNTO é comparável (mesma distribuição de roteamento)
30
+ let s2 = moe.fwd(&x, Papel::Decodificador).unwrap();
31
+ let perda_depois = s2.perda_aux.to_scalar::<f32>().unwrap() as f64;
32
+ let carga_depois: usize = s2.carga.iter().sum();
33
+ assert_eq!(carga_antes, carga_depois, "capacidade total conservada (M-R1)");
34
+ assert!(
35
+ (perda_antes - perda_depois).abs() < 2.0 + 1e-6,
36
+ "perda auxiliar da família conservada: {perda_antes:.4} vs {perda_depois:.4}"
37
+ );
38
+ }
39
+
40
+ #[test]
41
+ fn mr2_neutralidade_no_nascimento() {
42
+ let dev = Device::Cpu;
43
+ let x = dados_sinteticos(32, 8, 5);
44
+ let moe = MoeRedirecionavel::novo(8, 4, 2, 32, &dev).unwrap();
45
+ let g = moe.atencao.gate.as_tensor().to_scalar::<f32>().unwrap();
46
+ assert!(g == 0.0, "gate nasce zero");
47
+ let y = moe.atencao.fwd(&x).unwrap();
48
+ let dif = (&y - &x).unwrap().sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap();
49
+ assert!(dif == 0.0, "y = x bit-a-bit no nascimento (dif={dif})");
50
+ }
51
+
52
+ #[test]
53
+ fn mr3_paralelismo_e_determinismo() {
54
+ let dev = Device::Cpu;
55
+ let x = dados_sinteticos(96, 24, 7);
56
+ let moe = MoeRedirecionavel::novo(24, 8, 2, 96, &dev).unwrap();
57
+ let (_enc, _dec, t_par, t_seq, speedup) = moe.fwd_ambos_par(&x).unwrap();
58
+ // determinismo já garantido por assert interno; speedup registrado
59
+ assert!(t_par > 0.0 && t_seq > 0.0);
60
+ let _ = speedup;
61
+ }
62
+
63
+ #[test]
64
+ fn mr5_water_filling_minimiza_cota() {
65
+ // cota Σ q_i²/c_i: water-filling ≤ uniforme (Cauchy–Schwarz)
66
+ let qes = vec![0.2, 0.5, 1.0, 0.3];
67
+ let c_wf = alocar_por_qe(&qes, 8);
68
+ let c_un = vec![2usize; 4];
69
+ let f = |c: &[usize]| qes.iter().zip(c).map(|(q, ci)| q * q / *ci as f64).sum::<f64>();
70
+ assert!(
71
+ f(&c_wf) <= f(&c_un) + 1e-9,
72
+ "water-filling {c_wf:?} (cota {:.4}) deve ≤ uniforme (cota {:.4})",
73
+ f(&c_wf), f(&c_un)
74
+ );
75
+ assert_eq!(c_wf.iter().sum::<usize>(), 8);
76
+ }
77
+
78
+ #[test]
79
+ fn jepa_moe_aprende() {
80
+ let x = dados_sinteticos(64, 12, 11);
81
+ let r = treinar_jepa_com_moe(&x, "moe", 60, 0.5, 42).unwrap();
82
+ assert!(r.perda_pred_final < r.perda_pred_inicial, "perda preditiva cai");
83
+ }
84
+
85
+ /// **M-R6 (v0.8.0):** equivalência de VALORES entre `fwd` e
86
+ /// `forward` — mesma seleção (auxiliar compartilhado), mesma
87
+ /// saída, mesma perda auxiliar, mesma carga/descartados.
88
+ #[test]
89
+ fn mr6_equivalencia_de_valores() {
90
+ let dev = Device::Cpu;
91
+ for (n, d, e, k, cap, seed) in [
92
+ (64usize, 16usize, 6usize, 2usize, 64usize, 3u64),
93
+ (96, 24, 8, 2, 96, 5),
94
+ (48, 12, 4, 1, 24, 9), // capacidade apertada: força descartados
95
+ ] {
96
+ let x = dados_sinteticos(n, d, seed);
97
+ let moe = MoeRedirecionavel::novo(d, e, k, cap, &dev).unwrap();
98
+ let s_ref = moe.fwd(&x, Papel::Codificador).unwrap();
99
+ let s_vet = moe.forward(&x, Papel::Codificador).unwrap();
100
+ assert_eq!(s_ref.carga, s_vet.carga, "M-R6: mesma carga (n={n})");
101
+ assert_eq!(s_ref.usados, s_vet.usados, "M-R6: mesmos usados");
102
+ assert_eq!(s_ref.descartados, s_vet.descartados, "M-R6: mesmos descartados");
103
+ let dif = (&s_ref.saida - &s_vet.saida).unwrap()
104
+ .abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
105
+ assert!(dif < 1e-4, "M-R6: max|Δsaída| = {dif} (n={n}, cap={cap})");
106
+ let pa = s_ref.perda_aux.to_scalar::<f32>().unwrap();
107
+ let pb = s_vet.perda_aux.to_scalar::<f32>().unwrap();
108
+ assert!(
109
+ (pa - pb).abs() < 1e-4,
110
+ "M-R6: perda auxiliar {pa:.6} vs {pb:.6} (n={n})"
111
+ );
112
+ }
113
+ }
114
+
115
+ /// **M-R7 (v0.8.0):** restauração do fluxo de gradiente — bug v080-1.
116
+ /// Com a `fwd` (referência de valores) os especialistas e o roteador
117
+ /// NÃO recebem gradiente (grafo seccionado por to_vec2/from_vec); com
118
+ /// a `forward` TODOS recebem (index_add/index_select vivos).
119
+ #[test]
120
+ fn mr7_restauracao_do_gradiente() {
121
+ let dev = Device::Cpu;
122
+ let x = dados_sinteticos(48, 12, 13);
123
+ let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
124
+ let norma_grads = |vetorizada: bool| -> (f64, f64) {
125
+ let s = if vetorizada {
126
+ moe.forward(&x, Papel::Codificador).unwrap()
127
+ } else {
128
+ moe.fwd(&x, Papel::Codificador).unwrap()
129
+ };
130
+ let perda = s.saida.sqr().unwrap().sum_all().unwrap();
131
+ let grads = perda.backward().unwrap();
132
+ let mut n_roteador = 0.0f64;
133
+ for v in moe.roteador.vars() {
134
+ if let Some(g) = grads.get(&v) {
135
+ n_roteador += g.sqr().unwrap().sum_all().unwrap()
136
+ .to_scalar::<f32>().unwrap() as f64;
137
+ }
138
+ }
139
+ let mut n_experts = 0.0f64;
140
+ for e in &moe.especialistas {
141
+ for v in e.vars() {
142
+ if let Some(g) = grads.get(&v) {
143
+ n_experts += g.sqr().unwrap().sum_all().unwrap()
144
+ .to_scalar::<f32>().unwrap() as f64;
145
+ }
146
+ }
147
+ }
148
+ (n_roteador, n_experts)
149
+ };
150
+ // (a) o bug v080-1 é REAL: na fwd, roteador e especialistas têm
151
+ // gradiente ZERO (grafo seccionado) — documentado, não corrigido na
152
+ // fwd (referência de valores preservada por decisão de projeto)
153
+ let (r_ref, e_ref) = norma_grads(false);
154
+ assert!(
155
+ r_ref == 0.0 && e_ref == 0.0,
156
+ "M-R7: bug v080-1 documentado — fwd secciona o grafo (roteador {r_ref}, experts {e_ref})"
157
+ );
158
+ // (b) a correção: na vetorizada, TODOS recebem gradiente
159
+ let (r_vet, e_vet) = norma_grads(true);
160
+ assert!(
161
+ r_vet > 0.0,
162
+ "M-R7: roteador recebe gradiente na vetorizada ({r_vet})"
163
+ );
164
+ assert!(
165
+ e_vet > 0.0,
166
+ "M-R7: especialistas recebem gradiente na vetorizada ({e_vet})"
167
+ );
168
+ }
169
+
170
+ /// **M-R8 (v0.8.0):** fim-a-fim — com a vetorizada, o treino REALMENTE
171
+ /// atualiza os pesos dos especialistas (regressão do bug v080-1: antes
172
+ /// só a atenção própria treinava no braço MoE).
173
+ #[test]
174
+ fn mr8_treino_atualiza_especialistas() {
175
+ let dev = Device::Cpu;
176
+ let x = dados_sinteticos(48, 12, 17);
177
+ let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
178
+ // snapshot com dados PRÓPRIOS (to_vec1) — detach() compartilha o
179
+ // storage e o set() do otimizador sobrescreveria a "cópia"
180
+ let snapshot: Vec<Vec<f32>> = moe
181
+ .especialistas
182
+ .iter()
183
+ .map(|e| {
184
+ e.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap()
185
+ })
186
+ .collect();
187
+ // treino curto com gradiente fluindo
188
+ let vars = moe.vars();
189
+ let mut opt = crate::treino::AdamWManual::novo(vars.clone(), 3e-3, 0.0);
190
+ for _ in 0..5 {
191
+ let s = moe.forward(&x, Papel::Codificador).unwrap();
192
+ let perda = s.saida.sqr().unwrap().sum_all().unwrap();
193
+ let grads = crate::treino::grads_achatados(&perda, &vars).unwrap();
194
+ opt.passo(grads);
195
+ }
196
+ let mut delta_max = 0.0f64;
197
+ for (e, antes) in moe.especialistas.iter().zip(snapshot.iter()) {
198
+ let agora = e.vars()[0].as_tensor().flatten_all().unwrap()
199
+ .to_vec1::<f32>().unwrap();
200
+ let dif = agora.iter().zip(antes.iter()).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
201
+ delta_max = delta_max.max(dif as f64);
202
+ }
203
+ let _ = &vars;
204
+ assert!(
205
+ delta_max > 0.0,
206
+ "M-R8: especialistas devem mudar no treino (delta_max={delta_max})"
207
+ );
208
+ }
209
+ }
src/moe_redirecionavel/treino.rs ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `moe_redirecionavel::treino` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Aplicação JEPA — codificador/preditor redirecionáveis com MoE
9
+ // ---------------------------------------------------------------------------
10
+
11
+ pub struct RelatorioJepaMoe {
12
+ pub modo: &'static str,
13
+ pub perda_pred_inicial: f64,
14
+ pub perda_pred_final: f64,
15
+ pub perda_vc_final: f64,
16
+ pub std_emb: f64,
17
+ pub carga_max: usize,
18
+ pub descartados: usize,
19
+ }
20
+
21
+ /// Treino JEPA compacto com três braços (ablação honesta):
22
+ /// - `"pequena"`: codificador/preditor MLP simples (baseline);
23
+ /// - `"densa"`: MLP com o TOTAL de parâmetros dos especialistas (capacidade
24
+ /// equiparada, SEM roteamento — isola o ganho do roteamento);
25
+ /// - `"moe"`: `MoeRedirecionavel` nos dois caminhos (Codificador no
26
+ /// codificador, Decodificador no preditor) com atenção própria.
27
+ pub fn treinar_jepa_com_moe(
28
+ patches: &Tensor,
29
+ modo: &'static str,
30
+ passos: usize,
31
+ frac_mascara: f64,
32
+ semente: u64,
33
+ ) -> Result<RelatorioJepaMoe> {
34
+ let dev = patches.device();
35
+ let n = patches.dim(0)?;
36
+ let d = patches.dim(1)?;
37
+ let mut rng = RngDeterministico::novo(semente);
38
+ // alvo = patches originais (detach); contexto = patches mascarados
39
+ let alvo = patches.detach();
40
+ let contexto = {
41
+ let mut v = patches.flatten_all()?.to_vec1::<f32>()?;
42
+ for x in v.iter_mut() {
43
+ if rng.uniforme() < frac_mascara {
44
+ *x = 0.0;
45
+ }
46
+ }
47
+ Tensor::from_vec(v, (n, d), &dev)?
48
+ };
49
+ let e = 4usize; // especialistas por papel no braço moe
50
+ let n_top = 2;
51
+ let capacidade = n;
52
+
53
+ // braços
54
+ enum Braco {
55
+ Pequena(MlpJepa, MlpJepa),
56
+ Densa(MlpJepa, MlpJepa),
57
+ Moe(MoeRedirecionavel),
58
+ }
59
+ let braco = match modo {
60
+ "pequena" => Braco::Pequena(
61
+ MlpJepa::novo(d, 2 * d, d, &dev)?,
62
+ MlpJepa::novo(d, 2 * d, d, &dev)?,
63
+ ),
64
+ "densa" => {
65
+ // capacidade equiparada: oculta = soma das ocultas dos especialistas
66
+ let d_oculta = 2 * d * e;
67
+ Braco::Densa(
68
+ MlpJepa::novo(d, d_oculta, d, &dev)?,
69
+ MlpJepa::novo(d, d_oculta, d, &dev)?,
70
+ )
71
+ }
72
+ _ => Braco::Moe(MoeRedirecionavel::novo(d, 2 * e, n_top, capacidade, &dev)?),
73
+ };
74
+
75
+ let perda_de = |b: &Braco, x: &Tensor| -> Result<(Tensor, Tensor, usize)> {
76
+ match b {
77
+ Braco::Pequena(e1, p1) => {
78
+ let z = e1.fwd(x)?;
79
+ let yh = p1.fwd(&z)?;
80
+ let dif = (&yh - &alvo)?;
81
+ Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
82
+ z.sqr()?.mean_all()?, 0))
83
+ }
84
+ Braco::Densa(e1, p1) => {
85
+ let z = e1.fwd(x)?;
86
+ let yh = p1.fwd(&z)?;
87
+ let dif = (&yh - &alvo)?;
88
+ Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
89
+ z.sqr()?.mean_all()?, 0))
90
+ }
91
+ Braco::Moe(m) => {
92
+ // v0.9.0: forward (ex-fwd_vetorizado) — gradiente flui aos
93
+ // especialistas e ao roteador (a fwd seccionava o grafo)
94
+ let s_enc = m.forward(x, Papel::Codificador)?;
95
+ let s_dec = m.forward(&s_enc.saida, Papel::Decodificador)?;
96
+ let dif = (&s_dec.saida - &alvo)?;
97
+ let lp = (dif.sqr()?.sum_all()? / (n as f64 * d as f64))?;
98
+ // perda total = preditiva + aux balanceamento (ambos papéis)
99
+ let lt = (lp.affine(1.0, 0.0)? + s_enc.perda_aux.affine(m.lambda_aux, 0.0)?
100
+ + s_dec.perda_aux.affine(m.lambda_aux, 0.0)?)?;
101
+ let carga_max = s_enc.carga.iter().copied().fold(0, usize::max)
102
+ .max(s_dec.carga.iter().copied().fold(0, usize::max));
103
+ Ok((lt, lp, carga_max))
104
+ }
105
+ }
106
+ };
107
+
108
+ let (mut opt, vars): (AdamWManual, Vec<Var>) = match &braco {
109
+ Braco::Pequena(a, b) => {
110
+ let v = [a.vars(), b.vars()].concat();
111
+ (AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
112
+ }
113
+ Braco::Densa(a, b) => {
114
+ let v = [a.vars(), b.vars()].concat();
115
+ (AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
116
+ }
117
+ Braco::Moe(m) => (AdamWManual::novo(m.vars(), 3e-3, 0.0), m.vars()),
118
+ };
119
+ // atenção própria: gate nasce 0; sobe 1 passo em 8 (aquecimento honesto
120
+ // do mecanismo — T-P5 análogo: neutralidade no nascimento, gradiente flui)
121
+ if let Braco::Moe(m) = &braco {
122
+ let g0 = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
123
+ let _ = g0;
124
+ }
125
+
126
+ let (l0, _, _) = perda_de(&braco, &contexto)?;
127
+ let perda_inicial = l0.to_scalar::<f32>()? as f64;
128
+ let mut perda_final = perda_inicial;
129
+ let mut vc_final = 0.0;
130
+ let mut carga_max = 0usize;
131
+ let mut descartados = 0usize;
132
+ let mut std_emb = 0.0;
133
+ for passo in 0..passos {
134
+ let (lt, lp, cm) = perda_de(&braco, &contexto)?;
135
+ if let Braco::Moe(m) = &braco {
136
+ // abre o gate gradualmente (0 → 1 em 25% dos passos)
137
+ let alvo_g = ((passo as f64 / (passos as f64 / 4.0).max(1.0)).min(1.0)) as f32;
138
+ let g_atual = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
139
+ if (alvo_g as f64 - g_atual).abs() > 1e-9 {
140
+ m.atencao.gate.set(&Tensor::new(alvo_g, &dev)?)?;
141
+ }
142
+ }
143
+ let grads = crate::treino::grads_achatados(&lt, &vars)?;
144
+ AdamWManual::clipar(&mut grads.clone(), 1.0);
145
+ let mut g2 = grads;
146
+ AdamWManual::clipar(&mut g2, 5.0);
147
+ opt.passo(g2);
148
+ perda_final = lp.to_scalar::<f32>()? as f64;
149
+ carga_max = cm;
150
+ // VC / std do embedding (para o relatório)
151
+ let (_, vc, _) = perda_de(&braco, &contexto)?;
152
+ vc_final = vc.to_scalar::<f32>()? as f64;
153
+ std_emb = {
154
+ let z = match &braco {
155
+ Braco::Pequena(a, _) => a.fwd(&contexto)?,
156
+ Braco::Densa(a, _) => a.fwd(&contexto)?,
157
+ Braco::Moe(m) => m.forward(&contexto, Papel::Codificador)?.saida,
158
+ };
159
+ z.flatten_all()?.to_vec1::<f32>()?.iter().map(|x| *x as f64).collect::<Vec<f64>>()
160
+ .chunks(n).map(|c| {
161
+ let m = c.iter().sum::<f64>() / c.len() as f64;
162
+ (c.iter().map(|x| (x - m).powi(2)).sum::<f64>() / c.len() as f64).sqrt()
163
+ }).sum::<f64>() / n as f64
164
+ };
165
+ }
166
+ if let Braco::Moe(m) = &braco {
167
+ let s = m.forward(&contexto, Papel::Codificador)?;
168
+ descartados = s.descartados;
169
+ }
170
+ Ok(RelatorioJepaMoe {
171
+ modo,
172
+ perda_pred_inicial: perda_inicial,
173
+ perda_pred_final: perda_final,
174
+ perda_vc_final: vc_final,
175
+ std_emb,
176
+ carga_max,
177
+ descartados,
178
+ })
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Aplicação SOM — refinamento residual com alocação por QE (M-R5)
183
+ // ---------------------------------------------------------------------------
184
+
185
+ pub struct RelatorioSomMoe {
186
+ pub qe_antes: f64,
187
+ pub qe_depois: f64,
188
+ pub qe_alocacao_uniforme: f64,
189
+ pub carga_por_celula: Vec<usize>,
190
+ pub tempo_treino_s: f64,
191
+ /// qualidade da DECODIFICAÇÃO após redirecionamento (mse w_bmu → x).
192
+ pub mse_decodificador: f64,
193
+ }
194
+
195
+ /// Refina um SOM 4D treinado com um MoE redirecionável:
196
+ /// 1. aloca especialistas por célula ∝ QE (water-filling — M-R5);
197
+ /// 2. treina o papel Codificador como corretor residual
198
+ /// `x̂ = w_bmu(x) + E(x)`;
199
+ /// 3. mede QE′; repete com alocação uniforme (ablação do water-filling);
200
+ /// 4. REDIRECIONA os especialistas para Decodificador e treina
201
+ /// `x̂ = E(w_bmu)` (reconstrução a partir do código) — mede o MSE.
202
+ pub fn refinar_som_com_moe(
203
+ dados: &Tensor,
204
+ centroides: &[Vec<f64>],
205
+ passos: usize,
206
+ semente: u64,
207
+ ) -> Result<RelatorioSomMoe> {
208
+ use std::time::Instant;
209
+ let t0 = Instant::now();
210
+ let dev = dados.device();
211
+ let n = dados.dim(0)?;
212
+ let d = dados.dim(1)?;
213
+ let m = centroides.len();
214
+ // BMU + QE por célula (tolera F32/F64)
215
+ let dados_v: Vec<Vec<f64>> = match dados.to_vec2::<f64>() {
216
+ Ok(v) => v,
217
+ Err(_) => dados
218
+ .to_vec2::<f32>()?
219
+ .iter()
220
+ .map(|l| l.iter().map(|x| *x as f64).collect())
221
+ .collect(),
222
+ };
223
+ let bmu: Vec<usize> = dados_v
224
+ .iter()
225
+ .map(|x| {
226
+ centroides
227
+ .iter()
228
+ .enumerate()
229
+ .min_by(|(i, a), (j, b)| {
230
+ let da: f64 = a.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
231
+ let db: f64 = b.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
232
+ da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal).then(i.cmp(j))
233
+ })
234
+ .map(|(i, _)| i)
235
+ .unwrap_or(0)
236
+ })
237
+ .collect();
238
+ let mut qe_cel = vec![0f64; m];
239
+ let mut cont_cel = vec![0usize; m];
240
+ for (i, b) in bmu.iter().enumerate() {
241
+ let dist: f64 = centroides[*b].iter().zip(&dados_v[i]).map(|(w, v)| (w - v).powi(2)).sum();
242
+ qe_cel[*b] += dist.sqrt();
243
+ cont_cel[*b] += 1;
244
+ }
245
+ for i in 0..m {
246
+ if cont_cel[i] > 0 {
247
+ qe_cel[i] /= cont_cel[i] as f64;
248
+ }
249
+ }
250
+ let qe_antes: f64 = dados_v
251
+ .iter()
252
+ .enumerate()
253
+ .map(|(i, b)| {
254
+ let w = &centroides[bmu[i]];
255
+ (w.iter().zip(b).map(|(a, v)| (a - v).powi(2)).sum::<f64>()).sqrt()
256
+ })
257
+ .sum::<f64>()
258
+ / n as f64;
259
+
260
+ // ---------- braço water-filling ----------
261
+ // capacidade = m células + 8 especialistas EXTRA distribuídos ∝ QE
262
+ // (com C < m o water-filling degenera em uniforme — mínimo 1 por célula)
263
+ let capacidade_total = m + 8;
264
+ let aloca_wf = alocar_por_qe(&qe_cel, capacidade_total);
265
+ let qe_wf = treinar_corretor(dados, centroides, &bmu, &aloca_wf, passos, semente, &dev)?;
266
+ // ---------- braço uniforme (ablação: MESMO total e MESMA semente —
267
+ // única diferença é a alocação; round-robin soma exatamente C) ----------
268
+ let aloca_un = {
269
+ let mut a = vec![1usize; m];
270
+ let mut i = 0usize;
271
+ while a.iter().sum::<usize>() < capacidade_total {
272
+ a[i % m] += 1;
273
+ i += 1;
274
+ }
275
+ a
276
+ };
277
+ let qe_un = treinar_corretor(dados, centroides, &bmu, &aloca_un, passos, semente, &dev)?;
278
+
279
+ // ---------- redirecionamento → decodificador ----------
280
+ // mesma pool de especialistas, agora aprendendo w_bmu → x
281
+ let mse_dec = treinar_decodificador(dados, centroides, &bmu, &aloca_wf, passos, semente + 2, &dev)?;
282
+
283
+ Ok(RelatorioSomMoe {
284
+ qe_antes,
285
+ qe_depois: qe_wf,
286
+ qe_alocacao_uniforme: qe_un,
287
+ carga_por_celula: aloca_wf,
288
+ tempo_treino_s: t0.elapsed().as_secs_f64(),
289
+ mse_decodificador: mse_dec,
290
+ })
291
+ }
292
+
293
+ /// Corretor residual: x̂ = w_bmu + E_{célula}(x). Treina com AdamW.
294
+ fn treinar_corretor(
295
+ dados: &Tensor,
296
+ centroides: &[Vec<f64>],
297
+ bmu: &[usize],
298
+ alocacao: &[usize],
299
+ passos: usize,
300
+ semente: u64,
301
+ dev: &Device,
302
+ ) -> Result<f64> {
303
+ let n = dados.dim(0)?;
304
+ let d = dados.dim(1)?;
305
+ // um "especialista" = MlpJepa por CÉLULA (capacidade alocada = nº de
306
+ // especialistas extra treinados nessa célula, somados em ensemble)
307
+ let n_esp_tot: usize = alocacao.iter().sum();
308
+ let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
309
+ for c in alocacao {
310
+ let mut v = Vec::new();
311
+ for _ in 0..(*c).max(1) {
312
+ v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
313
+ }
314
+ ensembles.push(v);
315
+ }
316
+ let mut vars: Vec<Var> = Vec::new();
317
+ for ens in &ensembles {
318
+ for e in ens {
319
+ vars.extend(e.vars());
320
+ }
321
+ }
322
+ let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
323
+ // tensores de apoio
324
+ let w_bmu = {
325
+ let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
326
+ Tensor::from_vec(v, (n, d), dev)?
327
+ };
328
+ let alvo_res = (dados - &w_bmu)?;
329
+ for _ in 0..passos {
330
+ let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
331
+ for (i, ens) in ensembles.iter().enumerate() {
332
+ // tokens da célula i
333
+ let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
334
+ if idx.is_empty() {
335
+ continue;
336
+ }
337
+ let m_idx = idx.len();
338
+ let it = Tensor::from_vec(idx, (m_idx,), dev)?;
339
+ let x_c = dados.index_select(&it, 0)?;
340
+ let r_c = alvo_res.index_select(&it, 0)?;
341
+ // ensemble: média das saídas
342
+ let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
343
+ for e in ens {
344
+ y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
345
+ }
346
+ let k = ens.len() as f64;
347
+ let yh = y.affine(1.0 / k, 0.0)?;
348
+ let dif = (&yh - &r_c)?;
349
+ soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
350
+ }
351
+ let _ = n_esp_tot;
352
+ let grads = crate::treino::grads_achatados(&soma, &vars)?;
353
+ let mut g = grads;
354
+ AdamWManual::clipar(&mut g, 5.0);
355
+ opt.passo(g);
356
+ }
357
+ // QE refinado
358
+ let mut qe = 0f64;
359
+ for (i, ens) in ensembles.iter().enumerate() {
360
+ let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
361
+ if idx.is_empty() {
362
+ continue;
363
+ }
364
+ let m_idx = idx.len();
365
+ let it = Tensor::from_vec(idx, (m_idx,), dev)?;
366
+ let x_c = dados.index_select(&it, 0)?;
367
+ let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
368
+ for e in ens {
369
+ y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
370
+ }
371
+ let k = ens.len() as f64;
372
+ let yh = y.affine(1.0 / k, 0.0)?;
373
+ let xhat = (w_bmu.index_select(&it, 0)? + &yh)?;
374
+ let dif = (&x_c - &xhat)?;
375
+ qe += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
376
+ }
377
+ Ok((qe / (n as f64 * d as f64)).sqrt() * (d as f64).sqrt()) // QE médio por ponto (norma L2)
378
+ }
379
+
380
+ /// Decodificador (pós-redirecionamento): x̂ = E(w_bmu) — reconstrução
381
+ /// a partir apenas do código (centróide). Mede o MSE final.
382
+ fn treinar_decodificador(
383
+ dados: &Tensor,
384
+ centroides: &[Vec<f64>],
385
+ bmu: &[usize],
386
+ alocacao: &[usize],
387
+ passos: usize,
388
+ semente: u64,
389
+ dev: &Device,
390
+ ) -> Result<f64> {
391
+ let _ = semente;
392
+ let n = dados.dim(0)?;
393
+ let d = dados.dim(1)?;
394
+ let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
395
+ for c in alocacao {
396
+ let mut v = Vec::new();
397
+ for _ in 0..(*c).max(1) {
398
+ v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
399
+ }
400
+ ensembles.push(v);
401
+ }
402
+ let mut vars: Vec<Var> = Vec::new();
403
+ for ens in &ensembles {
404
+ for e in ens {
405
+ vars.extend(e.vars());
406
+ }
407
+ }
408
+ let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
409
+ let w_bmu = {
410
+ let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
411
+ Tensor::from_vec(v, (n, d), dev)?
412
+ };
413
+ for _ in 0..passos {
414
+ let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
415
+ for (i, ens) in ensembles.iter().enumerate() {
416
+ let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
417
+ if idx.is_empty() {
418
+ continue;
419
+ }
420
+ let m_idx = idx.len();
421
+ let it = Tensor::from_vec(idx, (m_idx,), dev)?;
422
+ let w_c = w_bmu.index_select(&it, 0)?;
423
+ let x_c = dados.index_select(&it, 0)?;
424
+ let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
425
+ for e in ens {
426
+ y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
427
+ }
428
+ let k = ens.len() as f64;
429
+ let yh = y.affine(1.0 / k, 0.0)?;
430
+ let dif = (&yh - &x_c)?;
431
+ soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
432
+ }
433
+ let grads = crate::treino::grads_achatados(&soma, &vars)?;
434
+ let mut g = grads;
435
+ AdamWManual::clipar(&mut g, 5.0);
436
+ opt.passo(g);
437
+ }
438
+ // MSE final do decodificador
439
+ let mut mse = 0f64;
440
+ for (i, ens) in ensembles.iter().enumerate() {
441
+ let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
442
+ if idx.is_empty() {
443
+ continue;
444
+ }
445
+ let m_idx = idx.len();
446
+ let it = Tensor::from_vec(idx, (m_idx,), dev)?;
447
+ let w_c = w_bmu.index_select(&it, 0)?;
448
+ let x_c = dados.index_select(&it, 0)?;
449
+ let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
450
+ for e in ens {
451
+ y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
452
+ }
453
+ let k = ens.len() as f64;
454
+ let yh = y.affine(1.0 / k, 0.0)?;
455
+ let dif = (&yh - &x_c)?;
456
+ mse += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
457
+ }
458
+ Ok(mse / (n as f64 * d as f64))
459
+ }
460
+
461
+ pub fn versao() -> &'static str {
462
+ "moe-redirecionavel-0.6.0"
463
+ }
src/pensamento/combinador.rs ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `pensamento::combinador` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Combinador algébrico (T-P3 — projeção ridge, Teo V-1' portado)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /// Projeção ridge: `r* = (WᵀW + εI)^{−1} Wᵀ r_alvo` — minimizador de
12
+ /// `‖W r − r_alvo‖² + ε‖r‖²` (o montador algébrico do "re-encaixe de peças").
13
+ ///
14
+ /// **Prova (T-P3):** a função é quadrática estritamente convexa (ε > 0);
15
+ /// o gradiente anula-se exatamente em `r*` acima; o resíduo mínimo é
16
+ /// `‖(I − W(WᵀW+εI)^{−1}Wᵀ) r_alvo‖` — análogo direto do Teo V-1'.
17
+ pub fn projetar_ridge(w: &Tensor, r_alvo: &Tensor, eps: f64) -> Result<Tensor> {
18
+ // w: (m, n), r_alvo: (m, d) ⇒ r*: (n, d)
19
+ let n = w.dim(1)?;
20
+ let dev = w.device();
21
+ let wt = w.transpose(0, 1)?;
22
+ let wtw = wt.matmul(w)?;
23
+ let ident = Tensor::eye(n, candle_core::DType::F32, &dev)?.affine(eps, 0.0)?;
24
+ let m = (&wtw + &ident)?;
25
+ let rhs = wt.matmul(r_alvo)?;
26
+ resolver_simetrica(&m, &rhs)
27
+ }
28
+
29
+ /// Resolve `M x = B` para M simétrica (n, n) por eliminação de Gauss com
30
+ /// pivoteamento parcial (Cholesky não assumido — M pode ser só PSD+εI).
31
+ pub fn resolver_simetrica(m: &Tensor, b: &Tensor) -> Result<Tensor> {
32
+ let n = m.dim(0)?;
33
+ let d = b.dim(1)?;
34
+ let dev = m.device();
35
+ let mut a = m.to_vec2::<f32>()?;
36
+ let mut rhs = b.to_vec2::<f32>()?;
37
+ for col in 0..n {
38
+ // pivoteamento parcial
39
+ let (mut pivo, mut linha_p) = (a[col][col].abs(), col);
40
+ for r in col + 1..n {
41
+ if a[r][col].abs() > pivo {
42
+ pivo = a[r][col].abs();
43
+ linha_p = r;
44
+ }
45
+ }
46
+ if pivo < 1e-12 {
47
+ candle_core::bail!("matriz singular (pivo ~ 0 na coluna {col})");
48
+ }
49
+ a.swap(col, linha_p);
50
+ rhs.swap(col, linha_p);
51
+ for r in col + 1..n {
52
+ let f = a[r][col] / a[col][col];
53
+ for c in col..n {
54
+ a[r][c] -= f * a[col][c];
55
+ }
56
+ for c in 0..d {
57
+ rhs[r][c] -= f * rhs[col][c];
58
+ }
59
+ }
60
+ }
61
+ let mut x = vec![vec![0f32; d]; n];
62
+ for r in (0..n).rev() {
63
+ for c in 0..d {
64
+ let mut s = rhs[r][c];
65
+ for k in r + 1..n {
66
+ s -= a[r][k] * x[k][c];
67
+ }
68
+ x[r][c] = s / a[r][r];
69
+ }
70
+ }
71
+ let flat: Vec<f32> = x.into_iter().flatten().collect();
72
+ Ok(Tensor::from_vec(flat, (n, d), &dev)?)
73
+ }
src/pensamento/mod.rs ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Tecnologia Pensamento — módulos: motor, combinador ridge,
2
+ //! quebra-cabeça pós-treino (divisão SRP v0.10.0 — r-35 §2).
3
+
4
+ //! Tecnologia **Pensamento** v0.5.0 — N-redes neurais flexíveis paralelizadas
5
+ //! integradas ao NCM, conectadas ao Mecanismo de Atenção, com raciocínio
6
+ //! temporal-estratégico (item d = a + b + c).
7
+ //!
8
+ //! **O que é (motivação do usuário):** Pensamento é semelhante ao
9
+ //! pós-treinamento — N-redes paralelas de tentativas cooperativas para
10
+ //! resolver o problema do item c (requisição do usuário) quando a
11
+ //! informação tem POUCA CONFIANÇA para gerar novo conhecimento (fora dos
12
+ //! padrões comuns), a partir de:
13
+ //! - **a)** conhecimento aprendido durante o treinamento;
14
+ //! - **b)** estado de inferência corrente;
15
+ //! - **c)** a requisição (prompt) do usuário;
16
+ //! e da ativação de **difusores** (exploração estocástica) e
17
+ //! **combinadores/montadores** (re-encaixe de peças por projeção algébrica).
18
+ //!
19
+ //! **Ciclo d (raciocínio temporal-estratégico):**
20
+ //! `planejamento → construção de etapas e distribuição/desmembramento de
21
+ //! competências → volta ao planejamento OU envio da resposta ao usuário`.
22
+ //!
23
+ //! **Provas (completas em `docs/memorias/r-21-provas-pensamento-v050.md`):**
24
+ //! - **T-P1 (gatilho mensurável e causal):** o gatilho
25
+ //! `1{conf(p) < τ_c}` com `conf = 1 − H(p)/log K` é função apenas da
26
+ //! distribuição preditiva CORRENTE — não transporta informação do futuro,
27
+ //! preservando a causalidade (mesma estrutura do gate G1 do NCM).
28
+ //! - **T-P2 (tentativas paralelas — auto-consistência):** com N tentativas
29
+ //! independentes, cada uma correta com prob. `p > 1/2`, a votação por
30
+ //! maioria tem `P(erro) ≤ exp(−2N(p−1/2)²)` (Hoeffding); com seleção
31
+ //! best-of-N por verificador exato, `P(sucesso) ≥ 1−(1−p)^N`.
32
+ //! - **T-P3 (combinador ótimo):** o montador por mínimos quadrados com
33
+ //! regularização ridge resolve `r* = (WᵀW+εI)^{−1}Wᵀr_alvo` — o resíduo é
34
+ //! o MÍNIMO possível na norma (pseudo-inversa, Teo V-1' do outro agente).
35
+ //! - **T-P4 (terminação e soundness do planejador):** o potencial
36
+ //! `Φ(k) = B − k` decresce estritamente a cada iteração do ciclo d ⇒ o
37
+ //! loop termina em ≤ B iterações (indução bem-fundada); a resposta só é
38
+ //! emitida com confiança ≥ τ_c OU com orçamento esgotado (declaração
39
+ //! honesta de queda de confiança na telemetria).
40
+ //! - **T-P5 (neutralidade no nascimento na atenção):** com o gate de
41
+ //! atenção das tentativas inicializado em ZERO, a saída da atenção é
42
+ //! bit-a-bit a da atenção base; a perturbação é limitada por
43
+ //! `‖Δy‖ ≤ L·|g|·‖W_k z‖` (Lipschitz com constante L do softmax).
44
+ //! - **T-P6 (composição d = a + b + c):** o condicionamento de cada
45
+ //! tentativa é `cond = [a; b; c]` — a estrutura de independência
46
+ //! condicional `p(resposta | a, b, c)` fatoriza pelo estado conjunto; as
47
+ //! tentativas só veem o PASSADO (causalidade preservada).
48
+
49
+ use candle_core::{Device, Result, Tensor, Var};
50
+ use crate::camadas::Linear;
51
+ use crate::jepa::MlpJepa;
52
+ use crate::rng::RngDeterministico;
53
+ use std::collections::HashMap;
54
+
55
+ // ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
56
+ #[cfg(test)]
57
+ mod testes;
58
+ mod combinador;
59
+ mod motor;
60
+ mod quebra_cabeca;
61
+
62
+ pub use combinador::*;
63
+ pub use motor::*;
64
+ pub use quebra_cabeca::*;
65
+
66
+ // ---- itens mantidos no próprio mod.rs ----
67
+
68
+
69
+ pub fn versao() -> &'static str {
70
+ "pensamento-0.7.0"
71
+ }
src/{pensamento.rs → pensamento/motor.rs} RENAMED
@@ -1,53 +1,8 @@
1
- //! Tecnologia **Pensamento** v0.5.0 — N-redes neurais flexíveis paralelizadas
2
- //! integradas ao NCM, conectadas ao Mecanismo de Atenção, com raciocínio
3
- //! temporal-estratégico (item d = a + b + c).
4
- //!
5
- //! **O que é (motivação do usuário):** Pensamento é semelhante ao
6
- //! pós-treinamento — N-redes paralelas de tentativas cooperativas para
7
- //! resolver o problema do item c (requisição do usuário) quando a
8
- //! informação tem POUCA CONFIANÇA para gerar novo conhecimento (fora dos
9
- //! padrões comuns), a partir de:
10
- //! - **a)** conhecimento aprendido durante o treinamento;
11
- //! - **b)** estado de inferência corrente;
12
- //! - **c)** a requisição (prompt) do usuário;
13
- //! e da ativação de **difusores** (exploração estocástica) e
14
- //! **combinadores/montadores** (re-encaixe de peças por projeção algébrica).
15
- //!
16
- //! **Ciclo d (raciocínio temporal-estratégico):**
17
- //! `planejamento → construção de etapas e distribuição/desmembramento de
18
- //! competências → volta ao planejamento OU envio da resposta ao usuário`.
19
- //!
20
- //! **Provas (completas em `docs/memorias/r-21-provas-pensamento-v050.md`):**
21
- //! - **T-P1 (gatilho mensurável e causal):** o gatilho
22
- //! `1{conf(p) < τ_c}` com `conf = 1 − H(p)/log K` é função apenas da
23
- //! distribuição preditiva CORRENTE — não transporta informação do futuro,
24
- //! preservando a causalidade (mesma estrutura do gate G1 do NCM).
25
- //! - **T-P2 (tentativas paralelas — auto-consistência):** com N tentativas
26
- //! independentes, cada uma correta com prob. `p > 1/2`, a votação por
27
- //! maioria tem `P(erro) ≤ exp(−2N(p−1/2)²)` (Hoeffding); com seleção
28
- //! best-of-N por verificador exato, `P(sucesso) ≥ 1−(1−p)^N`.
29
- //! - **T-P3 (combinador ótimo):** o montador por mínimos quadrados com
30
- //! regularização ridge resolve `r* = (WᵀW+εI)^{−1}Wᵀr_alvo` — o resíduo é
31
- //! o MÍNIMO possível na norma (pseudo-inversa, Teo V-1' do outro agente).
32
- //! - **T-P4 (terminação e soundness do planejador):** o potencial
33
- //! `Φ(k) = B − k` decresce estritamente a cada iteração do ciclo d ⇒ o
34
- //! loop termina em ≤ B iterações (indução bem-fundada); a resposta só é
35
- //! emitida com confiança ≥ τ_c OU com orçamento esgotado (declaração
36
- //! honesta de queda de confiança na telemetria).
37
- //! - **T-P5 (neutralidade no nascimento na atenção):** com o gate de
38
- //! atenção das tentativas inicializado em ZERO, a saída da atenção é
39
- //! bit-a-bit a da atenção base; a perturbação é limitada por
40
- //! `‖Δy‖ ≤ L·|g|·‖W_k z‖` (Lipschitz com constante L do softmax).
41
- //! - **T-P6 (composição d = a + b + c):** o condicionamento de cada
42
- //! tentativa é `cond = [a; b; c]` — a estrutura de independência
43
- //! condicional `p(resposta | a, b, c)` fatoriza pelo estado conjunto; as
44
- //! tentativas só veem o PASSADO (causalidade preservada).
45
 
46
- use candle_core::{Device, Result, Tensor, Var};
47
- use crate::camadas::Linear;
48
- use crate::jepa::MlpJepa;
49
- use crate::rng::RngDeterministico;
50
- use std::collections::HashMap;
51
 
52
  // ---------------------------------------------------------------------------
53
  // Confiança e gatilho (T-P1)
@@ -592,601 +547,3 @@ impl MotorPensamento {
592
  }
593
  }
594
  }
595
-
596
- // ---------------------------------------------------------------------------
597
- // Combinador algébrico (T-P3 — projeção ridge, Teo V-1' portado)
598
- // ---------------------------------------------------------------------------
599
-
600
- /// Projeção ridge: `r* = (WᵀW + εI)^{−1} Wᵀ r_alvo` — minimizador de
601
- /// `‖W r − r_alvo‖² + ε‖r‖²` (o montador algébrico do "re-encaixe de peças").
602
- ///
603
- /// **Prova (T-P3):** a função é quadrática estritamente convexa (ε > 0);
604
- /// o gradiente anula-se exatamente em `r*` acima; o resíduo mínimo é
605
- /// `‖(I − W(WᵀW+εI)^{−1}Wᵀ) r_alvo‖` — análogo direto do Teo V-1'.
606
- pub fn projetar_ridge(w: &Tensor, r_alvo: &Tensor, eps: f64) -> Result<Tensor> {
607
- // w: (m, n), r_alvo: (m, d) ⇒ r*: (n, d)
608
- let n = w.dim(1)?;
609
- let dev = w.device();
610
- let wt = w.transpose(0, 1)?;
611
- let wtw = wt.matmul(w)?;
612
- let ident = Tensor::eye(n, candle_core::DType::F32, &dev)?.affine(eps, 0.0)?;
613
- let m = (&wtw + &ident)?;
614
- let rhs = wt.matmul(r_alvo)?;
615
- resolver_simetrica(&m, &rhs)
616
- }
617
-
618
- /// Resolve `M x = B` para M simétrica (n, n) por eliminação de Gauss com
619
- /// pivoteamento parcial (Cholesky não assumido — M pode ser só PSD+εI).
620
- pub fn resolver_simetrica(m: &Tensor, b: &Tensor) -> Result<Tensor> {
621
- let n = m.dim(0)?;
622
- let d = b.dim(1)?;
623
- let dev = m.device();
624
- let mut a = m.to_vec2::<f32>()?;
625
- let mut rhs = b.to_vec2::<f32>()?;
626
- for col in 0..n {
627
- // pivoteamento parcial
628
- let (mut pivo, mut linha_p) = (a[col][col].abs(), col);
629
- for r in col + 1..n {
630
- if a[r][col].abs() > pivo {
631
- pivo = a[r][col].abs();
632
- linha_p = r;
633
- }
634
- }
635
- if pivo < 1e-12 {
636
- candle_core::bail!("matriz singular (pivo ~ 0 na coluna {col})");
637
- }
638
- a.swap(col, linha_p);
639
- rhs.swap(col, linha_p);
640
- for r in col + 1..n {
641
- let f = a[r][col] / a[col][col];
642
- for c in col..n {
643
- a[r][c] -= f * a[col][c];
644
- }
645
- for c in 0..d {
646
- rhs[r][c] -= f * rhs[col][c];
647
- }
648
- }
649
- }
650
- let mut x = vec![vec![0f32; d]; n];
651
- for r in (0..n).rev() {
652
- for c in 0..d {
653
- let mut s = rhs[r][c];
654
- for k in r + 1..n {
655
- s -= a[r][k] * x[k][c];
656
- }
657
- x[r][c] = s / a[r][r];
658
- }
659
- }
660
- let flat: Vec<f32> = x.into_iter().flatten().collect();
661
- Ok(Tensor::from_vec(flat, (n, d), &dev)?)
662
- }
663
-
664
- pub fn versao() -> &'static str {
665
- "pensamento-0.7.0"
666
- }
667
-
668
- // ---------------------------------------------------------------------------
669
- // QUEBRA-CABEÇA pós-treinamento (v0.7.0 — inovação com o conhecimento
670
- // treinado; requisito do usuário; provas Q-1..Q-6 em r-31)
671
- // ---------------------------------------------------------------------------
672
-
673
- /// Configuração do quebra-cabeça (r-28 §4.3).
674
- #[derive(Clone, Copy, Debug)]
675
- pub struct ConfigQuebraCabeca {
676
- /// K peças em que o conhecimento é quebrado.
677
- pub n_pecas: usize,
678
- /// orçamento B de rodadas (terminação Q-4).
679
- pub n_rodadas_max: usize,
680
- /// limiar de validade (mesmo τ_c calibrado do verificador — Q-2).
681
- pub tau_c: f64,
682
- /// teto do ajuste por componente na árvore (regulação).
683
- pub delta_max: f64,
684
- /// remonta as peças com mistura ORTOGONAL por peça (rotação/ortogonalidade).
685
- pub rotacao_ortogonal: bool,
686
- /// para a execução após este nº de rodadas consecutivas sem aceite.
687
- pub paciencia: usize,
688
- }
689
-
690
- impl Default for ConfigQuebraCabeca {
691
- fn default() -> Self {
692
- ConfigQuebraCabeca {
693
- n_pecas: 4,
694
- n_rodadas_max: 12,
695
- tau_c: 0.55,
696
- delta_max: 0.05,
697
- rotacao_ortogonal: true,
698
- paciencia: 5,
699
- }
700
- }
701
- }
702
-
703
- impl ConfigQuebraCabeca {
704
- pub fn novo(n_pecas: usize, n_rodadas: usize, tau_c: f64) -> Self {
705
- ConfigQuebraCabeca {
706
- n_pecas,
707
- n_rodadas_max: n_rodadas,
708
- tau_c,
709
- delta_max: 0.05,
710
- rotacao_ortogonal: true,
711
- paciencia: (n_rodadas / 2).max(2),
712
- }
713
- }
714
- }
715
-
716
- /// Uma rodada do quebra-cabeça.
717
- #[derive(Debug, Clone)]
718
- pub struct RodadaQuebraCabeca {
719
- pub rodada: usize,
720
- /// permutação das peças aplicada na remontagem (reordenação).
721
- pub permutacao_pecas: Vec<usize>,
722
- /// confiança NCM do candidato remontado (reavaliação).
723
- pub confianca: f64,
724
- /// conf ≥ τ_c ⇒ VÁLIDO (ajusta a árvore); senão PUNIDO (descarta).
725
- pub aceito: bool,
726
- /// delta aplicado na árvore (0 quando descartado).
727
- pub delta_aplicado: f64,
728
- /// rotações ortogonais usadas (quando ativas) — por peça.
729
- pub usou_rotacao: bool,
730
- }
731
-
732
- /// Resultado consolidado do quebra-cabeça.
733
- #[derive(Debug, Clone)]
734
- pub struct ResultadoQuebraCabeca {
735
- pub rodadas: Vec<RodadaQuebraCabeca>,
736
- pub aceitos: usize,
737
- pub rejeitados: usize,
738
- pub confianca_base: f64,
739
- pub confianca_melhor: f64,
740
- pub telemetria: HashMap<String, f64>,
741
- }
742
-
743
- /// **Quebra-cabeça de inovação pós-treinamento** (r-28 §4.3):
744
- /// 1. quebra o conhecimento treinado `conceitos` (m, d) em K peças por
745
- /// permutação aleatória das linhas (Fisher–Yates com RNG determinístico);
746
- /// 2. remonta as peças em nova ordem (e com mistura ortogonal por peça
747
- /// quando `rotacao_ortogonal`) — "descobrir novas possibilidades";
748
- /// 3. reavalia cada remontagem pelo verificador NCM (calibrado — V-2);
749
- /// VÁLIDO (conf ≥ τ_c) ⇒ ajuste regulado na árvore de pesos indexada
750
- /// (nó "pensamento", crédito fundo-topo); PUNIDO ⇒ descarta e o incremento
751
- /// de punição sobe no nó (alimenta o gate do FNH — integração v0.7.0);
752
- /// 4. repete até o orçamento B ou a paciência esgotar (Q-4).
753
- ///
754
- /// **Provas (r-31):** Q-1 cobertura (permutações uniformes ⇒ S_K no suporte;
755
- /// rotações varrem O(d)); Q-2 soundness herdada (mesmo limiar calibrado do
756
- /// verificador — P(aceite inválido) ≤ 1−τ_c+ECE); Q-3 potencial monotônico
757
- /// (Φ_r = Σ conf dos aceitos é não-decrescente; rejeições não tocam Φ);
758
- /// Q-4 terminação (B finito, rodadas O(m·d)); Q-5 invariantes da árvore
759
- /// preservadas (ajustes só tocam métricas, nunca a estrutura — T-A1 intacto;
760
- /// agregação pós-ajuste mantém T-A2); Q-6 causalidade (permutação e confiança
761
- /// medidas antes do ajuste — filtração F_r).
762
- pub fn quebra_cabeca_inovar(
763
- conceitos: &Tensor,
764
- verificador: &crate::verificador::VerificadorNcm,
765
- logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>,
766
- arvore: &mut crate::arvore_pesos::ArvorePesos,
767
- cfg: &ConfigQuebraCabeca,
768
- rng: &mut RngDeterministico,
769
- ) -> Result<ResultadoQuebraCabeca> {
770
- let dev = conceitos.device().clone();
771
- let (m, d) = (conceitos.dims()[0], conceitos.dims()[1]);
772
- let k = cfg.n_pecas.clamp(1, m);
773
- let no_pens = arvore.no("pensamento").unwrap_or(0);
774
-
775
- // linha de base: confiança do conhecimento original (agregado médio)
776
- let base_agg = conceitos.mean(0)?;
777
- let conf_base = {
778
- let lg = logits_fn(&base_agg)?;
779
- verificador.confianca_1d(&lg)?
780
- };
781
-
782
- let mut rodadas = Vec::new();
783
- let mut aceitos = 0usize;
784
- let mut rejeitados = 0usize;
785
- let mut conf_melhor = conf_base;
786
- let mut sem_aceite = 0usize;
787
-
788
- for r in 1..=cfg.n_rodadas_max {
789
- if sem_aceite >= cfg.paciencia {
790
- break;
791
- }
792
- // (1) QUEBRA: permutação de linhas Fisher–Yates (Q-6: amostrada ANTES)
793
- let mut perm: Vec<usize> = (0..m).collect();
794
- for i in (1..m).rev() {
795
- let j = rng.inteiro(i + 1);
796
- perm.swap(i, j);
797
- }
798
- // fatia em K peças contíguas (tamanhos quase iguais)
799
- let tamanho = (m + k - 1) / k;
800
- let mut pecas = Vec::with_capacity(k);
801
- let mut ini = 0usize;
802
- while ini < m {
803
- let fim = (ini + tamanho).min(m);
804
- let idx: Vec<u32> = perm[ini..fim].iter().map(|&x| x as u32).collect();
805
- let pedaco = conceitos.index_select(&Tensor::from_vec(idx, (fim - ini,), &dev)?, 0)?;
806
- pecas.push(pedaco);
807
- ini = fim;
808
- }
809
- // (2) REMONTA: nova ordem aleatória das peças (permutação de peças)
810
- let mut perm_pecas: Vec<usize> = (0..pecas.len()).collect();
811
- for i in (1..pecas.len()).rev() {
812
- let j = rng.inteiro(i + 1);
813
- perm_pecas.swap(i, j);
814
- }
815
- let mut remontada = Vec::with_capacity(pecas.len());
816
- for &pi in &perm_pecas {
817
- let mut pedaco = pecas[pi].clone();
818
- if cfg.rotacao_ortogonal && d >= 2 {
819
- // mistura ortogonal da peça: Q ∈ O(d) via polar de gaussiana
820
- // do RNG determinístico (preserva normas — ‖xQ‖_F = ‖x‖_F)
821
- let q = q_ortogonal_deterministica(d, rng, &dev)?;
822
- pedaco = pedaco.matmul(&q)?;
823
- }
824
- remontada.push(pedaco);
825
- }
826
- let candidata = Tensor::cat(&remontada, 0)?; // (m, d) remontado
827
- // (3) REAVALIAÇÃO pelo verificador NCM (Q-2: mesmo limiar calibrado)
828
- let agg = candidata.mean(0)?;
829
- let lg = logits_fn(&agg)?;
830
- let conf = verificador.confianca_1d(&lg)?;
831
- let aceito = conf >= cfg.tau_c;
832
- let delta = if aceito {
833
- // VÁLIDO: ajusta a árvore bidirecional (indexada, com teto)
834
- let escalado = (conf - cfg.tau_c).min(cfg.delta_max);
835
- arvore.aplicar_ajuste_indexado(no_pens, "confianca", escalado, cfg.delta_max)
836
- } else {
837
- // PUNIDO: descarta; a punição sobe no nó (sinal para o gate FNH)
838
- rejeitados += 1;
839
- sem_aceite += 1;
840
- arvore.aplicar_ajuste_indexado(no_pens, "punicao", cfg.delta_max * 0.5, cfg.delta_max);
841
- 0.0
842
- };
843
- if aceito {
844
- aceitos += 1;
845
- sem_aceite = 0;
846
- conf_melhor = conf_melhor.max(conf);
847
- }
848
- rodadas.push(RodadaQuebraCabeca {
849
- rodada: r,
850
- permutacao_pecas: perm_pecas,
851
- confianca: conf,
852
- aceito,
853
- delta_aplicado: delta,
854
- usou_rotacao: cfg.rotacao_ortogonal,
855
- });
856
- }
857
- // crédito fundo-topo após todas as rodadas (T-A2)
858
- arvore.agregar_fundo_topo("confianca");
859
- arvore.agregar_fundo_topo("punicao");
860
-
861
- let n_rod = rodadas.len();
862
- let mut telemetria = HashMap::new();
863
- telemetria.insert("rodadas".into(), n_rod as f64);
864
- telemetria.insert("aceitos".into(), aceitos as f64);
865
- telemetria.insert("rejeitados".into(), rejeitados as f64);
866
- telemetria.insert("confianca_base".into(), conf_base);
867
- telemetria.insert("confianca_melhor".into(), conf_melhor);
868
- telemetria.insert(
869
- "taxa_aceitacao".into(),
870
- if n_rod > 0 { aceitos as f64 / n_rod as f64 } else { 0.0 },
871
- );
872
- telemetria.insert("rotacao_ativa".into(), if cfg.rotacao_ortogonal { 1.0 } else { 0.0 });
873
- Ok(ResultadoQuebraCabeca {
874
- rodadas,
875
- aceitos,
876
- rejeitados,
877
- confianca_base: conf_base,
878
- confianca_melhor: conf_melhor,
879
- telemetria,
880
- })
881
- }
882
-
883
- /// Q ∈ O(d) determinística: gaussiana do RNG próprio → retração polar.
884
- /// Ortogonal EXATA (δ_ort ≤ 1e-6) e reprodutível (mesma semente ⇒ mesmo Q).
885
- pub fn q_ortogonal_deterministica(
886
- d: usize,
887
- rng: &mut RngDeterministico,
888
- dev: &Device,
889
- ) -> Result<Tensor> {
890
- let n = d * d;
891
- let v: Vec<f32> = (0..n).map(|_| rng.normal_padrao() as f32).collect();
892
- let g = Tensor::from_vec(v, (d, d), dev)?;
893
- crate::stiefel::retrair_polar_frobenius(&g, 90)
894
- }
895
-
896
-
897
- #[cfg(test)]
898
- mod testes {
899
- use super::*;
900
-
901
- #[test]
902
- fn teste_confianca_extremos() {
903
- // T-P1: Dirac ⇒ conf = 1; uniforme ⇒ conf = 0
904
- let dev = Device::Cpu;
905
- let dirac = Tensor::from_vec(vec![100.0f32, 0.0, 0.0, 0.0], (1, 4), &dev).unwrap();
906
- let uniforme = Tensor::from_vec(vec![0.0f32; 4], (1, 4), &dev).unwrap();
907
- let c1 = confianca_de_logits(&dirac).unwrap();
908
- let c0 = confianca_de_logits(&uniforme).unwrap();
909
- assert!(c1 > 0.999, "conf dirac = {c1}");
910
- assert!(c0 < 0.001, "conf uniforme = {c0}");
911
- }
912
-
913
- #[test]
914
- fn teste_tp3_projetar_ridge_minimiza() {
915
- // T-P3: r* da projeção ridge tem resíduo ≤ qualquer perturbação
916
- let dev = Device::Cpu;
917
- let w = Tensor::randn(0f32, 1.0, (8, 3), &dev).unwrap();
918
- let r_alvo = Tensor::randn(0f32, 1.0, (8, 5), &dev).unwrap();
919
- let r = projetar_ridge(&w, &r_alvo, 1e-6).unwrap();
920
- let residuo = |rr: &Tensor| {
921
- let d = (w.matmul(rr).unwrap() - &r_alvo).unwrap();
922
- d.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64
923
- };
924
- let f0 = residuo(&r);
925
- let mut rng = RngDeterministico::novo(1);
926
- for _ in 0..30 {
927
- let pert: Vec<f32> = (0..15).map(|_| rng.normal(0.0, 0.1) as f32).collect();
928
- let rp = (Tensor::from_vec(pert, (3, 5), &dev).unwrap() + &r).unwrap();
929
- let fp = residuo(&rp);
930
- assert!(f0 <= fp + 1e-6, "residuo r* = {f0} > perturbado {fp}");
931
- }
932
- }
933
-
934
- #[test]
935
- fn teste_tp4_terminacao_e_orcamento() {
936
- // o ciclo d SEMPRE termina em ≤ orçamento iterações (potencial
937
- // Φ = B − k); com verificador impossível, termina com
938
- // orcamento_esgotado = 1.
939
- let dev = Device::Cpu;
940
- let mut motor = MotorPensamento::novo(6, 4, 3, 0.99, 5, &dev).unwrap();
941
- let cond = Tensor::randn(0f32, 1.0, (6,), &dev).unwrap();
942
- let mut rng = RngDeterministico::novo(2);
943
- let res = motor
944
- .raciocinar(&cond, &|_| 0.0, &mut rng) // verificador sempre 0
945
- .unwrap();
946
- assert!(res.iteracoes <= 5, "iterações = {} > orçamento", res.iteracoes);
947
- assert_eq!(res.telemetria.get("orcamento_esgotado"), Some(&1.0));
948
- assert!(res.resposta.is_some(), "melhor candidato ainda é retornado");
949
- }
950
-
951
- #[test]
952
- fn teste_tp5_neutralidade_no_nascimento() {
953
- // gate = 0 ⇒ saída da atenção integrada é BIT-A-BIT a da base
954
- let dev = Device::Cpu;
955
- let motor = MotorPensamento::novo(8, 8, 3, 0.5, 3, &dev).unwrap();
956
- let q = Tensor::randn(0f32, 1.0, (1, 4, 8), &dev).unwrap();
957
- let k = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
958
- let v = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
959
- let cand = Tensor::randn(0f32, 1.0, (3, 8), &dev).unwrap();
960
- let y_int = motor.atencao_integrada(&q, &k, &v, &cand).unwrap();
961
- // atenção base pura
962
- let scores = (q.matmul(&k.transpose(1, 2).unwrap()).unwrap() / (8f64).sqrt()).unwrap();
963
- let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1).unwrap();
964
- let y_base = attn.matmul(&v).unwrap();
965
- let dif = (&y_int - &y_base).unwrap().abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
966
- assert!(dif == 0.0, "gate=0 deveria dar bit-a-bit, dif = {dif}");
967
- }
968
-
969
- #[test]
970
- fn teste_treino_cooperativo_e_agregacao() {
971
- // item a: as tentativas + combinador APRENDEM a função alvo; a
972
- // agregação converge para o alvo melhor que uma tentativa isolada
973
- // no pior caso (verificação numérica honesta em escala pequena).
974
- let dev = Device::Cpu;
975
- let mut motor = MotorPensamento::novo(6, 4, 3, 0.5, 3, &dev).unwrap();
976
- let vars = motor.vars();
977
- let mut otim = crate::treino::AdamWManual::novo(vars, 5e-3, 0.0);
978
- let mut rng = RngDeterministico::novo(4);
979
- // alvo: função não-linear determinística de cond
980
- let dados: Vec<(Vec<f32>, Vec<f32>)> = (0..256)
981
- .map(|_| {
982
- let c: Vec<f32> = (0..6).map(|_| rng.normal(0.0, 1.0) as f32).collect();
983
- let a: Vec<f32> = (0..4)
984
- .map(|j| {
985
- (c[j % 6] * 0.5 + (c[(j + 1) % 6] * c[(j + 2) % 6]) * 0.3 + 0.1) as f32
986
- })
987
- .collect();
988
- (c, a)
989
- })
990
- .collect();
991
- let cs: Vec<f32> = dados.iter().flat_map(|(c, _)| c.clone()).collect();
992
- let as_: Vec<f32> = dados.iter().flat_map(|(_, a)| a.clone()).collect();
993
- let cond = Tensor::from_vec(cs, (256, 6), &dev).unwrap();
994
- let alvo = Tensor::from_vec(as_, (256, 4), &dev).unwrap();
995
- let mut perda0 = f64::NAN;
996
- let mut perda1 = f64::NAN;
997
- for passo in 0..200 {
998
- let perda = motor.perda_treino(&cond, &alvo).unwrap();
999
- if passo == 0 {
1000
- perda0 = perda.to_scalar::<f32>().unwrap() as f64;
1001
- }
1002
- perda1 = perda.to_scalar::<f32>().unwrap() as f64;
1003
- otim.passo(crate::treino::grads_achatados(&perda, &motor.vars()).unwrap());
1004
- }
1005
- assert!(perda1 < perda0 * 0.5, "treino cooperativo: {perda0} → {perda1}");
1006
- // agregação: verificador = negativo do erro quadrático médio
1007
- let mut rng2 = RngDeterministico::novo(9);
1008
- let cand = motor.candidatos(&cond.get(0).unwrap(), 0.0, &mut rng2).unwrap();
1009
- let (resp, pesos) = motor.agregar(&cand).unwrap();
1010
- assert_eq!(resp.dims().to_vec(), vec![1, 4]);
1011
- let soma: f64 = pesos.iter().sum();
1012
- assert!((soma - 1.0).abs() < 1e-9, "pesos somam {soma}");
1013
- assert!(pesos.iter().all(|&p| p >= 0.0));
1014
- }
1015
-
1016
- #[test]
1017
- fn teste_tp2_auto_consistencia_numérica() {
1018
- // T-P2 numérico: N tentativas com viés comum pequeno; a média
1019
- // agregada tem erro menor que a tentativa mediana (redução de
1020
- // variância) na maioria das repetições.
1021
- let dev = Device::Cpu;
1022
- let mut rng = RngDeterministico::novo(6);
1023
- let alvo = [1.0f64, 2.0, -1.0, 0.5];
1024
- let mut venceu = 0;
1025
- let reps = 200;
1026
- for _ in 0..reps {
1027
- let n = 7;
1028
- let erros: Vec<f64> = (0..n)
1029
- .map(|_| {
1030
- alvo
1031
- .iter()
1032
- .map(|&a| {
1033
- let ruido = rng.normal(0.0, 1.0);
1034
- (a + ruido - a).abs()
1035
- })
1036
- .sum::<f64>()
1037
- })
1038
- .collect();
1039
- let media: Vec<f64> = (0..4)
1040
- .map(|j| {
1041
- (0..n).map(|i| alvo[j] + rng.normal(0.0, 1.0)).sum::<f64>() / n as f64
1042
- })
1043
- .collect();
1044
- let erro_media: f64 = media.iter().zip(alvo.iter()).map(|(m, a)| (m - a).abs()).sum();
1045
- let mut ordenado = erros.clone();
1046
- ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());
1047
- let mediana = ordenado[n / 2];
1048
- if erro_media <= mediana {
1049
- venceu += 1;
1050
- }
1051
- }
1052
- // a média vence a mediana na grande maioria dos casos (E[erro da
1053
- // média] = σ/√n < σ = E[erro de UMA tentativa])
1054
- assert!(
1055
- venceu as f64 / reps as f64 >= 0.6,
1056
- "auto-consistência venceu só {}/{}",
1057
- venceu,
1058
- reps
1059
- );
1060
- }
1061
-
1062
- // ------------------------------------------------------------------
1063
- // Quebra-cabeça (v0.7.0) — gates Q-1..Q-6 (provas r-31)
1064
- // ------------------------------------------------------------------
1065
-
1066
- fn cenario_quebra_cabeca()
1067
- -> (Tensor, crate::verificador::VerificadorNcm, crate::arvore_pesos::ArvorePesos) {
1068
- let dev = Device::Cpu;
1069
- let mut rng = RngDeterministico::novo(777);
1070
- // "conhecimento treinado": 12 conceitos (d=8) com estrutura
1071
- let m = 12usize;
1072
- let d = 8usize;
1073
- let v: Vec<f32> = (0..m * d).map(|i| rng.normal_padrao() as f32).collect();
1074
- let conceitos = Tensor::from_vec(v, (m, d), &dev).unwrap();
1075
- // verificador calibrado com um "verificador" sintético: logits
1076
- // favoráveis ao agregado médio dos conceitos
1077
- let mut verif = crate::verificador::VerificadorNcm::novo();
1078
- let _ = &mut verif;
1079
- let mut arvore = crate::arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
1080
- let _ = &mut arvore;
1081
- (conceitos, verif, arvore)
1082
- }
1083
-
1084
- fn logits_fn_exemplo(x: &Tensor) -> Result<Tensor> {
1085
- // logits determinísticos correlacionados com a norma do agregado —
1086
- // quanto mais "concentrado" o agregado, maior a confiança
1087
- let norma = x.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
1088
- let base = (norma * 2.0).min(8.0);
1089
- let v: Vec<f32> = vec![base as f32, 0.5, 0.2, 0.1];
1090
- Ok(Tensor::from_vec(v, (4,), x.device())?)
1091
- }
1092
-
1093
- /// Q-2/Q-3: todo ACEITO tem conf ≥ τ_c; rejeições não aplicam delta;
1094
- /// potencial monotônico (confiança acumulada só cresce nos aceites).
1095
- #[test]
1096
- fn quebra_cabeca_soundness_e_potencial() -> Result<()> {
1097
- let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
1098
- let cfg = ConfigQuebraCabeca::novo(4, 10, 0.55);
1099
- let mut rng = RngDeterministico::novo(4242);
1100
- let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
1101
- for rod in &r.rodadas {
1102
- if rod.aceito {
1103
- assert!(
1104
- rod.confianca >= cfg.tau_c,
1105
- "Q-2: aceito com conf {} < τ_c {}",
1106
- rod.confianca,
1107
- cfg.tau_c
1108
- );
1109
- assert!(rod.delta_aplicado > 0.0, "aceite aplica delta > 0");
1110
- } else {
1111
- assert_eq!(rod.delta_aplicado, 0.0, "Q-3: rejeitado não aplica delta");
1112
- }
1113
- }
1114
- // potencial monotônico: Σ conf dos aceitos ≤ Σ max(conf, τ_c) das rodadas
1115
- let pot: f64 = r.rodadas.iter().filter(|x| x.aceito).map(|x| x.confianca).sum();
1116
- assert!(pot >= 0.0);
1117
- // Q-5: invariantes da árvore preservadas
1118
- arvore.invariantes_ok().expect("Q-5: invariantes T-A1 preservadas");
1119
- Ok(())
1120
- }
1121
-
1122
- /// Q-4: terminação — nunca excede n_rodadas_max nem roda sem parar.
1123
- #[test]
1124
- fn quebra_cabeca_termina() -> Result<()> {
1125
- let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
1126
- let cfg = ConfigQuebraCabeca::novo(3, 6, 0.99); // τ inalcançável
1127
- let mut rng = RngDeterministico::novo(1);
1128
- let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
1129
- assert!(r.rodadas.len() <= 6, "Q-4: orçamento respeitado");
1130
- assert_eq!(r.aceitos, 0, "τ inalcançável ⇒ nenhum aceite");
1131
- assert!(r.rejeitados > 0);
1132
- Ok(())
1133
- }
1134
-
1135
- /// Q-4b: rejeições incrementam a PUNIÇÃO no nó pensamento (sinal FNH).
1136
- #[test]
1137
- fn quebra_cabeca_punicao_sobe() -> Result<()> {
1138
- let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
1139
- let no_pens = arvore.no("pensamento").unwrap();
1140
- let antes = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
1141
- let cfg = ConfigQuebraCabeca::novo(3, 5, 0.999);
1142
- let mut rng = RngDeterministico::novo(9);
1143
- let _ = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
1144
- let depois = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
1145
- assert!(depois > antes, "punição {depois} deve > {antes} após rejeições");
1146
- Ok(())
1147
- }
1148
-
1149
- /// Rotação/ortogonalidade: ‖xQ‖_F = ‖x‖_F (preserva energia) e a
1150
- /// remontagem com rotação difere da sem rotação (exploração real).
1151
- #[test]
1152
- fn quebra_cabeca_rotacao_preserva_norma() -> Result<()> {
1153
- let dev = Device::Cpu;
1154
- let mut rng = RngDeterministico::novo(31);
1155
- let v: Vec<f32> = (0..5 * 6).map(|_| rng.normal_padrao() as f32).collect();
1156
- let x = Tensor::from_vec(v, (5, 6), &dev)?;
1157
- let q = q_ortogonal_deterministica(6, &mut rng, &dev)?;
1158
- let xq = x.matmul(&q)?;
1159
- let nx = x.sqr()?.sum_all()?.to_scalar::<f32>()?;
1160
- let nxq = xq.sqr()?.sum_all()?.to_scalar::<f32>()?;
1161
- assert!(
1162
- (nx - nxq).abs() / nx < 1e-4,
1163
- "‖xQ‖_F = {nxq} deve ≈ ‖x‖_F = {nx} (ortogonalidade)"
1164
- );
1165
- // Q é ortogonal exato
1166
- let delta = crate::stiefel::erro_ortogonalidade(&q)?;
1167
- assert!(delta < 1e-6, "δ_ort(Q) = {delta}");
1168
- // remontagem COM rotação difere da SEM rotação
1169
- let d2 = (&xq - &x)?.abs()?.max_all()?.to_scalar::<f32>()?;
1170
- assert!(d2 > 1e-3, "rotação deve explorar (dif = {d2})");
1171
- Ok(())
1172
- }
1173
-
1174
- /// Determinismo: mesma semente ⇒ mesmas rodadas (reprodutibilidade).
1175
- #[test]
1176
- fn quebra_cabeca_deterministico() -> Result<()> {
1177
- let (c1, v1, mut a1) = cenario_quebra_cabeca();
1178
- let (_, v2, mut a2) = cenario_quebra_cabeca();
1179
- let cfg = ConfigQuebraCabeca::novo(4, 8, 0.5);
1180
- let mut r1 = RngDeterministico::novo(123);
1181
- let mut r2 = RngDeterministico::novo(123);
1182
- let q1 = quebra_cabeca_inovar(&c1, &v1, &logits_fn_exemplo, &mut a1, &cfg, &mut r1)?;
1183
- let q2 = quebra_cabeca_inovar(&c1, &v2, &logits_fn_exemplo, &mut a2, &cfg, &mut r2)?;
1184
- assert_eq!(q1.rodadas.len(), q2.rodadas.len());
1185
- for (x, y) in q1.rodadas.iter().zip(q2.rodadas.iter()) {
1186
- assert_eq!(x.permutacao_pecas, y.permutacao_pecas, "mesma semente ⇒ mesma remontagem");
1187
- assert!((x.confianca - y.confianca).abs() < 1e-12);
1188
- assert_eq!(x.aceito, y.aceito);
1189
- }
1190
- Ok(())
1191
- }
1192
- }
 
1
+ //! `pensamento::motor` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
 
 
 
 
 
6
 
7
  // ---------------------------------------------------------------------------
8
  // Confiança e gatilho (T-P1)
 
547
  }
548
  }
549
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/pensamento/quebra_cabeca.rs ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `pensamento::quebra_cabeca` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // QUEBRA-CABEÇA pós-treinamento (v0.7.0 — inovação com o conhecimento
9
+ // treinado; requisito do usuário; provas Q-1..Q-6 em r-31)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /// Configuração do quebra-cabeça (r-28 §4.3).
13
+ #[derive(Clone, Copy, Debug)]
14
+ pub struct ConfigQuebraCabeca {
15
+ /// K peças em que o conhecimento é quebrado.
16
+ pub n_pecas: usize,
17
+ /// orçamento B de rodadas (terminação Q-4).
18
+ pub n_rodadas_max: usize,
19
+ /// limiar de validade (mesmo τ_c calibrado do verificador — Q-2).
20
+ pub tau_c: f64,
21
+ /// teto do ajuste por componente na árvore (regulação).
22
+ pub delta_max: f64,
23
+ /// remonta as peças com mistura ORTOGONAL por peça (rotação/ortogonalidade).
24
+ pub rotacao_ortogonal: bool,
25
+ /// para a execução após este nº de rodadas consecutivas sem aceite.
26
+ pub paciencia: usize,
27
+ }
28
+
29
+ impl Default for ConfigQuebraCabeca {
30
+ fn default() -> Self {
31
+ ConfigQuebraCabeca {
32
+ n_pecas: 4,
33
+ n_rodadas_max: 12,
34
+ tau_c: 0.55,
35
+ delta_max: 0.05,
36
+ rotacao_ortogonal: true,
37
+ paciencia: 5,
38
+ }
39
+ }
40
+ }
41
+
42
+ impl ConfigQuebraCabeca {
43
+ pub fn novo(n_pecas: usize, n_rodadas: usize, tau_c: f64) -> Self {
44
+ ConfigQuebraCabeca {
45
+ n_pecas,
46
+ n_rodadas_max: n_rodadas,
47
+ tau_c,
48
+ delta_max: 0.05,
49
+ rotacao_ortogonal: true,
50
+ paciencia: (n_rodadas / 2).max(2),
51
+ }
52
+ }
53
+ }
54
+
55
+ /// Uma rodada do quebra-cabeça.
56
+ #[derive(Debug, Clone)]
57
+ pub struct RodadaQuebraCabeca {
58
+ pub rodada: usize,
59
+ /// permutação das peças aplicada na remontagem (reordenação).
60
+ pub permutacao_pecas: Vec<usize>,
61
+ /// confiança NCM do candidato remontado (reavaliação).
62
+ pub confianca: f64,
63
+ /// conf ≥ τ_c ⇒ VÁLIDO (ajusta a árvore); senão PUNIDO (descarta).
64
+ pub aceito: bool,
65
+ /// delta aplicado na árvore (0 quando descartado).
66
+ pub delta_aplicado: f64,
67
+ /// rotações ortogonais usadas (quando ativas) — por peça.
68
+ pub usou_rotacao: bool,
69
+ }
70
+
71
+ /// Resultado consolidado do quebra-cabeça.
72
+ #[derive(Debug, Clone)]
73
+ pub struct ResultadoQuebraCabeca {
74
+ pub rodadas: Vec<RodadaQuebraCabeca>,
75
+ pub aceitos: usize,
76
+ pub rejeitados: usize,
77
+ pub confianca_base: f64,
78
+ pub confianca_melhor: f64,
79
+ pub telemetria: HashMap<String, f64>,
80
+ }
81
+
82
+ /// **Quebra-cabeça de inovação pós-treinamento** (r-28 §4.3):
83
+ /// 1. quebra o conhecimento treinado `conceitos` (m, d) em K peças por
84
+ /// permutação aleatória das linhas (Fisher–Yates com RNG determinístico);
85
+ /// 2. remonta as peças em nova ordem (e com mistura ortogonal por peça
86
+ /// quando `rotacao_ortogonal`) — "descobrir novas possibilidades";
87
+ /// 3. reavalia cada remontagem pelo verificador NCM (calibrado — V-2);
88
+ /// VÁLIDO (conf ≥ τ_c) ⇒ ajuste regulado na árvore de pesos indexada
89
+ /// (nó "pensamento", crédito fundo-topo); PUNIDO ⇒ descarta e o incremento
90
+ /// de punição sobe no nó (alimenta o gate do FNH — integração v0.7.0);
91
+ /// 4. repete até o orçamento B ou a paciência esgotar (Q-4).
92
+ ///
93
+ /// **Provas (r-31):** Q-1 cobertura (permutações uniformes ⇒ S_K no suporte;
94
+ /// rotações varrem O(d)); Q-2 soundness herdada (mesmo limiar calibrado do
95
+ /// verificador — P(aceite inválido) ≤ 1−τ_c+ECE); Q-3 potencial monotônico
96
+ /// (Φ_r = Σ conf dos aceitos é não-decrescente; rejeições não tocam Φ);
97
+ /// Q-4 terminação (B finito, rodadas O(m·d)); Q-5 invariantes da árvore
98
+ /// preservadas (ajustes só tocam métricas, nunca a estrutura — T-A1 intacto;
99
+ /// agregação pós-ajuste mantém T-A2); Q-6 causalidade (permutação e confiança
100
+ /// medidas antes do ajuste — filtração F_r).
101
+ pub fn quebra_cabeca_inovar(
102
+ conceitos: &Tensor,
103
+ verificador: &crate::verificador::VerificadorNcm,
104
+ logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>,
105
+ arvore: &mut crate::arvore_pesos::ArvorePesos,
106
+ cfg: &ConfigQuebraCabeca,
107
+ rng: &mut RngDeterministico,
108
+ ) -> Result<ResultadoQuebraCabeca> {
109
+ let dev = conceitos.device().clone();
110
+ let (m, d) = (conceitos.dims()[0], conceitos.dims()[1]);
111
+ let k = cfg.n_pecas.clamp(1, m);
112
+ let no_pens = arvore.no("pensamento").unwrap_or(0);
113
+
114
+ // linha de base: confiança do conhecimento original (agregado médio)
115
+ let base_agg = conceitos.mean(0)?;
116
+ let conf_base = {
117
+ let lg = logits_fn(&base_agg)?;
118
+ verificador.confianca_1d(&lg)?
119
+ };
120
+
121
+ let mut rodadas = Vec::new();
122
+ let mut aceitos = 0usize;
123
+ let mut rejeitados = 0usize;
124
+ let mut conf_melhor = conf_base;
125
+ let mut sem_aceite = 0usize;
126
+
127
+ for r in 1..=cfg.n_rodadas_max {
128
+ if sem_aceite >= cfg.paciencia {
129
+ break;
130
+ }
131
+ // (1) QUEBRA: permutação de linhas Fisher–Yates (Q-6: amostrada ANTES)
132
+ let mut perm: Vec<usize> = (0..m).collect();
133
+ for i in (1..m).rev() {
134
+ let j = rng.inteiro(i + 1);
135
+ perm.swap(i, j);
136
+ }
137
+ // fatia em K peças contíguas (tamanhos quase iguais)
138
+ let tamanho = (m + k - 1) / k;
139
+ let mut pecas = Vec::with_capacity(k);
140
+ let mut ini = 0usize;
141
+ while ini < m {
142
+ let fim = (ini + tamanho).min(m);
143
+ let idx: Vec<u32> = perm[ini..fim].iter().map(|&x| x as u32).collect();
144
+ let pedaco = conceitos.index_select(&Tensor::from_vec(idx, (fim - ini,), &dev)?, 0)?;
145
+ pecas.push(pedaco);
146
+ ini = fim;
147
+ }
148
+ // (2) REMONTA: nova ordem aleatória das peças (permutação de peças)
149
+ let mut perm_pecas: Vec<usize> = (0..pecas.len()).collect();
150
+ for i in (1..pecas.len()).rev() {
151
+ let j = rng.inteiro(i + 1);
152
+ perm_pecas.swap(i, j);
153
+ }
154
+ let mut remontada = Vec::with_capacity(pecas.len());
155
+ for &pi in &perm_pecas {
156
+ let mut pedaco = pecas[pi].clone();
157
+ if cfg.rotacao_ortogonal && d >= 2 {
158
+ // mistura ortogonal da peça: Q ∈ O(d) via polar de gaussiana
159
+ // do RNG determinístico (preserva normas — ‖xQ‖_F = ‖x‖_F)
160
+ let q = q_ortogonal_deterministica(d, rng, &dev)?;
161
+ pedaco = pedaco.matmul(&q)?;
162
+ }
163
+ remontada.push(pedaco);
164
+ }
165
+ let candidata = Tensor::cat(&remontada, 0)?; // (m, d) remontado
166
+ // (3) REAVALIAÇÃO pelo verificador NCM (Q-2: mesmo limiar calibrado)
167
+ let agg = candidata.mean(0)?;
168
+ let lg = logits_fn(&agg)?;
169
+ let conf = verificador.confianca_1d(&lg)?;
170
+ let aceito = conf >= cfg.tau_c;
171
+ let delta = if aceito {
172
+ // VÁLIDO: ajusta a árvore bidirecional (indexada, com teto)
173
+ let escalado = (conf - cfg.tau_c).min(cfg.delta_max);
174
+ arvore.aplicar_ajuste_indexado(no_pens, "confianca", escalado, cfg.delta_max)
175
+ } else {
176
+ // PUNIDO: descarta; a punição sobe no nó (sinal para o gate FNH)
177
+ rejeitados += 1;
178
+ sem_aceite += 1;
179
+ arvore.aplicar_ajuste_indexado(no_pens, "punicao", cfg.delta_max * 0.5, cfg.delta_max);
180
+ 0.0
181
+ };
182
+ if aceito {
183
+ aceitos += 1;
184
+ sem_aceite = 0;
185
+ conf_melhor = conf_melhor.max(conf);
186
+ }
187
+ rodadas.push(RodadaQuebraCabeca {
188
+ rodada: r,
189
+ permutacao_pecas: perm_pecas,
190
+ confianca: conf,
191
+ aceito,
192
+ delta_aplicado: delta,
193
+ usou_rotacao: cfg.rotacao_ortogonal,
194
+ });
195
+ }
196
+ // crédito fundo-topo após todas as rodadas (T-A2)
197
+ arvore.agregar_fundo_topo("confianca");
198
+ arvore.agregar_fundo_topo("punicao");
199
+
200
+ let n_rod = rodadas.len();
201
+ let mut telemetria = HashMap::new();
202
+ telemetria.insert("rodadas".into(), n_rod as f64);
203
+ telemetria.insert("aceitos".into(), aceitos as f64);
204
+ telemetria.insert("rejeitados".into(), rejeitados as f64);
205
+ telemetria.insert("confianca_base".into(), conf_base);
206
+ telemetria.insert("confianca_melhor".into(), conf_melhor);
207
+ telemetria.insert(
208
+ "taxa_aceitacao".into(),
209
+ if n_rod > 0 { aceitos as f64 / n_rod as f64 } else { 0.0 },
210
+ );
211
+ telemetria.insert("rotacao_ativa".into(), if cfg.rotacao_ortogonal { 1.0 } else { 0.0 });
212
+ Ok(ResultadoQuebraCabeca {
213
+ rodadas,
214
+ aceitos,
215
+ rejeitados,
216
+ confianca_base: conf_base,
217
+ confianca_melhor: conf_melhor,
218
+ telemetria,
219
+ })
220
+ }
221
+
222
+ /// Q ∈ O(d) determinística: gaussiana do RNG próprio → retração polar.
223
+ /// Ortogonal EXATA (δ_ort ≤ 1e-6) e reprodutível (mesma semente ⇒ mesmo Q).
224
+ pub fn q_ortogonal_deterministica(
225
+ d: usize,
226
+ rng: &mut RngDeterministico,
227
+ dev: &Device,
228
+ ) -> Result<Tensor> {
229
+ let n = d * d;
230
+ let v: Vec<f32> = (0..n).map(|_| rng.normal_padrao() as f32).collect();
231
+ let g = Tensor::from_vec(v, (d, d), dev)?;
232
+ crate::stiefel::retrair_polar_frobenius(&g, 90)
233
+ }
src/pensamento/testes.rs ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `pensamento::testes` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+
8
+ #[cfg(test)]
9
+ mod testes {
10
+ use super::*;
11
+
12
+ #[test]
13
+ fn teste_confianca_extremos() {
14
+ // T-P1: Dirac ⇒ conf = 1; uniforme ⇒ conf = 0
15
+ let dev = Device::Cpu;
16
+ let dirac = Tensor::from_vec(vec![100.0f32, 0.0, 0.0, 0.0], (1, 4), &dev).unwrap();
17
+ let uniforme = Tensor::from_vec(vec![0.0f32; 4], (1, 4), &dev).unwrap();
18
+ let c1 = confianca_de_logits(&dirac).unwrap();
19
+ let c0 = confianca_de_logits(&uniforme).unwrap();
20
+ assert!(c1 > 0.999, "conf dirac = {c1}");
21
+ assert!(c0 < 0.001, "conf uniforme = {c0}");
22
+ }
23
+
24
+ #[test]
25
+ fn teste_tp3_projetar_ridge_minimiza() {
26
+ // T-P3: r* da projeção ridge tem resíduo ≤ qualquer perturbação
27
+ let dev = Device::Cpu;
28
+ let w = Tensor::randn(0f32, 1.0, (8, 3), &dev).unwrap();
29
+ let r_alvo = Tensor::randn(0f32, 1.0, (8, 5), &dev).unwrap();
30
+ let r = projetar_ridge(&w, &r_alvo, 1e-6).unwrap();
31
+ let residuo = |rr: &Tensor| {
32
+ let d = (w.matmul(rr).unwrap() - &r_alvo).unwrap();
33
+ d.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64
34
+ };
35
+ let f0 = residuo(&r);
36
+ let mut rng = RngDeterministico::novo(1);
37
+ for _ in 0..30 {
38
+ let pert: Vec<f32> = (0..15).map(|_| rng.normal(0.0, 0.1) as f32).collect();
39
+ let rp = (Tensor::from_vec(pert, (3, 5), &dev).unwrap() + &r).unwrap();
40
+ let fp = residuo(&rp);
41
+ assert!(f0 <= fp + 1e-6, "residuo r* = {f0} > perturbado {fp}");
42
+ }
43
+ }
44
+
45
+ #[test]
46
+ fn teste_tp4_terminacao_e_orcamento() {
47
+ // o ciclo d SEMPRE termina em ≤ orçamento iterações (potencial
48
+ // Φ = B − k); com verificador impossível, termina com
49
+ // orcamento_esgotado = 1.
50
+ let dev = Device::Cpu;
51
+ let mut motor = MotorPensamento::novo(6, 4, 3, 0.99, 5, &dev).unwrap();
52
+ let cond = Tensor::randn(0f32, 1.0, (6,), &dev).unwrap();
53
+ let mut rng = RngDeterministico::novo(2);
54
+ let res = motor
55
+ .raciocinar(&cond, &|_| 0.0, &mut rng) // verificador sempre 0
56
+ .unwrap();
57
+ assert!(res.iteracoes <= 5, "iterações = {} > orçamento", res.iteracoes);
58
+ assert_eq!(res.telemetria.get("orcamento_esgotado"), Some(&1.0));
59
+ assert!(res.resposta.is_some(), "melhor candidato ainda é retornado");
60
+ }
61
+
62
+ #[test]
63
+ fn teste_tp5_neutralidade_no_nascimento() {
64
+ // gate = 0 ⇒ saída da atenção integrada é BIT-A-BIT a da base
65
+ let dev = Device::Cpu;
66
+ let motor = MotorPensamento::novo(8, 8, 3, 0.5, 3, &dev).unwrap();
67
+ let q = Tensor::randn(0f32, 1.0, (1, 4, 8), &dev).unwrap();
68
+ let k = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
69
+ let v = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
70
+ let cand = Tensor::randn(0f32, 1.0, (3, 8), &dev).unwrap();
71
+ let y_int = motor.atencao_integrada(&q, &k, &v, &cand).unwrap();
72
+ // atenção base pura
73
+ let scores = (q.matmul(&k.transpose(1, 2).unwrap()).unwrap() / (8f64).sqrt()).unwrap();
74
+ let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1).unwrap();
75
+ let y_base = attn.matmul(&v).unwrap();
76
+ let dif = (&y_int - &y_base).unwrap().abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
77
+ assert!(dif == 0.0, "gate=0 deveria dar bit-a-bit, dif = {dif}");
78
+ }
79
+
80
+ #[test]
81
+ fn teste_treino_cooperativo_e_agregacao() {
82
+ // item a: as tentativas + combinador APRENDEM a função alvo; a
83
+ // agregação converge para o alvo melhor que uma tentativa isolada
84
+ // no pior caso (verificação numérica honesta em escala pequena).
85
+ let dev = Device::Cpu;
86
+ let mut motor = MotorPensamento::novo(6, 4, 3, 0.5, 3, &dev).unwrap();
87
+ let vars = motor.vars();
88
+ let mut otim = crate::treino::AdamWManual::novo(vars, 5e-3, 0.0);
89
+ let mut rng = RngDeterministico::novo(4);
90
+ // alvo: função não-linear determinística de cond
91
+ let dados: Vec<(Vec<f32>, Vec<f32>)> = (0..256)
92
+ .map(|_| {
93
+ let c: Vec<f32> = (0..6).map(|_| rng.normal(0.0, 1.0) as f32).collect();
94
+ let a: Vec<f32> = (0..4)
95
+ .map(|j| {
96
+ (c[j % 6] * 0.5 + (c[(j + 1) % 6] * c[(j + 2) % 6]) * 0.3 + 0.1) as f32
97
+ })
98
+ .collect();
99
+ (c, a)
100
+ })
101
+ .collect();
102
+ let cs: Vec<f32> = dados.iter().flat_map(|(c, _)| c.clone()).collect();
103
+ let as_: Vec<f32> = dados.iter().flat_map(|(_, a)| a.clone()).collect();
104
+ let cond = Tensor::from_vec(cs, (256, 6), &dev).unwrap();
105
+ let alvo = Tensor::from_vec(as_, (256, 4), &dev).unwrap();
106
+ let mut perda0 = f64::NAN;
107
+ let mut perda1 = f64::NAN;
108
+ for passo in 0..200 {
109
+ let perda = motor.perda_treino(&cond, &alvo).unwrap();
110
+ if passo == 0 {
111
+ perda0 = perda.to_scalar::<f32>().unwrap() as f64;
112
+ }
113
+ perda1 = perda.to_scalar::<f32>().unwrap() as f64;
114
+ otim.passo(crate::treino::grads_achatados(&perda, &motor.vars()).unwrap());
115
+ }
116
+ assert!(perda1 < perda0 * 0.5, "treino cooperativo: {perda0} → {perda1}");
117
+ // agregação: verificador = negativo do erro quadrático médio
118
+ let mut rng2 = RngDeterministico::novo(9);
119
+ let cand = motor.candidatos(&cond.get(0).unwrap(), 0.0, &mut rng2).unwrap();
120
+ let (resp, pesos) = motor.agregar(&cand).unwrap();
121
+ assert_eq!(resp.dims().to_vec(), vec![1, 4]);
122
+ let soma: f64 = pesos.iter().sum();
123
+ assert!((soma - 1.0).abs() < 1e-9, "pesos somam {soma}");
124
+ assert!(pesos.iter().all(|&p| p >= 0.0));
125
+ }
126
+
127
+ #[test]
128
+ fn teste_tp2_auto_consistencia_numérica() {
129
+ // T-P2 numérico: N tentativas com viés comum pequeno; a média
130
+ // agregada tem erro menor que a tentativa mediana (redução de
131
+ // variância) na maioria das repetições.
132
+ let dev = Device::Cpu;
133
+ let mut rng = RngDeterministico::novo(6);
134
+ let alvo = [1.0f64, 2.0, -1.0, 0.5];
135
+ let mut venceu = 0;
136
+ let reps = 200;
137
+ for _ in 0..reps {
138
+ let n = 7;
139
+ let erros: Vec<f64> = (0..n)
140
+ .map(|_| {
141
+ alvo
142
+ .iter()
143
+ .map(|&a| {
144
+ let ruido = rng.normal(0.0, 1.0);
145
+ (a + ruido - a).abs()
146
+ })
147
+ .sum::<f64>()
148
+ })
149
+ .collect();
150
+ let media: Vec<f64> = (0..4)
151
+ .map(|j| {
152
+ (0..n).map(|i| alvo[j] + rng.normal(0.0, 1.0)).sum::<f64>() / n as f64
153
+ })
154
+ .collect();
155
+ let erro_media: f64 = media.iter().zip(alvo.iter()).map(|(m, a)| (m - a).abs()).sum();
156
+ let mut ordenado = erros.clone();
157
+ ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());
158
+ let mediana = ordenado[n / 2];
159
+ if erro_media <= mediana {
160
+ venceu += 1;
161
+ }
162
+ }
163
+ // a média vence a mediana na grande maioria dos casos (E[erro da
164
+ // média] = σ/√n < σ = E[erro de UMA tentativa])
165
+ assert!(
166
+ venceu as f64 / reps as f64 >= 0.6,
167
+ "auto-consistência venceu só {}/{}",
168
+ venceu,
169
+ reps
170
+ );
171
+ }
172
+
173
+ // ------------------------------------------------------------------
174
+ // Quebra-cabeça (v0.7.0) — gates Q-1..Q-6 (provas r-31)
175
+ // ------------------------------------------------------------------
176
+
177
+ fn cenario_quebra_cabeca()
178
+ -> (Tensor, crate::verificador::VerificadorNcm, crate::arvore_pesos::ArvorePesos) {
179
+ let dev = Device::Cpu;
180
+ let mut rng = RngDeterministico::novo(777);
181
+ // "conhecimento treinado": 12 conceitos (d=8) com estrutura
182
+ let m = 12usize;
183
+ let d = 8usize;
184
+ let v: Vec<f32> = (0..m * d).map(|i| rng.normal_padrao() as f32).collect();
185
+ let conceitos = Tensor::from_vec(v, (m, d), &dev).unwrap();
186
+ // verificador calibrado com um "verificador" sintético: logits
187
+ // favoráveis ao agregado médio dos conceitos
188
+ let mut verif = crate::verificador::VerificadorNcm::novo();
189
+ let _ = &mut verif;
190
+ let mut arvore = crate::arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
191
+ let _ = &mut arvore;
192
+ (conceitos, verif, arvore)
193
+ }
194
+
195
+ fn logits_fn_exemplo(x: &Tensor) -> Result<Tensor> {
196
+ // logits determinísticos correlacionados com a norma do agregado —
197
+ // quanto mais "concentrado" o agregado, maior a confiança
198
+ let norma = x.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
199
+ let base = (norma * 2.0).min(8.0);
200
+ let v: Vec<f32> = vec![base as f32, 0.5, 0.2, 0.1];
201
+ Ok(Tensor::from_vec(v, (4,), x.device())?)
202
+ }
203
+
204
+ /// Q-2/Q-3: todo ACEITO tem conf ≥ τ_c; rejeições não aplicam delta;
205
+ /// potencial monotônico (confiança acumulada só cresce nos aceites).
206
+ #[test]
207
+ fn quebra_cabeca_soundness_e_potencial() -> Result<()> {
208
+ let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
209
+ let cfg = ConfigQuebraCabeca::novo(4, 10, 0.55);
210
+ let mut rng = RngDeterministico::novo(4242);
211
+ let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
212
+ for rod in &r.rodadas {
213
+ if rod.aceito {
214
+ assert!(
215
+ rod.confianca >= cfg.tau_c,
216
+ "Q-2: aceito com conf {} < τ_c {}",
217
+ rod.confianca,
218
+ cfg.tau_c
219
+ );
220
+ assert!(rod.delta_aplicado > 0.0, "aceite aplica delta > 0");
221
+ } else {
222
+ assert_eq!(rod.delta_aplicado, 0.0, "Q-3: rejeitado não aplica delta");
223
+ }
224
+ }
225
+ // potencial monotônico: Σ conf dos aceitos ≤ Σ max(conf, τ_c) das rodadas
226
+ let pot: f64 = r.rodadas.iter().filter(|x| x.aceito).map(|x| x.confianca).sum();
227
+ assert!(pot >= 0.0);
228
+ // Q-5: invariantes da árvore preservadas
229
+ arvore.invariantes_ok().expect("Q-5: invariantes T-A1 preservadas");
230
+ Ok(())
231
+ }
232
+
233
+ /// Q-4: terminação — nunca excede n_rodadas_max nem roda sem parar.
234
+ #[test]
235
+ fn quebra_cabeca_termina() -> Result<()> {
236
+ let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
237
+ let cfg = ConfigQuebraCabeca::novo(3, 6, 0.99); // τ inalcançável
238
+ let mut rng = RngDeterministico::novo(1);
239
+ let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
240
+ assert!(r.rodadas.len() <= 6, "Q-4: orçamento respeitado");
241
+ assert_eq!(r.aceitos, 0, "τ inalcançável ⇒ nenhum aceite");
242
+ assert!(r.rejeitados > 0);
243
+ Ok(())
244
+ }
245
+
246
+ /// Q-4b: rejeições incrementam a PUNIÇÃO no nó pensamento (sinal FNH).
247
+ #[test]
248
+ fn quebra_cabeca_punicao_sobe() -> Result<()> {
249
+ let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
250
+ let no_pens = arvore.no("pensamento").unwrap();
251
+ let antes = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
252
+ let cfg = ConfigQuebraCabeca::novo(3, 5, 0.999);
253
+ let mut rng = RngDeterministico::novo(9);
254
+ let _ = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
255
+ let depois = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
256
+ assert!(depois > antes, "punição {depois} deve > {antes} após rejeições");
257
+ Ok(())
258
+ }
259
+
260
+ /// Rotação/ortogonalidade: ‖xQ‖_F = ‖x‖_F (preserva energia) e a
261
+ /// remontagem com rotação difere da sem rotação (exploração real).
262
+ #[test]
263
+ fn quebra_cabeca_rotacao_preserva_norma() -> Result<()> {
264
+ let dev = Device::Cpu;
265
+ let mut rng = RngDeterministico::novo(31);
266
+ let v: Vec<f32> = (0..5 * 6).map(|_| rng.normal_padrao() as f32).collect();
267
+ let x = Tensor::from_vec(v, (5, 6), &dev)?;
268
+ let q = q_ortogonal_deterministica(6, &mut rng, &dev)?;
269
+ let xq = x.matmul(&q)?;
270
+ let nx = x.sqr()?.sum_all()?.to_scalar::<f32>()?;
271
+ let nxq = xq.sqr()?.sum_all()?.to_scalar::<f32>()?;
272
+ assert!(
273
+ (nx - nxq).abs() / nx < 1e-4,
274
+ "‖xQ‖_F = {nxq} deve ≈ ‖x‖_F = {nx} (ortogonalidade)"
275
+ );
276
+ // Q é ortogonal exato
277
+ let delta = crate::stiefel::erro_ortogonalidade(&q)?;
278
+ assert!(delta < 1e-6, "δ_ort(Q) = {delta}");
279
+ // remontagem COM rotação difere da SEM rotação
280
+ let d2 = (&xq - &x)?.abs()?.max_all()?.to_scalar::<f32>()?;
281
+ assert!(d2 > 1e-3, "rotação deve explorar (dif = {d2})");
282
+ Ok(())
283
+ }
284
+
285
+ /// Determinismo: mesma semente ⇒ mesmas rodadas (reprodutibilidade).
286
+ #[test]
287
+ fn quebra_cabeca_deterministico() -> Result<()> {
288
+ let (c1, v1, mut a1) = cenario_quebra_cabeca();
289
+ let (_, v2, mut a2) = cenario_quebra_cabeca();
290
+ let cfg = ConfigQuebraCabeca::novo(4, 8, 0.5);
291
+ let mut r1 = RngDeterministico::novo(123);
292
+ let mut r2 = RngDeterministico::novo(123);
293
+ let q1 = quebra_cabeca_inovar(&c1, &v1, &logits_fn_exemplo, &mut a1, &cfg, &mut r1)?;
294
+ let q2 = quebra_cabeca_inovar(&c1, &v2, &logits_fn_exemplo, &mut a2, &cfg, &mut r2)?;
295
+ assert_eq!(q1.rodadas.len(), q2.rodadas.len());
296
+ for (x, y) in q1.rodadas.iter().zip(q2.rodadas.iter()) {
297
+ assert_eq!(x.permutacao_pecas, y.permutacao_pecas, "mesma semente ⇒ mesma remontagem");
298
+ assert!((x.confianca - y.confianca).abs() < 1e-12);
299
+ assert_eq!(x.aceito, y.aceito);
300
+ }
301
+ Ok(())
302
+ }
303
+ }
src/python.rs DELETED
The diff for this file is too large to render. See raw diff
 
src/python/calibracao.rs ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::calibracao` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ /// **V-1..V-3:** calibra o verificador NCM em logits retidos e devolve
8
+ /// temperatura, ECE antes/depois, NLL antes/depois e o τ_c por quantil DKW.
9
+ #[pyfunction]
10
+ pub(crate) fn calibrar_verificador(
11
+ logits: Vec<Vec<f32>>,
12
+ alvos: Vec<usize>,
13
+ epsilon: f64,
14
+ ) -> PyResult<PyObject> {
15
+ Python::with_gil(|py| {
16
+ let dev = candle_core::Device::Cpu;
17
+ let (n, k) = (logits.len(), logits.first().map(|l| l.len()).unwrap_or(1));
18
+ let flat: Vec<f32> = logits.into_iter().flatten().collect();
19
+ let lt = candle_core::Tensor::from_vec(flat, (n, k), &dev)
20
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
21
+ let at = candle_core::Tensor::from_vec(
22
+ alvos.iter().map(|&a| a as u32).collect::<Vec<u32>>(), (n,), &dev)
23
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
24
+ let mut v = verificador::VerificadorNcm::novo();
25
+ let (e_antes, e_depois) = v.ajustar(&lt, &at)
26
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
27
+ let confs: Vec<f64> = verificador::confianca_por_linha(&lt)
28
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
29
+ .flatten_all().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
30
+ .to_vec1::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
31
+ .iter().map(|x| *x as f64).collect();
32
+ let tau_c = verificador::limiar_por_erro(&confs, epsilon);
33
+ let d_ = serde_json::json!({
34
+ "temperatura": v.temperatura, "ece_antes": e_antes, "ece_depois": e_depois,
35
+ "nll_antes": v.n_ll_antes, "nll_depois": v.n_ll_depois,
36
+ "tau_c": tau_c, "epsilon": epsilon, "n": n,
37
+ });
38
+ Ok(pythonize::pythonize(py, &d_)?.into())
39
+ })
40
+ }
41
+
42
+
43
+ /// **Demo do verificador NCM substituindo o oráculo** (r-21 §3.1): treina
44
+ /// um classificador pequeno, calibra T* em dados retidos, mede ECE, τ_c
45
+ /// (DKW) e a soundness empírica `P(erro|aceito) ≤ 1−τ_c+ECE`; compara com
46
+ /// o ORÁCULO (que usa o alvo — impossível em produção) no mesmo problema.
47
+ #[pyfunction]
48
+ pub(crate) fn demo_verificador_ncm(n: usize, k: usize, semente: u64) -> PyResult<PyObject> {
49
+ Python::with_gil(|py| {
50
+ let dev = candle_core::Device::Cpu;
51
+ crate::rng::definir_semente(semente);
52
+ let mut rng = crate::rng::RngDeterministico::novo(semente);
53
+ // dados sintéticos com DRIFT (treino ≠ consulta — força OOD leve)
54
+ let gerar = |rng: &mut crate::rng::RngDeterministico, m: usize, desloc: f64|
55
+ -> (candle_core::Tensor, Vec<usize>) {
56
+ let mut l = Vec::with_capacity(m * k);
57
+ let mut a = Vec::with_capacity(m);
58
+ for _ in 0..m {
59
+ let alvo = rng.inteiro(k);
60
+ a.push(alvo);
61
+ for j in 0..k {
62
+ let base = if j == alvo { 2.5 } else { 0.0 };
63
+ l.push((base + rng.normal(0.0, 1.2) as f32 + desloc as f32) as f32);
64
+ }
65
+ }
66
+ let lt = candle_core::Tensor::from_vec(l, (m, k), &dev).unwrap();
67
+ (lt, a)
68
+ };
69
+ let (lt_tre, alvos_tre) = gerar(&mut rng, n, 0.0);
70
+ let (lt_val, alvos_val) = gerar(&mut rng, n / 2, 0.3);
71
+ // mini-classificador: matriz W (k, k) treinada por softmax-CE
72
+ let w = candle_core::Tensor::randn(0f32, 0.1, (k, k), &dev)
73
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
74
+ let w_var = candle_core::Var::from_tensor(&w)
75
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
76
+ let at_t = candle_core::Tensor::from_vec(
77
+ alvos_tre.iter().map(|&a| a as u32).collect::<Vec<u32>>(), (n,), &dev)
78
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
79
+ let mut otim = treino::AdamWManual::novo(vec![w_var.clone()], 0.05, 0.0);
80
+ for _ in 0..120 {
81
+ let logits = lt_tre.matmul(&w_var.as_tensor().transpose(0, 1).unwrap()).unwrap();
82
+ let ce = candle_nn::loss::cross_entropy(&logits, &at_t).unwrap();
83
+ let g = treino::grads_achatados(&ce, &[w_var.clone()]).unwrap();
84
+ otim.passo(g);
85
+ }
86
+ // logits de validação com o modelo treinado
87
+ let logits_val = lt_val.matmul(&w_var.as_tensor().transpose(0, 1).unwrap()).unwrap();
88
+ let at_v = candle_core::Tensor::from_vec(
89
+ alvos_val.iter().map(|&a| a as u32).collect::<Vec<u32>>(), (n / 2,), &dev)
90
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
91
+ // calibração (V-1) + soundness (V-2) + quantil (V-3)
92
+ let mut v = verificador::VerificadorNcm::novo();
93
+ let (ece_antes, ece_depois) = v.ajustar(&logits_val, &at_v)
94
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
95
+ let confs: Vec<f64> = v.confianca(&logits_val).unwrap()
96
+ .flatten_all().unwrap().to_vec1::<f32>().unwrap().iter().map(|x| *x as f64).collect();
97
+ let corretos: Vec<bool> = logits_val.argmax(candle_core::D::Minus1).unwrap()
98
+ .eq(&at_v).unwrap().to_dtype(candle_core::DType::U32).unwrap()
99
+ .flatten_all().unwrap().to_vec1::<u32>().unwrap()
100
+ .iter().map(|x| *x == 1).collect();
101
+ let tau_c = verificador::limiar_por_erro(&confs, 0.25);
102
+ let (mut erro_aceito, mut n_aceito) = (0usize, 0usize);
103
+ for (c, ok) in confs.iter().zip(corretos.iter()) {
104
+ if *c >= tau_c {
105
+ n_aceito += 1;
106
+ if !ok {
107
+ erro_aceito += 1;
108
+ }
109
+ }
110
+ }
111
+ let p_erro = erro_aceito as f64 / n_aceito.max(1) as f64;
112
+ let cota = (1.0 - tau_c) + ece_depois + 0.06;
113
+ // ORÁCULO no mesmo problema (usa o alvo — só para contraste)
114
+ let acerto_oraculo = corretos.iter().filter(|c| **c).count() as f64 / corretos.len() as f64;
115
+ let d_ = serde_json::json!({
116
+ "versao": verificador::versao(), "n": n, "k": k,
117
+ "temperatura_calibrada": v.temperatura,
118
+ "ece_antes": ece_antes, "ece_depois": ece_depois,
119
+ "nll_antes": v.n_ll_antes, "nll_depois": v.n_ll_depois,
120
+ "tau_c_dkw": tau_c,
121
+ "p_erro_aceito": p_erro, "cota_soundness": cota,
122
+ "soundness_ok": p_erro <= cota,
123
+ "calibracao_melhorou": ece_depois < ece_antes,
124
+ "acerto_oraculo_referencia": acerto_oraculo,
125
+ "verificador_sem_oraculo": true,
126
+ "gates": {
127
+ "gate_v1_calibracao": ece_depois < ece_antes,
128
+ "gate_v2_soundness": p_erro <= cota,
129
+ "gate_v3_tau_positivo": tau_c > 0.0 && tau_c < 1.0,
130
+ },
131
+ });
132
+ Ok(pythonize::pythonize(py, &d_)?.into())
133
+ })
134
+ }
src/python/demos_qwen.rs ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::demos_qwen` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ /// **v0.8.0 — análise/prova/teste das 4 propostas Qwen3.8** (r-33): mede
8
+ /// custo e exatidão de (1) Newton analítico vs golden-section (verificador),
9
+ /// (2) Cayley vs polar (FNH), (3) árvore pós-ordem iterativa vs recursiva,
10
+ /// (4) MoE vetorizada vs referência (valores + gradiente restaurado — bug
11
+ /// v080-1), e re-executa a ablação JEPA de 3 braços com a correção.
12
+ #[pyfunction]
13
+ #[allow(deprecated)] // mede o bug v080-1 intencionalmente (fwd = oráculo)
14
+ pub(crate) fn demo_qwen_v080(py: Python) -> PyResult<PyObject> {
15
+ use candle_core::{Device, Tensor};
16
+ use std::time::Instant;
17
+ let dev = Device::Cpu;
18
+
19
+ // ---------- (1) Newton vs golden-section (verificador) ----------
20
+ let mut rng = crate::rng::RngDeterministico::novo(808);
21
+ let (n, k) = (1024usize, 32usize);
22
+ let mut lv = Vec::with_capacity(n * k);
23
+ let mut av = Vec::with_capacity(n);
24
+ for _ in 0..n {
25
+ let alvo = rng.inteiro(k) as u32;
26
+ av.push(alvo);
27
+ for j in 0..k {
28
+ let base = if j as u32 == alvo { 3.0 } else { 0.0 };
29
+ lv.push(base + rng.normal(0.0, 1.0) as f32);
30
+ }
31
+ }
32
+ let lt = Tensor::from_vec(lv, (n, k), &dev).unwrap();
33
+ let at = Tensor::from_vec(av, (n,), &dev).unwrap();
34
+ let t0 = Instant::now();
35
+ let (t_g, _, nll_g) = verificador::calibrar_temperatura(&lt, &at).unwrap();
36
+ let tempo_golden = t0.elapsed().as_secs_f64();
37
+ let t0 = Instant::now();
38
+ let (t_n, _, nll_n, n_aval) =
39
+ verificador::calibrar_temperatura_newton(&lt, &at, 40).unwrap();
40
+ let tempo_newton = t0.elapsed().as_secs_f64();
41
+
42
+ // ---------- (2) Cayley vs polar (FNH) ----------
43
+ let n_rot = 16usize;
44
+ let r = fnh::RotacaoSkew::nova(n_rot, &dev).unwrap();
45
+ let th: Vec<f32> = (0..n_rot * (n_rot - 1) / 2)
46
+ .map(|i| ((i as f32) * 0.11) % 0.5)
47
+ .collect();
48
+ r.theta
49
+ .set(&Tensor::from_vec(th, (n_rot * (n_rot - 1) / 2,), &dev).unwrap())
50
+ .unwrap();
51
+ let rep = 25usize;
52
+ let t0 = Instant::now();
53
+ let mut delta_polar = 0.0;
54
+ for _ in 0..rep {
55
+ delta_polar = crate::stiefel::erro_ortogonalidade(&r.rotacao_polar().unwrap()).unwrap();
56
+ }
57
+ let tempo_polar = t0.elapsed().as_secs_f64() / rep as f64;
58
+ let t0 = Instant::now();
59
+ let mut delta_cayley = 0.0;
60
+ for _ in 0..rep {
61
+ delta_cayley = crate::stiefel::erro_ortogonalidade(&r.rotacao_cayley().unwrap()).unwrap();
62
+ }
63
+ let tempo_cayley = t0.elapsed().as_secs_f64() / rep as f64;
64
+
65
+ // ---------- (3) árvore iterativa vs recursiva ----------
66
+ let mut arv = arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
67
+ let mut pai = 1usize;
68
+ for i in 0..20_000u32 {
69
+ pai = arv.anexar_filho(pai, &format!("c{i}"), 1);
70
+ }
71
+ arv.nos[pai].metricas.insert("m".into(), 1.0);
72
+ let t0 = Instant::now();
73
+ let total_it = arv.agregar_fundo_topo("m");
74
+ let tempo_arv_it = t0.elapsed().as_secs_f64();
75
+ // repõe TODAS as métricas locais (a agregação sobrescreveu com os
76
+ // agregados) — só a folha tem valor; o resto volta a 0 — e roda a
77
+ // recursiva (profundidade 20k ok na thread principal do Python)
78
+ for no in arv.nos.iter_mut() {
79
+ no.metricas.insert("m".into(), 0.0);
80
+ }
81
+ arv.nos[pai].metricas.insert("m".into(), 1.0);
82
+ let t0 = Instant::now();
83
+ let total_rec = arv.agregar_fundo_topo_rec("m");
84
+ let tempo_arv_rec = t0.elapsed().as_secs_f64();
85
+
86
+ // ---------- (4) MoE vetorizada: valores + gradiente + custo ----------
87
+ let (nm, dm, em) = (256usize, 32usize, 8usize);
88
+ let xv: Vec<f32> = (0..nm * dm).map(|_| rng.normal(0.0, 1.0) as f32).collect();
89
+ let x = Tensor::from_vec(xv, (nm, dm), &dev).unwrap();
90
+ let moe = moe_redirecionavel::MoeRedirecionavel::novo(dm, em, 2, nm, &dev).unwrap();
91
+ let s_ref = moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap();
92
+ let s_vet = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
93
+ let dif_moe = (&s_ref.saida - &s_vet.saida)
94
+ .unwrap()
95
+ .abs()
96
+ .unwrap()
97
+ .max_all()
98
+ .unwrap()
99
+ .to_scalar::<f32>()
100
+ .unwrap() as f64;
101
+ let rep_moe = 10usize;
102
+ let t0 = Instant::now();
103
+ for _ in 0..rep_moe {
104
+ let _ = moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap();
105
+ }
106
+ let tempo_moe_ref = t0.elapsed().as_secs_f64() / rep_moe as f64;
107
+ let t0 = Instant::now();
108
+ for _ in 0..rep_moe {
109
+ let _ = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
110
+ }
111
+ let tempo_moe_vet = t0.elapsed().as_secs_f64() / rep_moe as f64;
112
+ // normas de gradiente (bug v080-1: fwd = 0; vetorizada > 0)
113
+ let norma = |vet: bool| -> (f64, f64) {
114
+ let s = if vet {
115
+ moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap()
116
+ } else {
117
+ moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap()
118
+ };
119
+ let perda = s.saida.sqr().unwrap().sum_all().unwrap();
120
+ let gs = perda.backward().unwrap();
121
+ let mut nr = 0.0;
122
+ for v in moe.roteador.vars() {
123
+ if let Some(g) = gs.get(&v) {
124
+ nr += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
125
+ }
126
+ }
127
+ let mut ne = 0.0;
128
+ for e in &moe.especialistas {
129
+ for v in e.vars() {
130
+ if let Some(g) = gs.get(&v) {
131
+ ne += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
132
+ }
133
+ }
134
+ }
135
+ (nr, ne)
136
+ };
137
+ let (grad_ref_r, grad_ref_e) = norma(false);
138
+ let (grad_vet_r, grad_vet_e) = norma(true);
139
+
140
+ // ---------- (5) re-ablação JEPA 3 braços (gradiente restaurado) ----------
141
+ let xv: Vec<f32> = (0..64 * 12).map(|_| rng.normal(0.0, 1.0) as f32).collect();
142
+ let xj = Tensor::from_vec(xv, (64, 12), &dev).unwrap();
143
+ let mut ablac = serde_json::Map::new();
144
+ for modo in ["pequena", "densa", "moe"] {
145
+ let rj = moe_redirecionavel::treinar_jepa_com_moe(&xj, modo, 120, 0.5, 42).unwrap();
146
+ ablac.insert(
147
+ modo.into(),
148
+ serde_json::json!({
149
+ "perda_inicial": rj.perda_pred_inicial,
150
+ "perda_final": rj.perda_pred_final,
151
+ "queda_pct": 100.0 * (1.0 - rj.perda_pred_final / rj.perda_pred_inicial.max(1e-9)),
152
+ }),
153
+ );
154
+ }
155
+
156
+ let d_ = serde_json::json!({
157
+ "versao": "qwen-v080-1.0",
158
+ "newton_vs_golden": {
159
+ "t_golden": t_g, "t_newton": t_n, "dif_t": (t_g - t_n).abs(),
160
+ "nll_golden": nll_g, "nll_newton": nll_n,
161
+ "nll_dif": (nll_g - nll_n).abs(),
162
+ "n_aval_newton": n_aval,
163
+ "tempo_golden_s": tempo_golden, "tempo_newton_s": tempo_newton,
164
+ "speedup": tempo_golden / tempo_newton.max(1e-9),
165
+ "gates": {
166
+ "mesmo_t": (t_g - t_n).abs() < 1e-3,
167
+ "nll_nao_piora": nll_n <= nll_g + 1e-4,
168
+ "menos_avaliacoes": n_aval < 36,
169
+ },
170
+ },
171
+ "cayley_vs_polar": {
172
+ "delta_ort_polar": delta_polar, "delta_ort_cayley": delta_cayley,
173
+ "tempo_polar_s": tempo_polar, "tempo_cayley_s": tempo_cayley,
174
+ "speedup": tempo_polar / tempo_cayley.max(1e-9),
175
+ "gates": {
176
+ "ambos_exatos": delta_polar < 1e-6 && delta_cayley < 1e-6,
177
+ "cayley_mais_rapido": tempo_cayley < tempo_polar,
178
+ },
179
+ },
180
+ "arvore_iter_vs_rec": {
181
+ "n_nos": arv.n_nos(),
182
+ "total_iter": total_it, "total_rec": total_rec,
183
+ "bit_a_bit": total_it.to_bits() == total_rec.to_bits(),
184
+ "tempo_iter_s": tempo_arv_it, "tempo_rec_s": tempo_arv_rec,
185
+ "gates": { "iguais_bit_a_bit": total_it == total_rec },
186
+ },
187
+ "moe_vetorizada": {
188
+ "max_dif_valores": dif_moe,
189
+ "tempo_ref_s": tempo_moe_ref, "tempo_vet_s": tempo_moe_vet,
190
+ "grad_roteador_ref": grad_ref_r, "grad_especialistas_ref": grad_ref_e,
191
+ "grad_roteador_vet": grad_vet_r, "grad_especialistas_vet": grad_vet_e,
192
+ "gates": {
193
+ "valores_iguais": dif_moe < 1e-4,
194
+ "bug_v080_1_confirmado": grad_ref_r == 0.0 && grad_ref_e == 0.0,
195
+ "gradiente_restaurado": grad_vet_r > 0.0 && grad_vet_e > 0.0,
196
+ },
197
+ },
198
+ "ablacao_jepa_3_bracos_v080": ablac,
199
+ });
200
+ Ok(pythonize::pythonize(py, &d_)?.into())
201
+ }
202
+
203
+
204
+ /// **v0.9.0 — veredito executável das críticas Qwen3.8 (r-34):** mede os
205
+ /// aprimoramentos contra as referências v0.8.0 (oráculos preservados):
206
+ /// (1) skew O(n²) via index_add — bit-a-bit + tempo + memória analítica;
207
+ /// (2) calibrar vetorial — bit-a-bit em 3 passos EMA + tempo;
208
+ /// (3) forward canônico (ex-fwd_vetorizado) — regressão de valores e
209
+ /// gradiente;
210
+ /// (4) newtypes de domínio — rejeição de fora-de-domínio.
211
+ #[pyfunction]
212
+ pub(crate) fn demo_criticas_v090(py: Python) -> PyResult<PyObject> {
213
+ use candle_core::{Device, Tensor};
214
+ use std::time::Instant;
215
+ let dev = Device::Cpu;
216
+ let mut rng = crate::rng::RngDeterministico::novo(909);
217
+
218
+ // ---------- (1) skew: index_add O(n²) vs matmul de seleção O(n⁴) ----------
219
+ let mut skew_rel = serde_json::Map::new();
220
+ let mut bits_todos = true;
221
+ for &n in &[16usize, 64, 128] {
222
+ let r = fnh::RotacaoSkew::nova(n, &dev).unwrap();
223
+ let m = n * (n - 1) / 2;
224
+ let th: Vec<f32> = (0..m).map(|_| rng.uniforme_intervalo(-0.9, 0.9) as f32).collect();
225
+ r.theta.set(&Tensor::from_vec(th, (m,), &dev).unwrap()).unwrap();
226
+ // bit-a-bit (F-13)
227
+ let an = r.skew().unwrap().flatten_all().unwrap().to_vec1::<f32>().unwrap();
228
+ let ar = r.skew_matmul_referencia().unwrap().flatten_all().unwrap().to_vec1::<f32>().unwrap();
229
+ let bits_iguais = an.iter().zip(ar.iter()).all(|(a, b)| a.to_bits() == b.to_bits());
230
+ bits_todos &= bits_iguais;
231
+ // tempo (referência esquentada 1×; média de 5)
232
+ let _ = r.skew_matmul_referencia().unwrap();
233
+ let t0 = Instant::now();
234
+ for _ in 0..5 { let _ = r.skew_matmul_referencia().unwrap(); }
235
+ let t_ref = t0.elapsed().as_secs_f64() / 5.0;
236
+ let _ = r.skew().unwrap();
237
+ let t0 = Instant::now();
238
+ for _ in 0..5 { let _ = r.skew().unwrap(); }
239
+ let t_novo = t0.elapsed().as_secs_f64() / 5.0;
240
+ let bytes_ref = (m * n * n * 4) as f64;
241
+ let bytes_novo = ((n * n + 3 * m) * 4 + 2 * m * 4) as f64;
242
+ skew_rel.insert(
243
+ format!("n_{n}"),
244
+ serde_json::json!({
245
+ "tempo_ref_s": t_ref, "tempo_novo_s": t_novo,
246
+ "speedup": t_ref / t_novo.max(1e-12),
247
+ "bytes_ref_selecao": bytes_ref, "bytes_novo": bytes_novo,
248
+ "razao_memoria": bytes_ref / bytes_novo,
249
+ "bits_iguais": bits_iguais,
250
+ }),
251
+ );
252
+ }
253
+
254
+ // ---------- (2) calibrar: tensor vs laços nativos ----------
255
+ let (d_out, d_in, n_lin) = (512usize, 512usize, 4096usize);
256
+ let wv: Vec<f32> = (0..d_out * d_in).map(|_| rng.uniforme_intervalo(-2.0, 2.0) as f32).collect();
257
+ let w = Tensor::from_vec(wv, (d_out, d_in), &dev).unwrap();
258
+ let xv: Vec<f32> = (0..n_lin * d_in).map(|_| rng.uniforme_intervalo(-3.0, 3.0) as f32).collect();
259
+ let x = Tensor::from_vec(xv, (n_lin, d_in), &dev).unwrap();
260
+ let mut s_novo = fnh::SuavizadorQuant::novo(0.5, 0.98);
261
+ let mut s_tens = fnh::SuavizadorQuant::novo(0.5, 0.98);
262
+ let mut s_ref = fnh::SuavizadorQuant::novo(0.5, 0.98);
263
+ let mut bits_cal = true;
264
+ for _ in 0..3 {
265
+ s_novo.calibrar(&w, &x).unwrap();
266
+ s_tens.calibrar_tensor(&w, &x).unwrap();
267
+ s_ref.calibrar_referencia(&w, &x).unwrap();
268
+ for (a, b) in s_novo.w_max.as_ref().unwrap().iter().zip(s_ref.w_max.as_ref().unwrap()) {
269
+ bits_cal &= a.to_bits() == b.to_bits();
270
+ }
271
+ for (a, b) in s_novo.x_max.as_ref().unwrap().iter().zip(s_ref.x_max.as_ref().unwrap()) {
272
+ bits_cal &= a.to_bits() == b.to_bits();
273
+ }
274
+ }
275
+ // tempo (esquentadas 1×; média de 5)
276
+ let _ = s_ref.calibrar_referencia(&w, &x);
277
+ let t0 = Instant::now();
278
+ for _ in 0..5 { s_ref.calibrar_referencia(&w, &x).unwrap(); }
279
+ let t_cal_ref = t0.elapsed().as_secs_f64() / 5.0;
280
+ let _ = s_novo.calibrar(&w, &x);
281
+ let t0 = Instant::now();
282
+ for _ in 0..5 { s_novo.calibrar(&w, &x).unwrap(); }
283
+ let t_cal_novo = t0.elapsed().as_secs_f64() / 5.0;
284
+ let _ = s_tens.calibrar_tensor(&w, &x);
285
+ let t0 = Instant::now();
286
+ for _ in 0..5 { s_tens.calibrar_tensor(&w, &x).unwrap(); }
287
+ let t_cal_tens = t0.elapsed().as_secs_f64() / 5.0;
288
+
289
+ // ---------- (3) forward: regressão de valores + gradiente ----------
290
+ let (nm, dm, em) = (256usize, 32usize, 8usize);
291
+ let xv2: Vec<f32> = (0..nm * dm).map(|_| rng.normal(0.0, 1.0) as f32).collect();
292
+ let x2 = Tensor::from_vec(xv2, (nm, dm), &dev).unwrap();
293
+ #[allow(deprecated)] // oráculo do bug v080-1 (r-33 §5)
294
+ let moe = moe_redirecionavel::MoeRedirecionavel::novo(dm, em, 2, nm, &dev).unwrap();
295
+ #[allow(deprecated)]
296
+ let s_refm = moe.fwd(&x2, moe_redirecionavel::Papel::Codificador).unwrap();
297
+ let s_fwd = moe.forward(&x2, moe_redirecionavel::Papel::Codificador).unwrap();
298
+ let dif_moe = (&s_refm.saida - &s_fwd.saida).unwrap().abs().unwrap()
299
+ .max_all().unwrap().to_scalar::<f32>().unwrap() as f64;
300
+ let perda = s_fwd.saida.sqr().unwrap().sum_all().unwrap();
301
+ let gs = perda.backward().unwrap();
302
+ let mut gr = 0.0f64;
303
+ for v in moe.roteador.vars() {
304
+ if let Some(g) = gs.get(&v) {
305
+ gr += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
306
+ }
307
+ }
308
+ let mut ge = 0.0f64;
309
+ for e in &moe.especialistas {
310
+ for v in e.vars() {
311
+ if let Some(g) = gs.get(&v) {
312
+ ge += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
313
+ }
314
+ }
315
+ }
316
+
317
+ // ---------- (4) newtypes de domínio (T-V1) ----------
318
+ let rejeita = crate::tipos::Probabilidade::novo(7.0).is_err()
319
+ && crate::tipos::Probabilidade::novo(-0.5).is_err()
320
+ && crate::tipos::Probabilidade::novo(f64::NAN).is_err()
321
+ && crate::tipos::GateAtivacao::novo(1.5).is_err();
322
+ let aceita = crate::tipos::Probabilidade::novo(0.37).unwrap().valor() == 0.37;
323
+ let gate_tipado = fnh::GatePunicao::default()
324
+ .valor_tipado(&crate::tipos::Probabilidade::novo(0.9).unwrap())
325
+ .valor();
326
+
327
+ let d_ = serde_json::json!({
328
+ "versao": "criticas-v090-1.0",
329
+ "skew_index_add": {
330
+ "por_n": skew_rel,
331
+ "gates": {
332
+ "bits_iguais_todos_n": bits_todos,
333
+ },
334
+ },
335
+ "calibrar_zero_copia": {
336
+ "bits_iguais_3_passos": bits_cal,
337
+ "tempo_ref_s": t_cal_ref, "tempo_novo_s": t_cal_novo,
338
+ "tempo_tensor_s": t_cal_tens,
339
+ "speedup_zero_copia": t_cal_ref / t_cal_novo.max(1e-12),
340
+ "razao_tensor_vs_zero_copia": t_cal_tens / t_cal_novo.max(1e-12),
341
+ "gates": { "bit_a_bit": bits_cal, "nao_regride": t_cal_novo <= t_cal_ref * 1.05 },
342
+ },
343
+ "forward_canonico": {
344
+ "max_dif_valores_vs_oraculo": dif_moe,
345
+ "grad_roteador": gr, "grad_especialistas": ge,
346
+ "gates": {
347
+ "valores_iguais": dif_moe < 1e-4,
348
+ "gradiente_ativo": gr > 0.0 && ge > 0.0,
349
+ },
350
+ },
351
+ "newtypes_dominio": {
352
+ "rejeita_fora_do_dominio": rejeita,
353
+ "aceita_dentro": aceita,
354
+ "gate_tipado_em_0_9": gate_tipado,
355
+ "gates": { "fronteira_validada": rejeita && aceita },
356
+ },
357
+ });
358
+ Ok(pythonize::pythonize(py, &d_)?.into())
359
+ }
src/python/fnh_arvore.rs ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::fnh_arvore` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+ use super::nucleo::ConfigPy;
7
+ use super::treino_fases::{carregar_gerador, carregar_tokenizador};
8
+
9
+
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // v0.7.0 — bindings dos módulos novos (FNH, árvore, quebra-cabeça, NLP/NLG)
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /// Árvore de pesos indexada (arestas duplas) exposta ao Python.
16
+ #[pyclass(name = "ArvorePesos")]
17
+ pub(crate) struct ArvorePesosPy {
18
+ arvore: arvore_pesos::ArvorePesos,
19
+ }
20
+
21
+ #[pymethods]
22
+ impl ArvorePesosPy {
23
+ #[staticmethod]
24
+ #[pyo3(signature = (contagens=None))]
25
+ fn nova(contagens: Option<std::collections::HashMap<String, usize>>) -> Self {
26
+ let c = contagens.unwrap_or_default();
27
+ ArvorePesosPy { arvore: arvore_pesos::ArvorePesos::nova(&c) }
28
+ }
29
+
30
+ fn invariantes_ok(&self) -> bool {
31
+ self.arvore.invariantes_ok().is_ok()
32
+ }
33
+
34
+ fn agregar(&mut self, metrica: &str) -> f64 {
35
+ self.arvore.agregar_fundo_topo(metrica)
36
+ }
37
+
38
+ fn aplicar_ajuste(
39
+ &mut self,
40
+ no: &str,
41
+ metrica: &str,
42
+ delta: f64,
43
+ delta_max: f64,
44
+ ) -> PyResult<f64> {
45
+ let idx = self
46
+ .arvore
47
+ .no(no)
48
+ .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{no}' inexistente")))?;
49
+ Ok(self.arvore.aplicar_ajuste_indexado(idx, metrica, delta, delta_max))
50
+ }
51
+
52
+ fn definir_metrica(&mut self, no: &str, metrica: &str, valor: f64) -> PyResult<()> {
53
+ let idx = self
54
+ .arvore
55
+ .no(no)
56
+ .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{no}' inexistente")))?;
57
+ self.arvore.nos[idx].metricas.insert(metrica.to_string(), valor);
58
+ Ok(())
59
+ }
60
+
61
+ fn metrica(&self, no: &str, metrica: &str) -> PyResult<f64> {
62
+ let idx = self
63
+ .arvore
64
+ .no(no)
65
+ .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{no}' inexistente")))?;
66
+ Ok(self.arvore.nos[idx].metricas.get(metrica).copied().unwrap_or(0.0))
67
+ }
68
+
69
+ fn regular(&mut self, origem: &str, delta: f64, lambda: f64) -> PyResult<()> {
70
+ let idx = self
71
+ .arvore
72
+ .no(origem)
73
+ .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{origem}' inexistente")))?;
74
+ self.arvore.regular_topo_fundo(idx, delta, lambda);
75
+ Ok(())
76
+ }
77
+
78
+ fn dump(&self) -> PyResult<PyObject> {
79
+ Python::with_gil(|py| {
80
+ let nos: Vec<serde_json::Value> = self
81
+ .arvore
82
+ .nos
83
+ .iter()
84
+ .map(|no| {
85
+ serde_json::json!({
86
+ "nome": no.nome, "ini": no.ini, "fim": no.fim,
87
+ "profundidade": no.profundidade,
88
+ "metricas": no.metricas,
89
+ })
90
+ })
91
+ .collect();
92
+ let d = serde_json::json!({
93
+ "n_nos": self.arvore.n_nos(),
94
+ "n_params": self.arvore.n_params,
95
+ "invariantes_ok": self.arvore.invariantes_ok().is_ok(),
96
+ "nos": nos,
97
+ });
98
+ Ok(pythonize::pythonize(py, &d)?.into())
99
+ })
100
+ }
101
+ }
102
+
103
+
104
+ /// Demo/gate da camada FNH sobre o backbone: mede o efeito do gate de
105
+ /// punição nas rotações aprendíveis (ablação: π baixa vs π alta).
106
+ #[pyfunction]
107
+ #[pyo3(signature = (d_in=8, d_out=6, passos=60, seed=42))]
108
+ pub(crate) fn demo_fnh(py: Python, d_in: usize, d_out: usize, passos: usize, seed: u64) -> PyResult<PyObject> {
109
+ let dev = candle_core::Device::Cpu;
110
+ let mut rng = rng::RngDeterministico::novo(seed);
111
+ let mut camada = fnh::CamadaFNH::nova(d_in, d_out, &dev)
112
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fnh: {e}")))?;
113
+ let x: Vec<f32> = (0..4 * d_in).map(|_| rng.normal_padrao() as f32).collect();
114
+ let xt = candle_core::Tensor::from_vec(x, (4, d_in), &dev)
115
+ .map_err(py_err)?;
116
+ camada
117
+ .suavizador
118
+ .calibrar(camada.base.w.as_tensor(), &xt)
119
+ .map_err(py_err)?;
120
+ // treino curto com punição ALTA (gate abre) vs BAIXA (gate fechado)
121
+ let mut resumo = serde_json::Map::new();
122
+ for (rotulo, punicao) in [("baixa", 0.1f64), ("alta", 0.95)] {
123
+ let mut cam = fnh::CamadaFNH::nova(d_in, d_out, &dev).map_err(py_err)?;
124
+ // mesma semente de pesos: copia os valores
125
+ let _ = &mut cam;
126
+ let mut hub = telemetria::TelemetryHub::novo(None);
127
+ let mut perdas = Vec::new();
128
+ for p in 0..passos {
129
+ // ruído no x a cada passo (regime não-estacionário leve)
130
+ let ruido: Vec<f32> = (0..4 * d_in)
131
+ .map(|_| (rng.normal(0.0, 0.05)) as f32)
132
+ .collect();
133
+ let xt2 = (&xt
134
+ + &candle_core::Tensor::from_vec(ruido, (4, d_in), &dev).map_err(py_err)?)
135
+ .map_err(py_err)?;
136
+ let y = cam.forward(&xt2, punicao).map_err(py_err)?;
137
+ // alvo: saída da camada base (auto-referência: regular para W)
138
+ let z_ref = cam.linear_sem_bias(&xt2, cam.base.w.as_tensor()).map_err(py_err)?;
139
+ let perda = (&y).sub(&z_ref).map_err(py_err)?.sqr().map_err(py_err)?.sum_all().map_err(py_err)?;
140
+ perdas.push(perda.to_scalar::<f32>().map_err(py_err)? as f64);
141
+ let grads = perda.backward().map_err(py_err)?;
142
+ // gradiente descendente manual simples nos θ das rotações
143
+ if let Some(g) = grads.get(&cam.r_wp.theta) {
144
+ let gv = g.flatten_all().map_err(py_err)?.to_vec1::<f32>().map_err(py_err)?;
145
+ let th = cam.r_wp.theta.as_tensor().flatten_all().map_err(py_err)?.to_vec1::<f32>().map_err(py_err)?;
146
+ let novo: Vec<f32> = th.iter().zip(gv.iter()).map(|(t, g_)| t - 0.05 * g_).collect();
147
+ cam.r_wp
148
+ .theta
149
+ .set(&candle_core::Tensor::from_vec(novo, th.len(), &dev).map_err(py_err)?)
150
+ .map_err(py_err)?;
151
+ }
152
+ hub.registrar("fnh_gate", cam.gate_ultimo);
153
+ hub.registrar("fnh_punicao", punicao);
154
+ hub.registrar("perda", perdas[p]);
155
+ let _ = p;
156
+ }
157
+ let theta_norma: f64 = cam
158
+ .r_wp
159
+ .theta
160
+ .as_tensor()
161
+ .flatten_all()
162
+ .map_err(py_err)?
163
+ .to_vec1::<f32>()
164
+ .map_err(py_err)?
165
+ .iter()
166
+ .map(|v| (*v as f64).powi(2))
167
+ .sum::<f64>()
168
+ .sqrt();
169
+ let delta_ort = cam.r_wp.erro_ortogonalidade().map_err(py_err)?;
170
+ resumo.insert(
171
+ format!("punicao_{rotulo}"),
172
+ serde_json::json!({
173
+ "gate_final": cam.gate_ultimo,
174
+ "theta_norma": theta_norma,
175
+ "delta_ort": delta_ort,
176
+ "perda_inicial": perdas[0],
177
+ "perda_final": perdas[perdas.len() - 1],
178
+ }),
179
+ );
180
+ }
181
+ let _ = &mut camada;
182
+ let d = serde_json::json!({
183
+ "versao": "fnh-0.7.0",
184
+ "resumo": resumo,
185
+ });
186
+ Ok(pythonize::pythonize(py, &d)?.into())
187
+ }
188
+
189
+
190
+ /// **Quebra-cabeça sobre o conhecimento TREINADO REAL** (script 63):
191
+ /// carrega o tokenizador (codebook do nível `nivel` — o conhecimento
192
+ /// aprendido dos dados reais) e o gerador (a cabeça real como logits_fn
193
+ /// do verificador); executa o protocolo completo de r-30 Q-AD.
194
+ #[pyfunction]
195
+ #[pyo3(signature = (cfg, caminho_tokenizador, caminho_gerador, n_pecas=4, n_rodadas=12, tau_c=0.4, nivel=0, seed=777))]
196
+ pub(crate) fn quebra_cabeca_real(
197
+ cfg: &ConfigPy,
198
+ caminho_tokenizador: &str,
199
+ caminho_gerador: &str,
200
+ n_pecas: usize,
201
+ n_rodadas: usize,
202
+ tau_c: f64,
203
+ nivel: usize,
204
+ seed: u64,
205
+ ) -> PyResult<PyObject> {
206
+ Python::with_gil(|py| {
207
+ let dev = candle_core::Device::Cpu;
208
+ crate::rng::definir_semente(seed);
209
+ let mut rng = rng::RngDeterministico::novo(seed);
210
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
211
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("tok: {e}")))?;
212
+ carregar_tokenizador(&mut tok, caminho_tokenizador)
213
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
214
+ let mut ger = gerador::GeradorConceitos::novo(&cfg.cfg, &dev)
215
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("ger: {e}")))?;
216
+ ger.carregar(caminho_gerador)
217
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("ger carregar: {e}")))?;
218
+ let nivel = nivel.min(tok.quants.len().saturating_sub(1));
219
+ // conhecimento treinado REAL: codebook Stiefel (K, d_lat)
220
+ let codebook = tok
221
+ .quants[nivel]
222
+ .codebook()
223
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("codebook: {e}")))?;
224
+ let d_lat = codebook.dims()[1];
225
+ let k = ger.k;
226
+ // δ_ort REAL do codebook treinado (rotação Stiefel em treino — A2)
227
+ let delta_ort_codebook = tok.quants[nivel]
228
+ .delta_ort()
229
+ .unwrap_or(1.0);
230
+ // logits_fn REAL: agregado (d_lat,) → proj_entrada → norm → cabeça
231
+ let proj = &ger.proj_entrada;
232
+ let norm = &ger.norm_entrada;
233
+ let cabeca = &ger.cabeca.cabecas[nivel];
234
+ let logits_fn = move |x: &candle_core::Tensor| -> candle_core::Result<candle_core::Tensor> {
235
+ let h = proj.fwd(&x.reshape((1, d_lat))?)?;
236
+ let h = norm.fwd(&h)?;
237
+ let lg = cabeca.fwd(&h)?;
238
+ lg.reshape((k,))
239
+ };
240
+ // verificador com temperatura calibrada pela fase C quando
241
+ // disponível; default 1,0 (a cota V-2 aplica-se com ECE medido)
242
+ let verif = verificador::VerificadorNcm::novo();
243
+ let mut arvore = arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
244
+ let cfg_qc = pensamento::ConfigQuebraCabeca::novo(n_pecas, n_rodadas, tau_c);
245
+ let r = pensamento::quebra_cabeca_inovar(&codebook, &verif, &logits_fn, &mut arvore, &cfg_qc, &mut rng)
246
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("qc: {e}")))?;
247
+ let rodadas: Vec<serde_json::Value> = r
248
+ .rodadas
249
+ .iter()
250
+ .map(|x| {
251
+ serde_json::json!({
252
+ "rodada": x.rodada, "confianca": x.confianca,
253
+ "aceito": x.aceito, "delta": x.delta_aplicado,
254
+ })
255
+ })
256
+ .collect();
257
+ let d_ = serde_json::json!({
258
+ "versao": "quebra-cabeca-real-0.7.0",
259
+ "nivel": nivel,
260
+ "delta_ort_codebook": delta_ort_codebook,
261
+ "n_conceitos": codebook.dims()[0],
262
+ "d_lat": d_lat,
263
+ "aceitos": r.aceitos,
264
+ "rejeitados": r.rejeitados,
265
+ "confianca_base": r.confianca_base,
266
+ "confianca_melhor": r.confianca_melhor,
267
+ "telemetria": r.telemetria,
268
+ "rodadas": rodadas,
269
+ "arvore_invariantes": arvore.invariantes_ok().is_ok(),
270
+ "punicao_no_pensamento": arvore
271
+ .no("pensamento")
272
+ .and_then(|i| arvore.nos[i].metricas.get("punicao").copied())
273
+ .unwrap_or(0.0),
274
+ });
275
+ Ok(pythonize::pythonize(py, &d_)?.into())
276
+ })
277
+ }
278
+
279
+
280
+ /// Demo/gate do quebra-cabeça sobre conceitos SINTÉTICOS estruturados
281
+ /// (validação de mecanismo; a avaliação com dados reais vive no script 63).
282
+ #[pyfunction]
283
+ #[pyo3(signature = (m=24, d=8, n_pecas=4, n_rodadas=12, tau_c=0.35, seed=777))]
284
+ pub(crate) fn demo_quebra_cabeca(
285
+ py: Python,
286
+ m: usize,
287
+ d: usize,
288
+ n_pecas: usize,
289
+ n_rodadas: usize,
290
+ tau_c: f64,
291
+ seed: u64,
292
+ ) -> PyResult<PyObject> {
293
+ let dev = candle_core::Device::Cpu;
294
+ let mut rng = rng::RngDeterministico::novo(seed);
295
+ // conceitos estruturados: cluster dominante +1 (média direcional) com
296
+ // 25% de outliers fracos — o agregado TEM direção (sem cancelamento)
297
+ let mut v = Vec::with_capacity(m * d);
298
+ for i in 0..m {
299
+ let centro = if i < m / 4 { 0.2 } else { 1.0 };
300
+ for j in 0..d {
301
+ v.push((centro + rng.normal(0.0, 0.25)) as f32);
302
+ }
303
+ }
304
+ let conceitos = candle_core::Tensor::from_vec(v, (m, d), &dev).map_err(py_err)?;
305
+ // logits_fn ASSIMÉTRICO: projeção fixa W_proj (rotação-sensível — a
306
+ // agregação simétrica seria invariante sob permutação/rotação, e o
307
+ // backbone real é exatamente o avaliador NÃO-invariente da prática).
308
+ let mut wv = Vec::with_capacity(d * 4);
309
+ for _ in 0..d * 4 {
310
+ wv.push((rng.normal_padrao() as f32) * 2.5);
311
+ }
312
+ let w_proj = candle_core::Tensor::from_vec(wv, (d, 4), &dev).map_err(py_err)?;
313
+ // verificador com temperatura padrão
314
+ let verif = verificador::VerificadorNcm::novo();
315
+ let logits_fn = |x: &candle_core::Tensor| -> candle_core::Result<candle_core::Tensor> {
316
+ // logits = xᵀ·W_proj (4,) — sensível à direção do agregado
317
+ x.reshape((1, d))?.matmul(&w_proj)?.reshape((4,))
318
+ };
319
+ let mut arvore = arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
320
+ let cfg = pensamento::ConfigQuebraCabeca::novo(n_pecas, n_rodadas, tau_c);
321
+ let r = pensamento::quebra_cabeca_inovar(&conceitos, &verif, &logits_fn, &mut arvore, &cfg, &mut rng)
322
+ .map_err(py_err)?;
323
+ let rodadas: Vec<serde_json::Value> = r
324
+ .rodadas
325
+ .iter()
326
+ .map(|x| {
327
+ serde_json::json!({
328
+ "rodada": x.rodada, "confianca": x.confianca,
329
+ "aceito": x.aceito, "delta": x.delta_aplicado,
330
+ })
331
+ })
332
+ .collect();
333
+ let d_ = serde_json::json!({
334
+ "versao": "quebra-cabeca-0.7.0",
335
+ "aceitos": r.aceitos,
336
+ "rejeitados": r.rejeitados,
337
+ "confianca_base": r.confianca_base,
338
+ "confianca_melhor": r.confianca_melhor,
339
+ "telemetria": r.telemetria,
340
+ "rodadas": rodadas,
341
+ "arvore_invariantes": arvore.invariantes_ok().is_ok(),
342
+ });
343
+ Ok(pythonize::pythonize(py, &d_)?.into())
344
+ }
src/python/jepa_treinos.rs ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::jepa_treinos` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ /// Treina uma variante JEPA (7 variantes por máscara; lejepa/mcjepa/bjepa
8
+ /// têm funções próprias) sobre patches (n, d) com grade dada.
9
+ #[pyfunction]
10
+ pub(crate) fn treinar_jepa(py: Python, dados: Vec<Vec<f64>>, grid: Vec<usize>, variante: String, cfg: Option<String>) -> PyResult<PyObject> {
11
+ let dev = candle_core::Device::Cpu;
12
+ let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
13
+ let mut c = jepa::CfgJepa::default();
14
+ if let Some(js) = &cfg {
15
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
16
+ if let Some(o) = v.as_object() {
17
+ for (k, val) in o {
18
+ match k.as_str() {
19
+ "d_emb" => c.d_emb = val.as_u64().unwrap_or(32) as usize,
20
+ "epocas" => c.epocas = val.as_u64().unwrap_or(60) as usize,
21
+ "lr" => c.lr = val.as_f64().unwrap_or(3e-3),
22
+ "m" => c.m = val.as_f64().unwrap_or(0.99),
23
+ "lambda_vc" => c.lambda_vc = val.as_f64().unwrap_or(0.5),
24
+ "gamma_vc" => c.gamma_vc = val.as_f64().unwrap_or(1.0),
25
+ "frac_mascara" => c.frac_mascara = val.as_f64().unwrap_or(0.35),
26
+ "semente" => c.semente = val.as_u64().unwrap_or(42),
27
+ _ => {}
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ let var = match variante.as_str() {
34
+ "image" => jepa::VarianteJepa::ImageJepa,
35
+ "vjepa2" => jepa::VarianteJepa::VJepa2,
36
+ "audio" => jepa::VarianteJepa::AudioJepa,
37
+ "point" => jepa::VarianteJepa::PointJepa,
38
+ "jepa3d" => jepa::VarianteJepa::Jepa3D,
39
+ "brain" => jepa::VarianteJepa::BrainJepa,
40
+ "tjepa" => jepa::VarianteJepa::TJepa,
41
+ _ => jepa::VarianteJepa::TJepa,
42
+ };
43
+ let grid2 = if grid.is_empty() { vec![t.dim(0).unwrap_or(1)] } else { grid };
44
+ let rel = jepa::treinar_variante(
45
+ &t,
46
+ &|_n, r| jepa::mascara_por_variante(var, &grid2, c.frac_mascara, r),
47
+ &c,
48
+ var,
49
+ None,
50
+ &dev,
51
+ )
52
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
53
+ let d = serde_json::json!({
54
+ "versao": jepa::versao(),
55
+ "variante": rel.variante,
56
+ "perda_inicial": rel.perda_inicial,
57
+ "perda_final": rel.perda_final,
58
+ "perda_pred_inicial": rel.perda_pred_inicial,
59
+ "perda_pred_final": rel.perda_pred_final,
60
+ "queda_pct": if rel.perda_pred_inicial.is_finite() && rel.perda_pred_inicial > 0.0 {
61
+ 100.0 * (1.0 - rel.perda_pred_final / rel.perda_pred_inicial)
62
+ } else { 0.0 },
63
+ "std_emb_inicial": rel.std_emb_inicial,
64
+ "std_emb_final": rel.std_emb_final,
65
+ "colapso_evitado": rel.std_emb_final > 0.05,
66
+ "n_patches": rel.n_patches,
67
+ "epocas": rel.epocas,
68
+ });
69
+ Ok(pythonize::pythonize(py, &d)?.into())
70
+ }
71
+
72
+
73
+ /// Treina LeJEPA (edição-condicionado): dados + instruções + editados.
74
+ #[pyfunction]
75
+ pub(crate) fn treinar_lejepa(py: Python, dados: Vec<Vec<f64>>, edits: Vec<Vec<f64>>, editados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
76
+ let dev = candle_core::Device::Cpu;
77
+ let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
78
+ let e = tensor_de_2d(edits, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
79
+ let y = tensor_de_2d(editados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
80
+ let mut c = jepa::CfgJepa::default();
81
+ if let Some(js) = &cfg {
82
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
83
+ if let Some(o) = v.as_object() {
84
+ if let Some(x) = o.get("epocas") { c.epocas = x.as_u64().unwrap_or(60) as usize; }
85
+ if let Some(x) = o.get("d_emb") { c.d_emb = x.as_u64().unwrap_or(32) as usize; }
86
+ if let Some(x) = o.get("lr") { c.lr = x.as_f64().unwrap_or(3e-3); }
87
+ if let Some(x) = o.get("semente") { c.semente = x.as_u64().unwrap_or(42); }
88
+ }
89
+ }
90
+ }
91
+ let rel = jepa::treinar_lejepa(&t, &e, &y, &c, &dev)
92
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
93
+ let d = serde_json::json!({
94
+ "variante": rel.variante, "perda_inicial": rel.perda_inicial,
95
+ "perda_final": rel.perda_final, "std_emb_final": rel.std_emb_final,
96
+ "colapso_evitado": rel.std_emb_final > 0.05,
97
+ });
98
+ Ok(pythonize::pythonize(py, &d)?.into())
99
+ }
100
+
101
+
102
+ /// Treina MC-JEPA (cruz-modal): duas modalidades pareadas.
103
+ #[pyfunction]
104
+ pub(crate) fn treinar_mcjepa(py: Python, mod_a: Vec<Vec<f64>>, mod_b: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
105
+ let dev = candle_core::Device::Cpu;
106
+ let a = tensor_de_2d(mod_a, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
107
+ let b = tensor_de_2d(mod_b, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
108
+ let mut c = jepa::CfgJepa::default();
109
+ if let Some(js) = &cfg {
110
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
111
+ if let Some(o) = v.as_object() {
112
+ if let Some(x) = o.get("epocas") { c.epocas = x.as_u64().unwrap_or(60) as usize; }
113
+ if let Some(x) = o.get("d_emb") { c.d_emb = x.as_u64().unwrap_or(32) as usize; }
114
+ if let Some(x) = o.get("lr") { c.lr = x.as_f64().unwrap_or(3e-3); }
115
+ }
116
+ }
117
+ }
118
+ let rel = jepa::treinar_mcjepa(&a, &b, &c, &dev)
119
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
120
+ let d = serde_json::json!({
121
+ "variante": rel.variante, "perda_inicial": rel.perda_inicial,
122
+ "perda_final": rel.perda_final, "std_emb_final": rel.std_emb_final,
123
+ "colapso_evitado": rel.std_emb_final > 0.05,
124
+ });
125
+ Ok(pythonize::pythonize(py, &d)?.into())
126
+ }
127
+
128
+
129
+ /// Treina BJEPA (bidirecional com consistência de ciclo) sobre sequência.
130
+ #[pyfunction]
131
+ pub(crate) fn treinar_bjepa(py: Python, serie: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
132
+ let dev = candle_core::Device::Cpu;
133
+ let t = tensor_de_2d(serie, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
134
+ let mut c = jepa::CfgJepa::default();
135
+ if let Some(js) = &cfg {
136
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
137
+ if let Some(o) = v.as_object() {
138
+ if let Some(x) = o.get("epocas") { c.epocas = x.as_u64().unwrap_or(60) as usize; }
139
+ if let Some(x) = o.get("d_emb") { c.d_emb = x.as_u64().unwrap_or(32) as usize; }
140
+ if let Some(x) = o.get("lr") { c.lr = x.as_f64().unwrap_or(3e-3); }
141
+ }
142
+ }
143
+ }
144
+ let rel = jepa::treinar_bjepa(&t, &c, &dev)
145
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
146
+ let d = serde_json::json!({
147
+ "variante": rel.variante, "perda_inicial": rel.perda_inicial,
148
+ "perda_final": rel.perda_final, "std_emb_final": rel.std_emb_final,
149
+ "colapso_evitado": rel.std_emb_final > 0.05,
150
+ });
151
+ Ok(pythonize::pythonize(py, &d)?.into())
152
+ }
153
+
154
+
155
+ /// Demo SonicMoE: roteamento top-k com capacidade e perda de balanceamento.
156
+ #[pyfunction]
157
+ pub(crate) fn demo_sonic_moe(py: Python, n: usize, d: usize, n_expertos: usize, top_k: usize, capacidade: usize) -> PyResult<PyObject> {
158
+ let dev = candle_core::Device::Cpu;
159
+ let moe = jepa::SonicMoe::novo(d, n_expertos, top_k, capacidade, &dev)
160
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
161
+ let x = candle_core::Tensor::randn(0f32, 1.0, (n, d), &dev)
162
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
163
+ let s = moe.fwd(&x).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
164
+ let aux = s.perda_aux.to_scalar::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64;
165
+ let d_ = serde_json::json!({
166
+ "versao": jepa::versao(),
167
+ "carga_por_especialista": s.carga,
168
+ "descartados": s.descartados,
169
+ "perda_aux": aux,
170
+ "capacidade_respeitada": s.carga.iter().all(|&c| c <= capacidade),
171
+ "saida_forma": s.saida.dims().to_vec(),
172
+ });
173
+ Ok(pythonize::pythonize(py, &d_)?.into())
174
+ }
175
+
176
+
177
+ /// Demo ReplaySSM: estabilidade + replay em cenário de troca de tarefa.
178
+ #[pyfunction]
179
+ pub(crate) fn demo_replay_ssm(py: Python, passos: usize, alpha: f64) -> PyResult<PyObject> {
180
+ let dev = candle_core::Device::Cpu;
181
+ let mut ssm = jepa::ReplaySsm::novo(4, 6, alpha, 8, &dev)
182
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
183
+ let mut otim = crate::treino::AdamWManual::novo(ssm.vars(), 1e-2, 0.0);
184
+ let x_antigo = candle_core::Tensor::from_vec(vec![1.0f32; 16 * 4], (16, 4), &dev)
185
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
186
+ let mut hist = Vec::new();
187
+ for _ in 0..passos {
188
+ let h = ssm.scan(&x_antigo).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
189
+ let alvo = h.detach();
190
+ let (pt, pn, pr) = ssm.passo_treino(&x_antigo, &alvo, &mut otim)
191
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
192
+ hist.push(serde_json::json!({"total": pt, "novo": pn, "replay": pr}));
193
+ }
194
+ let x_novo = candle_core::Tensor::from_vec(vec![-1.0f32; 16 * 4], (16, 4), &dev)
195
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
196
+ for _ in 0..passos {
197
+ let h = ssm.scan(&x_novo).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
198
+ let alvo = h.detach();
199
+ let _ = ssm.passo_treino(&x_novo, &alvo, &mut otim)
200
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
201
+ }
202
+ let h_fin = ssm.scan(&x_antigo).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
203
+ let mag = h_fin.sqr().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
204
+ .max_all().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
205
+ .to_scalar::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64;
206
+ let d_ = serde_json::json!({
207
+ "versao": jepa::versao(),
208
+ "alpha_replay": alpha,
209
+ "historico": hist,
210
+ "magnitudes_finitas": mag.is_finite(),
211
+ "magnitude_max_pos_troca": mag,
212
+ });
213
+ Ok(pythonize::pythonize(py, &d_)?.into())
214
+ }
215
+
216
+
217
+ /// Extrai patches 2D de uma imagem achatada (C, H, W) — adaptador Image-JEPA.
218
+ #[pyfunction]
219
+ pub(crate) fn patches_2d(img: Vec<f32>, c: usize, h: usize, w: usize, ph: usize, pw: usize) -> PyResult<Vec<Vec<f32>>> {
220
+ let dev = candle_core::Device::Cpu;
221
+ let t = candle_core::Tensor::from_vec(img, (c, h, w), &dev)
222
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
223
+ let p = jepa::em_patches_2d(&t, ph, pw)
224
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
225
+ p.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
226
+ }
227
+
228
+
229
+ /// Extrai patches canal-independentes de série temporal (C, T) — T-JEPA.
230
+ #[pyfunction]
231
+ pub(crate) fn patches_serie(ts: Vec<f32>, c: usize, t: usize, pt: usize) -> PyResult<Vec<Vec<f32>>> {
232
+ let dev = candle_core::Device::Cpu;
233
+ let tt = candle_core::Tensor::from_vec(ts, (c, t), &dev)
234
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
235
+ let p = jepa::em_patches_serie(&tt, pt)
236
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
237
+ p.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
238
+ }
239
+
240
+
241
+ /// Extrai grupos FPS de nuvem de pontos (n, d) — Point-JEPA.
242
+ #[pyfunction]
243
+ pub(crate) fn grupos_pontos(pc: Vec<Vec<f64>>, k: usize, m: usize, semente: u64) -> PyResult<Vec<Vec<f64>>> {
244
+ let dev = candle_core::Device::Cpu;
245
+ let t = tensor_de_2d(pc, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
246
+ let mut rng = crate::rng::RngDeterministico::novo(semente);
247
+ let g = jepa::em_grupos_pontos(&t, k, m, &mut rng)
248
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
249
+ let out = g.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
250
+ Ok(out.into_iter().map(|l| l.into_iter().map(|v| v as f64).collect()).collect())
251
+ }
src/python/mod.rs ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! FFI Python (PyO3) — dividida por domínio (v0.10.0, r-35 §2).
2
+
3
+ //! FFI Python (PyO3) do KHTST_RUST — todo o `#[pymodule]`, os
4
+ //! `#[pyfunction]`/`#[pyclass]` e a cola Python (conversões,
5
+ //! callbacks, demos) vivem AQUI desde a v0.9.0 (antes: lib.rs,
6
+ //! ~105 kB — crítica "God Object" do agente Qwen3.8, r-34 §5).
7
+ //! A lógica de domínio (src/*.rs) não importa pyo3: `cargo test
8
+ //! --no-default-features` executa a suíte SEM linkar o Python.
9
+ //!
10
+ //! Regra da casa mantida: nenhuma funcionalidade da superfície
11
+ //! Python foi removida — os mesmos símbolos são registrados no
12
+ //! `nucleo_rust`.
13
+
14
+ use pyo3::prelude::*;
15
+
16
+ use crate::*;
17
+
18
+ // ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
19
+ mod calibracao;
20
+ mod demos_qwen;
21
+ mod fnh_arvore;
22
+ mod jepa_treinos;
23
+ mod moe;
24
+ mod nlp_nlg;
25
+ mod nucleo;
26
+ mod pensamento_demos;
27
+ mod som;
28
+ mod treino_fases;
29
+
30
+
31
+ // ---- itens mantidos no próprio mod.rs ----
32
+
33
+
34
+
35
+ /// Adaptador FFI→domínio (v0.9.0): embrulha um callable Python no tipo de
36
+ /// domínio `treino::CallbackTreino` — `treino.rs` não importa pyo3.
37
+ fn callback_treino_de_py(callback: Option<pyo3::Bound<'_, pyo3::PyAny>>) -> treino::CallbackTreino {
38
+ use std::sync::Arc;
39
+ match callback {
40
+ None => None,
41
+ Some(c) => {
42
+ let f: pyo3::Py<pyo3::PyAny> = c.unbind();
43
+ let g: Arc<dyn Fn(&serde_json::Value) + Send + Sync> =
44
+ Arc::new(move |dados: &serde_json::Value| {
45
+ pyo3::Python::with_gil(|py| {
46
+ if let Ok(obj) = pythonize::pythonize(py, dados) {
47
+ let _ = f.call1(py, (obj,));
48
+ }
49
+ });
50
+ });
51
+ Some(g)
52
+ }
53
+ }
54
+ }
55
+
56
+
57
+ // ===========================================================================
58
+ // v0.5.0 — suíte SOM 4D, suíte JEPA, Pensamento, telemetria robusta
59
+ // ===========================================================================
60
+
61
+ fn tensor_de_2d(dados: Vec<Vec<f64>>, dev: &candle_core::Device) -> candle_core::Result<candle_core::Tensor> {
62
+ let d = dados.first().map(|r| r.len()).unwrap_or(1).max(1);
63
+ let n = dados.len();
64
+ let flat: Vec<f32> = dados.into_iter().flat_map(|r| r.into_iter().map(|x| x as f32).collect::<Vec<_>>()).collect();
65
+ candle_core::Tensor::from_vec(flat, (n, d), dev)
66
+ }
67
+
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // v0.6.0 — verificador NCM (sem oráculo), fase C, MoE redirecionável,
71
+ // acoplador duplex e autoajuste
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /// Converte matriz f64 → Tensor (n, d) F32.
75
+ fn tensor_f64_2d(v: &[Vec<f64>], dev: &candle_core::Device) -> candle_core::Result<candle_core::Tensor> {
76
+ let (n, d) = (v.len(), v.first().map(|l| l.len()).unwrap_or(0));
77
+ candle_core::Tensor::from_vec(v.iter().flatten().cloned().collect(), (n, d), dev)
78
+ }
79
+
80
+
81
+ fn py_err(e: candle_core::Error) -> pyo3::PyErr {
82
+ pyo3::exceptions::PyRuntimeError::new_err(format!("{e}"))
83
+ }
84
+
85
+
86
+ #[pymodule]
87
+ fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
88
+ pyo3_log::init();
89
+ m.add_function(wrap_pyfunction!(nucleo::versao, m)?)?;
90
+ m.add_function(wrap_pyfunction!(nucleo::info_build, m)?)?;
91
+ m.add_function(wrap_pyfunction!(nucleo::lint_config, m)?)?;
92
+ m.add_function(wrap_pyfunction!(treino_fases::treinar_fase_a, m)?)?;
93
+ m.add_function(wrap_pyfunction!(treino_fases::treinar_fase_b, m)?)?;
94
+ m.add_function(wrap_pyfunction!(treino_fases::gerar_amostras, m)?)?;
95
+ m.add_function(wrap_pyfunction!(nucleo::verificar_mascara, m)?)?;
96
+ m.add_function(wrap_pyfunction!(nucleo::verificar_stiefel, m)?)?;
97
+ // v0.10.0 — checkpoints atômicos com prova de integridade
98
+ m.add_function(wrap_pyfunction!(nucleo::checkpoint_valido, m)?)?;
99
+ m.add_function(wrap_pyfunction!(nucleo::checkpoint_manifesto, m)?)?;
100
+ m.add_function(wrap_pyfunction!(nucleo::sha256_arquivo, m)?)?;
101
+ // v0.5.0 — SOM 4D / JEPA / Pensamento
102
+ m.add_function(wrap_pyfunction!(som::treinar_som4d, m)?)?;
103
+ m.add_function(wrap_pyfunction!(som::treinar_gng, m)?)?;
104
+ m.add_function(wrap_pyfunction!(som::treinar_hsom, m)?)?;
105
+ m.add_function(wrap_pyfunction!(som::demo_tsom, m)?)?;
106
+ m.add_function(wrap_pyfunction!(jepa_treinos::treinar_jepa, m)?)?;
107
+ m.add_function(wrap_pyfunction!(jepa_treinos::treinar_lejepa, m)?)?;
108
+ m.add_function(wrap_pyfunction!(jepa_treinos::treinar_mcjepa, m)?)?;
109
+ m.add_function(wrap_pyfunction!(jepa_treinos::treinar_bjepa, m)?)?;
110
+ m.add_function(wrap_pyfunction!(jepa_treinos::demo_sonic_moe, m)?)?;
111
+ m.add_function(wrap_pyfunction!(jepa_treinos::demo_replay_ssm, m)?)?;
112
+ m.add_function(wrap_pyfunction!(pensamento_demos::classificar_erro, m)?)?;
113
+ m.add_function(wrap_pyfunction!(pensamento_demos::confianca_de_logits, m)?)?;
114
+ m.add_function(wrap_pyfunction!(pensamento_demos::projetar_ridge, m)?)?;
115
+ m.add_function(wrap_pyfunction!(pensamento_demos::demo_pensamento, m)?)?;
116
+ m.add_function(wrap_pyfunction!(jepa_treinos::patches_2d, m)?)?;
117
+ m.add_function(wrap_pyfunction!(jepa_treinos::patches_serie, m)?)?;
118
+ m.add_function(wrap_pyfunction!(jepa_treinos::grupos_pontos, m)?)?;
119
+ // v0.6.0
120
+ m.add_function(wrap_pyfunction!(calibracao::calibrar_verificador, m)?)?;
121
+ m.add_function(wrap_pyfunction!(calibracao::demo_verificador_ncm, m)?)?;
122
+ m.add_function(wrap_pyfunction!(treino_fases::treinar_fase_c, m)?)?;
123
+ m.add_function(wrap_pyfunction!(moe::demo_moe_redirecionavel, m)?)?;
124
+ m.add_function(wrap_pyfunction!(moe::jepa_com_moe, m)?)?;
125
+ m.add_function(wrap_pyfunction!(moe::som_com_moe, m)?)?;
126
+ m.add_function(wrap_pyfunction!(moe::demo_acoplador_duplo, m)?)?;
127
+ m.add_function(wrap_pyfunction!(pensamento_demos::demo_pensamento_ncm, m)?)?;
128
+ m.add_function(wrap_pyfunction!(pensamento_demos::demo_autoajuste, m)?)?;
129
+ m.add_function(wrap_pyfunction!(fnh_arvore::demo_fnh, m)?)?;
130
+ m.add_function(wrap_pyfunction!(fnh_arvore::demo_quebra_cabeca, m)?)?;
131
+ m.add_function(wrap_pyfunction!(fnh_arvore::quebra_cabeca_real, m)?)?;
132
+ m.add_function(wrap_pyfunction!(nlp_nlg::demo_nlp_nlg, m)?)?;
133
+ m.add_function(wrap_pyfunction!(som::demo_autoajuste_som, m)?)?;
134
+ // v0.8.0 — análise/prova/teste das propostas Qwen3.8
135
+ m.add_function(wrap_pyfunction!(demos_qwen::demo_qwen_v080, m)?)?;
136
+ // v0.9.0 — veredito executável das críticas Qwen3.8 (r-34)
137
+ m.add_function(wrap_pyfunction!(demos_qwen::demo_criticas_v090, m)?)?;
138
+ m.add_class::<nucleo::BpePy>()?;
139
+ m.add_class::<nucleo::ConfigPy>()?;
140
+ m.add_class::<nucleo::PonteDuplaPy>()?;
141
+ #[cfg(feature = "ponte_async")]
142
+ m.add_class::<nucleo::PonteAsyncPy>()?;
143
+ m.add_class::<fnh_arvore::ArvorePesosPy>()?;
144
+ Ok(())
145
+ }
src/python/moe.rs ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::moe` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ /// **MoE redirecionável — demo completa (M-R1..M-R5):** neutralidade da
8
+ /// atenção própria (M-R2), invariância de capacidade/perda auxiliar sob
9
+ /// redirecionamento (M-R1), paralelismo medido (M-R3), water-filling por
10
+ /// QE vs uniforme (M-R5) e fechamento da classe (M-R4 — via permutação).
11
+ #[pyfunction]
12
+ pub(crate) fn demo_moe_redirecionavel(d: usize, n: usize, semente: u64) -> PyResult<PyObject> {
13
+ Python::with_gil(|py| {
14
+ let dev = candle_core::Device::Cpu;
15
+ crate::rng::definir_semente(semente);
16
+ let mut rng = crate::rng::RngDeterministico::novo(semente);
17
+ let x: Vec<f32> = (0..n * d).map(|_| rng.normal(0.0, 1.0) as f32).collect();
18
+ let x = candle_core::Tensor::from_vec(x, (n, d), &dev)
19
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
20
+ let mut moe = moe_redirecionavel::MoeRedirecionavel::novo(d, 8, 2, n, &dev)
21
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
22
+ // M-R2: neutralidade no nascimento
23
+ let g0 = moe.atencao.gate.as_tensor().to_scalar::<f32>().unwrap();
24
+ let y0 = moe.atencao.fwd(&x).unwrap();
25
+ let dif0 = (&y0 - &x).unwrap().sqr().unwrap().sum_all().unwrap()
26
+ .to_scalar::<f32>().unwrap() as f64;
27
+ // abre o gate (mecanismo ativo)
28
+ moe.atencao.gate.set(&candle_core::Tensor::new(1.0f32, &dev).unwrap()).unwrap();
29
+ // M-R1: invariância sob redirecionamento
30
+ let s_enc = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
31
+ let carga_antes: usize = s_enc.carga.iter().sum();
32
+ let perda_antes = s_enc.perda_aux.to_scalar::<f32>().unwrap() as f64;
33
+ let permuta: Vec<usize> = (0..8).map(|i| (i + 1) % 8).collect();
34
+ moe.redirecionar(&permuta);
35
+ let s_enc2 = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
36
+ let carga_depois: usize = s_enc2.carga.iter().sum();
37
+ let perda_depois = s_enc2.perda_aux.to_scalar::<f32>().unwrap() as f64;
38
+ // M-R3: paralelismo medido (enc ∥ dec em 2 threads)
39
+ let (_e, _dc, t_par, t_seq, speedup) = moe.fwd_ambos_par(&x).unwrap();
40
+ // M-R5: water-filling vs uniforme (cota analítica)
41
+ let qes: Vec<f64> = vec![0.15, 0.4, 1.0, 0.25, 0.6, 0.1, 0.35, 0.5];
42
+ let c_wf = moe_redirecionavel::alocar_por_qe(&qes, 16);
43
+ let c_un = vec![2usize; 8];
44
+ let cota = |c: &[usize]| qes.iter().zip(c).map(|(q, ci)| q * q / *ci as f64).sum::<f64>();
45
+ let (cota_wf, cota_un) = (cota(&c_wf), cota(&c_un));
46
+ let d_ = serde_json::json!({
47
+ "versao": moe_redirecionavel::versao(), "d": d, "n": n,
48
+ "valor_gate_no_nascimento": g0, "neutralidade_dif": dif0,
49
+ "carga_antes": carga_antes, "carga_depois": carga_depois,
50
+ "perda_aux_antes": perda_antes, "perda_aux_depois": perda_depois,
51
+ "t_paralelo_s": t_par, "t_sequencial_s": t_seq, "speedup": speedup,
52
+ "alocacao_water_filling": c_wf, "cota_wf": cota_wf, "cota_uniforme": cota_un,
53
+ "gates": {
54
+ "gate_mr1_capacidade": carga_antes == carga_depois,
55
+ "gate_mr1_perda_aux": (perda_antes - perda_depois).abs() < 2.0 + 1e-6,
56
+ "gate_mr2_neutralidade": dif0 == 0.0 && g0 == 0.0,
57
+ "gate_mr3_paralelismo": t_par < t_seq,
58
+ "gate_mr5_water_filling": cota_wf <= cota_un + 1e-9,
59
+ },
60
+ });
61
+ Ok(pythonize::pythonize(py, &d_)?.into())
62
+ })
63
+ }
64
+
65
+
66
+ /// **JEPA com MoE redirecionável (ablação de 3 braços):** `modo` ∈
67
+ /// {"pequena", "densa", "moe"} — o braço "densa" tem a capacidade TOTAL dos
68
+ /// especialistas SEM roteamento (isola o ganho do roteamento em si).
69
+ #[pyfunction]
70
+ pub(crate) fn jepa_com_moe(
71
+ patches: Vec<Vec<f32>>,
72
+ modo: String,
73
+ passos: usize,
74
+ frac_mascara: f64,
75
+ semente: u64,
76
+ ) -> PyResult<PyObject> {
77
+ Python::with_gil(|py| {
78
+ let dev = candle_core::Device::Cpu;
79
+ let t = tensor_f64_2d(&patches.into_iter().map(|l| l.into_iter().map(|x| x as f64).collect()).collect::<Vec<Vec<f64>>>(), &dev)
80
+ .and_then(|t| t.to_dtype(candle_core::DType::F32))
81
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
82
+ let modo: &'static str = match modo.as_str() {
83
+ "pequena" => "pequena",
84
+ "densa" => "densa",
85
+ _ => "moe",
86
+ };
87
+ let r = moe_redirecionavel::treinar_jepa_com_moe(&t, modo, passos, frac_mascara, semente)
88
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
89
+ let d_ = serde_json::json!({
90
+ "modo": r.modo, "perda_pred_inicial": r.perda_pred_inicial,
91
+ "perda_pred_final": r.perda_pred_final, "perda_vc_final": r.perda_vc_final,
92
+ "std_emb": r.std_emb, "carga_max": r.carga_max, "descartados": r.descartados,
93
+ "queda_abs": r.perda_pred_inicial - r.perda_pred_final,
94
+ "aprendeu": r.perda_pred_final < r.perda_pred_inicial,
95
+ });
96
+ Ok(pythonize::pythonize(py, &d_)?.into())
97
+ })
98
+ }
99
+
100
+
101
+ /// **SOM refinado por MoE com water-filling (M-R5) + redirecionamento
102
+ /// para decodificador:** treina um 4D-SOM, refina com MoE (QE′ < QE),
103
+ /// compara com alocação uniforme e mede a decodificação pós-redirect.
104
+ #[pyfunction]
105
+ pub(crate) fn som_com_moe(
106
+ dados: Vec<Vec<f64>>,
107
+ lado: usize,
108
+ passos: usize,
109
+ semente: u64,
110
+ ) -> PyResult<PyObject> {
111
+ Python::with_gil(|py| {
112
+ let dev = candle_core::Device::Cpu;
113
+ let t = tensor_f64_2d(&dados, &dev)
114
+ .and_then(|t| t.to_dtype(candle_core::DType::F32))
115
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
116
+ // treina o 4D-SOM (r-19: S4-1/S4-2/S4-3) — som4d opera em F32
117
+ let cfg_som = som4d::CfgSom4D { nx: lado, ny: lado, nz: lado, nt: lado, ..Default::default() };
118
+ let som = som4d::treinar_som4d(&t, &cfg_som, &dev)
119
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
120
+ let centroides: Vec<Vec<f64>> = som.prototipos.clone();
121
+ // refinamento MoE (mesmo tensor F32; centroides seguem em f64)
122
+ let r = moe_redirecionavel::refinar_som_com_moe(&t, &centroides, passos, semente)
123
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
124
+ let d_ = serde_json::json!({
125
+ "qe_antes": r.qe_antes, "qe_depois_water_filling": r.qe_depois,
126
+ "qe_depois_uniforme": r.qe_alocacao_uniforme,
127
+ "carga_por_celula": r.carga_por_celula,
128
+ "tempo_treino_s": r.tempo_treino_s,
129
+ "mse_decodificador_pos_redirect": r.mse_decodificador,
130
+ "reducao_qe_pct": 100.0 * (r.qe_antes - r.qe_depois) / r.qe_antes.max(1e-12),
131
+ "gates": {
132
+ "gate_moe_refina": r.qe_depois < r.qe_antes,
133
+ "gate_wf_melhor_uniforme": r.qe_depois <= r.qe_alocacao_uniforme * 1.02,
134
+ "gate_redirect_decodifica": r.mse_decodificador < r.qe_antes.powi(2),
135
+ },
136
+ });
137
+ Ok(pythonize::pythonize(py, &d_)?.into())
138
+ })
139
+ }
140
+
141
+
142
+ /// **AcopladorDuplo — demo do protocolo lag-1 síncrono** (AC-1/AC-2):
143
+ /// rodadas duplex Pensamento↔backbone com correções decaindo (co-adaptação)
144
+ /// e confiança subindo; zero deadlocks/timeouts (P.1/P.2 herdados).
145
+ #[pyfunction]
146
+ pub(crate) fn demo_acoplador_duplo(rodadas: usize, timeout_ms: u64) -> PyResult<PyObject> {
147
+ Python::with_gil(|py| {
148
+ let mut ac = acoplador::AcopladorDuplo::nova(16, timeout_ms);
149
+ let mut rng = crate::rng::RngDeterministico::novo(77);
150
+ let mut conf = 0.05f64;
151
+ let mut traj = Vec::new();
152
+ for k in 0..rodadas {
153
+ let estado = acoplador::EstadoModelo {
154
+ a: vec![0.1 * (k % 7) as f32; 16],
155
+ b: vec![rng.normal(0.0, 0.3) as f32; 16],
156
+ c: vec![0.05; 8],
157
+ confianca: conf,
158
+ nivel: k % 7,
159
+ passo: k,
160
+ };
161
+ let _ = ac.rodada(estado).map_err(|e|
162
+ pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
163
+ // correção fresca: decaindo com a confiança (co-adaptação)
164
+ let amplitude = (1.0 - conf).max(0.05);
165
+ let delta: Vec<f32> = (0..16).map(|i| amplitude as f32 * ((i % 5) as f32 - 2.0) * 0.1).collect();
166
+ ac.registrar_correcao(acoplador::Correcao {
167
+ delta, acordo: 0.6 + 0.3 * conf, confianca_agregada: conf,
168
+ iteracao: k, pesos: vec![0.25; 4],
169
+ });
170
+ traj.push(conf);
171
+ conf = (conf + 0.04 + rng.uniforme() * 0.02).min(0.99);
172
+ }
173
+ let t = &ac.telemetria;
174
+ let p = ac.estatisticas();
175
+ let d_ = serde_json::json!({
176
+ "versao": acoplador::versao(), "rodadas": t.rodadas,
177
+ "timeouts": t.timeouts, "tempo_total_ms": t.tempo_total_ms,
178
+ "soma_norma_delta": t.soma_norma_delta,
179
+ "conf_final": traj.last(), "conf_trajetoria_len": traj.len(),
180
+ "envios_a_para_b": p.envios_a_para_b, "envios_b_para_a": p.envios_b_para_a,
181
+ "maior_sobreposicao_us": t.maior_sobreposicao_us,
182
+ "gates": {
183
+ "gate_terminou": t.rodadas == rodadas as u64,
184
+ "gate_sem_timeout": t.timeouts == 0,
185
+ "gate_duplex_simultaneo": p.envios_a_para_b == p.envios_b_para_a && p.envios_a_para_b == rodadas as u64,
186
+ },
187
+ });
188
+ Ok(pythonize::pythonize(py, &d_)?.into())
189
+ })
190
+ }
src/python/nlp_nlg.rs ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::nlp_nlg` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ /// Demo/gate das unidades NLP/NLG sobre estados ocultos sintéticos
8
+ /// (nascimento neutro + gates + coerência de bigramas + especulativo).
9
+ #[pyfunction]
10
+ #[pyo3(signature = (d=32, t=6, seed=11))]
11
+ pub(crate) fn demo_nlp_nlg(py: Python, d: usize, t: usize, seed: u64) -> PyResult<PyObject> {
12
+ let dev = candle_core::Device::Cpu;
13
+ let mut rng = rng::RngDeterministico::novo(seed);
14
+ let v: Vec<f32> = (0..2 * t * d).map(|_| rng.normal_padrao() as f32).collect();
15
+ let h = candle_core::Tensor::from_vec(v, (2, t, d), &dev).map_err(py_err)?;
16
+ // NLP: enriquecimento aditivo — neutro no nascimento
17
+ let mut nlp = nlp::UnidadeNlp::nova(d, &dev).map_err(|e| {
18
+ pyo3::exceptions::PyRuntimeError::new_err(format!("nlp (d%4): {e}"))
19
+ })?;
20
+ let h_enr = nlp.enriquecer(&h).map_err(py_err)?;
21
+ let dif_nlp = (&h).sub(&h_enr).map_err(py_err)?.abs().map_err(py_err)?.max_all().map_err(py_err)?.to_scalar::<f32>().map_err(py_err)? as f64;
22
+ // intenção
23
+ let (intencoes, confs) = nlp.intencao_argmax(&h).map_err(py_err)?;
24
+ // NLG: plano + condicionamento neutro + coerência
25
+ let mut nlg = nlg::UnidadeNlg::nova(d, 4096, &dev).map_err(py_err)?;
26
+ let plano = nlg.plano(&h).map_err(py_err)?;
27
+ let p8 = candle_core::Tensor::randn(0f32, 1f32, (nlg::N_EIXOS_ESTILO,), &dev).map_err(py_err)?;
28
+ let p01 = candle_nn::ops::sigmoid(&p8).map_err(py_err)?;
29
+ let h_cond = nlg.condicionar(&h, Some(&p01)).map_err(py_err)?;
30
+ let dif_nlg = (&h).sub(&h_cond).map_err(py_err)?.abs().map_err(py_err)?.max_all().map_err(py_err)?.to_scalar::<f32>().map_err(py_err)? as f64;
31
+ // coerência: contexto observado vs candidato novo
32
+ let ctx = vec![5u32, 6, 7, 8, 9];
33
+ nlg.observar_contexto(&ctx);
34
+ let s_mesmo = nlg.score_coerencia(&ctx);
35
+ let s_novo = nlg.score_coerencia(&[50u32, 51, 52, 53]);
36
+ // especulativo: aceitação típica + stats
37
+ let mut espec = nlg::DecodificadorEspeculativo::novo(4, 0.95, 10);
38
+ let logits = candle_core::Tensor::from_vec(vec![3.0f32, 0.2, 0.1, 0.05], (4,), &dev)
39
+ .map_err(py_err)?;
40
+ let mut aceites = 0usize;
41
+ for _ in 0..30 {
42
+ let (_, ok) = espec.verificar_e_corrigir(&logits, &[0, 1]).map_err(py_err)?;
43
+ if ok {
44
+ aceites += 1;
45
+ }
46
+ }
47
+ let d_ = serde_json::json!({
48
+ "versao": "nlp-nlg-0.7.0",
49
+ "nlp_gate_medio": nlp.gate_medio,
50
+ "nlp_dif_nascimento": dif_nlp,
51
+ "nlp_intencoes": intencoes,
52
+ "nlp_confiancas": confs,
53
+ "nlg_plano_forma": plano.dims(),
54
+ "nlg_dif_nascimento": dif_nlg,
55
+ "nlg_coerencia_mesmo": s_mesmo,
56
+ "nlg_coerencia_novo": s_novo,
57
+ "espec_aceitacao_media": espec.stats.aceitacao_media,
58
+ "espec_speedup": espec.stats.speedup_estimado,
59
+ "espec_aceites_30": aceites,
60
+ "gates": {
61
+ "nlp_neutro": dif_nlp == 0.0,
62
+ "nlg_neutro": dif_nlg == 0.0,
63
+ "gate_medio_ok": (nlp.gate_medio - 0.119).abs() < 0.05,
64
+ "coerencia_ok": s_mesmo > 0.9 && s_novo < 0.1,
65
+ "espec_ok": espec.stats.aceitacao_media > 0.8,
66
+ },
67
+ });
68
+ Ok(pythonize::pythonize(py, &d_)?.into())
69
+ }
src/python/nucleo.rs ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::nucleo` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ #[pyfunction]
8
+ pub(crate) fn versao() -> &'static str {
9
+ concat!("khtst-rust-", env!("CARGO_PKG_VERSION"))
10
+ }
11
+
12
+
13
+ #[pyfunction]
14
+ pub(crate) fn info_build() -> String {
15
+ format!(
16
+ "rustc={}; perfil={}; features={}; alvo={}",
17
+ build_info::RUSTC_VERSION,
18
+ build_info::PROFILE,
19
+ build_info::FEATURES_STR,
20
+ build_info::TARGET,
21
+ )
22
+ }
23
+
24
+
25
+ /// Lint de config com sugestões por Levenshtein (diagnóstico de digitação).
26
+ #[pyfunction]
27
+ pub(crate) fn lint_config(py: Python, caminho: &str) -> PyResult<PyObject> {
28
+ let txt = std::fs::read_to_string(caminho)
29
+ .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{caminho}: {e}")))?;
30
+ let v: serde_json::Value = serde_json::from_str(&txt)
31
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
32
+ let achatado = diagnostico::achatar(&v);
33
+ let avisos = diagnostico::lint_chaves(&achatado);
34
+ let saida: Vec<serde_json::Value> = avisos
35
+ .iter()
36
+ .map(|a| serde_json::json!({
37
+ "chave": a.chave, "sugestao": a.sugestao, "causa": a.causa, "correcao": a.correcao,
38
+ }))
39
+ .collect();
40
+ Ok(pythonize::pythonize(py, &serde_json::json!(saida))?.into())
41
+ }
42
+
43
+
44
+ #[pyclass(name = "Bpe")]
45
+ pub(crate) struct BpePy {
46
+ bpe: tokens::Bpe,
47
+ }
48
+
49
+ #[pymethods]
50
+ impl BpePy {
51
+ #[staticmethod]
52
+ fn treinar(textos: Vec<String>, vocab_alvo: usize) -> PyResult<Self> {
53
+ Ok(BpePy { bpe: tokens::Bpe::treinar(&textos, vocab_alvo) })
54
+ }
55
+
56
+ #[staticmethod]
57
+ fn carregar(caminho: &str) -> PyResult<Self> {
58
+ let txt = std::fs::read_to_string(caminho)
59
+ .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{e}")))?;
60
+ let v: serde_json::Value = serde_json::from_str(&txt)
61
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
62
+ let bpe = tokens::Bpe::de_json(&v)
63
+ .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("formato BPE inválido"))?;
64
+ Ok(BpePy { bpe })
65
+ }
66
+
67
+ fn codificar(&mut self, texto: &str) -> Vec<u32> {
68
+ self.bpe.codificar(texto)
69
+ }
70
+
71
+ fn decodificar(&self, ids: Vec<u32>) -> String {
72
+ self.bpe.decodificar(&ids)
73
+ }
74
+
75
+ fn salvar(&self, caminho: &str) -> PyResult<()> {
76
+ let txt = serde_json::to_string(&self.bpe.para_json()).unwrap();
77
+ crate::checkpoint::salvar_atomico(caminho, &txt)
78
+ .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{e}")))?;
79
+ Ok(())
80
+ }
81
+
82
+ #[getter]
83
+ fn n_vocab(&self) -> usize {
84
+ self.bpe.n_vocab()
85
+ }
86
+ }
87
+
88
+
89
+ #[pyclass(name = "Config")]
90
+ pub(crate) struct ConfigPy {
91
+ pub(crate) cfg: config::ConfigKhtst,
92
+ caminho: Option<String>,
93
+ }
94
+
95
+ #[pymethods]
96
+ impl ConfigPy {
97
+ #[new]
98
+ #[allow(never_type_fallback)]
99
+ #[pyo3(signature = (caminho=None))]
100
+ fn new(caminho: Option<String>) -> PyResult<Self> {
101
+ let cfg = match &caminho {
102
+ Some(c) => {
103
+ let txt = std::fs::read_to_string(c)
104
+ .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{c}: {e}")))?;
105
+ let v: serde_json::Value = serde_json::from_str(&txt)
106
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
107
+ serde_json::from_value(v)
108
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("config: {e}")))?
109
+ }
110
+ None => config::ConfigKhtst::default(),
111
+ };
112
+ cfg.validar().map_err(pyo3::exceptions::PyValueError::new_err)?;
113
+ Ok(ConfigPy { cfg, caminho })
114
+ }
115
+
116
+ fn validar(&self) -> PyResult<()> {
117
+ self.cfg.validar().map_err(pyo3::exceptions::PyValueError::new_err)
118
+ }
119
+
120
+ fn checar_orcamento_cache(&self) -> PyResult<usize> {
121
+ self.cfg.checar_orcamento_cache().map_err(pyo3::exceptions::PyValueError::new_err)
122
+ }
123
+
124
+ fn resumo(&self) -> String {
125
+ format!(
126
+ "modelo(vocab={}, d={}, camadas={}, ctx={}) ncm(L={}, niveis={:?}, K={}, d_lat={}, modo={}, ativo={})",
127
+ self.cfg.modelo.vocab, self.cfg.modelo.d_modelo, self.cfg.modelo.n_camadas,
128
+ self.cfg.modelo.comprimento_ctx, self.cfg.l_niveis(), self.cfg.ncm.niveis,
129
+ self.cfg.ncm.k_por_nivel, self.cfg.ncm.d_lat, self.cfg.ncm.modo_quantizacao, self.cfg.ncm.ativo,
130
+ )
131
+ }
132
+
133
+ #[getter]
134
+ fn l_niveis(&self) -> usize {
135
+ self.cfg.l_niveis()
136
+ }
137
+ }
138
+
139
+
140
+ #[pyclass(name = "PonteDupla")]
141
+ pub(crate) struct PonteDuplaPy {
142
+ ponte: ponte::PonteDupla,
143
+ }
144
+
145
+ #[pymethods]
146
+ impl PonteDuplaPy {
147
+ #[new]
148
+ #[pyo3(signature = (capacidade=64, timeout_ms=2000))]
149
+ fn new(capacidade: usize, timeout_ms: u64) -> Self {
150
+ PonteDuplaPy { ponte: ponte::PonteDupla::nova(capacidade, timeout_ms) }
151
+ }
152
+
153
+ fn rodada(&self, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<(Vec<f32>, Vec<f32>)> {
154
+ let (ma, mb) = self.ponte.rodada(carga_a, carga_b, nivel)
155
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
156
+ Ok((ma.carga, mb.carga))
157
+ }
158
+
159
+ fn demo_professor_aluno(&self, rodadas: usize, dim: usize) -> PyResult<PyObject> {
160
+ let inicio: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.01).collect();
161
+ let stats = ponte::demo_professor_aluno(&self.ponte, rodadas, inicio, &|a, _| {
162
+ a.iter().map(|x| x * 1.05).collect()
163
+ })
164
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
165
+ Python::with_gil(|py| {
166
+ Ok(pythonize::pythonize(py, &serde_json::to_value(&stats).unwrap())?.into())
167
+ })
168
+ }
169
+
170
+ fn estatisticas(&self) -> PyResult<PyObject> {
171
+ let s = self.ponte.estatisticas();
172
+ Python::with_gil(|py| {
173
+ Ok(pythonize::pythonize(py, &serde_json::to_value(&s).unwrap())?.into())
174
+ })
175
+ }
176
+ }
177
+
178
+
179
+ #[cfg(feature = "ponte_async")]
180
+ #[pyclass(name = "PonteAsync")]
181
+ pub(crate) struct PonteAsyncPy {
182
+ interna: std::sync::Arc<ponte::assincrona::PonteAsync>,
183
+ }
184
+
185
+ #[cfg(feature = "ponte_async")]
186
+ #[pymethods]
187
+ impl PonteAsyncPy {
188
+ #[new]
189
+ #[pyo3(signature = (capacidade=64, timeout_ms=2000))]
190
+ fn new(capacidade: usize, timeout_ms: u64) -> Self {
191
+ PonteAsyncPy { interna: std::sync::Arc::new(ponte::assincrona::PonteAsync::nova(capacidade, timeout_ms)) }
192
+ }
193
+
194
+ /// Corrotina: `await ponte.trocar(a, b, nivel)` — duplex simultânea.
195
+ fn trocar<'p>(&self, py: Python<'p>, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<Bound<'p, PyAny>> {
196
+ let p = self.interna.clone();
197
+ pyo3_async_runtimes::tokio::future_into_py(py, async move {
198
+ let (ma, mb) = p.rodada_async(carga_a, carga_b, nivel)
199
+ .await
200
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
201
+ Ok::<_, PyErr>((ma.carga, mb.carga))
202
+ })
203
+ }
204
+ }
205
+
206
+
207
+ /// Utilitários de prova expostos ao Python (V-série da suíte).
208
+ #[pyfunction]
209
+ pub(crate) fn verificar_mascara(cfg: &ConfigPy) -> PyResult<usize> {
210
+ let dev = candle_core::Device::Cpu;
211
+ let m = mascaras::mascara_block_causal(&cfg.cfg.ncm.niveis, gerador::N_PREFIXO, &dev)
212
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
213
+ mascaras::contar_vazamentos(&cfg.cfg.ncm.niveis, gerador::N_PREFIXO, &m)
214
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
215
+ }
216
+
217
+
218
+ #[pyfunction]
219
+ pub(crate) fn verificar_stiefel(k: usize, d: usize) -> PyResult<f64> {
220
+ let dev = candle_core::Device::Cpu;
221
+ let c = stiefel::init_stiefel(k, d, &dev)
222
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
223
+ stiefel::erro_ortogonalidade(&c.transpose(0, 1).unwrap()).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
224
+ }
225
+
226
+ #[pyfunction]
227
+ pub(crate) fn checkpoint_valido(caminho: &str) -> bool {
228
+ crate::checkpoint::validar(caminho)
229
+ }
230
+
231
+ #[pyfunction]
232
+ pub(crate) fn checkpoint_manifesto(caminho: &str) -> Option<String> {
233
+ std::fs::read_to_string(crate::checkpoint::caminho_manifesto(caminho)).ok()
234
+ }
235
+
236
+ #[pyfunction]
237
+ pub(crate) fn sha256_arquivo(caminho: &str) -> Option<String> {
238
+ std::fs::read(caminho).ok().map(|d| crate::checkpoint::sha256_hex(&d))
239
+ }
src/python/pensamento_demos.rs ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::pensamento_demos` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ /// Classificação de erro por taxonomia (diagnóstico de refatoração).
8
+ #[pyfunction]
9
+ pub(crate) fn classificar_erro(msg: String) -> Vec<(String, String, String)> {
10
+ diagnostico::classificar_erro(&msg)
11
+ .into_iter()
12
+ .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string()))
13
+ .collect()
14
+ }
15
+
16
+
17
+ /// Confiança de uma distribuição (T-P1): `1 − H(softmax(z))/log K`.
18
+ #[pyfunction]
19
+ pub(crate) fn confianca_de_logits(logits: Vec<f32>) -> PyResult<f64> {
20
+ let dev = candle_core::Device::Cpu;
21
+ let k = logits.len();
22
+ let t = candle_core::Tensor::from_vec(logits, (1, k), &dev)
23
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
24
+ pensamento::confianca_de_logits(&t).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
25
+ }
26
+
27
+
28
+ /// Projeção ridge (T-P3 — montador algébrico): `r* = (WᵀW+εI)⁻¹Wᵀr`.
29
+ #[pyfunction]
30
+ pub(crate) fn projetar_ridge(w: Vec<Vec<f64>>, r_alvo: Vec<Vec<f64>>, eps: f64) -> PyResult<Vec<Vec<f64>>> {
31
+ let dev = candle_core::Device::Cpu;
32
+ let wt = tensor_de_2d(w, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
33
+ let rt = tensor_de_2d(r_alvo, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
34
+ let r = pensamento::projetar_ridge(&wt, &rt, eps)
35
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
36
+ let out = r.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
37
+ Ok(out.into_iter().map(|l| l.into_iter().map(|v| v as f64).collect()).collect())
38
+ }
39
+
40
+
41
+ /// Demo completa da tecnologia Pensamento: treino cooperativo das N redes
42
+ /// (item a) + ciclo d com verificador externo simulado + telemetria.
43
+ #[pyfunction]
44
+ pub(crate) fn demo_pensamento(
45
+ py: Python,
46
+ d_cond: usize,
47
+ d_saida: usize,
48
+ n_tentativas: usize,
49
+ passos_treino: usize,
50
+ limiar_conf: f64,
51
+ orcamento: usize,
52
+ semente: u64,
53
+ ) -> PyResult<PyObject> {
54
+ use candle_core::Tensor;
55
+ let dev = candle_core::Device::Cpu;
56
+ crate::rng::definir_semente(semente);
57
+ let mut motor = pensamento::MotorPensamento::novo(d_cond, d_saida, n_tentativas, limiar_conf, orcamento, &dev)
58
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
59
+ let mut otim = crate::treino::AdamWManual::novo(motor.vars(), 5e-3, 0.0);
60
+ let mut rng = crate::rng::RngDeterministico::novo(semente);
61
+
62
+ // dados sintéticos: alvo = função determinística não-linear de cond (d=a+b+c)
63
+ let n_dados = 256usize;
64
+ let conds: Vec<Vec<f32>> = (0..n_dados)
65
+ .map(|_| (0..d_cond).map(|_| rng.normal(0.0, 1.0) as f32).collect())
66
+ .collect();
67
+ let alvos: Vec<Vec<f32>> = conds
68
+ .iter()
69
+ .map(|c| {
70
+ (0..d_saida)
71
+ .map(|j| {
72
+ let cj = c[j % d_cond];
73
+ let ck = c[(j + 1) % d_cond];
74
+ 0.5 * cj + 0.3 * cj * ck + 0.1
75
+ })
76
+ .collect()
77
+ })
78
+ .collect();
79
+ let cond_t = Tensor::from_vec(conds.into_iter().flatten().collect(), (n_dados, d_cond), &dev)
80
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
81
+ let alvo_t = Tensor::from_vec(alvos.into_iter().flatten().collect(), (n_dados, d_saida), &dev)
82
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
83
+
84
+ // item a: treino cooperativo
85
+ let mut perda_hist = Vec::new();
86
+ for passo in 0..passos_treino {
87
+ let perda = motor.perda_treino(&cond_t, &alvo_t)
88
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
89
+ perda_hist.push(perda.to_scalar::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64);
90
+ otim.passo(crate::treino::grads_achatados(&perda, &motor.vars())
91
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
92
+ let _ = passo;
93
+ }
94
+
95
+ // item c+d: consulta fora do padrão (OOD) — confiança baixa ⇒ Pensamento
96
+ let consulta: Vec<f32> = (0..d_cond).map(|j| 3.0 + (j as f32) * 0.1).collect();
97
+ let consulta_t = Tensor::from_vec(consulta, (d_cond,), &dev)
98
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
99
+ // verificador externo: negativo do MSE contra o alvo verdadeiro da consulta
100
+ let alvo_verdadeiro: Vec<f32> = (0..d_saida)
101
+ .map(|j| {
102
+ let cj = 3.0 + (j % d_cond) as f32 * 0.1;
103
+ let ck = 3.0 + ((j + 1) % d_cond) as f32 * 0.1;
104
+ 0.5 * cj + 0.3 * cj * ck + 0.1
105
+ })
106
+ .collect();
107
+ let alvo_verd = Tensor::from_vec(alvo_verdadeiro.clone(), (d_saida,), &dev)
108
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
109
+ let verifica = |resp: &Tensor| -> f64 {
110
+ // confiança = 1/(1+10·MSE) — medível e causaL (só da resposta)
111
+ let dif = (resp.broadcast_as((1, d_saida)).unwrap() - &alvo_verd.broadcast_as((1, d_saida)).unwrap()).unwrap();
112
+ let mse = dif.sqr().unwrap().mean_all().unwrap().to_scalar::<f32>().unwrap() as f64;
113
+ 1.0 / (1.0 + 10.0 * mse)
114
+ };
115
+ let mut rng2 = crate::rng::RngDeterministico::novo(semente + 1);
116
+ let res = motor.raciocinar(&consulta_t, &verifica, &mut rng2)
117
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
118
+
119
+ // linha de base SEM pensamento: uma tentativa isolada, sem agregação
120
+ let base = motor.tentativas[0].fwd(&consulta_t.broadcast_as((1, d_cond)).unwrap())
121
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
122
+ let conf_base = verifica(&base);
123
+ let conf_pensamento = res.confianca;
124
+
125
+ let d_ = serde_json::json!({
126
+ "versao": pensamento::versao(),
127
+ "n_tentativas": n_tentativas,
128
+ "perda_treino_inicial": perda_hist.first(),
129
+ "perda_treino_final": perda_hist.last(),
130
+ "treino_aprendeu": perda_hist.last().copied().unwrap_or(f64::INFINITY) < perda_hist.first().copied().map(|p| p * 0.5).unwrap_or(f64::INFINITY),
131
+ "iteracoes_ciclo_d": res.iteracoes,
132
+ "confianca_base_1_rede": conf_base,
133
+ "confianca_pensamento": conf_pensamento,
134
+ "ganho_confianca": conf_pensamento - conf_base,
135
+ "orcamento_esgotado": res.telemetria.get("orcamento_esgotado").copied().unwrap_or(0.0),
136
+ "telemetria": res.telemetria,
137
+ "eventos_ciclo": res.trajetoria.iter().map(|e| match e {
138
+ pensamento::EventoCiclo::Planejamento { iteracao, temperatura } =>
139
+ serde_json::json!({"tipo": "planejamento", "iteracao": iteracao, "temperatura": temperatura}),
140
+ pensamento::EventoCiclo::DistribuicaoCompetencias { por_tentativa } =>
141
+ serde_json::json!({"tipo": "distribuicao", "pesos": por_tentativa}),
142
+ pensamento::EventoCiclo::RePlanejamento { motivo } =>
143
+ serde_json::json!({"tipo": "replanejamento", "motivo": motivo}),
144
+ pensamento::EventoCiclo::Resposta { confianca, iteracoes } =>
145
+ serde_json::json!({"tipo": "resposta", "confianca": confianca, "iteracoes": iteracoes}),
146
+ }).collect::<Vec<_>>(),
147
+ });
148
+ Ok(pythonize::pythonize(py, &d_)?.into())
149
+ }
150
+
151
+
152
+ /// **Ciclo d do Pensamento com o VERIFICADOR NCM** (produção, sem oráculo):
153
+ /// backbone CABEÇA TREINADA (alvo → classes) + verificador calibrado em
154
+ /// dados retidos; N tentativas agregadas; ganho medido em consultas retidas
155
+ /// EM DISTRIBUIÇÃO (onde a calibração vale — V-2) e OOD reportado à parte.
156
+ #[pyfunction]
157
+ pub(crate) fn demo_pensamento_ncm(
158
+ d_cond: usize,
159
+ d_saida: usize,
160
+ n_tentativas: usize,
161
+ passos_treino: usize,
162
+ limiar_conf: f64,
163
+ orcamento: usize,
164
+ semente: u64,
165
+ ) -> PyResult<PyObject> {
166
+ Python::with_gil(|py| {
167
+ use candle_core::Tensor;
168
+ let dev = candle_core::Device::Cpu;
169
+ crate::rng::definir_semente(semente);
170
+ let mut motor = pensamento::MotorPensamento::novo(
171
+ d_cond, d_saida, n_tentativas, limiar_conf, orcamento, &dev)
172
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
173
+ let mut otim = treino::AdamWManual::novo(motor.vars(), 5e-3, 0.0);
174
+ let mut rng = crate::rng::RngDeterministico::novo(semente);
175
+ // ---- dados sintéticos (item a: treino cooperativo) ----
176
+ let n_dados = 320usize;
177
+ let conds: Vec<Vec<f32>> = (0..n_dados)
178
+ .map(|_| (0..d_cond).map(|_| rng.normal(0.0, 1.0) as f32).collect())
179
+ .collect();
180
+ let alvos: Vec<Vec<f32>> = conds.iter().map(|c| {
181
+ (0..d_saida).map(|j| {
182
+ let cj = c[j % d_cond];
183
+ let ck = c[(j + 1) % d_cond];
184
+ 0.5 * cj + 0.3 * cj * ck + 0.1
185
+ }).collect()
186
+ }).collect();
187
+ let mk = |c: &[f32], j: usize| -> f32 {
188
+ let cj = c[j % d_cond];
189
+ let ck = c[(j + 1) % d_cond];
190
+ 0.5 * cj + 0.3 * cj * ck + 0.1
191
+ };
192
+ let cond_t = Tensor::from_vec(conds.iter().flatten().cloned().collect(), (n_dados, d_cond), &dev)
193
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
194
+ let alvo_t = Tensor::from_vec(alvos.iter().flatten().cloned().collect(), (n_dados, d_saida), &dev)
195
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
196
+ // ---- BACKBONE (cabeça treinada): alvo → classes K ----
197
+ // classe = faixa do 1º componente do alvo (aprendível, mensurável)
198
+ let k_classes = 12usize;
199
+ let classes: Vec<u32> = alvos
200
+ .iter()
201
+ .map(|a| {
202
+ let v = a[0];
203
+ let f = (((v + 2.0) / 4.0) * (k_classes as f32)).floor();
204
+ (f.clamp(0.0, (k_classes - 1) as f32)) as u32
205
+ })
206
+ .collect();
207
+ let cabeca = crate::jepa::MlpJepa::novo(d_saida, 4 * d_saida, k_classes, &dev)
208
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
209
+ let classe_t = Tensor::from_vec(classes.clone(), (n_dados,), &dev)
210
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
211
+ let mut otim_cab = treino::AdamWManual::novo(cabeca.vars(), 1e-2, 0.0);
212
+ for _ in 0..200 {
213
+ let logits = cabeca.fwd(&alvo_t)
214
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
215
+ let ce = candle_nn::loss::cross_entropy(&logits, &classe_t)
216
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
217
+ otim_cab.passo(treino::grads_achatados(&ce, &cabeca.vars())
218
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
219
+ }
220
+ // ---- VERIFICADOR calibrado em RETIDOS (metade final) ----
221
+ let n_ret = n_dados / 2;
222
+ let (alvo_ret, clas_ret) = (
223
+ alvo_t.narrow(0, n_ret, n_dados - n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?,
224
+ classe_t.narrow(0, n_ret, n_dados - n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?,
225
+ );
226
+ let logits_ret = cabeca.fwd(&alvo_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
227
+ let mut verif = verificador::VerificadorNcm::novo();
228
+ let (ece_antes, ece_depois) = verif.ajustar(&logits_ret, &clas_ret)
229
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
230
+ // logits_fn: resposta (1, d_saida) → logits do backbone (1, K)
231
+ let logits_fn = |resp: &Tensor| -> candle_core::Result<Tensor> {
232
+ cabeca.fwd(resp)
233
+ };
234
+ // ---- treino cooperativo do motor (item a) ----
235
+ let cond_tr = cond_t.narrow(0, 0, n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
236
+ let alvo_tr = alvo_t.narrow(0, 0, n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
237
+ let mut hist = Vec::new();
238
+ for _ in 0..passos_treino {
239
+ let perda = motor.perda_treino(&cond_tr, &alvo_tr)
240
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
241
+ let pv = perda.to_scalar::<f32>()
242
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64;
243
+ hist.push(pv);
244
+ otim.passo(treino::grads_achatados(&perda, &motor.vars())
245
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
246
+ }
247
+ // ---- consultas RETIDAS em distribuição (ganho medido AQUI) ----
248
+ let n_consultas = 8usize;
249
+ let mut confs_base = Vec::new();
250
+ let mut confs_pens = Vec::new();
251
+ let mut iteracoes = Vec::new();
252
+ for qi in 0..n_consultas {
253
+ let consulta: Vec<f32> = conds[n_ret + qi].clone();
254
+ let consulta_t = Tensor::from_vec(consulta, (d_cond,), &dev)
255
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
256
+ let mut rng2 = crate::rng::RngDeterministico::novo(semente + 1 + qi as u64);
257
+ let res = motor.raciocinar_ncm(&consulta_t, &verif, &logits_fn, &mut rng2)
258
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
259
+ // base: 1 tentativa isolada, mesma verificação NCM
260
+ let base = motor.tentativas[0]
261
+ .fwd(&consulta_t.broadcast_as((1, d_cond)).unwrap()).unwrap();
262
+ let conf_base = verif.confianca_1d(&logits_fn(&base).unwrap()).unwrap();
263
+ confs_base.push(conf_base);
264
+ confs_pens.push(res.confianca);
265
+ iteracoes.push(res.iteracoes);
266
+ }
267
+ let media = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
268
+ let (conf_base_m, conf_pens_m) = (media(&confs_base), media(&confs_pens));
269
+ let iteracoes_media = iteracoes.iter().sum::<usize>() as f64 / iteracoes.len().max(1) as f64;
270
+ // ---- consulta OOD (reporte honesto — sem gate de ganho) ----
271
+ let consulta_ood: Vec<f32> = (0..d_cond).map(|j| 3.0 + (j as f32) * 0.1).collect();
272
+ let consulta_ood_t = Tensor::from_vec(consulta_ood, (d_cond,), &dev)
273
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
274
+ let mut rng3 = crate::rng::RngDeterministico::novo(semente + 99);
275
+ let res_ood = motor.raciocinar_ncm(&consulta_ood_t, &verif, &logits_fn, &mut rng3)
276
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
277
+ let base_ood = motor.tentativas[0]
278
+ .fwd(&consulta_ood_t.broadcast_as((1, d_cond)).unwrap()).unwrap();
279
+ let conf_base_ood = verif.confianca_1d(&logits_fn(&base_ood).unwrap()).unwrap();
280
+ let _ = mk;
281
+ let d_ = serde_json::json!({
282
+ "versao": "pensamento-0.6.0-ncm",
283
+ "n_tentativas": n_tentativas,
284
+ "perda_treino_inicial": hist.first(), "perda_treino_final": hist.last(),
285
+ "treino_aprendeu": hist.last().copied() < hist.first().map(|p| p * 0.5),
286
+ "ece_verificador_antes": ece_antes, "ece_verificador_depois": ece_depois,
287
+ "temperatura_calibrada": verif.temperatura,
288
+ "iteracoes_ciclo_d_media": iteracoes_media,
289
+ "confianca_base_1_rede": conf_base_m,
290
+ "confianca_pensamento": conf_pens_m,
291
+ "ganho_confianca": conf_pens_m - conf_base_m,
292
+ "confianca_ood_base": conf_base_ood,
293
+ "confianca_ood_pensamento": res_ood.confianca,
294
+ "iteracoes_ood": res_ood.iteracoes,
295
+ "orcamento_esgotado_ood": res_ood.telemetria.get("orcamento_esgotado").copied().unwrap_or(0.0),
296
+ "verificador": "ncm-calibrado (sem oraculo)",
297
+ "gates": {
298
+ "gate_verificador_sem_oraculo": true,
299
+ "gate_calibrado": ece_depois <= ece_antes,
300
+ "gate_ganho_nao_negativo": conf_pens_m >= conf_base_m - 1e-3,
301
+ "gate_terminou": iteracoes_media <= orcamento as f64,
302
+ },
303
+ });
304
+ Ok(pythonize::pythonize(py, &d_)?.into())
305
+ })
306
+ }
307
+
308
+
309
+
310
+ /// **Autoajuste do Pensamento por telemetria (A-1..A-5) — demo:** alimenta
311
+ /// o controlador com sinais sintéticos medidos e verifica cada regra.
312
+ #[pyfunction]
313
+ pub(crate) fn demo_autoajuste(semente: u64) -> PyResult<PyObject> {
314
+ Python::with_gil(|py| {
315
+ let mut hub = telemetria::TelemetryHub::novo(None);
316
+ let mut rng = crate::rng::RngDeterministico::novo(semente);
317
+ let mut auto = controlador::AutoAjustePensamento::novo(0.5, 8, 2);
318
+ // A-1: confianças com mistura (60% altas ~0.85–0.95, 40% baixas ~0.25–0.40)
319
+ // ⇒ quantil 0.75 cai no grupo alto (posições 0.6–1.0) ≈ 0.85+0.375·0.1 ≈ 0.89
320
+ for _ in 0..200 {
321
+ let c = if rng.uniforme() < 0.6 { 0.85 + rng.uniforme() * 0.1 } else { 0.25 + rng.uniforme() * 0.15 };
322
+ auto.confs_retidas.push(c);
323
+ }
324
+ let tau = auto.ajustar_tau_c(0.25, &mut hub);
325
+ // A-3: acordos altos (0.88 ⇒ p̂ = (1+√0.76)/2 ≈ 0.936 ⇒ N = ln(20)/(2·0.436²) ≈ 7.9 ⇒ 8)
326
+ for _ in 0..32 {
327
+ auto.acordos.push(0.88);
328
+ }
329
+ let n_ajustado = auto.ajustar_n(0.05, 16, &mut hub);
330
+ // A-5: ganhos estáveis positivos (n=64 ⇒ 3º termo Bernstein ≈ 0.14 < 0.30) ⇒ gate sobe
331
+ for _ in 0..64 {
332
+ auto.ajustar_gate(0.30, 0.05, &mut hub);
333
+ }
334
+ let g_final = auto.gate_g;
335
+ // A-2: ganhos decaindo geometricamente
336
+ for k in 0..12 {
337
+ auto.ganhos_por_iteracao.push(0.25 * 0.6f64.powi(k));
338
+ }
339
+ let b_ajustado = auto.ajustar_orcamento(0.02, &mut hub);
340
+ // A-4: UCB1 com 3 braços
341
+ let mut u = controlador::Ucb1::novo(vec!["n1".into(), "n4_moe".into(), "n4".into()]);
342
+ for _ in 0..200 {
343
+ let braco = u.escolher();
344
+ let media = match braco { 0 => 0.35, 1 => 0.80, _ => 0.55 };
345
+ let r = (media + rng.normal(0.0, 0.06) as f64).clamp(0.0, 1.0);
346
+ u.registrar(braco, r);
347
+ }
348
+ let medias = u.medias();
349
+ let melhor = medias.iter().enumerate().max_by(|a, b| a.1.partial_cmp(b.1).unwrap()).map(|(i, _)| i).unwrap_or(0);
350
+ let d_ = serde_json::json!({
351
+ "versao": "autoajuste-0.6.0",
352
+ "tau_c_final": tau, "n_tentativas_final": n_ajustado,
353
+ "gate_final": g_final, "orcamento_b_final": b_ajustado,
354
+ "ajustes_auditados": hub.eventos_ajuste.len(),
355
+ "ucb1_medias": medias, "ucb1_melhor_braco": u.nomes[melhor],
356
+ "ucb1_arrependimento_medio": u.arrependimento_empirico() / 200.0,
357
+ "gates": {
358
+ "gate_a1_tau_quantil": (tau - 0.89).abs() < 0.08,
359
+ "gate_a3_n_reduzido": n_ajustado <= 8,
360
+ "gate_a5_gate_subiu": g_final > 0.0,
361
+ "gate_a2_b_finito": b_ajustado >= 1 && b_ajustado <= 16,
362
+ "gate_a4_ucb1_achou": u.nomes[melhor] == "n4_moe",
363
+ },
364
+ });
365
+ Ok(pythonize::pythonize(py, &d_)?.into())
366
+ })
367
+ }
src/python/som.rs ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::som` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+
7
+ /// Treina um 4D-SOM e devolve telemetria completa (EQ/TE/massa/cobertura).
8
+ #[pyfunction]
9
+ pub(crate) fn treinar_som4d(py: Python, dados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
10
+ let dev = candle_core::Device::Cpu;
11
+ let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
12
+ let mut c = som4d::CfgSom4D::default();
13
+ if let Some(js) = &cfg {
14
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
15
+ if let Some(o) = v.as_object() {
16
+ for (k, val) in o {
17
+ match k.as_str() {
18
+ "nx" => c.nx = val.as_u64().unwrap_or(4) as usize,
19
+ "ny" => c.ny = val.as_u64().unwrap_or(4) as usize,
20
+ "nz" => c.nz = val.as_u64().unwrap_or(4) as usize,
21
+ "nt" => c.nt = val.as_u64().unwrap_or(4) as usize,
22
+ "epocas" => c.epocas = val.as_u64().unwrap_or(50) as usize,
23
+ "sigma0" => c.sigma0 = val.as_f64().unwrap_or(2.0),
24
+ "sigma1" => c.sigma1 = val.as_f64().unwrap_or(0.5),
25
+ "eta0" => c.eta0 = val.as_f64().unwrap_or(0.5),
26
+ "eta1" => c.eta1 = val.as_f64().unwrap_or(0.05),
27
+ "semente" => c.semente = val.as_u64().unwrap_or(42),
28
+ "espalhada" => {
29
+ c.inicializacao = if val.as_bool().unwrap_or(false) {
30
+ som4d::Inicializacao4D::Espalhada
31
+ } else {
32
+ som4d::Inicializacao4D::Dados
33
+ }
34
+ }
35
+ _ => {}
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ let som = som4d::treinar_som4d(&t, &c, &dev)
42
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
43
+ let d = serde_json::json!({
44
+ "versao": som4d::versao(),
45
+ "celulas": som.n_celulas(),
46
+ "grade": [som.nx, som.ny, som.nz, som.nt],
47
+ "eq_inicial": som.eq_inicial,
48
+ "eq_final": som.eq_final,
49
+ "te_final": som.te_final,
50
+ "historico_eq_te": som.historia,
51
+ "massa_soma": som.massa.iter().sum::<f64>(),
52
+ });
53
+ Ok(pythonize::pythonize(py, &d)?.into())
54
+ }
55
+
56
+
57
+ /// Treina um GNG e devolve telemetria (vértices, arestas, componentes, EQ).
58
+ #[pyfunction]
59
+ pub(crate) fn treinar_gng(py: Python, dados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
60
+ let dev = candle_core::Device::Cpu;
61
+ let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
62
+ let mut c = som4d::CfgGng::default();
63
+ if let Some(js) = &cfg {
64
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
65
+ if let Some(o) = v.as_object() {
66
+ for (k, val) in o {
67
+ match k.as_str() {
68
+ "passos" => c.passos = val.as_u64().unwrap_or(4000) as usize,
69
+ "max_vertices" => c.max_vertices = val.as_u64().unwrap_or(60) as usize,
70
+ "lambda" => c.lambda = val.as_u64().unwrap_or(50) as usize,
71
+ "eps_b" => c.eps_b = val.as_f64().unwrap_or(0.05),
72
+ "eps_n" => c.eps_n = val.as_f64().unwrap_or(0.006),
73
+ "idade_max" => c.idade_max = val.as_u64().unwrap_or(50) as u32,
74
+ "semente" => c.semente = val.as_u64().unwrap_or(42),
75
+ _ => {}
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ let g = som4d::treinar_gng(&t, &c)
82
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
83
+ let d = serde_json::json!({
84
+ "versao": som4d::versao(),
85
+ "vertices": g.vertices.len(),
86
+ "arestas": g.arestas.len(),
87
+ "componentes_conexas": g.n_componentes_conexas(),
88
+ "insercoes": g.insercoes,
89
+ "eq_medio": g.eq_medio,
90
+ });
91
+ Ok(pythonize::pythonize(py, &d)?.into())
92
+ }
93
+
94
+
95
+ /// Treina um H-SOM e devolve a decomposição hierárquica (Teo H-1).
96
+ #[pyfunction]
97
+ pub(crate) fn treinar_hsom(py: Python, dados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
98
+ let dev = candle_core::Device::Cpu;
99
+ let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
100
+ let mut c = som4d::CfgHSom::default();
101
+ if let Some(js) = &cfg {
102
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
103
+ if let Some(o) = v.as_object() {
104
+ if let Some(x) = o.get("celulas_finas") {
105
+ c.celulas_finas = x.as_u64().unwrap_or(9) as usize;
106
+ }
107
+ if let Some(x) = o.get("epocas_finas") {
108
+ c.epocas_finas = x.as_u64().unwrap_or(20) as usize;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ let h = som4d::treinar_hsom(&t, &c, &dev)
114
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
115
+ let cota = h.eq_grosso + 2.0 * h.r_fino + 2.0 * (h.eq_grosso * h.r_fino).sqrt();
116
+ let d = serde_json::json!({
117
+ "versao": som4d::versao(),
118
+ "eq_hierarquico": h.eq_hierarquico,
119
+ "eq_grosso": h.eq_grosso,
120
+ "r_fino": h.r_fino,
121
+ "cota_teo_h1": cota,
122
+ "cota_respeitada": h.eq_hierarquico <= cota + 1e-9,
123
+ });
124
+ Ok(pythonize::pythonize(py, &d)?.into())
125
+ }
126
+
127
+
128
+ /// Demo T-SOM: streaming com deriva controlada — rastreio (Teo T-1).
129
+ #[pyfunction]
130
+ pub(crate) fn demo_tsom(py: Python, dados_iniciais: Vec<Vec<f64>>, janela: Vec<Vec<f64>>, passos: usize, gamma: f64) -> PyResult<PyObject> {
131
+ let dev = candle_core::Device::Cpu;
132
+ let t0 = tensor_de_2d(dados_iniciais, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
133
+ let cfg = som4d::CfgTSom {
134
+ cfg_grade: som4d::CfgSom4D { nx: 3, ny: 3, nz: 2, nt: 2, epocas: 15, ..Default::default() },
135
+ gamma,
136
+ janela: janela.len(),
137
+ };
138
+ let mut ts = som4d::TSom4D::novo(&t0, &cfg, &dev)
139
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
140
+ let mut historico = Vec::new();
141
+ for _ in 0..passos {
142
+ let eq = ts.passo(&janela);
143
+ historico.push(eq);
144
+ }
145
+ let d = serde_json::json!({
146
+ "versao": som4d::versao(),
147
+ "gamma": gamma,
148
+ "eq_temporal_historico": historico,
149
+ "t_passos": ts.t,
150
+ });
151
+ Ok(pythonize::pythonize(py, &d)?.into())
152
+ }
153
+
154
+
155
+ /// Demo/gate do auto-ajuste SOM com tetos (S-AB/S-SL).
156
+ #[pyfunction]
157
+ pub(crate) fn demo_autoajuste_som(py: Python) -> PyResult<PyObject> {
158
+ let mut a = controlador::AutoAjusteSom::novo(0.5, 1.0);
159
+ let (al_qe, _) = a.passo(1.0, 0.0, 0.0);
160
+ let (al_cross, sg_cross) = a.passo(0.0, 0.0, 1.0);
161
+ let (al_max, sg_max) = a.passo(1.0, 1.0, 1.0);
162
+ let mut parou_5chunks = false;
163
+ for _ in 0..5 {
164
+ parou_5chunks = a.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false);
165
+ }
166
+ let d_ = serde_json::json!({
167
+ "versao": "som-autoadjust-0.7.0",
168
+ "alfa_qe_alta": al_qe, "alfa_crossing": al_cross, "sigma_crossing": sg_cross,
169
+ "alfa_max": al_max, "sigma_max": sg_max,
170
+ "parou_5chunks": parou_5chunks,
171
+ "motivo": a.motivo_parada,
172
+ "gates": {
173
+ "tetos_ok": al_max <= 1.0 + 1e-9 && sg_max <= 2.0 + 1e-9,
174
+ "piso_ok": al_cross >= 0.5 - 1e-9,
175
+ "qe_sobe": al_qe > 0.5,
176
+ "stop_dispara": parou_5chunks,
177
+ },
178
+ });
179
+ Ok(pythonize::pythonize(py, &d_)?.into())
180
+ }
src/python/treino_fases.rs ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `python::treino_fases` — extraído do monólito na v0.10.0
2
+ //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
3
+
4
+ use super::*;
5
+
6
+ use super::nucleo::ConfigPy;
7
+
8
+
9
+ /// Treina a fase A (tokenizador). Devolve dict com resumo + telemetria.
10
+ #[pyfunction]
11
+ #[pyo3(signature = (cfg, sequencias, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None))]
12
+ pub(crate) fn treinar_fase_a(
13
+ cfg: &ConfigPy,
14
+ sequencias: Vec<Vec<u32>>,
15
+ caminho_telemetria: Option<String>,
16
+ caminho_checkpoint: Option<String>,
17
+ callback: Option<Bound<'_, PyAny>>,
18
+ teto_seg: Option<f64>,
19
+ ) -> PyResult<PyObject> {
20
+ let cb = callback_treino_de_py(callback);
21
+ let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
22
+ let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
23
+ let mut resumo = treino::treinar_fase_a(&cfg.cfg, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg)
24
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase A: {e}")))?;
25
+ let _ = &mut resumo;
26
+ Python::with_gil(|py| {
27
+ let dados = serde_json::json!({
28
+ "resumo": {
29
+ "passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
30
+ "perda_final": resumo.perda_final, "acc_recon": resumo.acc_recon,
31
+ "delta_ort_max": resumo.delta_ort_max,
32
+ "perplexidade_codebook": resumo.perplexidade_codebook,
33
+ "monotonia_prefixo": resumo.monotonia_prefixo,
34
+ "escore_aprendizado": resumo.escore_aprendizado,
35
+ },
36
+ "telemetria": hub.dump(),
37
+ });
38
+ Ok(pythonize::pythonize(py, &dados)?.into())
39
+ })
40
+ }
41
+
42
+
43
+ /// Treina a fase B (gerador) com tokenizador congelado carregado de checkpoint.
44
+ #[pyfunction]
45
+ #[pyo3(signature = (cfg, sequencias, caminho_tokenizador, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None))]
46
+ pub(crate) fn treinar_fase_b(
47
+ cfg: &ConfigPy,
48
+ sequencias: Vec<Vec<u32>>,
49
+ caminho_tokenizador: &str,
50
+ caminho_telemetria: Option<String>,
51
+ caminho_checkpoint: Option<String>,
52
+ callback: Option<Bound<'_, PyAny>>,
53
+ teto_seg: Option<f64>,
54
+ ) -> PyResult<PyObject> {
55
+ let cb = callback_treino_de_py(callback);
56
+ let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
57
+ let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
58
+ // carrega o tokenizador congelado
59
+ let dev = candle_core::Device::Cpu;
60
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
61
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
62
+ carregar_tokenizador(&mut tok, caminho_tokenizador)
63
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
64
+ let resumo = treino::treinar_fase_b(&cfg.cfg, &tok, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg)
65
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase B: {e}")))?;
66
+ Python::with_gil(|py| {
67
+ let dados = serde_json::json!({
68
+ "resumo": {
69
+ "passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
70
+ "perda_final": resumo.perda_final, "acc_conceitos": resumo.acc_recon,
71
+ "escore_aprendizado": resumo.escore_aprendizado,
72
+ },
73
+ "telemetria": hub.dump(),
74
+ });
75
+ Ok(pythonize::pythonize(py, &dados)?.into())
76
+ })
77
+ }
78
+
79
+
80
+ pub(crate) fn carregar_tokenizador(tok: &mut tokenizador::TokenizadorConceitos, caminho: &str) -> Result<(), String> {
81
+ let txt = std::fs::read_to_string(caminho).map_err(|e| format!("{e}"))?;
82
+ let v: serde_json::Value = serde_json::from_str(&txt).map_err(|e| format!("{e}"))?;
83
+ let obj = v.as_object().ok_or("formato de checkpoint inválido")?;
84
+ let vars = tok.vars();
85
+ for (i, var) in vars.iter().enumerate() {
86
+ if let Some(entrada) = obj.get(&format!("v{i}")) {
87
+ let forma: Vec<usize> = entrada["forma"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect()).unwrap_or_default();
88
+ let dados: Vec<f32> = entrada["dados"].as_array().map(|a| a.iter().filter_map(|x| x.as_f64().map(|y| y as f32)).collect()).unwrap_or_default();
89
+ if forma.is_empty() || dados.is_empty() {
90
+ continue;
91
+ }
92
+ let dev = candle_core::Device::Cpu;
93
+ let t2 = candle_core::Tensor::from_vec(dados, forma.clone(), &dev)
94
+ .and_then(|t| t.reshape(forma))
95
+ .map_err(|e| format!("{e}"))?;
96
+ var.set(&t2).map_err(|e| format!("{e}"))?;
97
+ }
98
+ }
99
+ Ok(())
100
+ }
101
+
102
+
103
+ /// Gera amostras com NFE = L e devolve textos decodificados (modo conceitos).
104
+ #[pyfunction]
105
+ #[pyo3(signature = (cfg, caminho_tokenizador, caminho_gerador, n_amostras=2, parar_no_nivel=None, greedy=true))]
106
+ pub(crate) fn gerar_amostras(
107
+ cfg: &ConfigPy,
108
+ caminho_tokenizador: &str,
109
+ caminho_gerador: &str,
110
+ n_amostras: usize,
111
+ parar_no_nivel: Option<usize>,
112
+ greedy: bool,
113
+ ) -> PyResult<PyObject> {
114
+ let dev = candle_core::Device::Cpu;
115
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
116
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
117
+ carregar_tokenizador(&mut tok, caminho_tokenizador)
118
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
119
+ let mut ger = gerador::GeradorConceitos::novo(&cfg.cfg, &dev)
120
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
121
+ carregar_gerador(&mut ger, caminho_gerador)
122
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
123
+ let mut amostrador = amostrador::AmostradorConceitos::novo(
124
+ &mut ger, &tok, cfg.cfg.ncm.cfg_scale, cfg.cfg.ncm.temperaturas.limites, cfg.cfg.ncm.temperaturas.entropia_alvo,
125
+ );
126
+ let mut rng = <rand::prelude::StdRng as rand::SeedableRng>::seed_from_u64(4242);
127
+ let inicio = std::time::Instant::now();
128
+ let (codigos, nfe) = amostrador.gerar(n_amostras.max(1), &dev, &mut rng, parar_no_nivel, greedy)
129
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("amostragem: {e}")))?;
130
+ let tempo_ms = inicio.elapsed().as_secs_f64() * 1000.0;
131
+ // decodifica: ẑ final → decoder AR greedy → tokens
132
+ let nivel_final = codigos.len();
133
+ let zhats = tok.reconstruir_de_codigos(&codigos, nivel_final, n_amostras.max(1), &dev)
134
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
135
+ let textos_ids = decodificar_textos(&tok, zhats.last(), n_amostras.max(1))
136
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
137
+ Python::with_gil(|py| {
138
+ let d = serde_json::json!({
139
+ "nfe": nfe, "l": cfg.cfg.l_niveis(), "tempo_ms": tempo_ms,
140
+ "niveis_usados": nivel_final, "ids_tokens": textos_ids,
141
+ "temperaturas": amostrador.temperaturas,
142
+ });
143
+ Ok(pythonize::pythonize(py, &d)?.into())
144
+ })
145
+ }
146
+
147
+
148
+ fn decodificar_textos(tok: &tokenizador::TokenizadorConceitos, zhat: Option<&candle_core::Tensor>, b: usize) -> Result<Vec<Vec<u32>>, String> {
149
+ let Some(z) = zhat else { return Ok(vec![vec![]; b]) };
150
+ tok.decodificar_livre(z, 48).map_err(|e| format!("{e}"))
151
+ }
152
+
153
+
154
+ pub(crate) fn carregar_gerador(ger: &mut gerador::GeradorConceitos, caminho: &str) -> Result<(), String> {
155
+ let txt = std::fs::read_to_string(caminho).map_err(|e| format!("{e}"))?;
156
+ let v: serde_json::Value = serde_json::from_str(&txt).map_err(|e| format!("{e}"))?;
157
+ let obj = v.as_object().ok_or("formato inválido")?;
158
+ let vars = ger.vars();
159
+ for (i, var) in vars.iter().enumerate() {
160
+ if let Some(entrada) = obj.get(&format!("v{i}")) {
161
+ let forma: Vec<usize> = entrada["forma"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect()).unwrap_or_default();
162
+ let dados: Vec<f32> = entrada["dados"].as_array().map(|a| a.iter().filter_map(|x| x.as_f64().map(|y| y as f32)).collect()).unwrap_or_default();
163
+ if forma.is_empty() || dados.is_empty() {
164
+ continue;
165
+ }
166
+ let dev = candle_core::Device::Cpu;
167
+ let t = candle_core::Tensor::from_vec(dados, forma.clone(), &dev)
168
+ .and_then(|t| t.reshape(forma))
169
+ .map_err(|e| format!("{e}"))?;
170
+ var.set(&t).map_err(|e| format!("{e}"))?;
171
+ }
172
+ }
173
+ Ok(())
174
+ }
175
+
176
+
177
+ /// **FASE C — treino conjunto Pensamento↔backbone** (dados reais).
178
+ #[pyfunction]
179
+ pub(crate) fn treinar_fase_c(
180
+ cfg: &ConfigPy,
181
+ sequencias: Vec<Vec<u32>>,
182
+ caminho_tokenizador: &str,
183
+ passos: usize,
184
+ caminho_gerador: Option<String>,
185
+ caminho_telemetria: Option<String>,
186
+ callback: Option<Bound<'_, PyAny>>,
187
+ teto_seg: Option<f64>,
188
+ ) -> PyResult<PyObject> {
189
+ let cb = callback_treino_de_py(callback);
190
+ let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
191
+ let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
192
+ let dev = candle_core::Device::Cpu;
193
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
194
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
195
+ carregar_tokenizador(&mut tok, caminho_tokenizador)
196
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
197
+ let resumo = treino::treinar_fase_c(&cfg.cfg, &tok, &dados, &mut hub, &cb, teto_seg, passos, caminho_gerador.as_deref())
198
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase C: {e}")))?;
199
+ Python::with_gil(|py| {
200
+ let dados = serde_json::json!({
201
+ "resumo": {
202
+ "passos": resumo.passos,
203
+ "ce_inicial": resumo.ce_inicial,
204
+ "ce_final": resumo.ce_final,
205
+ "ce_gatilhada_sem_correcao": resumo.ce_gatilhada_sem_correcao,
206
+ "ce_gatilhada_com_correcao": resumo.ce_gatilhada_com_correcao,
207
+ "confianca_media_inicial": resumo.confianca_media_inicial,
208
+ "confianca_media_final": resumo.confianca_media_final,
209
+ "confianca_pos_correcao": resumo.confianca_pos_correcao,
210
+ "taxa_gatilho": resumo.taxa_gatilho,
211
+ "ece_antes": resumo.ece_antes,
212
+ "ece_depois": resumo.ece_depois,
213
+ "temperatura_calibrada": resumo.temperatura_calibrada,
214
+ "tau_c": resumo.tau_c,
215
+ "taxa_conflito_pcgrad": resumo.taxa_conflito_pcgrad,
216
+ "decaimento_delta": resumo.decaimento_delta,
217
+ "acordos_medios": resumo.acordos_medios,
218
+ "escore_aprendizado": resumo.escore_aprendizado,
219
+ "rodadas_acoplador": resumo.rodadas_acoplador,
220
+ "timeouts_acoplador": resumo.timeouts_acoplador,
221
+ "maior_sobreposicao_us": resumo.maior_sobreposicao_us,
222
+ },
223
+ "painel_v060": hub.painel_v060(),
224
+ "telemetria": hub.dump(),
225
+ });
226
+ Ok(pythonize::pythonize(py, &dados)?.into())
227
+ })
228
+ }