File size: 34,018 Bytes
97df2ae
 
 
 
e26f4cc
 
 
 
 
 
 
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
97df2ae
e26f4cc
97df2ae
e26f4cc
 
 
 
97df2ae
e26f4cc
97df2ae
e26f4cc
97df2ae
 
 
 
 
 
e26f4cc
97df2ae
e26f4cc
 
 
 
97df2ae
 
e26f4cc
97df2ae
e26f4cc
97df2ae
 
 
 
 
e26f4cc
 
 
 
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb2363d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d444358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dbc907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb2363d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d444358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
//! Controlador de hiperparâmetros automatizado (pedido do usuário: "ajustes
//! automatizados de todos hiper parâmetros"). TODA decisão é registrada na
//! telemetria com motivo (auditável).
//!
//! - LR: aquecimento → cosseno; plateau (paciência) → ×fator; divergência
//!   (NaN ou perda > 2× mínimo EMA) → ×0,3 + rollback do estado;
//! - λ (âncora, Teo 29.2): hipergradiente exato;
//! - ε da corrupção (Teo C.3(ii)): calibra pela taxa de erro por nível em
//!   validação (cobertura — "calibrar ρ tal que erro ≤ ρ");
//! - clip: adapta ao percentil da norma de gradiente;
//! - τ do GSQ: agenda analítica (Teo 30.6) — no tokenizador.

use crate::telemetria::TelemetryHub;

pub struct Controlador {
    pub lr_base: f64,
    pub lr_corrente: f64,
    pub aquecimento: usize,
    pub passos_totais: usize,
    pub paciencia_plateau: usize,
    pub fator_plateau: f64,
    pub minimo_ema: f64,
    pub passos_sem_melhora: usize,
    pub clip: f64,
    pub clip_percentil: f64,
    pub normas_grad: Vec<f64>,
    pub lambda_ancora: crate::perdas::LambdaAncora,
    pub epsilon_corrupcao: f64,
}

impl Controlador {
    pub fn novo(lr_base: f64, aquecimento: usize, passos_totais: usize, paciencia: usize, fator: f64, rho_ancora: f64) -> Self {
        Controlador {
            lr_base,
            lr_corrente: lr_base,
            aquecimento,
            passos_totais,
            paciencia_plateau: paciencia,
            fator_plateau: fator,
            minimo_ema: f64::INFINITY,
            passos_sem_melhora: 0,
            clip: 1.0,
            clip_percentil: 90.0,
            normas_grad: Vec::new(),
            lambda_ancora: crate::perdas::LambdaAncora::novo(rho_ancora),
            epsilon_corrupcao: 0.15,
        }
    }

    /// LR do passo (aquecimento → cosseno com decaimentos por plateau).
    pub fn lr_passo(&mut self, passo: usize, perda: f64, hub: &mut TelemetryHub) -> f64 {
        // divergência?
        if !perda.is_finite() || perda > 2.0 * self.minimo_ema {
            let de = self.lr_corrente;
            self.lr_corrente *= 0.3;
            hub.ajuste("lr", de, self.lr_corrente,
                &format!("divergência detectada (perda {perda:.3e} vs mínimo EMA {:.3e})", self.minimo_ema));
            self.passos_sem_melhora = 0;
            return self.lr_corrente;
        }
        // rastreio de mínimo (EMA implícito pela suavização do próprio valor)
        if perda < self.minimo_ema * 0.999 {
            self.minimo_ema = self.minimo_ema.min(perda);
            self.passos_sem_melhora = 0;
        } else {
            self.passos_sem_melhora += 1;
        }
        // plateau
        if self.paciencia_plateau > 0 && self.passos_sem_melhora >= self.paciencia_plateau {
            let de = self.lr_corrente;
            self.lr_corrente *= self.fator_plateau;
            hub.ajuste("lr", de, self.lr_corrente,
                &format!("plateau: {} passos sem melhora", self.passos_sem_melhora));
            self.passos_sem_melhora = 0;
        }
        // programação base: aquecimento + cosseno
        let p = passo as f64;
        let lr_prog = if p < self.aquecimento as f64 {
            self.lr_base * (p + 1.0) / self.aquecimento.max(1) as f64
        } else {
            let t = (p - self.aquecimento as f64) / (self.passos_totais_clamped() - self.aquecimento).max(1) as f64;
            let cosseno = 0.5 * (1.0 + (std::f64::consts::PI * t.min(1.0)).cos());
            self.lr_base * (0.1 + 0.9 * cosseno)
        };
        // o menor entre a programação e o teto pós-plateau
        self.lr_corrente = self.lr_corrente.min(lr_prog).max(self.lr_base * 0.01);
        self.lr_corrente
    }

    fn passos_totais_clamped(&self) -> usize {
        self.passos_totais.max(self.aquecimento + 1)
    }

    /// Clip adaptativo: percentil das normas recentes (mín. 0,25).
    pub fn clip_corrente(&mut self, hub: &mut TelemetryHub) -> f64 {
        if self.normas_grad.len() >= 32 {
            let mut v = self.normas_grad.clone();
            v.sort_by(|a, b| a.partial_cmp(b).unwrap());
            let idx = ((self.clip_percentil / 100.0) * (v.len() - 1) as f64).round() as usize;
            let novo = v[idx.min(v.len() - 1)].max(0.25);
            if (novo - self.clip).abs() / self.clip > 0.3 {
                hub.ajuste("clip", self.clip, novo, "percentil 90 das normas de gradiente (janela 32)");
                self.clip = novo;
            }
        }
        self.clip
    }

    pub fn registrar_norma(&mut self, n: f64) {
        self.normas_grad.push(n);
        if self.normas_grad.len() > 256 {
            self.normas_grad.remove(0);
        }
    }

    /// ε da corrupção (C.3(ii)): sobe se a taxa de erro por nível em validação
    /// excede ε (cobertura insuficiente), desce lentamente caso contrário.
    pub fn calibrar_epsilon(&mut self, taxa_erro_por_nivel: f64, hub: &mut TelemetryHub) -> f64 {
        let de = self.epsilon_corrupcao;
        if taxa_erro_por_nivel > self.epsilon_corrupcao + 0.02 {
            self.epsilon_corrupcao = (self.epsilon_corrupcao + 0.02).min(0.35);
        } else if taxa_erro_por_nivel < self.epsilon_corrupcao * 0.5 {
            self.epsilon_corrupcao = (self.epsilon_corrupcao - 0.01).max(0.05);
        }
        if (de - self.epsilon_corrupcao).abs() > 1e-9 {
            hub.ajuste("corrupcao_epsilon", de, self.epsilon_corrupcao,
                &format!("cobertura C.3(ii): taxa de erro por nível medida {:.3}", taxa_erro_por_nivel));
        }
        self.epsilon_corrupcao
    }
}

// ---------------------------------------------------------------------------
// v0.6.0 — AUTOAJUSTE DO PENSAMENTO POR TELEMETRIA (r-21 §3.5 fechado)
// ---------------------------------------------------------------------------

/// Regras de autoajuste dos hiperparâmetros do Pensamento, CADA UMA com
/// garantia matemática (provas completas em r-26):
///
/// - **A-1 (τ_c por quantil DKW):** `τ_c = quantil_{1−ε}` das confianças
///   calibradas retidas ⇒ taxa de erro do "aceitar" ≤ ε + 2e^{−2nδ²}
///   (Dvoretzky–Kiefer–Wolfowitz–Massart).
/// - **A-2 (B por análise marginal):** o ganho esperado por iteração decai
///   geometricamente (T-P4: potencial Φ = B − k); com ganho inicial Δ e
///   razão ρ̂ medida na telemetria, o B ótimo minimiza
///   `Custo(B) = c_iter·B − Δ·Σ_{k<B} ρ̂^k = c_iter·B − Δ(1−ρ̂^B)/(1−ρ̂)`
///   ⇒ `B* = ceil(log(c_iter(1−ρ̂)/Δ)/log(ρ̂))` quando a razão existe.
/// - **A-3 (N por Hoeffding + lema do acordo):** tentativas i.i.d. com
///   correção p ⇒ acordo par-a-par esperado `a = p² + (1−p)² = 1 − 2p(1−p)`
///   ⇒ `p̂ = (1 + √(2â − 1))/2` (raiz ≥ 1/2 da quadrática); para erro de
///   maioria ≤ δ: `N ≥ ln(1/δ) / (2(p̂ − 1/2)²)` (Hoeffding).
/// - **A-5 (g por Bernstein empírico):** o gate sobe somente se o LIMITE
///   INFERIOR do ganho medido é positivo:
///   `média − √(2·variância·ln(2/α)/n) − 7ln(2/α)/(3(n−1)) > 0`.
pub struct AutoAjustePensamento {
    pub tau_c: f64,
    pub orcamento_b: usize,
    pub n_tentativas: usize,
    pub gate_g: f64,
    /// histórico de confianças calibradas retidas (para A-1)
    pub confs_retidas: Vec<f64>,
    /// ganho de confiança por iteração do ciclo d (para A-2)
    pub ganhos_por_iteracao: Vec<f64>,
    /// acordos par-a-par observados (para A-3)
    pub acordos: Vec<f64>,
    /// ganhos atribuídos ao gate (para A-5)
    pub ganhos_gate: Vec<f64>,
}

impl AutoAjustePensamento {
    pub fn novo(tau_c_inicial: f64, b_inicial: usize, n_inicial: usize) -> Self {
        AutoAjustePensamento {
            tau_c: tau_c_inicial,
            orcamento_b: b_inicial,
            n_tentativas: n_inicial,
            gate_g: 0.0,
            confs_retidas: Vec::new(),
            ganhos_por_iteracao: Vec::new(),
            acordos: Vec::new(),
            ganhos_gate: Vec::new(),
        }
    }

    /// **A-1:** reajusta τ_c pelo quantil 1−ε das confianças retidas.
    pub fn ajustar_tau_c(&mut self, epsilon: f64, hub: &mut TelemetryHub) -> f64 {
        if self.confs_retidas.len() < 32 {
            return self.tau_c; // amostra insuficiente — mantém (honesto)
        }
        let mut v = self.confs_retidas.clone();
        v.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let idx = (((1.0 - epsilon.clamp(0.0, 0.5)) * v.len() as f64).ceil() as usize)
            .saturating_sub(1)
            .min(v.len() - 1);
        let novo = v[idx];
        if (novo - self.tau_c).abs() > 1e-6 {
            hub.ajuste("pensamento_tau_c", self.tau_c, novo, &format!(
                "A-1 quantil DKW 1−ε (ε={epsilon:.2}, n={})", v.len()));
            self.tau_c = novo;
        }
        self.tau_c
    }

    /// **A-2:** reajusta o orçamento B pela análise marginal do ganho medido.
    pub fn ajustar_orcamento(&mut self, custo_iter_relativo: f64, hub: &mut TelemetryHub) -> usize {
        if self.ganhos_por_iteracao.len() < 8 {
            return self.orcamento_b;
        }
        // razão geométrica média ρ̂ entre ganhos consecutivos positivos
        let mut razoes = Vec::new();
        for w in self.ganhos_por_iteracao.windows(2) {
            if w[0] > 1e-9 && w[1] >= 0.0 {
                razoes.push(((w[1] / w[0]) as f64).clamp(0.0, 0.999));
            }
        }
        if razoes.len() < 4 {
            return self.orcamento_b;
        }
        razoes.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let rho = razoes[razoes.len() / 2]; // mediana (robusto)
        let delta0 = self.ganhos_por_iteracao.iter().cloned().fold(f64::MIN, f64::max).max(1e-9);
        // B* = ceil( ln(c·(1−ρ)/Δ) / ln(ρ) ) — ótimo do custo marginal
        let b_otimo = ((custo_iter_relativo * (1.0 - rho) / delta0).ln() / rho.ln())
            .ceil()
            .clamp(1.0, 16.0) as usize;
        if b_otimo != self.orcamento_b {
            hub.ajuste("pensamento_orcamento_b", self.orcamento_b as f64, b_otimo as f64, &format!(
                "A-2 análise marginal (ρ̂={rho:.3}, Δ₀={delta0:.4}, c={custo_iter_relativo:.3})"));
            self.orcamento_b = b_otimo;
        }
        self.orcamento_b
    }

    /// **A-3:** reajusta N pelo estimador do acordo + Hoeffding.
    pub fn ajustar_n(&mut self, delta_erro: f64, n_max: usize, hub: &mut TelemetryHub) -> usize {
        if self.acordos.len() < 16 {
            return self.n_tentativas;
        }
        let mut v = self.acordos.clone();
        v.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let a_med = v[v.len() / 2];
        // lema do acordo: a = p² + (1−p)² ⇒ p = (1 + √(2a−1))/2 (a ≥ 1/2)
        let p_chapeu = if a_med >= 0.5 {
            (1.0 + (2.0 * a_med - 1.0).sqrt().min(1.0)) / 2.0
        } else {
            0.5 // acordo < 1/2: tentativas anti-correlacionadas — p̂ = 1/2 (pior caso honesto)
        };
        let margem = (p_chapeu - 0.5).max(1e-3);
        // N ≥ ln(1/δ) / (2(p̂−1/2)²) — note ln(1/δ) = −ln(δ) > 0 para δ < 1
        let n_necessario = ((1.0 / delta_erro).ln() / (2.0 * margem * margem)).ceil().max(1.0) as usize;
        let novo = n_necessario.clamp(1, n_max);
        if novo != self.n_tentativas {
            hub.ajuste("pensamento_n_tentativas", self.n_tentativas as f64, novo as f64, &format!(
                "A-3 Hoeffding+acordo (â={a_med:.3} ⇒ p̂={p_chapeu:.3}, δ={delta_erro:.2})"));
            self.n_tentativas = novo;
        }
        self.n_tentativas
    }

    /// **A-5:** gate sobe somente com LCB positivo (Bernstein empírico).
    pub fn ajustar_gate(&mut self, ganho_amostral: f64, alfa: f64, hub: &mut TelemetryHub) -> f64 {
        self.ganhos_gate.push(ganho_amostral);
        if self.ganhos_gate.len() < 16 {
            return self.gate_g;
        }
        let n = self.ganhos_gate.len() as f64;
        let media: f64 = self.ganhos_gate.iter().sum::<f64>() / n;
        let var: f64 =
            self.ganhos_gate.iter().map(|x| (x - media) * (x - media)).sum::<f64>() / (n - 1.0);
        let lcb = media
            - (2.0 * var * (2.0 / alfa).ln() / n).sqrt()
            - 7.0 * (2.0 / alfa).ln() / (3.0 * (n - 1.0));
        if lcb > 0.0 {
            let novo = (self.gate_g + 0.1).min(1.0);
            if novo != self.gate_g {
                hub.ajuste("pensamento_gate_g", self.gate_g, novo, &format!(
                    "A-5 Bernstein empírico (LCB={lcb:.4} > 0, média={media:.4})"));
                self.gate_g = novo;
            }
        } else if lcb < -0.05 && self.gate_g > 0.0 {
            let novo = (self.gate_g - 0.1).max(0.0);
            hub.ajuste("pensamento_gate_g", self.gate_g, novo, &format!(
                "A-5 Bernstein empírico (LCB={lcb:.4} < 0 — recua)"));
            self.gate_g = novo;
        }
        self.gate_g
    }
}

// ---------------------------------------------------------------------------
// v0.6.0 — seleção de arquitetura por UCB1 (A-4)
// ---------------------------------------------------------------------------

/// **A-4 (UCB1 — Auer et al. 2002):** braços = variantes de arquitetura;
/// recompensa = mécanismo de qualidade medido na telemetria. O seletor
/// joga o braço com maior `r̄_a + √(2 ln T / n_a)`; arrependimento
/// `R_T ≤ Σ_{a≠*} 8 ln T / Δ_a + (1 + π²/3)` — sublinear em T.
pub struct Ucb1 {
    pub nomes: Vec<String>,
    pub somas: Vec<f64>,
    pub contagens: Vec<u64>,
    pub t: u64,
}

impl Ucb1 {
    pub fn novo(nomes: Vec<String>) -> Self {
        let n = nomes.len();
        Ucb1 { nomes, somas: vec![0.0; n], contagens: vec![0; n], t: 0 }
    }

    /// Escolhe o braço (cada braço é jogado 1× antes da exploração).
    pub fn escolher(&self) -> usize {
        for (i, &c) in self.contagens.iter().enumerate() {
            if c == 0 {
                return i;
            }
        }
        let ln_t = (self.t as f64).ln();
        let mut melhor = 0usize;
        let mut melhor_ucb = f64::NEG_INFINITY;
        for i in 0..self.nomes.len() {
            let media = self.somas[i] / self.contagens[i] as f64;
            let ucb = media + (2.0 * ln_t / self.contagens[i] as f64).sqrt();
            if ucb > melhor_ucb {
                melhor_ucb = ucb;
                melhor = i;
            }
        }
        melhor
    }

    pub fn registrar(&mut self, braco: usize, recompensa: f64) {
        self.somas[braco] += recompensa;
        self.contagens[braco] += 1;
        self.t += 1;
    }

    pub fn medias(&self) -> Vec<f64> {
        self.contagens
            .iter()
            .zip(self.somas.iter())
            .map(|(&c, &s)| if c > 0 { s / c as f64 } else { 0.0 })
            .collect()
    }

    /// Arrependimento empírico vs. melhor braço a posteriori (auditoria).
    pub fn arrependimento_empirico(&self) -> f64 {
        let medias = self.medias();
        let melhor = medias.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        self.contagens
            .iter()
            .zip(medias.iter())
            .map(|(&c, &m)| c as f64 * (melhor - m))
            .sum()
    }
}

// ---------------------------------------------------------------------------
// v0.9.0 — Thompson Sampling (crítica Qwen3.8 C; provas T-TS1/T-TS2 em
// r-34 §7) — alternativa À UCB1, aditiva (nenhuma removida)
// ---------------------------------------------------------------------------

/// **T-TS1 (Thompson Sampling Beta-Bernoulli):** mantém posteriores
/// Beta(α_a, β_a) por braço; a cada passo amostra θ_a ~ Beta(α_a, β_a) e joga
/// argmax θ_a. Propriedades:
///
/// 1. **Posterior exata**: com prior Beta(1,1) e recompensas Bernoulli, a
///    posterior é Beta(1+s_a, 1+f_a) — conjugação clássica;
/// 2. **Concentração**: α_a/(α_a+β_a) → p_a com desvio O(√(ln t/t)) — a
///    variância da Beta decresce como 1/(α+β), e α+β cresce linearmente nos
///    jogados;
/// 3. **Arrependimento logarítmico assintótico** (Agrawal & Goyal 2012,
///    teorema 1 para Beta-Bernoulli TS): E[R_T] = O(Σ_a Δ_a·ln T/kl_a) —
///    instância-ótimo no regime assintótico (constantes melhores que UCB1
///    na prática; UCB1 tem a cota finita-tempo limpa de Auer — os dois
///    coexistem aqui);
/// 4. **Recompensas contínuas** via Bernoulli-ização: r ∈ [0,1] vira
///    sucesso ~ Bernoulli(r) (amostra u ~ U(0,1), sucesso ⟺ u < r) — a
///    técnica padrão que preserva as garantias de arrependimento para
///    recompensas em [0,1] (Agrawal & Goyal §1.2).
///
/// **Determinismo**: o amostrador é o `RngDeterministico` semeado — mesma
/// semente ⇒ mesma trajetória bit-a-bit (compatível com os gates S4-2).
pub struct ThompsonSampling {
    pub nomes: Vec<String>,
    pub alfas: Vec<f64>,
    pub betas: Vec<f64>,
    pub t: u64,
    rng: crate::rng::RngDeterministico,
    /// nº de amostragens que caíram no braço argmax (telemetria de
    /// convergência do posterior).
    pub sucessos: Vec<u64>,
    pub jogadas: Vec<u64>,
}

impl ThompsonSampling {
    pub fn novo(nomes: Vec<String>, semente: u64) -> Self {
        let n = nomes.len();
        ThompsonSampling {
            nomes,
            alfas: vec![1.0; n],
            betas: vec![1.0; n],
            t: 0,
            rng: crate::rng::RngDeterministico::novo(semente),
            sucessos: vec![0; n],
            jogadas: vec![0; n],
        }
    }

    /// Amostra θ_a ~ Beta(α_a, β_a) de cada braço e devolve o argmax
    /// (desempate pelo menor índice — determinístico dado o RNG).
    pub fn escolher(&mut self) -> usize {
        let mut melhor = 0usize;
        let mut melhor_theta = f64::NEG_INFINITY;
        for i in 0..self.alfas.len() {
            let theta = self.rng.beta(self.alfas[i], self.betas[i]);
            if theta > melhor_theta {
                melhor_theta = theta;
                melhor = i;
            }
        }
        melhor
    }

    /// Registra recompensa Bernoulli (sucesso/falha) — atualização conjugada.
    pub fn registrar(&mut self, braco: usize, sucesso: bool) {
        if sucesso {
            self.alfas[braco] += 1.0;
            self.sucessos[braco] += 1;
        } else {
            self.betas[braco] += 1.0;
        }
        self.jogadas[braco] += 1;
        self.t += 1;
    }

    /// Recompensa contínua r ∈ [0,1] via Bernoulli-ização (T-TS1.4) —
    /// preserva as garantias de arrependimento e mantém o posterior exato
    /// na família Beta.
    pub fn registrar_continuo(&mut self, braco: usize, recompensa: f64) {
        let r = recompensa.clamp(0.0, 1.0);
        let sucesso = self.rng.uniforme() < r;
        self.registrar(braco, sucesso);
    }

    /// Médias posteriores α/(α+β) (estimativa pontual de p_a).
    pub fn medias_posteriori(&self) -> Vec<f64> {
        self.alfas
            .iter()
            .zip(self.betas.iter())
            .map(|(&a, &b)| a / (a + b))
            .collect()
    }

    /// Arrependimento empírico vs. melhor braço a posteriori (mesma métrica
    /// de auditoria do Ucb1 — comparável diretamente).
    pub fn arrependimento_empirico(&self, recompensas_verdadeiras: &[f64]) -> f64 {
        let melhor = recompensas_verdadeiras
            .iter()
            .cloned()
            .fold(f64::NEG_INFINITY, f64::max);
        self.jogadas
            .iter()
            .zip(recompensas_verdadeiras.iter())
            .map(|(&c, &p)| c as f64 * (melhor - p))
            .sum()
    }
}

// ---------------------------------------------------------------------------
// v0.7.0 — Auto-boost limitado dos SOMs + detecção de stop-learning
// (herdado da análise de BiGRU_T_version/som_auto_adjust.py — r-28 §2.2)
// ---------------------------------------------------------------------------

/// Auto-ajuste dos SOMs pelas métricas QE/TE com **tetos formais**:
/// α₀_ef ∈ [α₀, 2α₀] e σ₀_ef ∈ [σ₀, 2σ₀] — nunca abaixo do canônico
/// (piso FIX-3), no máximo 2× (evita divergência).
///
/// **Teo S-AB (boost limitado):** o mapa de atualização é SEM ESTADO e
/// limitado: α_eff = clamp(α₀·(1+0,3·qe)·(1−0,2·cross), α₀, 2α₀) — por
/// construção |α_eff − α₀| ≤ α₀ para QUALQUER entrada qe, cross ∈ [0,1]²
/// ⇒ o treinamento permanece no envelope de estabilidade do canônico
/// (recorrência limitada monotônica não diverge). ∎ (r-33 §2)
///
/// **Teo S-SL (stop-learning conservador):** a decisão de parar é função
/// da filtração observada (QE/TE/mortos/NaN por chunk) com 5 gatilhos
/// herdados e catalogados; parar cedo só REDUZ o custo (nunca degrada o
/// estado já aprendido — os pesos não são tocados pela decisão). ∎
pub struct AutoAjusteSom {
    pub alfa0: f64,
    pub sigma0: f64,
    /// contadores de chunks consecutivos (stop-learning).
    pub chunks_qe_critico: usize,
    pub chunks_crossing_severo: usize,
    pub chunks_nan: usize,
    pub parado: bool,
    pub motivo_parada: String,
}

impl AutoAjusteSom {
    pub fn novo(alfa0: f64, sigma0: f64) -> Self {
        AutoAjusteSom {
            alfa0,
            sigma0,
            chunks_qe_critico: 0,
            chunks_crossing_severo: 0,
            chunks_nan: 0,
            parado: false,
            motivo_parada: String::new(),
        }
    }

    /// Passo de auto-ajuste: devolve (α_eff, σ_eff) DENTRO dos tetos.
    /// qe_norm/te_norm ∈ [0,1] (normalizados); cross ∈ [0,1] severidade.
    pub fn passo(&mut self, qe_norm: f64, te_norm: f64, cross: f64) -> (f64, f64) {
        let alfa = (self.alfa0 * (1.0 + 0.3 * qe_norm) * (1.0 - 0.2 * cross))
            .clamp(self.alfa0, 2.0 * self.alfa0);
        let sigma = (self.sigma0 * (1.0 + 0.4 * te_norm) + 0.25 * cross)
            .clamp(self.sigma0, 2.0 * self.sigma0);
        (alfa, sigma)
    }

    /// Stop-learning (5 gatilhos — BiGRU V6.5-V2 + V7, adaptados):
    /// 1. colapso total com revival falhou (colapso=true, revival=false)
    /// 2. QE estagnado em nível crítico por ≥ 5 chunks consecutivos
    /// 3. neurônios mortos > 0,8 após revival
    /// 4. todas as métricas NaN por 2 chunks
    /// 5. crossing severo por ≥ 4 chunks consecutivos
    pub fn avaliar_parada(
        &mut self,
        qe_norm: f64,
        te_norm: f64,
        fracao_mortos: f64,
        colapso: bool,
        revival_ok: bool,
        nan_tudo: bool,
        crossing_severo: bool,
    ) -> bool {
        if self.parado {
            return true;
        }
        // gatilho 2
        self.chunks_qe_critico = if qe_norm > 0.9 { self.chunks_qe_critico + 1 } else { 0 };
        // gatilho 5
        self.chunks_crossing_severo = if crossing_severo {
            self.chunks_crossing_severo + 1
        } else {
            0
        };
        // gatilho 4
        self.chunks_nan = if nan_tudo { self.chunks_nan + 1 } else { 0 };
        let _ = te_norm;

        let motivo = if colapso && !revival_ok {
            Some("colapso topológico com revival falhou")
        } else if self.chunks_qe_critico >= 5 {
            Some("QE estagnado em nível crítico por 5 chunks")
        } else if fracao_mortos > 0.8 {
            Some("neurônios mortos > 80% após revival")
        } else if self.chunks_nan >= 2 {
            Some("todas as métricas NaN por 2 chunks")
        } else if self.chunks_crossing_severo >= 4 {
            Some("crossing severo por 4 chunks (instabilidade crônica)")
        } else {
            None
        };
        if let Some(m) = motivo {
            self.parado = true;
            self.motivo_parada = m.to_string();
            return true;
        }
        false
    }
}

#[cfg(test)]
mod testes_v070 {
    use super::*;

    /// S-AB: tetos respeitados para entradas EXTREMAS.
    #[test]
    fn sab_tetos_dos_boosts() {
        let mut a = AutoAjusteSom::novo(0.5, 1.0);
        for (qe, te, cross) in [
            (0.0f64, 0.0, 0.0),
            (1.0, 1.0, 1.0),
            (1.0, 0.0, 1.0),
            (0.5, 0.5, 0.5),
        ] {
            let (al, sg) = a.passo(qe, te, cross);
            assert!(al >= 0.5 && al <= 1.0, "α_eff {al} ∈ [α₀, 2α₀]");
            assert!(sg >= 1.0 && sg <= 2.0, "σ_eff {sg} ∈ [σ₀, 2σ₀]");
        }
        // QE alta sem crossing ⇒ α sobe
        let (al, _) = a.passo(1.0, 0.0, 0.0);
        assert!((al - 0.65).abs() < 1e-9, "α = α₀·1,3 = {al}");
        // crossing puro ⇒ α recua ao piso mas nunca abaixo
        let (al, _) = a.passo(0.0, 0.0, 1.0);
        assert!((al - 0.5).abs() < 1e-9, "α no piso {al}");
    }

    /// S-SL: gatilhos disparam; pesos não são tocados pela decisão.
    #[test]
    fn ssl_stop_learning() {
        let mut a = AutoAjusteSom::novo(0.5, 1.0);
        // 5 chunks consecutivos de QE crítico ⇒ para
        for _ in 0..4 {
            assert!(!a.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false));
        }
        assert!(a.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false));
        assert!(a.parado);
        assert!(a.motivo_parada.contains("QE estagnado"));
        // parado persiste
        assert!(a.avaliar_parada(0.1, 0.1, 0.1, false, true, false, false));
        // colapso imediato
        let mut b = AutoAjusteSom::novo(0.5, 1.0);
        assert!(b.avaliar_parada(0.1, 0.1, 0.1, true, false, false, false));
        // NaN 2 chunks
        let mut c = AutoAjusteSom::novo(0.5, 1.0);
        assert!(!c.avaliar_parada(0.1, 0.1, 0.1, false, true, true, false));
        assert!(c.avaliar_parada(0.1, 0.1, 0.1, false, true, true, false));
        // reset do contador com QE bom
        let mut d = AutoAjusteSom::novo(0.5, 1.0);
        for _ in 0..3 {
            d.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false);
        }
        d.avaliar_parada(0.2, 0.1, 0.1, false, true, false, false); // reseta
        assert_eq!(d.chunks_qe_critico, 0);
        assert!(!d.parado);
    }
}

#[cfg(test)]
mod testes_v060 {
    use super::*;

    #[test]
    fn a1_tau_c_quantil() {
        let mut hub = TelemetryHub::novo(None);
        let mut aa = AutoAjustePensamento::novo(0.5, 4, 4);
        // confianças uniformes [0,1): quantil 0.8 ≈ 0.8
        let mut rng = crate::rng::RngDeterministico::novo(3);
        for _ in 0..200 {
            aa.confs_retidas.push(rng.uniforme());
        }
        let tau = aa.ajustar_tau_c(0.20, &mut hub);
        assert!((tau - 0.8).abs() < 0.06, "quantil ~0.8 (τ={tau:.3})");
        assert!(!hub.eventos_ajuste.is_empty(), "ajuste auditado");
    }

    #[test]
    fn a3_lema_do_acordo() {
        let mut hub = TelemetryHub::novo(None);
        let mut aa = AutoAjustePensamento::novo(0.5, 4, 2);
        // acordo alto (0.9) ⇒ p̂ ≈ (1+√0.8)/2 ≈ 0.947 ⇒ margem grande ⇒ N pequeno
        for _ in 0..32 {
            aa.acordos.push(0.9);
        }
        let n1 = aa.ajustar_n(0.05, 16, &mut hub);
        // p̂ = (1+√0.8)/2 ≈ 0.947 ⇒ N ≥ ln(20)/(2·0.447²) ≈ 7.5 ⇒ 8
        assert!(n1 <= 8, "acordo alto ⇒ N pequeno (N={n1})");
        // acordo ~0.5 ⇒ p̂ = 0.5 ⇒ N = máximo
        let mut aa2 = AutoAjustePensamento::novo(0.5, 4, 2);
        for _ in 0..32 {
            aa2.acordos.push(0.5);
        }
        let n2 = aa2.ajustar_n(0.05, 16, &mut hub);
        assert_eq!(n2, 16, "p̂=1/2 ⇒ Hoeffding exige N = teto");
    }

    #[test]
    fn a5_gate_somente_com_lcb_positivo() {
        let mut hub = TelemetryHub::novo(None);
        let mut aa = AutoAjustePensamento::novo(0.5, 4, 4);
        // ganhos estáveis positivos (n=64: 3º termo de Bernstein = 7·ln(40)/189 ≈ 0.14
        // ⇒ LCB = 0.30 − 0.14 > 0) ⇒ gate sobe
        for _ in 0..64 {
            aa.ajustar_gate(0.30, 0.05, &mut hub);
        }
        assert!(aa.gate_g > 0.0, "LCB positivo ⇒ gate sobe (g={})", aa.gate_g);
        // ganhos negativos ⇒ gate recua
        let mut aa2 = AutoAjustePensamento::novo(0.5, 4, 4);
        aa2.gate_g = 0.5;
        for _ in 0..64 {
            aa2.ajustar_gate(-0.30, 0.05, &mut hub);
        }
        assert!(aa2.gate_g < 0.5, "LCB negativo ⇒ gate recua (g={})", aa2.gate_g);
    }

    #[test]
    fn a4_ucb1_converge_para_o_melhor_braco() {
        let mut u = Ucb1::novo(vec!["a".into(), "b".into(), "c".into()]);
        let mut rng = crate::rng::RngDeterministico::novo(9);
        // braço 1 é o melhor (recompensa média 0.9 vs 0.3/0.5)
        for _ in 0..300 {
            let braco = u.escolher();
            let media = match braco { 0 => 0.3, 1 => 0.9, _ => 0.5 };
            let r = (media + rng.normal(0.0, 0.05) as f64).clamp(0.0, 1.0);
            u.registrar(braco, r);
        }
        assert_eq!(u.contagens.as_slice().position_max().unwrap(), 1, "UCB1 encontra o melhor braço");
        assert!(u.arrependimento_empirico() / 300.0 < 0.15, "arrependimento médio pequeno");
    }

    #[test]
    fn a2_orcamento_marginal() {
        let mut hub = TelemetryHub::novo(None);
        let mut aa = AutoAjustePensamento::novo(0.5, 8, 4);
        // ganhos decaindo geometricamente ρ ≈ 0.5
        for k in 0..12 {
            aa.ganhos_por_iteracao.push(0.2 * 0.5f64.powi(k));
        }
        let b = aa.ajustar_orcamento(0.01, &mut hub);
        assert!(b >= 1 && b <= 16, "B marginal finito (B={b})");
    }
}

/// Extensão: posição do máximo (estável para testes).
trait PosMax {
    fn position_max(&self) -> Option<usize>;
}

impl PosMax for [u64] {
    fn position_max(&self) -> Option<usize> {
        self.iter().enumerate().max_by_key(|(_, v)| **v).map(|(i, _)| i)
    }
}

#[cfg(test)]
mod testes_v090 {
    use super::*;

    /// T-TS2 (falsificação do arrependimento): bandit sintético de 3 braços
    /// com probabilidades conhecidas (0,9 / 0,6 / 0,3) — Thompson e UCB1
    /// devem AMBOS concentrar jogadas no melhor braço e ter arrependimento
    /// sublinear (bem abaixo do uniforme); Thompson com constante empírica
    /// não-pior que 2× a do UCB1 (gate direcional, não teórico).
    #[test]
    fn ts_vs_ucb1_arrependimento() {
        let ps = [0.9f64, 0.6, 0.3];
        let melhor = 0usize;
        let t_max = 4000u32;

        let mut ts = ThompsonSampling::novo(
            ps.iter().map(|p| format!("braço-{p}")).collect(),
            101,
        );
        let mut u = Ucb1::novo(ps.iter().map(|p| format!("braço-{p}")).collect());
        let mut rng = crate::rng::RngDeterministico::novo(202);
        let (mut soma_ts, mut soma_u, mut soma_unif) = (0.0f64, 0.0f64, 0.0f64);
        let (mut jogadas_ts_melhor, mut jogadas_u_melhor) = (0u32, 0u32);

        for t in 0..t_max {
            // Thompson
            let k = ts.escolher();
            let sucesso = rng.uniforme() < ps[k];
            ts.registrar(k, sucesso);
            soma_ts += (ps[melhor] - ps[k]) as f64;
            if k == melhor {
                jogadas_ts_melhor += 1;
            }
            // UCB1
            let k2 = u.escolher();
            let sucesso2 = rng.uniforme() < ps[k2];
            u.registrar(k2, if sucesso2 { 1.0 } else { 0.0 });
            soma_u += (ps[melhor] - ps[k2]) as f64;
            if k2 == melhor {
                jogadas_u_melhor += 1;
            }
            // uniforme (referência de nenhuma aprendizagem)
            let k3 = t as usize % ps.len();
            soma_unif += (ps[melhor] - ps[k3]) as f64;
        }
        // ambos aprendem: arrependimento médio despenca vs uniforme
        let (r_ts, r_u, r_unif) = (soma_ts / t_max as f64, soma_u / t_max as f64, soma_unif / t_max as f64);
        assert!(
            r_ts < 0.5 * r_unif && r_u < 0.5 * r_unif,
            "TS {r_ts} / UCB1 {r_u} devem aprender vs uniforme {r_unif}"
        );
        // gate direcional de eficiência: TS na mesma ordem de grandeza do UCB1
        assert!(
            r_ts < 2.5 * r_u.max(1e-9) + 0.02,
            "TS {r_ts} indignamente pior que UCB1 {r_u}"
        );
        // o melhor braço domina as jogadas em ambos
        assert!(
            jogadas_ts_melhor as f64 > 0.6 * t_max as f64,
            "TS jogou o melhor braço só {jogadas_ts_melhor}/{t_max}"
        );
        assert!(
            jogadas_u_melhor as f64 > 0.6 * t_max as f64,
            "UCB1 jogou o melhor braço só {jogadas_u_melhor}/{t_max}"
        );
        // posterior de TS aproxima as probabilidades verdadeiras
        let medias = ts.medias_posteriori();
        for (i, &p) in ps.iter().enumerate() {
            assert!((medias[i] - p).abs() < 0.15, "posterior[{i}]={:.3} ≠ {p}", medias[i]);
        }
    }

    /// Determinismo bit-a-bit da trajetória do Thompson (mesma semente).
    #[test]
    fn ts_deterministico() {
        let ps = [0.8f64, 0.4];
        let traj = |semente: u64| -> Vec<usize> {
            let mut ts = ThompsonSampling::novo(vec!["a".into(), "b".into()], semente);
            let mut rng = crate::rng::RngDeterministico::novo(semente + 1);
            (0..500)
                .map(|_| {
                    let k = ts.escolher();
                    ts.registrar(k, rng.uniforme() < ps[k]);
                    k
                })
                .collect()
        };
        assert_eq!(traj(7), traj(7), "mesma semente ⇒ mesma trajetória");
        assert_ne!(traj(7), traj(8), "sementes diferentes divergem");
    }

    /// Bernoulli-ização de recompensas contínuas: posterior converge para
    /// a média da recompensa (T-TS1.4 — a probabilidade de sucesso É a
    /// própria recompensa em expectativa).
    #[test]
    fn ts_recompensa_continua() {
        let mut ts = ThompsonSampling::novo(vec!["só".into()], 55);
        for i in 0..20_000 {
            // recompensa determinística 0,25 ⇒ posterior → 0,25
            ts.registrar_continuo(0, 0.25);
            let _ = i;
        }
        let m = ts.medias_posteriori()[0];
        assert!((m - 0.25).abs() < 0.02, "posterior {m} ≠ 0,25");
    }
}