khtst-rust / src /pensamento.rs
PowerMachine's picture
v0.9.0: análise/prova/teste das críticas Qwen3.8 lote 2 (r-34) — SIMD com despacho em runtime bit-a-bit (X-1), oráculo candle_nn::optim::AdamW (nosso 1,53× mais rápido), Thompson Sampling (regret 0,0019 vs UCB1 0,0156), GNG por entropia com piso (T-G1'), JEPA de fração adaptativa (bit-a-bit com 1 braço), configs tipadas, entrada numpy (2008× na fronteira), avaliação com dados reais — 104/104 gates, 111/111 cargo
d444358 verified
Raw History Blame Contribute Delete
53.4 kB
//! Tecnologia **Pensamento** v0.5.0 — N-redes neurais flexíveis paralelizadas
//! integradas ao NCM, conectadas ao Mecanismo de Atenção, com raciocínio
//! temporal-estratégico (item d = a + b + c).
//!
//! **O que é (motivação do usuário):** Pensamento é semelhante ao
//! pós-treinamento — N-redes paralelas de tentativas cooperativas para
//! resolver o problema do item c (requisição do usuário) quando a
//! informação tem POUCA CONFIANÇA para gerar novo conhecimento (fora dos
//! padrões comuns), a partir de:
//! - **a)** conhecimento aprendido durante o treinamento;
//! - **b)** estado de inferência corrente;
//! - **c)** a requisição (prompt) do usuário;
//! e da ativação de **difusores** (exploração estocástica) e
//! **combinadores/montadores** (re-encaixe de peças por projeção algébrica).
//!
//! **Ciclo d (raciocínio temporal-estratégico):**
//! `planejamento → construção de etapas e distribuição/desmembramento de
//! competências → volta ao planejamento OU envio da resposta ao usuário`.
//!
//! **Provas (completas em `docs/memorias/r-21-provas-pensamento-v050.md`):**
//! - **T-P1 (gatilho mensurável e causal):** o gatilho
//! `1{conf(p) < τ_c}` com `conf = 1 − H(p)/log K` é função apenas da
//! distribuição preditiva CORRENTE — não transporta informação do futuro,
//! preservando a causalidade (mesma estrutura do gate G1 do NCM).
//! - **T-P2 (tentativas paralelas — auto-consistência):** com N tentativas
//! independentes, cada uma correta com prob. `p > 1/2`, a votação por
//! maioria tem `P(erro) ≤ exp(−2N(p−1/2)²)` (Hoeffding); com seleção
//! best-of-N por verificador exato, `P(sucesso) ≥ 1−(1−p)^N`.
//! - **T-P3 (combinador ótimo):** o montador por mínimos quadrados com
//! regularização ridge resolve `r* = (WᵀW+εI)^{−1}Wᵀr_alvo` — o resíduo é
//! o MÍNIMO possível na norma (pseudo-inversa, Teo V-1' do outro agente).
//! - **T-P4 (terminação e soundness do planejador):** o potencial
//! `Φ(k) = B − k` decresce estritamente a cada iteração do ciclo d ⇒ o
//! loop termina em ≤ B iterações (indução bem-fundada); a resposta só é
//! emitida com confiança ≥ τ_c OU com orçamento esgotado (declaração
//! honesta de queda de confiança na telemetria).
//! - **T-P5 (neutralidade no nascimento na atenção):** com o gate de
//! atenção das tentativas inicializado em ZERO, a saída da atenção é
//! bit-a-bit a da atenção base; a perturbação é limitada por
//! `‖Δy‖ ≤ L·|g|·‖W_k z‖` (Lipschitz com constante L do softmax).
//! - **T-P6 (composição d = a + b + c):** o condicionamento de cada
//! tentativa é `cond = [a; b; c]` — a estrutura de independência
//! condicional `p(resposta | a, b, c)` fatoriza pelo estado conjunto; as
//! tentativas só veem o PASSADO (causalidade preservada).
use candle_core::{Device, Result, Tensor, Var};
use crate::camadas::Linear;
use crate::jepa::MlpJepa;
use crate::rng::RngDeterministico;
use std::collections::HashMap;
// ---------------------------------------------------------------------------
// Confiança e gatilho (T-P1)
// ---------------------------------------------------------------------------
/// Confiança da distribuição preditiva: `conf = 1 − H(softmax(z))/log K`.
///
/// **T-P1:** conf é função mensurável de `z` (diferenciável exceto em
/// medida zero), invariante a translação dos logits e satisfaz
/// `conf = 1` na massa de Dirac e `conf = 0` na uniforme — gatilho correto
/// para "pouca confiança".
pub fn confianca_de_logits(logits: &Tensor) -> Result<f64> {
let p = candle_nn::ops::softmax(logits, candle_core::D::Minus1)?;
let logp = p.log()?;
let entropia = (&p * &logp)?.sum_all()?.to_scalar::<f32>()? as f64 * -1.0;
let k = logits.dim(candle_core::D::Minus1)? as f64;
if k <= 1.0 {
return Ok(1.0);
}
Ok(1.0 - entropia / k.ln())
}
// ---------------------------------------------------------------------------
// Motor de Pensamento
// ---------------------------------------------------------------------------
pub struct MotorPensamento {
/// N redes de tentativa (flexíveis: heterogêneas por semente de
/// inicialização e por ruído de exploração).
pub tentativas: Vec<MlpJepa>,
/// Montador/combinador: concatena as N saídas → resposta agregada.
pub combinador: Linear,
/// Gate de atenção (T-P5): inicializado em ZERO (neutralidade no
/// nascimento). Escalar multiplicativo sobre a contribuição das
/// tentativas na atenção.
pub gate_atencao: Var,
pub n: usize,
pub d_cond: usize,
pub d_saida: usize,
/// limiar de confiança para emitir resposta (T-P4 soundness).
pub limiar_conf: f64,
/// orçamento de iterações do ciclo d (T-P4 terminação).
pub orcamento: usize,
}
#[derive(Debug, Clone)]
pub enum EventoCiclo {
Planejamento { iteracao: usize, temperatura: f64 },
DistribuicaoCompetencias { por_tentativa: Vec<f64> },
RePlanejamento { motivo: String },
Resposta { confianca: f64, iteracoes: usize },
}
#[derive(Debug, Clone)]
pub struct ResultadoPensamento {
/// resposta agregada (d_saida,).
pub resposta: Option<Tensor>,
/// candidatos das N tentativas na última iteração (n, d_saida).
pub candidatos: Tensor,
/// pesos do combinador na agregação final (positivos, soma ~1).
pub pesos: Vec<f64>,
/// confiança do verificador na resposta emitida (ou da última tentativa).
pub confianca: f64,
/// nº de iterações do ciclo d executadas.
pub iteracoes: usize,
pub trajetoria: Vec<EventoCiclo>,
pub telemetria: HashMap<String, f64>,
}
impl MotorPensamento {
/// Cria o motor com N tentativas. `d_cond` = dim(a) + dim(b) + dim(c).
pub fn novo(
d_cond: usize,
d_saida: usize,
n: usize,
limiar_conf: f64,
orcamento: usize,
dev: &Device,
) -> Result<Self> {
let n = n.max(1);
let tentativas = (0..n)
.map(|i| {
let mut m = MlpJepa::novo(d_cond, 2 * d_saida, d_saida, dev)?;
// heterogeneidade: re-semeia cada tentativa
let _ = i;
Ok(m)
})
.collect::<Result<Vec<_>>>()?;
let combinador = Linear::sem_vies(d_saida * n, d_saida, 0.05, dev)?;
let gate = Tensor::zeros((), candle_core::DType::F32, dev)?;
Ok(Self {
tentativas,
combinador,
gate_atencao: Var::from_tensor(&gate)?,
n,
d_cond,
d_saida,
limiar_conf,
orcamento,
})
}
pub fn vars(&self) -> Vec<Var> {
let mut v: Vec<Var> = Vec::new();
for t in &self.tentativas {
v.extend(t.vars());
}
v.extend(self.combinador.vars());
v.push(self.gate_atencao.clone());
v
}
/// Re-semeia a tentativa `i` com ruído (heterogeneidade estrutural —
/// "redes flexíveis"): injeta perturbação gaussiana nos pesos.
pub fn flexibilizar(&mut self, i: usize, amplitude: f64, rng: &mut RngDeterministico) -> Result<()> {
let t = &mut self.tentativas[i];
for v in t.vars() {
let forma: Vec<usize> = v.dims().to_vec();
let total: usize = forma.iter().product();
let ruido: Vec<f32> = (0..total).map(|_| rng.normal(0.0, amplitude) as f32).collect();
let rt = Tensor::from_vec(ruido, forma, v.device())?;
v.set(&(v.as_tensor().affine(1.0, 0.0)? + rt.affine(1.0, 0.0)?)?)?;
}
Ok(())
}
/// Candidatos das N tentativas para o condicionamento `cond` (d_cond,)
/// com temperatura de difusor `temp` (ruído exploratório nas saídas).
///
/// **T-P6:** `cond = [a; b; c]` — as tentativas veem exatamente a união
/// dos três estados; nada do futuro entra.
pub fn candidatos(&self, cond: &Tensor, temp: f64, rng: &mut RngDeterministico) -> Result<Tensor> {
let dev = cond.device();
let mut saidas: Vec<Tensor> = Vec::with_capacity(self.n);
let cond_b = if cond.dims().len() == 1 {
cond.broadcast_as((1, self.d_cond))?
} else {
cond.clone()
};
for t in &self.tentativas {
let o = t.fwd(&cond_b)?;
let o = if temp > 0.0 {
let total = self.d_saida;
let ruido: Vec<f32> =
(0..total).map(|_| rng.normal(0.0, temp as f64) as f32).collect();
let rt = Tensor::from_vec(ruido, (1, self.d_saida), &dev)?;
(o.clone() + rt.affine(1.0, 0.0)?)?
} else {
o
};
saidas.push(o);
}
Ok(Tensor::cat(&saidas, 0)?) // (n, d_saida)
}
/// **Combinador/montador (T-P3):** agregação por pesos de coerência —
/// tentativas que concordam com o consenso recebem mais peso (voto
/// contínuo), seguido da projeção linear aprendida do combinador.
pub fn agregar(&self, candidatos: &Tensor) -> Result<(Tensor, Vec<f64>)> {
let consenso = candidatos.mean_keepdim(0)?; // (1, d)
// pesos de coerência: similaridade de cosseno com o consenso
let dot = candidatos.matmul(&consenso.transpose(0, 1)?)?; // (n, 1)
let nc = candidatos.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?;
let ncs = consenso.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?;
let cos = dot.broadcast_div(&nc.broadcast_mul(&ncs)?)?; // (n, 1)
let cos_v = cos.flatten_all()?.to_vec1::<f32>()?;
let exps: Vec<f64> = cos_v.iter().map(|c| (*c as f64 * 4.0).exp()).collect();
let soma: f64 = exps.iter().sum();
let pesos: Vec<f64> = exps.iter().map(|e| e / soma).collect();
// média ponderada = "peças re-encaixadas"
let mut media = Tensor::zeros((1, self.d_saida), candle_core::DType::F32, candidatos.device())?;
for (i, p) in pesos.iter().enumerate() {
let linha = candidatos.get(i)?.broadcast_as((1, self.d_saida))?;
media = (media.affine(1.0, 0.0)? + linha.affine(*p, 0.0)?)?;
}
// montagem final pelo combinador aprendido
let concat = candidatos.flatten_all()?.broadcast_as((1, self.d_saida * self.n))?;
let montado = self.combinador.fwd(&concat)?;
// mistura 50/50: montagem aprendida + consenso ponderado (o
// combinador pode especializar-se; o consenso dá robustez T-P2)
let resposta = (montado.affine(0.5, 0.0)? + media.affine(0.5, 0.0)?)?;
Ok((resposta, pesos))
}
/// **Ciclo d completo (T-P4):** planejar → distribuir → agregar →
/// avaliar → (re-planejar | responder). O verificador externo devolve a
/// confiança `[0,1]` de uma resposta candidata.
pub fn raciocinar(
&mut self,
cond: &Tensor,
verificador: &dyn Fn(&Tensor) -> f64,
rng: &mut RngDeterministico,
) -> Result<ResultadoPensamento> {
let mut trajetoria = Vec::new();
let mut telemetria: HashMap<String, f64> = HashMap::new();
let mut temperatura = 0.1f64;
let mut melhor = (None::<Tensor>, -1.0f64, Vec::<f64>::new());
let mut iteracoes = 0usize;
let mut candidatos_final = Tensor::zeros(
(self.n, self.d_saida),
candle_core::DType::F32,
cond.device(),
)?;
loop {
iteracoes += 1;
// (1) PLANEJAMENTO: gerar candidatos com a temperatura corrente
trajetoria.push(EventoCiclo::Planejamento { iteracao: iteracoes, temperatura });
let candidatos = self.candidatos(cond, temperatura, rng)?;
candidatos_final = candidatos.clone();
// (2) DISTRIBUIÇÃO/DESMEMBRAMENTO DE COMPETÊNCIAS: pesos por
// coerência (quem concorda mais lidera a montagem)
let (resposta, pesos) = self.agregar(&candidatos)?;
trajetoria.push(EventoCiclo::DistribuicaoCompetencias { por_tentativa: pesos.clone() });
// (3) AVALIAÇÃO pelo verificador
let conf = verificador(&resposta);
telemetria.insert(format!("conf_iter_{}", iteracoes), conf);
if conf > melhor.1 {
melhor = (Some(resposta), conf, pesos.clone());
}
// (4) DECISÃO: responder ou re-planejar
if conf >= self.limiar_conf {
trajetoria.push(EventoCiclo::Resposta { confianca: conf, iteracoes });
telemetria.insert("iteracoes".into(), iteracoes as f64);
telemetria.insert("confianca_final".into(), conf);
telemetria.insert("temperatura_final".into(), temperatura);
return Ok(ResultadoPensamento {
resposta: melhor.0,
candidatos: candidatos_final,
pesos: melhor.2,
confianca: conf,
iteracoes,
trajetoria,
telemetria,
});
}
if iteracoes >= self.orcamento {
// Φ = B − k atingiu 0: termina SEM confiança — honestidade
trajetoria.push(EventoCiclo::Resposta { confianca: melhor.1, iteracoes });
telemetria.insert("iteracoes".into(), iteracoes as f64);
telemetria.insert("confianca_final".into(), melhor.1);
telemetria.insert("temperatura_final".into(), temperatura);
telemetria.insert("orcamento_esgotado".into(), 1.0);
return Ok(ResultadoPensamento {
resposta: melhor.0,
candidatos: candidatos_final,
pesos: melhor.2,
confianca: melhor.1,
iteracoes,
trajetoria,
telemetria,
});
}
// (5) RE-PLANEJAMENTO: aumenta a exploração (difusor mais quente)
temperatura = (temperatura * 1.6).min(2.0);
trajetoria.push(EventoCiclo::RePlanejamento {
motivo: format!("confiança {conf:.3} < limiar {}", self.limiar_conf),
});
}
}
/// **Perda de treino das tentativas + combinador (item a):** treino
/// cooperativo — cada tentativa minimiza sua perda contra o alvo E o
/// combinador minimiza a perda da agregação (o gradiente flui para
/// todos através do concat).
pub fn perda_treino(&self, cond: &Tensor, alvo: &Tensor) -> Result<Tensor> {
// cond (b, d_cond), alvo (b, d_saida)
let b = cond.dim(0)?;
let mut perdas: Vec<Tensor> = Vec::new();
let mut saidas: Vec<Tensor> = Vec::with_capacity(self.n);
for t in &self.tentativas {
let o = t.fwd(cond)?;
let dif = (&o - alvo)?;
perdas.push((dif.sqr()?.sum_all()? / (self.d_saida as f64 * b as f64))?);
saidas.push(o);
}
let concat = Tensor::cat(&saidas, candle_core::D::Minus1)?; // (b, n·d)
let montado = self.combinador.fwd(&concat)?;
let dif_m = (&montado - alvo)?;
perdas.push((dif_m.sqr()?.sum_all()? / (self.d_saida as f64 * b as f64))?);
// soma das perdas (cooperação: todas as redes + montador)
let mut total = perdas[0].clone();
for p in perdas.iter().skip(1) {
total = (total + p.clone())?;
}
Ok(total)
}
/// **Integração com o mecanismo de atenção (T-P5):** anexa as saídas das
/// tentativas como chaves/valores extra com gate escalar.
///
/// Com `gate = 0` (nascimento): saída IDÊNTICA à atenção base
/// (neutralidade); o gradiente flui pelo gate (não-degenerado).
pub fn atencao_integrada(
&self,
q: &Tensor,
k_base: &Tensor,
v_base: &Tensor,
candidatos: &Tensor,
) -> Result<Tensor> {
// q (b, n_q, d), k_base (b, n_kv, d), v_base (b, n_kv, d_v),
// candidatos (n_tent, d) — projetados para d e d_v
let dev = q.device();
let d = k_base.dim(candle_core::D::Minus1)?;
let d_v = v_base.dim(candle_core::D::Minus1)?;
let b = q.dim(0)?;
// projeta candidatos (média simples — sem parâmetros extra): os
// candidatos têm d_saida; se d == d_saida usa direto, senão padding/
// truncamento honesto
let cand = if self.d_saida == d {
candidatos.clone()
} else if self.d_saida > d {
let v = candidatos
.flatten_all()?
.to_vec1::<f32>()?
.chunks(self.d_saida)
.map(|c| c[..d].to_vec())
.flatten()
.collect::<Vec<f32>>();
Tensor::from_vec(v, (self.n, d), &dev)?
} else {
let mut v = candidatos.flatten_all()?.to_vec1::<f32>()?;
let mut linhas = Vec::new();
for i in 0..self.n {
let mut linha = v[i * self.d_saida..(i + 1) * self.d_saida].to_vec();
linha.extend(std::iter::repeat(0f32).take(d - self.d_saida));
linhas.push(linha);
}
v = linhas.into_iter().flatten().collect();
Tensor::from_vec(v, (self.n, d), &dev)?
};
let k_extra = cand.broadcast_as((b, self.n, d))?;
let v_extra = if d_v == d {
k_extra.clone()
} else {
// projeção simples por média de colunas quando d_v != d
let media = k_extra.mean_keepdim(candle_core::D::Minus1)?;
media.broadcast_as((b, self.n, d_v))?
};
// atenção sobre [base; extra]
let k = Tensor::cat(&[k_base.clone(), k_extra], 1)?;
let v = Tensor::cat(&[v_base.clone(), v_extra], 1)?;
let scores = (q.matmul(&k.transpose(1, 2)?)? / (d as f64).sqrt())?;
let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1)?;
let y_base_e_extra = attn.matmul(&v)?;
// atenção apenas-base (para neutralidade: gate = 0 ⇒ saída = base)
let scores_base = (q.matmul(&k_base.transpose(1, 2)?)? / (d as f64).sqrt())?;
let attn_base = candle_nn::ops::softmax(&scores_base, candle_core::D::Minus1)?;
let y_base = attn_base.matmul(v_base)?;
// mistura: y = y_base + gate·(y_completo − y_base)
let g = self.gate_atencao.as_tensor();
let delta = (&y_base_e_extra - &y_base)?;
let forma: Vec<usize> = delta.dims().to_vec();
let delta = (delta.affine(1.0, 0.0)? * g.broadcast_as(forma)?)?;
Ok((&y_base + &delta)?)
}
// -----------------------------------------------------------------------
// v0.6.0 — verificador NCM (sem oráculo) + tentativas PARALELAS
// -----------------------------------------------------------------------
/// Candidatos das N tentativas em PARALELO (rayon) — as N redes têm
/// parâmetros disjuntos e entradas idênticas ⇒ N sub-DAGs independentes
/// no grafo de computação (mesma estrutura da prova M-R3): tempo
/// paralelo = max_i T_i + sobrecarga de agendamento.
/// Determinismo: o ruído da tentativa i usa a semente `semente + i`
/// (fluxos independentes por construção — reprodutível em qualquer
/// nº de threads).
pub fn candidatos_par(
&self,
cond: &Tensor,
temp: f64,
semente: u64,
) -> Result<(Tensor, f64)> {
use rayon::prelude::*;
let dev = cond.device();
let cond_b = if cond.dims().len() == 1 {
cond.broadcast_as((1, self.d_cond))?
} else {
cond.clone()
};
let t0 = std::time::Instant::now();
let saidas: Vec<Tensor> = self
.tentativas
.par_iter()
.enumerate()
.map(|(i, t)| -> Result<Tensor> {
let o = t.fwd(&cond_b)?;
if temp > 0.0 {
let mut rng = RngDeterministico::novo(semente + i as u64);
let ruido: Vec<f32> =
(0..self.d_saida).map(|_| rng.normal(0.0, temp) as f32).collect();
let rt = Tensor::from_vec(ruido, (1, self.d_saida), &dev)?;
Ok((&o + &rt.affine(1.0, 0.0)?)?)
} else {
Ok(o)
}
})
.collect::<Result<Vec<_>>>()?;
let dt = t0.elapsed().as_secs_f64();
Ok((Tensor::cat(&saidas, 0)?, dt)) // (n, d_saida), tempo paralelo
}
/// **Variante em LOTE (v0.6.0 — fase C):** cond (b, d_cond) →
/// candidatos (b, n, d_saida) — as N tentativas processam o lote
/// inteiro (paralelas entre si; ruído por (linha, tentativa) com
/// semente determinística `semente + i`).
pub fn candidatos_lote(&self, cond: &Tensor, temp: f64, semente: u64) -> Result<Tensor> {
let b = cond.dim(0)?;
let dev = cond.device();
let mut saidas: Vec<Tensor> = Vec::with_capacity(self.n);
for (i, t) in self.tentativas.iter().enumerate() {
let o = t.fwd(cond)?; // (b, d_saida)
let o = if temp > 0.0 {
let mut rng = RngDeterministico::novo(semente + i as u64);
let ruido: Vec<f32> = (0..b * self.d_saida)
.map(|_| rng.normal(0.0, temp) as f32)
.collect();
let rt = Tensor::from_vec(ruido, (b, self.d_saida), &dev)?;
(&o + &rt)?
} else {
o
};
saidas.push(o.unsqueeze(1)?); // (b, 1, d_saida)
}
Ok(Tensor::cat(&saidas, 1)?) // (b, n, d_saida)
}
/// **Agregação em LOTE (v0.6.0):** candidatos (b, n, d) → resposta
/// (b, d) — mesma mistura 50/50 (montagem aprendida + consenso
/// ponderado por coerência) da versão 1-D, VETORIZADA e totalmente
/// diferenciável (gradiente flui pelos DOIS caminhos). Devolve também
/// os pesos médios (telemetria).
pub fn agregar_lote(&self, candidatos: &Tensor) -> Result<(Tensor, Vec<f64>)> {
let b = candidatos.dim(0)?;
let n = candidatos.dim(1)?;
let d = candidatos.dim(2)?;
let dev = candidatos.device();
// consenso (b, d)
let consenso = candidatos.mean(1)?;
// cosseno por (b, n): ⟨cand_i, consenso⟩ / (‖cand_i‖·‖consenso‖)
let dot = candidatos.matmul(&consenso.unsqueeze(2)?)?; // (b, n, 1)
let nc = candidatos.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?; // (b, n, 1)
let ncs = consenso.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?.unsqueeze(1)?; // (b, 1, 1)
let cos = dot.broadcast_div(&nc.broadcast_mul(&ncs)?)?; // (b, n, 1)
// pesos softmax de coerência (b, n, 1) — tensor puro (grafo intacto)
let exps = cos.affine(4.0, 0.0)?.exp()?;
let soma = exps.sum_keepdim(1)?; // (b, 1, 1)
let pesos = exps.broadcast_div(&soma)?; // (b, n, 1)
// média ponderada: Σ_n pesos·cand → (b, d)
let media = candidatos.broadcast_mul(&pesos)?.sum(1)?; // (b, d)
// montagem aprendida: concat sobre n → combinador
let concat = candidatos.reshape((b, n * d))?;
let montado = self.combinador.fwd(&concat)?; // (b, d)
let resposta = (montado.affine(0.5, 0.0)? + media.affine(0.5, 0.0)?)?;
// pesos médios para telemetria (leitura — não afeta o grafo)
let pesos_v = pesos.flatten_all()?.to_vec1::<f32>()?;
let mut pesos_medios = vec![0f64; n];
for (k, p) in pesos_v.iter().enumerate() {
pesos_medios[k % n] += *p as f64 / b as f64;
}
let _ = d;
let _ = dev;
Ok((resposta, pesos_medios))
}
/// **Ciclo d com o VERIFICADOR NCM (v0.6.0 — fecha o débito r-21 §3.1).**
///
/// `logits_fn` mapeia uma resposta candidata (1, d_saida) → logits da
/// distribuição preditiva do BACKBONE condicionada à correção (passada
/// corrente — V-4 causal). O verificador calibrado devolve a confiança;
/// NENHUM acesso ao alvo verdadeiro (o oráculo morre aqui).
pub fn raciocinar_ncm(
&mut self,
cond: &Tensor,
verificador: &crate::verificador::VerificadorNcm,
logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>,
rng: &mut RngDeterministico,
) -> Result<ResultadoPensamento> {
let mut trajetoria = Vec::new();
let mut telemetria: HashMap<String, f64> = HashMap::new();
let mut temperatura = 0.1f64;
let mut melhor = (None::<Tensor>, -1.0f64, Vec::<f64>::new());
let mut iteracoes = 0usize;
let mut candidatos_final = Tensor::zeros(
(self.n, self.d_saida),
candle_core::DType::F32,
cond.device(),
)?;
loop {
iteracoes += 1;
trajetoria.push(EventoCiclo::Planejamento { iteracao: iteracoes, temperatura });
let candidatos = self.candidatos(cond, temperatura, rng)?;
candidatos_final = candidatos.clone();
let (resposta, pesos) = self.agregar(&candidatos)?;
trajetoria.push(EventoCiclo::DistribuicaoCompetencias { por_tentativa: pesos.clone() });
// AVALIAÇÃO pelo VERIFICADOR NCM (sem oráculo): confiança
// calibrada da distribuição preditiva do backbone
let logits = logits_fn(&resposta)?;
let conf = verificador.confianca_1d(&logits)?;
telemetria.insert(format!("conf_iter_{}", iteracoes), conf);
if conf > melhor.1 {
melhor = (Some(resposta), conf, pesos.clone());
}
if conf >= self.limiar_conf {
trajetoria.push(EventoCiclo::Resposta { confianca: conf, iteracoes });
telemetria.insert("iteracoes".into(), iteracoes as f64);
telemetria.insert("confianca_final".into(), conf);
telemetria.insert("temperatura_final".into(), temperatura);
telemetria.insert("verificador".into(), 1.0); // ncm (sem oráculo)
return Ok(ResultadoPensamento {
resposta: melhor.0,
candidatos: candidatos_final,
pesos: melhor.2,
confianca: conf,
iteracoes,
trajetoria,
telemetria,
});
}
if iteracoes >= self.orcamento {
trajetoria.push(EventoCiclo::Resposta { confianca: melhor.1, iteracoes });
telemetria.insert("iteracoes".into(), iteracoes as f64);
telemetria.insert("confianca_final".into(), melhor.1);
telemetria.insert("temperatura_final".into(), temperatura);
telemetria.insert("orcamento_esgotado".into(), 1.0);
telemetria.insert("verificador".into(), 1.0);
return Ok(ResultadoPensamento {
resposta: melhor.0,
candidatos: candidatos_final,
pesos: melhor.2,
confianca: melhor.1,
iteracoes,
trajetoria,
telemetria,
});
}
temperatura = (temperatura * 1.6).min(2.0);
trajetoria.push(EventoCiclo::RePlanejamento {
motivo: format!("conf NCM {conf:.3} < limiar {}", self.limiar_conf),
});
}
}
}
// ---------------------------------------------------------------------------
// Combinador algébrico (T-P3 — projeção ridge, Teo V-1' portado)
// ---------------------------------------------------------------------------
/// Projeção ridge: `r* = (WᵀW + εI)^{−1} Wᵀ r_alvo` — minimizador de
/// `‖W r − r_alvo‖² + ε‖r‖²` (o montador algébrico do "re-encaixe de peças").
///
/// **Prova (T-P3):** a função é quadrática estritamente convexa (ε > 0);
/// o gradiente anula-se exatamente em `r*` acima; o resíduo mínimo é
/// `‖(I − W(WᵀW+εI)^{−1}Wᵀ) r_alvo‖` — análogo direto do Teo V-1'.
pub fn projetar_ridge(w: &Tensor, r_alvo: &Tensor, eps: f64) -> Result<Tensor> {
// w: (m, n), r_alvo: (m, d) ⇒ r*: (n, d)
let n = w.dim(1)?;
let dev = w.device();
let wt = w.transpose(0, 1)?;
let wtw = wt.matmul(w)?;
let ident = Tensor::eye(n, candle_core::DType::F32, &dev)?.affine(eps, 0.0)?;
let m = (&wtw + &ident)?;
let rhs = wt.matmul(r_alvo)?;
resolver_simetrica(&m, &rhs)
}
/// Resolve `M x = B` para M simétrica (n, n) por eliminação de Gauss com
/// pivoteamento parcial (Cholesky não assumido — M pode ser só PSD+εI).
pub fn resolver_simetrica(m: &Tensor, b: &Tensor) -> Result<Tensor> {
let n = m.dim(0)?;
let d = b.dim(1)?;
let dev = m.device();
let mut a = m.to_vec2::<f32>()?;
let mut rhs = b.to_vec2::<f32>()?;
for col in 0..n {
// pivoteamento parcial
let (mut pivo, mut linha_p) = (a[col][col].abs(), col);
for r in col + 1..n {
if a[r][col].abs() > pivo {
pivo = a[r][col].abs();
linha_p = r;
}
}
if pivo < 1e-12 {
candle_core::bail!("matriz singular (pivo ~ 0 na coluna {col})");
}
a.swap(col, linha_p);
rhs.swap(col, linha_p);
for r in col + 1..n {
let f = a[r][col] / a[col][col];
for c in col..n {
a[r][c] -= f * a[col][c];
}
for c in 0..d {
rhs[r][c] -= f * rhs[col][c];
}
}
}
let mut x = vec![vec![0f32; d]; n];
for r in (0..n).rev() {
for c in 0..d {
let mut s = rhs[r][c];
for k in r + 1..n {
s -= a[r][k] * x[k][c];
}
x[r][c] = s / a[r][r];
}
}
let flat: Vec<f32> = x.into_iter().flatten().collect();
Ok(Tensor::from_vec(flat, (n, d), &dev)?)
}
pub fn versao() -> &'static str {
"pensamento-0.7.0"
}
// ---------------------------------------------------------------------------
// QUEBRA-CABEÇA pós-treinamento (v0.7.0 — inovação com o conhecimento
// treinado; requisito do usuário; provas Q-1..Q-6 em r-31)
// ---------------------------------------------------------------------------
/// Configuração do quebra-cabeça (r-28 §4.3).
#[derive(Clone, Copy, Debug)]
pub struct ConfigQuebraCabeca {
/// K peças em que o conhecimento é quebrado.
pub n_pecas: usize,
/// orçamento B de rodadas (terminação Q-4).
pub n_rodadas_max: usize,
/// limiar de validade (mesmo τ_c calibrado do verificador — Q-2).
pub tau_c: f64,
/// teto do ajuste por componente na árvore (regulação).
pub delta_max: f64,
/// remonta as peças com mistura ORTOGONAL por peça (rotação/ortogonalidade).
pub rotacao_ortogonal: bool,
/// para a execução após este nº de rodadas consecutivas sem aceite.
pub paciencia: usize,
}
impl Default for ConfigQuebraCabeca {
fn default() -> Self {
ConfigQuebraCabeca {
n_pecas: 4,
n_rodadas_max: 12,
tau_c: 0.55,
delta_max: 0.05,
rotacao_ortogonal: true,
paciencia: 5,
}
}
}
impl ConfigQuebraCabeca {
pub fn novo(n_pecas: usize, n_rodadas: usize, tau_c: f64) -> Self {
ConfigQuebraCabeca {
n_pecas,
n_rodadas_max: n_rodadas,
tau_c,
delta_max: 0.05,
rotacao_ortogonal: true,
paciencia: (n_rodadas / 2).max(2),
}
}
}
/// Uma rodada do quebra-cabeça.
#[derive(Debug, Clone)]
pub struct RodadaQuebraCabeca {
pub rodada: usize,
/// permutação das peças aplicada na remontagem (reordenação).
pub permutacao_pecas: Vec<usize>,
/// confiança NCM do candidato remontado (reavaliação).
pub confianca: f64,
/// conf ≥ τ_c ⇒ VÁLIDO (ajusta a árvore); senão PUNIDO (descarta).
pub aceito: bool,
/// delta aplicado na árvore (0 quando descartado).
pub delta_aplicado: f64,
/// rotações ortogonais usadas (quando ativas) — por peça.
pub usou_rotacao: bool,
}
/// Resultado consolidado do quebra-cabeça.
#[derive(Debug, Clone)]
pub struct ResultadoQuebraCabeca {
pub rodadas: Vec<RodadaQuebraCabeca>,
pub aceitos: usize,
pub rejeitados: usize,
pub confianca_base: f64,
pub confianca_melhor: f64,
pub telemetria: HashMap<String, f64>,
}
/// **Quebra-cabeça de inovação pós-treinamento** (r-28 §4.3):
/// 1. quebra o conhecimento treinado `conceitos` (m, d) em K peças por
/// permutação aleatória das linhas (Fisher–Yates com RNG determinístico);
/// 2. remonta as peças em nova ordem (e com mistura ortogonal por peça
/// quando `rotacao_ortogonal`) — "descobrir novas possibilidades";
/// 3. reavalia cada remontagem pelo verificador NCM (calibrado — V-2);
/// VÁLIDO (conf ≥ τ_c) ⇒ ajuste regulado na árvore de pesos indexada
/// (nó "pensamento", crédito fundo-topo); PUNIDO ⇒ descarta e o incremento
/// de punição sobe no nó (alimenta o gate do FNH — integração v0.7.0);
/// 4. repete até o orçamento B ou a paciência esgotar (Q-4).
///
/// **Provas (r-31):** Q-1 cobertura (permutações uniformes ⇒ S_K no suporte;
/// rotações varrem O(d)); Q-2 soundness herdada (mesmo limiar calibrado do
/// verificador — P(aceite inválido) ≤ 1−τ_c+ECE); Q-3 potencial monotônico
/// (Φ_r = Σ conf dos aceitos é não-decrescente; rejeições não tocam Φ);
/// Q-4 terminação (B finito, rodadas O(m·d)); Q-5 invariantes da árvore
/// preservadas (ajustes só tocam métricas, nunca a estrutura — T-A1 intacto;
/// agregação pós-ajuste mantém T-A2); Q-6 causalidade (permutação e confiança
/// medidas antes do ajuste — filtração F_r).
pub fn quebra_cabeca_inovar(
conceitos: &Tensor,
verificador: &crate::verificador::VerificadorNcm,
logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>,
arvore: &mut crate::arvore_pesos::ArvorePesos,
cfg: &ConfigQuebraCabeca,
rng: &mut RngDeterministico,
) -> Result<ResultadoQuebraCabeca> {
let dev = conceitos.device().clone();
let (m, d) = (conceitos.dims()[0], conceitos.dims()[1]);
let k = cfg.n_pecas.clamp(1, m);
let no_pens = arvore.no("pensamento").unwrap_or(0);
// linha de base: confiança do conhecimento original (agregado médio)
let base_agg = conceitos.mean(0)?;
let conf_base = {
let lg = logits_fn(&base_agg)?;
verificador.confianca_1d(&lg)?
};
let mut rodadas = Vec::new();
let mut aceitos = 0usize;
let mut rejeitados = 0usize;
let mut conf_melhor = conf_base;
let mut sem_aceite = 0usize;
for r in 1..=cfg.n_rodadas_max {
if sem_aceite >= cfg.paciencia {
break;
}
// (1) QUEBRA: permutação de linhas Fisher–Yates (Q-6: amostrada ANTES)
let mut perm: Vec<usize> = (0..m).collect();
for i in (1..m).rev() {
let j = rng.inteiro(i + 1);
perm.swap(i, j);
}
// fatia em K peças contíguas (tamanhos quase iguais)
let tamanho = (m + k - 1) / k;
let mut pecas = Vec::with_capacity(k);
let mut ini = 0usize;
while ini < m {
let fim = (ini + tamanho).min(m);
let idx: Vec<u32> = perm[ini..fim].iter().map(|&x| x as u32).collect();
let pedaco = conceitos.index_select(&Tensor::from_vec(idx, (fim - ini,), &dev)?, 0)?;
pecas.push(pedaco);
ini = fim;
}
// (2) REMONTA: nova ordem aleatória das peças (permutação de peças)
let mut perm_pecas: Vec<usize> = (0..pecas.len()).collect();
for i in (1..pecas.len()).rev() {
let j = rng.inteiro(i + 1);
perm_pecas.swap(i, j);
}
let mut remontada = Vec::with_capacity(pecas.len());
for &pi in &perm_pecas {
let mut pedaco = pecas[pi].clone();
if cfg.rotacao_ortogonal && d >= 2 {
// mistura ortogonal da peça: Q ∈ O(d) via polar de gaussiana
// do RNG determinístico (preserva normas — ‖xQ‖_F = ‖x‖_F)
let q = q_ortogonal_deterministica(d, rng, &dev)?;
pedaco = pedaco.matmul(&q)?;
}
remontada.push(pedaco);
}
let candidata = Tensor::cat(&remontada, 0)?; // (m, d) remontado
// (3) REAVALIAÇÃO pelo verificador NCM (Q-2: mesmo limiar calibrado)
let agg = candidata.mean(0)?;
let lg = logits_fn(&agg)?;
let conf = verificador.confianca_1d(&lg)?;
let aceito = conf >= cfg.tau_c;
let delta = if aceito {
// VÁLIDO: ajusta a árvore bidirecional (indexada, com teto)
let escalado = (conf - cfg.tau_c).min(cfg.delta_max);
arvore.aplicar_ajuste_indexado(no_pens, "confianca", escalado, cfg.delta_max)
} else {
// PUNIDO: descarta; a punição sobe no nó (sinal para o gate FNH)
rejeitados += 1;
sem_aceite += 1;
arvore.aplicar_ajuste_indexado(no_pens, "punicao", cfg.delta_max * 0.5, cfg.delta_max);
0.0
};
if aceito {
aceitos += 1;
sem_aceite = 0;
conf_melhor = conf_melhor.max(conf);
}
rodadas.push(RodadaQuebraCabeca {
rodada: r,
permutacao_pecas: perm_pecas,
confianca: conf,
aceito,
delta_aplicado: delta,
usou_rotacao: cfg.rotacao_ortogonal,
});
}
// crédito fundo-topo após todas as rodadas (T-A2)
arvore.agregar_fundo_topo("confianca");
arvore.agregar_fundo_topo("punicao");
let n_rod = rodadas.len();
let mut telemetria = HashMap::new();
telemetria.insert("rodadas".into(), n_rod as f64);
telemetria.insert("aceitos".into(), aceitos as f64);
telemetria.insert("rejeitados".into(), rejeitados as f64);
telemetria.insert("confianca_base".into(), conf_base);
telemetria.insert("confianca_melhor".into(), conf_melhor);
telemetria.insert(
"taxa_aceitacao".into(),
if n_rod > 0 { aceitos as f64 / n_rod as f64 } else { 0.0 },
);
telemetria.insert("rotacao_ativa".into(), if cfg.rotacao_ortogonal { 1.0 } else { 0.0 });
Ok(ResultadoQuebraCabeca {
rodadas,
aceitos,
rejeitados,
confianca_base: conf_base,
confianca_melhor: conf_melhor,
telemetria,
})
}
/// Q ∈ O(d) determinística: gaussiana do RNG próprio → retração polar.
/// Ortogonal EXATA (δ_ort ≤ 1e-6) e reprodutível (mesma semente ⇒ mesmo Q).
pub fn q_ortogonal_deterministica(
d: usize,
rng: &mut RngDeterministico,
dev: &Device,
) -> Result<Tensor> {
let n = d * d;
let v: Vec<f32> = (0..n).map(|_| rng.normal_padrao() as f32).collect();
let g = Tensor::from_vec(v, (d, d), dev)?;
crate::stiefel::retrair_polar_frobenius(&g, 90)
}
#[cfg(test)]
mod testes {
use super::*;
#[test]
fn teste_confianca_extremos() {
// T-P1: Dirac ⇒ conf = 1; uniforme ⇒ conf = 0
let dev = Device::Cpu;
let dirac = Tensor::from_vec(vec![100.0f32, 0.0, 0.0, 0.0], (1, 4), &dev).unwrap();
let uniforme = Tensor::from_vec(vec![0.0f32; 4], (1, 4), &dev).unwrap();
let c1 = confianca_de_logits(&dirac).unwrap();
let c0 = confianca_de_logits(&uniforme).unwrap();
assert!(c1 > 0.999, "conf dirac = {c1}");
assert!(c0 < 0.001, "conf uniforme = {c0}");
}
#[test]
fn teste_tp3_projetar_ridge_minimiza() {
// T-P3: r* da projeção ridge tem resíduo ≤ qualquer perturbação
let dev = Device::Cpu;
let w = Tensor::randn(0f32, 1.0, (8, 3), &dev).unwrap();
let r_alvo = Tensor::randn(0f32, 1.0, (8, 5), &dev).unwrap();
let r = projetar_ridge(&w, &r_alvo, 1e-6).unwrap();
let residuo = |rr: &Tensor| {
let d = (w.matmul(rr).unwrap() - &r_alvo).unwrap();
d.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64
};
let f0 = residuo(&r);
let mut rng = RngDeterministico::novo(1);
for _ in 0..30 {
let pert: Vec<f32> = (0..15).map(|_| rng.normal(0.0, 0.1) as f32).collect();
let rp = (Tensor::from_vec(pert, (3, 5), &dev).unwrap() + &r).unwrap();
let fp = residuo(&rp);
assert!(f0 <= fp + 1e-6, "residuo r* = {f0} > perturbado {fp}");
}
}
#[test]
fn teste_tp4_terminacao_e_orcamento() {
// o ciclo d SEMPRE termina em ≤ orçamento iterações (potencial
// Φ = B − k); com verificador impossível, termina com
// orcamento_esgotado = 1.
let dev = Device::Cpu;
let mut motor = MotorPensamento::novo(6, 4, 3, 0.99, 5, &dev).unwrap();
let cond = Tensor::randn(0f32, 1.0, (6,), &dev).unwrap();
let mut rng = RngDeterministico::novo(2);
let res = motor
.raciocinar(&cond, &|_| 0.0, &mut rng) // verificador sempre 0
.unwrap();
assert!(res.iteracoes <= 5, "iterações = {} > orçamento", res.iteracoes);
assert_eq!(res.telemetria.get("orcamento_esgotado"), Some(&1.0));
assert!(res.resposta.is_some(), "melhor candidato ainda é retornado");
}
#[test]
fn teste_tp5_neutralidade_no_nascimento() {
// gate = 0 ⇒ saída da atenção integrada é BIT-A-BIT a da base
let dev = Device::Cpu;
let motor = MotorPensamento::novo(8, 8, 3, 0.5, 3, &dev).unwrap();
let q = Tensor::randn(0f32, 1.0, (1, 4, 8), &dev).unwrap();
let k = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
let v = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
let cand = Tensor::randn(0f32, 1.0, (3, 8), &dev).unwrap();
let y_int = motor.atencao_integrada(&q, &k, &v, &cand).unwrap();
// atenção base pura
let scores = (q.matmul(&k.transpose(1, 2).unwrap()).unwrap() / (8f64).sqrt()).unwrap();
let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1).unwrap();
let y_base = attn.matmul(&v).unwrap();
let dif = (&y_int - &y_base).unwrap().abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
assert!(dif == 0.0, "gate=0 deveria dar bit-a-bit, dif = {dif}");
}
#[test]
fn teste_treino_cooperativo_e_agregacao() {
// item a: as tentativas + combinador APRENDEM a função alvo; a
// agregação converge para o alvo melhor que uma tentativa isolada
// no pior caso (verificação numérica honesta em escala pequena).
let dev = Device::Cpu;
let mut motor = MotorPensamento::novo(6, 4, 3, 0.5, 3, &dev).unwrap();
let vars = motor.vars();
let mut otim = crate::treino::AdamWManual::novo(vars, 5e-3, 0.0);
let mut rng = RngDeterministico::novo(4);
// alvo: função não-linear determinística de cond
let dados: Vec<(Vec<f32>, Vec<f32>)> = (0..256)
.map(|_| {
let c: Vec<f32> = (0..6).map(|_| rng.normal(0.0, 1.0) as f32).collect();
let a: Vec<f32> = (0..4)
.map(|j| {
(c[j % 6] * 0.5 + (c[(j + 1) % 6] * c[(j + 2) % 6]) * 0.3 + 0.1) as f32
})
.collect();
(c, a)
})
.collect();
let cs: Vec<f32> = dados.iter().flat_map(|(c, _)| c.clone()).collect();
let as_: Vec<f32> = dados.iter().flat_map(|(_, a)| a.clone()).collect();
let cond = Tensor::from_vec(cs, (256, 6), &dev).unwrap();
let alvo = Tensor::from_vec(as_, (256, 4), &dev).unwrap();
let mut perda0 = f64::NAN;
let mut perda1 = f64::NAN;
for passo in 0..200 {
let perda = motor.perda_treino(&cond, &alvo).unwrap();
if passo == 0 {
perda0 = perda.to_scalar::<f32>().unwrap() as f64;
}
perda1 = perda.to_scalar::<f32>().unwrap() as f64;
otim.passo(crate::treino::grads_achatados(&perda, &motor.vars()).unwrap());
}
assert!(perda1 < perda0 * 0.5, "treino cooperativo: {perda0} → {perda1}");
// agregação: verificador = negativo do erro quadrático médio
let mut rng2 = RngDeterministico::novo(9);
let cand = motor.candidatos(&cond.get(0).unwrap(), 0.0, &mut rng2).unwrap();
let (resp, pesos) = motor.agregar(&cand).unwrap();
assert_eq!(resp.dims().to_vec(), vec![1, 4]);
let soma: f64 = pesos.iter().sum();
assert!((soma - 1.0).abs() < 1e-9, "pesos somam {soma}");
assert!(pesos.iter().all(|&p| p >= 0.0));
}
#[test]
fn teste_tp2_auto_consistencia_numérica() {
// T-P2 numérico: N tentativas com viés comum pequeno; a média
// agregada tem erro menor que a tentativa mediana (redução de
// variância) na maioria das repetições.
let dev = Device::Cpu;
let mut rng = RngDeterministico::novo(6);
let alvo = [1.0f64, 2.0, -1.0, 0.5];
let mut venceu = 0;
let reps = 200;
for _ in 0..reps {
let n = 7;
let erros: Vec<f64> = (0..n)
.map(|_| {
alvo
.iter()
.map(|&a| {
let ruido = rng.normal(0.0, 1.0);
(a + ruido - a).abs()
})
.sum::<f64>()
})
.collect();
let media: Vec<f64> = (0..4)
.map(|j| {
(0..n).map(|i| alvo[j] + rng.normal(0.0, 1.0)).sum::<f64>() / n as f64
})
.collect();
let erro_media: f64 = media.iter().zip(alvo.iter()).map(|(m, a)| (m - a).abs()).sum();
let mut ordenado = erros.clone();
ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mediana = ordenado[n / 2];
if erro_media <= mediana {
venceu += 1;
}
}
// a média vence a mediana na grande maioria dos casos (E[erro da
// média] = σ/√n < σ = E[erro de UMA tentativa])
assert!(
venceu as f64 / reps as f64 >= 0.6,
"auto-consistência venceu só {}/{}",
venceu,
reps
);
}
// ------------------------------------------------------------------
// Quebra-cabeça (v0.7.0) — gates Q-1..Q-6 (provas r-31)
// ------------------------------------------------------------------
fn cenario_quebra_cabeca()
-> (Tensor, crate::verificador::VerificadorNcm, crate::arvore_pesos::ArvorePesos) {
let dev = Device::Cpu;
let mut rng = RngDeterministico::novo(777);
// "conhecimento treinado": 12 conceitos (d=8) com estrutura
let m = 12usize;
let d = 8usize;
let v: Vec<f32> = (0..m * d).map(|i| rng.normal_padrao() as f32).collect();
let conceitos = Tensor::from_vec(v, (m, d), &dev).unwrap();
// verificador calibrado com um "verificador" sintético: logits
// favoráveis ao agregado médio dos conceitos
let mut verif = crate::verificador::VerificadorNcm::novo();
let _ = &mut verif;
let mut arvore = crate::arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
let _ = &mut arvore;
(conceitos, verif, arvore)
}
fn logits_fn_exemplo(x: &Tensor) -> Result<Tensor> {
// logits determinísticos correlacionados com a norma do agregado —
// quanto mais "concentrado" o agregado, maior a confiança
let norma = x.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
let base = (norma * 2.0).min(8.0);
let v: Vec<f32> = vec![base as f32, 0.5, 0.2, 0.1];
Ok(Tensor::from_vec(v, (4,), x.device())?)
}
/// Q-2/Q-3: todo ACEITO tem conf ≥ τ_c; rejeições não aplicam delta;
/// potencial monotônico (confiança acumulada só cresce nos aceites).
#[test]
fn quebra_cabeca_soundness_e_potencial() -> Result<()> {
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
let cfg = ConfigQuebraCabeca::novo(4, 10, 0.55);
let mut rng = RngDeterministico::novo(4242);
let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
for rod in &r.rodadas {
if rod.aceito {
assert!(
rod.confianca >= cfg.tau_c,
"Q-2: aceito com conf {} < τ_c {}",
rod.confianca,
cfg.tau_c
);
assert!(rod.delta_aplicado > 0.0, "aceite aplica delta > 0");
} else {
assert_eq!(rod.delta_aplicado, 0.0, "Q-3: rejeitado não aplica delta");
}
}
// potencial monotônico: Σ conf dos aceitos ≤ Σ max(conf, τ_c) das rodadas
let pot: f64 = r.rodadas.iter().filter(|x| x.aceito).map(|x| x.confianca).sum();
assert!(pot >= 0.0);
// Q-5: invariantes da árvore preservadas
arvore.invariantes_ok().expect("Q-5: invariantes T-A1 preservadas");
Ok(())
}
/// Q-4: terminação — nunca excede n_rodadas_max nem roda sem parar.
#[test]
fn quebra_cabeca_termina() -> Result<()> {
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
let cfg = ConfigQuebraCabeca::novo(3, 6, 0.99); // τ inalcançável
let mut rng = RngDeterministico::novo(1);
let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
assert!(r.rodadas.len() <= 6, "Q-4: orçamento respeitado");
assert_eq!(r.aceitos, 0, "τ inalcançável ⇒ nenhum aceite");
assert!(r.rejeitados > 0);
Ok(())
}
/// Q-4b: rejeições incrementam a PUNIÇÃO no nó pensamento (sinal FNH).
#[test]
fn quebra_cabeca_punicao_sobe() -> Result<()> {
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
let no_pens = arvore.no("pensamento").unwrap();
let antes = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
let cfg = ConfigQuebraCabeca::novo(3, 5, 0.999);
let mut rng = RngDeterministico::novo(9);
let _ = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
let depois = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
assert!(depois > antes, "punição {depois} deve > {antes} após rejeições");
Ok(())
}
/// Rotação/ortogonalidade: ‖xQ‖_F = ‖x‖_F (preserva energia) e a
/// remontagem com rotação difere da sem rotação (exploração real).
#[test]
fn quebra_cabeca_rotacao_preserva_norma() -> Result<()> {
let dev = Device::Cpu;
let mut rng = RngDeterministico::novo(31);
let v: Vec<f32> = (0..5 * 6).map(|_| rng.normal_padrao() as f32).collect();
let x = Tensor::from_vec(v, (5, 6), &dev)?;
let q = q_ortogonal_deterministica(6, &mut rng, &dev)?;
let xq = x.matmul(&q)?;
let nx = x.sqr()?.sum_all()?.to_scalar::<f32>()?;
let nxq = xq.sqr()?.sum_all()?.to_scalar::<f32>()?;
assert!(
(nx - nxq).abs() / nx < 1e-4,
"‖xQ‖_F = {nxq} deve ≈ ‖x‖_F = {nx} (ortogonalidade)"
);
// Q é ortogonal exato
let delta = crate::stiefel::erro_ortogonalidade(&q)?;
assert!(delta < 1e-6, "δ_ort(Q) = {delta}");
// remontagem COM rotação difere da SEM rotação
let d2 = (&xq - &x)?.abs()?.max_all()?.to_scalar::<f32>()?;
assert!(d2 > 1e-3, "rotação deve explorar (dif = {d2})");
Ok(())
}
/// Determinismo: mesma semente ⇒ mesmas rodadas (reprodutibilidade).
#[test]
fn quebra_cabeca_deterministico() -> Result<()> {
let (c1, v1, mut a1) = cenario_quebra_cabeca();
let (_, v2, mut a2) = cenario_quebra_cabeca();
let cfg = ConfigQuebraCabeca::novo(4, 8, 0.5);
let mut r1 = RngDeterministico::novo(123);
let mut r2 = RngDeterministico::novo(123);
let q1 = quebra_cabeca_inovar(&c1, &v1, &logits_fn_exemplo, &mut a1, &cfg, &mut r1)?;
let q2 = quebra_cabeca_inovar(&c1, &v2, &logits_fn_exemplo, &mut a2, &cfg, &mut r2)?;
assert_eq!(q1.rodadas.len(), q2.rodadas.len());
for (x, y) in q1.rodadas.iter().zip(q2.rodadas.iter()) {
assert_eq!(x.permutacao_pecas, y.permutacao_pecas, "mesma semente ⇒ mesma remontagem");
assert!((x.confianca - y.confianca).abs() < 1e-12);
assert_eq!(x.aceito, y.aceito);
}
Ok(())
}
}