//! Telemetria completa (pedido do usuário: "realize todo tipo de telemetria //! inclusive para a detecção de aprendizado do modelo e dos ajustes //! automatizados de todos hiper parâmetros e demais necessários para ajustes //! e correções e detecção de erros"). //! //! Hub em Rust com: buffer por passo; exportação JSONL; detecção de //! aprendizado (escore composto); eventos de ajuste automático (auditáveis); //! eventos de erro com classificação de causa comum (diagnostico.rs); //! séries expostas ao Python como arrays numpy. use serde::Serialize; use std::collections::BTreeMap; use std::io::Write; #[derive(Debug, Clone, Serialize)] pub struct EventoAjuste { pub passo: usize, pub parametro: String, pub de: f64, pub para: f64, pub motivo: String, } #[derive(Debug, Clone, Serialize)] pub struct EventoErro { pub passo: usize, pub modulo: String, pub mensagem: String, pub causa_provavel: String, pub correcao_sugerida: String, } #[derive(Default)] pub struct TelemetryHub { /// séries nomeadas por passo pub series: BTreeMap>, pub eventos_ajuste: Vec, pub eventos_erro: Vec, pub caminho_jsonl: Option, pub passo_corrente: usize, } impl TelemetryHub { pub fn novo(caminho_jsonl: Option) -> Self { if let Some(p) = &caminho_jsonl { if let Some(dir) = std::path::Path::new(p).parent() { let _ = std::fs::create_dir_all(dir); } // truncates no início da corrida let _ = std::fs::File::create(p); } TelemetryHub { caminho_jsonl, ..Default::default() } } pub fn registrar(&mut self, nome: &str, valor: f64) { self.series.entry(nome.to_string()).or_default().push((self.passo_corrente, valor)); } pub fn registrar_multi(&mut self, pares: &[(&str, f64)]) { for (n, v) in pares { self.registrar(n, *v); } } pub fn ajuste(&mut self, parametro: &str, de: f64, para: f64, motivo: &str) { let ev = EventoAjuste { passo: self.passo_corrente, parametro: parametro.to_string(), de, para, motivo: motivo.to_string(), }; log::info!("ajuste: {} {:.4e}→{:.4e} ({})", parametro, de, para, motivo); self.append_jsonl("ajuste", &ev); self.eventos_ajuste.push(ev); } pub fn erro(&mut self, modulo: &str, mensagem: &str) { let causas = crate::diagnostico::classificar_erro(mensagem); let (causa, correcao, _) = causas .first() .copied() .unwrap_or(( "não classificado", "inspecionar mensagem completa; se recente à refatoração, suspeitar primeiro de digitação/renomeação", "fallback", )); let ev = EventoErro { passo: self.passo_corrente, modulo: modulo.to_string(), mensagem: mensagem.to_string(), causa_provavel: causa.to_string(), correcao_sugerida: correcao.to_string(), }; self.append_jsonl("erro", &ev); self.eventos_erro.push(ev); } /// Escreve o dump completo das séries no JSONL (evento "dump") — chamado /// ao fim de cada fase para o painel v0.5.0 (Theil–Sen por série). pub fn dump_jsonl(&self) { if let Some(p) = &self.caminho_jsonl { if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) { let _ = writeln!(f, "{{\"tipo\":\"dump\",\"dados\":{}}}", serde_json::to_string(&self.dump().get("series").cloned() .unwrap_or(serde_json::Value::Null)).unwrap_or_default()); } } } fn append_jsonl(&self, tipo: &str, ev: &T) { if let Some(p) = &self.caminho_jsonl { if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) { let _ = writeln!(f, "{{\"tipo\":\"{tipo}\",\"dados\":{}}}", serde_json::to_string(ev).unwrap_or_default()); } } } /// EMA de uma série (para suavização exponencial nos detectores). pub fn ema(&self, nome: &str, alfa: f64) -> Option { let s = self.series.get(nome)?; let mut acc = s[0].1; for (_, v) in &s[1..] { acc = alfa * v + (1.0 - alfa) * acc; } Some(acc) } /// Inclinação da EMA nos últimos k passos (detecção de aprendizado). pub fn inclinacao_ema(&self, nome: &str, k: usize, alfa: f64) -> Option { let s = self.series.get(nome)?; if s.len() < 2 { return None; } let ini = s.len().saturating_sub(k); let mut acc_ini = s[ini].1; for (_, v) in &s[ini + 1..] { acc_ini = alfa * v + (1.0 - alfa) * acc_ini; } let mut acc_fim = s[0].1; for (_, v) in &s[1..] { acc_fim = alfa * v + (1.0 - alfa) * acc_fim; } let passos = (s.len() - ini) as f64; Some((acc_fim - acc_ini) / passos) } /// Escore de aprendizado composto em [0,1]: sinal = inclinação negativa da /// perda (EMA) + nível absoluto relativo ao inicial + saúde do codebook. pub fn escore_aprendizado(&self) -> f64 { let Some(perda) = self.series.get("perda") else { return 0.0 }; if perda.len() < 4 { return 0.0; } let p0 = perda.iter().map(|(_, v)| *v).fold(f64::INFINITY, f64::min); let pl = perda.last().map(|(_, v)| *v).unwrap_or(p0); let reducao = ((p0 - pl) / p0.max(1e-9)).clamp(0.0, 1.0); let incl = self.inclinacao_ema("perda", 64, 0.05).unwrap_or(0.0); let sinal = (-incl * 1000.0).clamp(-1.0, 1.0) * 0.5 + 0.5; // saúde do codebook (se medida): perplexidade relativa let saude = self.series.get("codebook_perplexidade_media").and_then(|s| s.last().map(|(_, v)| *v)).unwrap_or(1.0); let saude_n = (saude / 16.0).clamp(0.0, 1.0); // K=64: perplexidade 16+ = saudável 0.45 * reducao + 0.35 * sinal + 0.20 * saude_n } // ------------------------------------------------------------------- // v0.5.0 — detecção de aprendizado ROBUSTA (regressão de Theil–Sen com // R²) e ajustes automatizados auditados // ------------------------------------------------------------------- /// Detecção robusta de aprendizado numa série: regressão de Theil–Sen /// (mediana das inclinações par a par — quebra com até ~29% de outliers) /// + R² contra o ajuste linear + veredito qualitativo. /// /// **Teo TD-1 (robustez do Theil–Sen):** com < 29,3% de pontos /// contaminados, a mediana das inclinações par a par permanece no /// intervalo interpolado dos resíduos limpos (breakdown point exato do /// estimador par-a-par); a média OLS quebra com UM único outlier. /// /// Retorna (inclinacao_robusta, r2, veredito) onde o veredito ∈ /// {"aprendendo", "estagnado", "divergindo", "amostra_insuficiente"}. pub fn deteccao_aprendizado(&self, nome: &str, janela: usize) -> (f64, f64, &'static str) { let Some(s) = self.series.get(nome) else { return (0.0, 0.0, "amostra_insuficiente") }; if s.len() < 4 { return (0.0, 0.0, "amostra_insuficiente"); } let ini = s.len().saturating_sub(janela.max(4)); let pts: Vec<(f64, f64)> = s[ini..].iter().map(|(t, v)| (*t as f64, *v)).collect(); let n = pts.len(); if n < 4 { return (0.0, 0.0, "amostra_insuficiente"); } // Theil–Sen: mediana das inclinações (i < j) let mut inclinacoes: Vec = Vec::with_capacity(n * (n - 1) / 2); for i in 0..n { for j in i + 1..n { let dx = pts[j].0 - pts[i].0; if dx.abs() > 1e-12 { inclinacoes.push((pts[j].1 - pts[i].1) / dx); } } } if inclinacoes.is_empty() { return (0.0, 0.0, "amostra_insuficiente"); } inclinacoes.sort_by(|a, b| a.partial_cmp(b).unwrap()); let incl = inclinacoes[inclinacoes.len() / 2]; // intercepto pela mediana dos resíduos let mut res_intercept: Vec = pts.iter().map(|(x, y)| y - incl * x).collect(); res_intercept.sort_by(|a, b| a.partial_cmp(b).unwrap()); let b0 = res_intercept[res_intercept.len() / 2]; // R² do ajuste robusto (1 − SS_res/SS_tot) let media_y: f64 = pts.iter().map(|(_, y)| y).sum::() / n as f64; let ss_tot: f64 = pts.iter().map(|(_, y)| (y - media_y) * (y - media_y)).sum(); let ss_res: f64 = pts .iter() .map(|(x, y)| { let e = y - (incl * x + b0); e * e }) .sum(); let r2 = if ss_tot > 1e-12 { (1.0 - ss_res / ss_tot).clamp(-1.0, 1.0) } else { 0.0 }; // veredito: a escala da inclinação é relativa à magnitude da série let escala = media_y.abs().max(1e-9); let rel = incl / escala; let veredito = if rel < -1e-4 { "aprendendo" } else if rel > 1e-4 { "divergindo" } else { "estagnado" }; (incl, r2, veredito) } /// Painel de telemetria v0.5.0: detecção de aprendizado em TODAS as /// séries registradas + eventos de ajuste computados + saúde geral. pub fn painel_v050(&self) -> serde_json::Value { let deteccoes: serde_json::Map = self .series .iter() .map(|(k, _)| { let (incl, r2, vered) = self.deteccao_aprendizado(k, 256); ( k.clone(), serde_json::json!({ "inclinacao_theil_sen": incl, "r2": r2, "veredito": vered, "n_amostras": self.series[k].len(), }), ) }) .collect(); serde_json::json!({ "versao": "0.5.0", "deteccoes": deteccoes, "escore_aprendizado": self.escore_aprendizado(), "ajustes_computados": self.eventos_ajuste.len(), "erros_registrados": self.eventos_erro.len(), }) } /// Dump completo (para o relatório Python). pub fn dump(&self) -> serde_json::Value { let series: serde_json::Map = self .series .iter() .map(|(k, v)| { ( k.clone(), serde_json::Value::Array( v.iter().map(|(p, x)| serde_json::json!([p, x])).collect(), ), ) }) .collect(); serde_json::json!({ "series": series, "eventos_ajuste": self.eventos_ajuste, "eventos_erro": self.eventos_erro, "escore_aprendizado": self.escore_aprendizado(), }) } /// Últimos valores de todas as séries (snapshot para callback Python). pub fn instantaneo(&self) -> Vec<(String, f64)> { self.series .iter() .filter_map(|(k, v)| v.last().map(|(_, x)| (k.clone(), *x))) .collect() } // ------------------------------------------------------------------- // v0.6.0 — acordo par-a-par, polaridade por métrica, painel v0.6.0 // ------------------------------------------------------------------- /// **Acordo par-a-par das tentativas** (insumo do A-3): média das /// similaridades de cosseno entre pares de candidatos (n, d). /// Sob o modelo binário i.i.d. (correto c/ prob. p), o acordo em ///expectância é `a = p² + (1−p)²` (lema do acordo — r-26 A-3). pub fn acordo_par_a_par(candidatos: &[(String, f64)]) -> f64 { // variante escalar: média das concordâncias sinalizadas entre pares if candidatos.len() < 2 { return 0.0; } let mut soma = 0f64; let mut pares = 0usize; for i in 0..candidatos.len() { for j in i + 1..candidatos.len() { let (_, vi) = &candidatos[i]; let (_, vj) = &candidatos[j]; // concordância = 1 − |vi − vj| (métrica em [0,1]) soma += 1.0 - (vi - vj).abs(); pares += 1; } } soma / pares.max(1) as f64 } /// **Polaridade por métrica** (fecha o débito r-22 §7): para séries /// cujo nome indica ERRO (perda, ce, erro, eq, mse, delta, norma) a /// direção BOA é DESCER (−1); para as demais (confiança, acurácia, /// perplexidade de codebook, escore, accordo) a direção boa é SUBIR /// (+1). O veredito "aprendendo" agora é POLARIDADE-AWARE: /// aprendendo ⇔ inclinação na direção boa. pub fn polaridade(nome: &str) -> i8 { let n = nome.to_ascii_lowercase(); const NEGATIVAS: [&str; 9] = ["perda", "ce", "erro", "eq", "mse", "delta_ort", "norma_grad", "diverg", "tempo"]; if NEGATIVAS.iter().any(|p| n.starts_with(p) || n.contains(&format!("_{p}"))) { -1 } else { 1 } } /// Painel de telemetria v0.6.0: detecção polaridade-aware em TODAS as /// séries + veredito consolidado por direção. pub fn painel_v060(&self) -> serde_json::Value { let mut aprendendo = 0usize; let mut piorando = 0usize; let mut estagnado = 0usize; let deteccoes: serde_json::Map = self .series .iter() .map(|(k, _)| { let (incl, r2, vered) = self.deteccao_aprendizado(k, 256); let pol = Self::polaridade(k) as i8; // veredito polaridade-aware: incl·pol < 0 ⇒ aprendendo let prod = incl * pol as f64; let vered_pol: &'static str = if self.series[k].len() < 4 { "amostra_insuficiente" } else if prod < -1e-9 { "aprendendo" } else if prod > 1e-9 { "piorando" } else { "estagnado" }; match vered_pol { "aprendendo" => aprendendo += 1, "piorando" => piorando += 1, _ => estagnado += 1, } let _ = vered; ( k.clone(), serde_json::json!({ "inclinacao_theil_sen": incl, "r2": r2, "polaridade": pol, "veredito_polarizado": vered_pol, "n_amostras": self.series[k].len(), }), ) }) .collect(); serde_json::json!({ "versao": "0.6.0", "deteccoes": deteccoes, "resumo_direcoes": { "aprendendo": aprendendo, "piorando": piorando, "estagnado": estagnado, }, "escore_aprendizado": self.escore_aprendizado(), "ajustes_computados": self.eventos_ajuste.len(), "erros_registrados": self.eventos_erro.len(), }) } /// Painel v0.7.0 — herda o v0.6.0 (polaridade-aware) e adiciona o /// bloco de sinais dos módulos novos: FNH (gate/punição/δ_ort), árvore /// de pesos (crédito/regulação por nó), quebra-cabeça (aceitação/ /// confiança) e NLP/NLG (gates/coerência). Métricas ausentes são /// simplesmente omitidas (painel ADITIVO — sem acoplamento rígido). /// Painel v0.9.0 — despacho SIMD + contexto das medições da r-34. pub fn painel_v090(&self) -> serde_json::Value { let mut p = self.painel_v070(None); if let Some(obj) = p.as_object_mut() { obj.insert("simd".into(), crate::simd::relatorio()); obj.insert( "v090_nota".into(), serde_json::json!( "kernels AVX-512 com despacho em runtime (bit-a-bit); medições em r-34 §4/§6: \ AdamW 1,0-1,2× (limitado por memória), BMU argmin: escalar ótimo em d=4; \ bf16 em CPU candle: 3× MAIS LENTO (upcast por op); nosso AdamW 1,5-1,7× mais \ rápido que candle_nn::optim" ), ); } p } pub fn painel_v070(&self, arvore: Option<&crate::arvore_pesos::ArvorePesos>) -> serde_json::Value { let mut p = self.painel_v060(); // promove a versão sem perder o conteúdo p["versao"] = serde_json::json!("0.7.0"); // sinais FNH (registrados no hub como fnh_*) let fnh = serde_json::json!({ "gate_ultimo": self.ema("fnh_gate", 0.9), "punicao_ema": self.ema("fnh_punicao", 0.9), "delta_ort_rotacao": self.ema("fnh_delta_ort", 0.9), "theta_norma": self.ema("fnh_theta_norma", 0.9), }); p["fnh"] = fnh; // quebra-cabeça p["quebra_cabeca"] = serde_json::json!({ "taxa_aceitacao": self.ema("qc_taxa_aceitacao", 0.8), "confianca_melhor": self.ema("qc_confianca_melhor", 0.8), "confianca_base": self.ema("qc_confianca_base", 0.8), "punicao_acumulada": self.ema("qc_punicao", 0.8), }); // NLP/NLG p["nlp_nlg"] = serde_json::json!({ "nlp_gate_medio": self.ema("nlp_gate_medio", 0.9), "nlg_coerencia": self.ema("nlg_score_coerencia", 0.9), "espec_aceitacao": self.ema("espec_aceitacao", 0.9), "espec_speedup": self.ema("espec_speedup", 0.9), }); // árvore de pesos: métricas agregadas por nó (fundo-topo) if let Some(a) = arvore { let nos: serde_json::Map = a .nos .iter() .map(|no| { let m: serde_json::Map = no .metricas .iter() .map(|(k, v)| (k.clone(), serde_json::json!(v))) .collect(); ( no.nome.clone(), serde_json::json!({ "intervalo": [no.ini, no.fim], "profundidade": no.profundidade, "metricas": m, }), ) }) .collect(); p["arvore_pesos"] = serde_json::json!({ "n_nos": a.n_nos(), "n_enlaces": a.enlaces.len(), "n_params": a.n_params, "nos": nos, "invariantes_ok": a.invariantes_ok().is_ok(), }); } p } }