//! 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 { let p = candle_nn::ops::softmax(logits, candle_core::D::Minus1)?; let logp = p.log()?; let entropia = (&p * &logp)?.sum_all()?.to_scalar::()? 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, /// 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 }, RePlanejamento { motivo: String }, Resposta { confianca: f64, iteracoes: usize }, } #[derive(Debug, Clone)] pub struct ResultadoPensamento { /// resposta agregada (d_saida,). pub resposta: Option, /// 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, /// 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, pub telemetria: HashMap, } 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 { 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::>>()?; 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 { let mut v: Vec = 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 = v.dims().to_vec(); let total: usize = forma.iter().product(); let ruido: Vec = (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 { let dev = cond.device(); let mut saidas: Vec = 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 = (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)> { 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::()?; let exps: Vec = cos_v.iter().map(|c| (*c as f64 * 4.0).exp()).collect(); let soma: f64 = exps.iter().sum(); let pesos: Vec = 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 { let mut trajetoria = Vec::new(); let mut telemetria: HashMap = HashMap::new(); let mut temperatura = 0.1f64; let mut melhor = (None::, -1.0f64, Vec::::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 { // cond (b, d_cond), alvo (b, d_saida) let b = cond.dim(0)?; let mut perdas: Vec = Vec::new(); let mut saidas: Vec = 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 { // 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::()? .chunks(self.d_saida) .map(|c| c[..d].to_vec()) .flatten() .collect::>(); Tensor::from_vec(v, (self.n, d), &dev)? } else { let mut v = candidatos.flatten_all()?.to_vec1::()?; 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 = 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 = self .tentativas .par_iter() .enumerate() .map(|(i, t)| -> Result { let o = t.fwd(&cond_b)?; if temp > 0.0 { let mut rng = RngDeterministico::novo(semente + i as u64); let ruido: Vec = (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::>>()?; 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 { let b = cond.dim(0)?; let dev = cond.device(); let mut saidas: Vec = 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 = (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)> { 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::()?; 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, rng: &mut RngDeterministico, ) -> Result { let mut trajetoria = Vec::new(); let mut telemetria: HashMap = HashMap::new(); let mut temperatura = 0.1f64; let mut melhor = (None::, -1.0f64, Vec::::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 { // 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 { let n = m.dim(0)?; let d = b.dim(1)?; let dev = m.device(); let mut a = m.to_vec2::()?; let mut rhs = b.to_vec2::()?; 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 = 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, /// 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, pub aceitos: usize, pub rejeitados: usize, pub confianca_base: f64, pub confianca_melhor: f64, pub telemetria: HashMap, } /// **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, arvore: &mut crate::arvore_pesos::ArvorePesos, cfg: &ConfigQuebraCabeca, rng: &mut RngDeterministico, ) -> Result { 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 = (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 = 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 = (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 { let n = d * d; let v: Vec = (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::().unwrap() as f64 }; let f0 = residuo(&r); let mut rng = RngDeterministico::novo(1); for _ in 0..30 { let pert: Vec = (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::().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, Vec)> = (0..256) .map(|_| { let c: Vec = (0..6).map(|_| rng.normal(0.0, 1.0) as f32).collect(); let a: Vec = (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 = dados.iter().flat_map(|(c, _)| c.clone()).collect(); let as_: Vec = 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::().unwrap() as f64; } perda1 = perda.to_scalar::().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 = (0..n) .map(|_| { alvo .iter() .map(|&a| { let ruido = rng.normal(0.0, 1.0); (a + ruido - a).abs() }) .sum::() }) .collect(); let media: Vec = (0..4) .map(|j| { (0..n).map(|i| alvo[j] + rng.normal(0.0, 1.0)).sum::() / 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 = (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 { // 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::()? as f64; let base = (norma * 2.0).min(8.0); let v: Vec = 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 = (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::()?; let nxq = xq.sqr()?.sum_all()?.to_scalar::()?; 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::()?; 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(()) } }