v0.8.0: análise/prova/teste das 4 propostas Qwen3.8 (r-33) — Newton analítico no verificador (convexidade em gamma=1/T), árvore pós-ordem iterativa bit-a-bit com detecção de ciclos, Cayley exato no FNH (4,1× mais rápido), MoE vetorizada com gradiente RESTAURADO (bug v080-1) — 85/85 gates, 99/99 cargo
28430a8 verified Download src/arvore_pesos.rs from PowerMachine/khtst-rust: direct link, hf CLI and curl.
- Browser
- Download file 27.8 kB
-
https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/arvore_pesos.rs
- Command line
-
hf download hf://PowerMachine/khtst-rust/src/arvore_pesos.rs
-
curl -L -o arvore_pesos.rs https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/arvore_pesos.rs
27.8 kB
| //! arvore_pesos.rs — **Árvore Enraizada com Arestas Duplas** indexada dos | |
| //! pesos do projeto (requisito v0.7.0: "arquitetura de pesos em árvore e | |
| //! subárvores indexada" + ajuste bidirecional do quebra-cabeça). | |
| //! | |
| //! Estrutura (r-28 §4.2, provas r-30): | |
| //! - **Nós** = módulos do projeto (raiz "khtst"; filhos: tronco, quantizador, | |
| //! som, jepa, moe, pensamento, verificador, fnh, nlp, nlg, acoplador). | |
| //! - **Partição de índices**: cada nó possui um intervalo [ini, fim) do índice | |
| //! global de parâmetros — disjuntos e cobrindo [0, N) (T-A1). | |
| //! - **Arestas duplas**: cada relação pai↔filho é UMA aresta com DOIS | |
| //! enlaces direcionados (pai→filho = regulação; filho→pai = crédito) — | |
| //! `vizinho` e `aresta_de` são O(1) por índice de adjacência (T-A4). | |
| //! - **Fundo-topo**: agregação de métricas (telemetria/crédito) sobe — | |
| //! soma exata por indução (T-A2). | |
| //! - **Topo-fundo**: regulação desce com decaimento por profundidade — | |
| //! causal por filtração de níveis e terminante por potencial (T-A3). | |
| use std::collections::HashMap; | |
| /// Um nó da árvore — um módulo com sua fatia do índice de parâmetros. | |
| pub struct NoArvore { | |
| pub nome: String, | |
| pub pai: Option<usize>, | |
| pub filhos: Vec<usize>, | |
| /// Intervalo [ini, fim) no índice global de parâmetros (partição). | |
| pub ini: usize, | |
| pub fim: usize, | |
| /// Profundidade (raiz = 0). | |
| pub profundidade: usize, | |
| /// Métricas agregadas do nó (a própria + descendentes). | |
| pub metricas: HashMap<String, f64>, | |
| } | |
| /// Enlace direcionado de uma aresta dupla. | |
| pub struct Enlace { | |
| /// id do PAR (compartilhado pelos dois enlaces da aresta dupla). | |
| pub id_par: usize, | |
| pub de: usize, | |
| pub para: usize, | |
| } | |
| /// Árvore de pesos indexada com arestas duplas. | |
| pub struct ArvorePesos { | |
| pub nos: Vec<NoArvore>, | |
| por_nome: HashMap<String, usize>, | |
| /// Enlaces (2 por aresta: regulação desce, crédito sobe). | |
| pub enlaces: Vec<Enlace>, | |
| /// Índice de adjacência: nó → enlaces incidentes — O(1). | |
| adj: Vec<Vec<usize>>, | |
| /// Fluxo de crédito por enlace (sob na direção filho→pai). | |
| pub credito: Vec<f64>, | |
| /// Regulação por enlace (desce na direção pai→filho). | |
| pub regulacao: Vec<f64>, | |
| pub n_params: usize, | |
| } | |
| /// Layout padrão do projeto KHTST_RUST v0.7.0 — (nome, n_params, filhos). | |
| /// A ordem define os intervalos da partição (T-A1). | |
| pub const LAYOUT_PADRAO: &[(&str, usize)] = &[ | |
| ("khtst", 0), // raiz — sem parâmetros próprios (agrega) | |
| ("tronco", 1), | |
| ("quantizador", 1), | |
| ("som", 1), | |
| ("jepa", 1), | |
| ("moe", 1), | |
| ("pensamento", 1), | |
| ("verificador", 1), | |
| ("fnh", 1), | |
| ("nlp", 1), | |
| ("nlg", 1), | |
| ("acoplador", 1), | |
| ]; | |
| impl ArvorePesos { | |
| /// Constrói a árvore padrão: raiz "khtst" com os módulos como filhos | |
| /// diretos (profundidade 1) — subárvores podem ser anexadas depois. | |
| /// `contagens`: número de parâmetros por módulo (default 1 quando 0). | |
| pub fn nova(contagens: &HashMap<String, usize>) -> Self { | |
| let mut nos: Vec<NoArvore> = Vec::new(); | |
| let mut por_nome = HashMap::new(); | |
| let mut pos = 0usize; | |
| for (i, (nome, _)) in LAYOUT_PADRAO.iter().enumerate() { | |
| let n = if i == 0 { | |
| 0 | |
| } else { | |
| contagens.get(*nome).copied().unwrap_or(1).max(1) | |
| }; | |
| let no = NoArvore { | |
| nome: nome.to_string(), | |
| pai: if i == 0 { None } else { Some(0) }, | |
| filhos: Vec::new(), | |
| ini: pos, | |
| fim: pos + n, | |
| profundidade: if i == 0 { 0 } else { 1 }, | |
| metricas: HashMap::new(), | |
| }; | |
| pos = no.fim; | |
| por_nome.insert(nome.to_string(), i); | |
| if i != 0 { | |
| nos[0].filhos.push(i); | |
| } | |
| nos.push(no); | |
| } | |
| let n_nos = nos.len(); | |
| // arestas duplas: raiz↔filho | |
| let mut enlaces = Vec::new(); | |
| let mut adj = vec![Vec::new(); n_nos]; | |
| for (id_par, &f) in nos[0].filhos.iter().enumerate() { | |
| // enlace pai→filho (regulação desce) | |
| adj[0].push(enlaces.len()); | |
| enlaces.push(Enlace { id_par, de: 0, para: f }); | |
| // enlace filho→pai (crédito sobe) | |
| adj[f].push(enlaces.len()); | |
| enlaces.push(Enlace { id_par, de: f, para: 0 }); | |
| } | |
| let n_enlaces = enlaces.len(); | |
| ArvorePesos { | |
| nos, | |
| por_nome, | |
| enlaces, | |
| adj, | |
| credito: vec![0.0; n_enlaces], | |
| regulacao: vec![0.0; n_enlaces], | |
| n_params: pos, | |
| } | |
| } | |
| /// Consulta O(1) por nome (T-A4). | |
| pub fn no(&self, nome: &str) -> Option<usize> { | |
| self.por_nome.get(nome).copied() | |
| } | |
| pub fn n_nos(&self) -> usize { | |
| self.nos.len() | |
| } | |
| /// Enlace direcionado `de`→`para` (aresta dupla — T-A1 simetria). | |
| pub fn enlace(&self, de: usize, para: usize) -> Option<usize> { | |
| self.adj[de] | |
| .iter() | |
| .copied() | |
| .find(|&e| self.enlaces[e].para == para) | |
| } | |
| /// Enlaces incidentes ao nó (ambas as direções) — O(1) pelo índice. | |
| pub fn incidentes(&self, no: usize) -> &[usize] { | |
| &self.adj[no] | |
| } | |
| /// FatiA do índice de parâmetros da subárvore enraizada em `no` | |
| /// (o próprio intervalo quando não há filhos anexados). | |
| pub fn subarvore(&self, no: usize) -> (usize, usize) { | |
| // varre a subárvore em profundidade e devolve o envoltório dos | |
| // intervalos (a partição é contígua por construção pós-ordem se os | |
| // filhos forem anexados em ordem — aqui calculamos o envoltório) | |
| let (mut ini, mut fim) = (self.nos[no].ini, self.nos[no].fim); | |
| for &f in &self.nos[no].filhos { | |
| let (i2, f2) = self.subarvore(f); | |
| ini = ini.min(i2); | |
| fim = fim.max(f2); | |
| } | |
| (ini, fim) | |
| } | |
| /// Caminho do nó até a raiz (sobe pelos pais — termina: pai único, raiz | |
| /// sem pai; T-A3 terminação). | |
| pub fn caminho_ate_raiz(&self, no: usize) -> Vec<usize> { | |
| let mut caminho = vec![no]; | |
| let mut atual = no; | |
| while let Some(p) = self.nos[atual].pai { | |
| caminho.push(p); | |
| atual = p; | |
| } | |
| caminho | |
| } | |
| /// Anexa um filho com ARESTA DUPLA ao nó `pai` (nome, n_params) — | |
| /// mantém o índice de adjacência e a partição do índice global (o | |
| /// intervalo do novo nó continua o índice). Devolve o índice do novo nó. | |
| /// v0.8.0: habilita subárvores profundas (teste T-A7b) sem reconstruir | |
| /// a árvore — a pós-ordem iterativa não depende da profundidade. | |
| pub fn anexar_filho(&mut self, pai: usize, nome: &str, n_params: usize) -> usize { | |
| let idx = self.nos.len(); | |
| let prof = self.nos[pai].profundidade + 1; | |
| let ini = self.n_params; | |
| let fim = ini + n_params.max(1); | |
| self.nos.push(NoArvore { | |
| nome: nome.to_string(), | |
| pai: Some(pai), | |
| filhos: Vec::new(), | |
| ini, | |
| fim, | |
| profundidade: prof, | |
| metricas: HashMap::new(), | |
| }); | |
| self.por_nome.insert(nome.to_string(), idx); | |
| self.nos[pai].filhos.push(idx); | |
| // aresta dupla pai↔filho (novo id_par) | |
| let id_par = self.enlaces.len() / 2; | |
| self.adj.push(Vec::new()); | |
| self.adj[pai].push(self.enlaces.len()); | |
| self.enlaces.push(Enlace { id_par, de: pai, para: idx }); | |
| self.adj[idx].push(self.enlaces.len()); | |
| self.enlaces.push(Enlace { id_par, de: idx, para: pai }); | |
| // 1 entrada por ENLACE (2 por aresta dupla) — credito/regulacao são | |
| // indexadas por enlace, não por aresta | |
| self.credito.push(0.0); | |
| self.credito.push(0.0); | |
| self.regulacao.push(0.0); | |
| self.regulacao.push(0.0); | |
| self.n_params = fim; | |
| idx | |
| } | |
| /// **Fundo-topo**: agrega a métrica dos filhos no pai (pós-ordem — | |
| /// T-A2: a soma na raiz = soma global de todas as folhas + próprias). | |
| /// Devolve o total agregado na raiz. | |
| /// v0.8.0: **ITERATIVA** (proposta Qwen-3.8 nº 2 VALIDADA, com correções | |
| /// de robustez) — pilha explícita no heap, SEM recursão e SEM clones de | |
| /// `filhos`: O(V+E) tempo, O(V) espaço, zero alocação por nó; segura | |
| /// para árvores profundas degeneradas (T-A7b). Equivalência BIT-A-BIT | |
| /// com a recursiva (T-A7): mesma ordem de pós-ordem e mesma ordem de | |
| /// soma — prova em r-33 §3. | |
| pub fn agregar_fundo_topo(&mut self, metrica: &str) -> f64 { | |
| match self.agregar_pos_ordem(metrica) { | |
| Ok(v) => v, | |
| Err(e) => { | |
| // estrutura malformada (ciclo/filho duplicado): a recursiva | |
| // somaria duas vezes ou não terminaria — aqui é explícito | |
| log::warn!("agregar_fundo_topo: {e}"); | |
| f64::NAN | |
| } | |
| } | |
| } | |
| /// Pós-ordem iterativa com verificação estrutural — erro em ciclo ou | |
| /// filho duplicado. O flag `empilhado` é marcado NO MOMENTO DO PUSH: | |
| /// um nó empilhado duas vezes (duplicata no mesmo pai, ou re-entrada de | |
| /// ciclo) é recusado imediatamente — a marcação pós-expansão deixaria | |
| /// escapar o caso "pai lista o filho duas vezes" (o 2º push ocorre | |
| /// antes da expansão do 1º — errata da própria correção, r-33 §3.4). | |
| fn agregar_pos_ordem(&mut self, metrica: &str) -> Result<f64, String> { | |
| let n = self.nos.len(); | |
| if n == 0 { | |
| return Ok(0.0); | |
| } | |
| // fase 1: ordem de visita (pós-ordem: filhos ANTES do pai) | |
| let mut estado = vec![0u8; n]; // 0 = novo, 1 = filhos empilhados | |
| let mut empilhado = vec![false; n]; // marcado NO PUSH (unicidade) | |
| let mut pilha: Vec<usize> = Vec::with_capacity(n); | |
| let mut pos_ordem: Vec<usize> = Vec::with_capacity(n); | |
| pilha.push(0usize); | |
| empilhado[0] = true; | |
| while let Some(&atual) = pilha.last() { | |
| if estado[atual] == 0 { | |
| estado[atual] = 1; | |
| for &f in self.nos[atual].filhos.iter().rev() { | |
| if f >= n { | |
| return Err(format!("filho {f} fora do intervalo (nó {atual})")); | |
| } | |
| if empilhado[f] { | |
| return Err(format!( | |
| "filho {f} duplicado ou ciclo detectado no nó {atual}" | |
| )); | |
| } | |
| empilhado[f] = true; | |
| pilha.push(f); | |
| } | |
| } else { | |
| pilha.pop(); | |
| pos_ordem.push(atual); | |
| } | |
| } | |
| // fase 2: processa folhas primeiro (a pós-ordem já é fundo-topo) | |
| for &no in pos_ordem.iter() { | |
| let mut total = self.nos[no].metricas.get(metrica).copied().unwrap_or(0.0); | |
| for &f in self.nos[no].filhos.iter() { | |
| let v_filho = self.nos[f].metricas.get(metrica).copied().unwrap_or(0.0); | |
| total += v_filho; // MESMA ordem da recursiva (bit-a-bit — T-A7) | |
| if let Some(e) = self.enlace(f, no) { | |
| self.credito[e] = v_filho; // crédito sobe filho→pai | |
| } | |
| } | |
| self.nos[no].metricas.insert(metrica.to_string(), total); | |
| } | |
| Ok(self.nos[0].metricas.get(metrica).copied().unwrap_or(0.0)) | |
| } | |
| /// Referência RECURSIVA (v0.6.0) — mantida como oráculo de equivalência | |
| /// do teste T-A7 e documentação do ganho (não usa em produção: clone de | |
| /// `filhos` por nó + quadros de pilha O(profundidade)). | |
| pub fn agregar_fundo_topo_rec(&mut self, metrica: &str) -> f64 { | |
| self.agregar_rec(0, metrica) | |
| } | |
| fn agregar_rec(&mut self, no: usize, metrica: &str) -> f64 { | |
| let mut total = self.nos[no].metricas.get(metrica).copied().unwrap_or(0.0); | |
| let filhos = self.nos[no].filhos.clone(); | |
| for f in filhos { | |
| let v = self.agregar_rec(f, metrica); | |
| // crédito do filho sobe pelo enlace filho→pai | |
| if let Some(e) = self.enlace(f, no) { | |
| self.credito[e] = v; | |
| } | |
| total += v; | |
| } | |
| self.nos[no].metricas.insert(metrica.to_string(), total); | |
| total | |
| } | |
| /// **Topo-fundo**: regulação desce do nó `origem` com decaimento | |
| /// λ^k por nível de profundidade relativa (T-A3: causal — desce por | |
| /// níveis da filtração da árvore; terminante — profundidade finita). | |
| /// v0.8.0: ITERATIVA (pré-ordem com pilha explícita — mesmo ganho da | |
| /// agregação: sem clones, sem recursão; mesma ordem de escrita). | |
| pub fn regular_topo_fundo(&mut self, origem: usize, delta: f64, lambda: f64) { | |
| let base = self.nos[origem].profundidade; | |
| let mut pilha = vec![origem]; | |
| while let Some(u) = pilha.pop() { | |
| // k do PAI u (semântica v0.6.0 exata: a aresta u→f usa λ^k(u)) | |
| let k = self.nos[u].profundidade.saturating_sub(base); | |
| let d = delta * lambda.powi(k as i32); | |
| for &f in self.nos[u].filhos.iter().rev() { | |
| if let Some(e) = self.enlace(u, f) { | |
| self.regulacao[e] = d; // desce pelo enlace pai→filho | |
| } | |
| pilha.push(f); | |
| } | |
| } | |
| } | |
| /// **Ajuste indexado regulado** (quebra-cabeça do Pensamento — r-31): | |
| /// aplica `delta` (com teto por componente |δ| ≤ delta_max) na métrica | |
| /// `metrica` do nó `no` e propaga o crédito fundo-topo até a raiz. | |
| /// Devolve o delta efetivamente aplicado (clamped). | |
| pub fn aplicar_ajuste_indexado( | |
| &mut self, | |
| no: usize, | |
| metrica: &str, | |
| delta: f64, | |
| delta_max: f64, | |
| ) -> f64 { | |
| let d = delta.clamp(-delta_max, delta_max); | |
| let atual = self.nos[no].metricas.get(metrica).copied().unwrap_or(0.0); | |
| self.nos[no].metricas.insert(metrica.to_string(), atual + d); | |
| d | |
| } | |
| /// Verificação dos invariantes estruturais (T-A1) — usada nos gates. | |
| pub fn invariantes_ok(&self) -> Result<(), String> { | |
| // (i) pai único exceto raiz; profundidade consistente | |
| for (i, no) in self.nos.iter().enumerate() { | |
| if i == 0 { | |
| if no.pai.is_some() { | |
| return Err("raiz não pode ter pai".into()); | |
| } | |
| } else if let Some(p) = no.pai { | |
| if p >= self.nos.len() { | |
| return Err(format!("pai {p} fora do intervalo (nó {i})")); | |
| } | |
| if !self.nos[p].filhos.contains(&i) { | |
| return Err(format!("pai de {i} não o lista como filho")); | |
| } | |
| } else { | |
| return Err(format!("não-raiz {i} sem pai")); | |
| } | |
| } | |
| // (ii) partição: intervalos disjuntos dentro de [0, n_params) | |
| let mut marcas = vec![false; self.n_params]; | |
| for no in &self.nos { | |
| if no.ini >= no.fim && no.nome != "khtst" { | |
| return Err(format!("nó {} com intervalo vazio", no.nome)); | |
| } | |
| for k in no.ini..no.fim { | |
| if marcas[k] { | |
| return Err(format!("índice {k} particionado duas vezes")); | |
| } | |
| marcas[k] = true; | |
| } | |
| } | |
| if marcas.iter().any(|m| !m) { | |
| return Err("partição não cobre todo o índice".into()); | |
| } | |
| // (iii) simetria das arestas duplas: (u,v) ∈ E ⇔ (v,u) ∈ E, mesmo id_par | |
| for e in &self.enlaces { | |
| let espelhado = self | |
| .enlace(e.para, e.de) | |
| .ok_or_else(|| format!("aresta ({},{}) sem espelho", e.de, e.para))?; | |
| if self.enlaces[espelhado].id_par != e.id_par { | |
| return Err(format!( | |
| "espelho de ({},{}) com id_par diferente", | |
| e.de, e.para | |
| )); | |
| } | |
| } | |
| // (iv) simetria pai↔filho (v0.8.0 — T-A8): todo filho listado por u | |
| // deve ter pai = u (captura o 2-ciclo A↔B anexado à raiz, que as | |
| // checagens (i)-(iii) deixam passar) | |
| for (u, no) in self.nos.iter().enumerate() { | |
| for &f in &no.filhos { | |
| if self.nos[f].pai != Some(u) { | |
| return Err(format!( | |
| "nó {u} lista filho {f} cujo pai é {:?} (assimetria)", | |
| self.nos[f].pai | |
| )); | |
| } | |
| } | |
| } | |
| // (v) alcançabilidade (v0.8.0 — T-A9): todo nó atingível da raiz — | |
| // com pai único + simetria (iv), isso equivale a ACICLICO+conectado | |
| let mut visto = vec![false; self.nos.len()]; | |
| let mut fila = vec![0usize]; | |
| visto[0] = true; | |
| let mut alcancados = 1usize; | |
| while let Some(u) = fila.pop() { | |
| for &f in &self.nos[u].filhos { | |
| if !visto[f] { | |
| visto[f] = true; | |
| alcancados += 1; | |
| fila.push(f); | |
| } | |
| } | |
| } | |
| if alcancados != self.nos.len() { | |
| return Err(format!( | |
| "{} nós inalcançáveis da raiz (ciclo desconectado?)", | |
| self.nos.len() - alcancados | |
| )); | |
| } | |
| Ok(()) | |
| } | |
| } | |
| mod testes { | |
| use super::*; | |
| fn arvore_exemplo() -> ArvorePesos { | |
| let mut contagens = HashMap::new(); | |
| contagens.insert("tronco".to_string(), 40usize); | |
| contagens.insert("quantizador".to_string(), 10); | |
| contagens.insert("som".to_string(), 6); | |
| contagens.insert("jepa".to_string(), 6); | |
| contagens.insert("moe".to_string(), 5); | |
| contagens.insert("pensamento".to_string(), 8); | |
| contagens.insert("verificador".to_string(), 3); | |
| contagens.insert("fnh".to_string(), 4); | |
| contagens.insert("nlp".to_string(), 5); | |
| contagens.insert("nlg".to_string(), 5); | |
| contagens.insert("acoplador".to_string(), 2); | |
| ArvorePesos::nova(&contagens) | |
| } | |
| /// T-A1: invariantes — pai único, partição disjunta completa, simetria. | |
| fn ta1_invariantes() { | |
| let a = arvore_exemplo(); | |
| a.invariantes_ok().expect("invariantes T-A1"); | |
| assert_eq!(a.n_params, 94); | |
| } | |
| /// T-A2: agregação fundo-topo = soma global (indução pós-ordem). | |
| fn ta2_agregacao_fundo_topo() { | |
| let mut a = arvore_exemplo(); | |
| // métricas locais por nó | |
| let valores = [ | |
| ("tronco", 3.0), | |
| ("quantizador", 5.0), | |
| ("som", 1.0), | |
| ("jepa", 2.0), | |
| ("moe", 4.0), | |
| ("pensamento", 7.0), | |
| ("verificador", 1.0), | |
| ("fnh", 6.0), | |
| ("nlp", 2.0), | |
| ("nlg", 3.0), | |
| ("acoplador", 1.0), | |
| ]; | |
| let mut esperado = 0.0; | |
| for (nome, v) in valores { | |
| let no = a.no(nome).unwrap(); | |
| a.nos[no].metricas.insert("punicao".into(), v); | |
| esperado += v; | |
| } | |
| let total = a.agregar_fundo_topo("punicao"); | |
| assert!((total - esperado).abs() < 1e-12, "T-A2: {total} ≠ {esperado}"); | |
| let raiz = a.no("khtst").unwrap(); | |
| let na_raiz = a.nos[raiz].metricas["punicao"]; | |
| assert!((na_raiz - esperado).abs() < 1e-12, "T-A2: raiz {na_raiz}"); | |
| // crédito nos enlaces filho→pai = valor agregado do filho | |
| let f = a.no("fnh").unwrap(); | |
| let e = a.enlace(f, raiz).unwrap(); | |
| assert!((a.credito[e] - 6.0).abs() < 1e-12, "T-A2: crédito do fnh"); | |
| } | |
| /// T-A3: regulação topo-fundo — decaimento λ^k e causalidade por níveis. | |
| fn ta3_regulacao_topo_fundo() { | |
| let mut a = arvore_exemplo(); | |
| let raiz = a.no("khtst").unwrap(); | |
| a.regular_topo_fundo(raiz, 0.8, 0.5); | |
| // filhos diretos da raiz: k=0 ⇒ regulação = 0,8 (λ^0) | |
| for &f in &a.nos[raiz].filhos.clone() { | |
| let e = a.enlace(raiz, f).unwrap(); | |
| assert!((a.regulacao[e] - 0.8).abs() < 1e-12, "T-A3: nível 1"); | |
| } | |
| assert!(a.invariantes_ok().is_ok()); | |
| } | |
| /// T-A4: consultas O(1) — por nome, enlace e incidentes. | |
| fn ta4_consultas_o1() { | |
| let a = arvore_exemplo(); | |
| let f = a.no("fnh").expect("fnh"); | |
| assert_eq!(a.nos[f].nome, "fnh"); | |
| let raiz = a.no("khtst").unwrap(); | |
| assert!(a.enlace(f, raiz).is_some()); | |
| assert!(a.enlace(raiz, f).is_some()); | |
| assert_eq!(a.incidentes(f).len(), 1); // 1 aresta dupla = 1 enlace incidente listado? não: 1 enlace filho→pai | |
| // subárvore do fnh = seu próprio intervalo | |
| let (ini, fim) = a.subarvore(f); | |
| assert_eq!((ini, fim), (a.nos[f].ini, a.nos[f].fim)); | |
| // caminho fnh→raiz | |
| assert_eq!(a.caminho_ate_raiz(f), vec![f, raiz]); | |
| } | |
| /// T-A5: ajuste indexado regulado com teto — clamping simétrico. | |
| fn ta5_ajuste_indexado_com_teto() { | |
| let mut a = arvore_exemplo(); | |
| let f = a.no("pensamento").unwrap(); | |
| let d1 = a.aplicar_ajuste_indexado(f, "confianca", 0.3, 0.1); | |
| assert!((d1 - 0.1).abs() < 1e-12, "T-A5: teto superior"); | |
| let d2 = a.aplicar_ajuste_indexado(f, "confianca", -50.0, 0.1); | |
| assert!((d2 + 0.1).abs() < 1e-12, "T-A5: teto inferior"); | |
| let v = a.nos[f].metricas["confianca"]; | |
| assert!((v - 0.0).abs() < 1e-12, "T-A5: 0,1 − 0,1 = 0"); | |
| } | |
| /// **T-A7 (v0.8.0):** equivalência BIT-A-BIT da agregação iterativa com | |
| /// a recursiva em árvores aleatórias (mesma pós-ordem, mesma ordem de | |
| /// soma — prova r-33 §3.3). Proposta Qwen-3.8 nº 2 validada. | |
| fn ta7_equivalencia_bit_a_bit() { | |
| let mut rng = crate::rng::RngDeterministico::novo(909); | |
| for tentativa in 0..8u32 { | |
| let mut a = arvore_exemplo(); | |
| // anexa subárvores aleatórias (ramificação E profundidade) | |
| for j in 0..12u32 { | |
| let pai = rng.inteiro(a.n_nos()); | |
| let nome = format!("extra_{tentativa}_{j}"); | |
| a.anexar_filho(pai, &nome, 1 + rng.inteiro(4)); | |
| } | |
| // métricas locais aleatórias (registradas para repor depois) | |
| let locais: Vec<f64> = (0..a.n_nos()) | |
| .map(|no| { | |
| let v = rng.normal(0.0, 2.0); | |
| a.nos[no].metricas.insert("punicao".into(), v); | |
| v | |
| }) | |
| .collect(); | |
| let esperado: f64 = locais.iter().sum(); | |
| a.invariantes_ok().expect("árvore aleatória válida"); | |
| // iterativa | |
| let total_it = a.agregar_fundo_topo("punicao"); | |
| let credito_it = a.credito.clone(); | |
| // repõe as métricas locais e roda a recursiva (oráculo v0.6.0) | |
| for (no, v) in locais.iter().enumerate() { | |
| a.nos[no].metricas.insert("punicao".into(), *v); | |
| } | |
| let total_rec = a.agregar_fundo_topo_rec("punicao"); | |
| let credito_rec = a.credito.clone(); | |
| assert_eq!( | |
| total_it.to_bits(), total_rec.to_bits(), | |
| "T-A7: total bit-a-bit (tentativa {tentativa}: {total_it} vs {total_rec})" | |
| ); | |
| for (i, (x, y)) in credito_it.iter().zip(credito_rec.iter()).enumerate() { | |
| assert_eq!(x.to_bits(), y.to_bits(), "T-A7: crédito[{i}] bit-a-bit"); | |
| } | |
| assert!( | |
| (total_it - esperado).abs() < 1e-9, | |
| "T-A7: soma global (tentativa {tentativa})" | |
| ); | |
| } | |
| } | |
| /// **T-A7b (v0.8.0):** cadeia degenerada de 100.000 nós — a iterativa | |
| /// completa SEM estourar a pilha (a recursiva exigiria ~100k quadros, | |
| /// excedendo o limite padrão de 8 MiB — risco documentado da v0.6.0; | |
| /// por isso ela NÃO é executada aqui). | |
| fn ta7b_cadeia_profunda_100k() { | |
| let mut a = arvore_exemplo(); | |
| let mut pai = 1usize; // tronco | |
| for i in 0..100_000u32 { | |
| pai = a.anexar_filho(pai, &format!("c{i}"), 1); | |
| } | |
| assert_eq!(a.n_nos(), 100_000 + 12); | |
| a.nos[pai].metricas.insert("conf".into(), 1.0); | |
| let total = a.agregar_fundo_topo("conf"); | |
| assert!((total - 1.0).abs() < 1e-12, "T-A7b: total {total}"); | |
| // o crédito sobe INTEIRO por toda a cadeia (T-A2) | |
| let e = a.enlace(pai, a.nos[pai].pai.unwrap()).unwrap(); | |
| assert!((a.credito[e] - 1.0).abs() < 1e-12, "T-A7b: crédito na folha"); | |
| assert!(a.invariantes_ok().is_ok(), "T-A7b: invariantes na cadeia"); | |
| } | |
| /// **T-A7c (v0.8.0):** regulação iterativa preserva a semântica λ^k do | |
| /// PAI (v0.6.0) em subárvores de profundidade 3. | |
| fn ta7c_regular_iterativa_equivalente() { | |
| let mut a = arvore_exemplo(); | |
| let n1 = a.anexar_filho(1, "sub1", 2); // profundidade 2 (sob o tronco) | |
| let n2 = a.anexar_filho(n1, "sub2", 2); // profundidade 3 | |
| let _n3 = a.anexar_filho(n2, "sub3", 2); // profundidade 4 | |
| let raiz = 0usize; | |
| a.regular_topo_fundo(raiz, 0.8, 0.5); | |
| // aresta raiz→tronco: k(raiz) = 0 ⇒ λ⁰ | |
| let e = a.enlace(raiz, 1).unwrap(); | |
| assert!((a.regulacao[e] - 0.8).abs() < 1e-12, "T-A7c: nível raiz"); | |
| // aresta tronco→sub1: k(tronco) = 1 ⇒ 0,8·0,5 | |
| let e1 = a.enlace(1, n1).unwrap(); | |
| assert!((a.regulacao[e1] - 0.4).abs() < 1e-12, "T-A7c: nível 1"); | |
| // aresta sub1→sub2: k(sub1) = 2 ⇒ 0,8·0,25 | |
| let e2 = a.enlace(n1, n2).unwrap(); | |
| assert!((a.regulacao[e2] - 0.2).abs() < 1e-12, "T-A7c: nível 2"); | |
| assert!(a.invariantes_ok().is_ok()); | |
| } | |
| /// **T-A8 (v0.8.0):** detecção de estruturas malformadas — filho | |
| /// duplicado (a iterativa recusa; a recursiva somaria DUAS vezes) e | |
| /// ciclos (checagens (iv) simetria e (v) alcançabilidade — que as | |
| /// checagens (i)-(iii) da v0.6.0 deixavam passar). | |
| fn ta8_deteccao_ciclos_e_duplicatas() { | |
| let mut a = arvore_exemplo(); | |
| // (a) filho duplicado: raiz lista o tronco duas vezes | |
| a.nos[0].filhos.push(1); | |
| let total = a.agregar_fundo_topo("punicao"); | |
| assert!(total.is_nan(), "T-A8: duplicata deve recusar (NaN), não somar 2×"); | |
| a.nos[0].filhos.pop(); | |
| assert!(a.invariantes_ok().is_ok(), "T-A8: restaurada"); | |
| // (b) assimetria pai↔filho: tronco lista o quantizador (pai = raiz) | |
| a.nos[1].filhos.push(2); | |
| let err = a.invariantes_ok().unwrap_err(); | |
| assert!(err.contains("assimetria"), "T-A8(iv): {err}"); | |
| a.nos[1].filhos.pop(); | |
| // (c) 2-ciclo desconectado x↔y: passa (i)-(iv), capturado por (v) | |
| let x = a.nos.len(); | |
| let y = x + 1; | |
| a.nos.push(NoArvore { | |
| nome: "x".into(), pai: Some(y), filhos: vec![y], | |
| ini: 94, fim: 95, profundidade: 9, metricas: HashMap::new(), | |
| }); | |
| a.nos.push(NoArvore { | |
| nome: "y".into(), pai: Some(x), filhos: vec![x], | |
| ini: 95, fim: 96, profundidade: 9, metricas: HashMap::new(), | |
| }); | |
| a.n_params = 96; | |
| let err = a.invariantes_ok().unwrap_err(); | |
| assert!(err.contains("inalcançáveis"), "T-A8(v): {err}"); | |
| } | |
| } | |