//! `pensamento::motor` — extraído do monólito na v0.10.0 //! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2). use super::*; // --------------------------------------------------------------------------- // 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), }); } } }