khtst-rust / src /arvore_pesos.rs
PowerMachine's picture
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
Raw History Blame Contribute Delete
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.
#[derive(Clone, Debug)]
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.
#[derive(Clone, Copy, Debug)]
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(())
}
}
#[cfg(test)]
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.
#[test]
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).
#[test]
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.
#[test]
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.
#[test]
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.
#[test]
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.
#[test]
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).
#[test]
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.
#[test]
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).
#[test]
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}");
}
}