File size: 19,247 Bytes
97df2ae
 
 
 
 
e26f4cc
 
 
 
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
 
97df2ae
e26f4cc
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
97df2ae
 
e26f4cc
 
97df2ae
 
 
 
 
 
 
 
e26f4cc
97df2ae
e26f4cc
97df2ae
 
 
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4ee5c4
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4ee5c4
e26f4cc
e4ee5c4
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4ee5c4
e26f4cc
 
 
 
 
 
 
 
 
 
e4ee5c4
 
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb2363d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dbc907
 
 
 
 
 
d444358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dbc907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97df2ae
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! 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<String, Vec<(usize, f64)>>,
    pub eventos_ajuste: Vec<EventoAjuste>,
    pub eventos_erro: Vec<EventoErro>,
    pub caminho_jsonl: Option<String>,
    pub passo_corrente: usize,
}

impl TelemetryHub {
    pub fn novo(caminho_jsonl: Option<String>) -> 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<T: Serialize>(&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<f64> {
        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<f64> {
        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<f64> = 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<f64> = 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::<f64>() / 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<String, serde_json::Value> = 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<String, serde_json::Value> = 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<String, serde_json::Value> = 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<String, serde_json::Value> = a
                .nos
                .iter()
                .map(|no| {
                    let m: serde_json::Map<String, serde_json::Value> = 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
    }
}