//! `pensamento::testes` — 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::*; #[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(()) } }