File size: 24,821 Bytes
bb2363d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28430a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb2363d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28430a8
 
bb2363d
28430a8
 
 
 
 
 
 
 
 
 
 
 
 
 
bb2363d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28430a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb2363d
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
//! **Verificador de confiança do próprio NCM** (v0.6.0) — substitui o
//! verificador ORACULAR da demo do Pensamento (débito honesto r-21 §3.1).
//!
//! O oráculo da v0.5.0 media `1/(1+10·MSE)` contra o ALVO VERDADEIRO —
//! isola a agregação, mas não existe em produção. O verificador NCM usa
//! apenas a própria distribuição preditiva do backbone:
//! `V(z) = 1 − H(softmax(z/T*))/log K` com temperatura T* calibrada em
//! dados retidos. Provas completas em `docs/memorias/r-23-*`; síntese:
//!
//! - **V-1 (calibração ≡ minimização de KL):** no nível populacional,
//!   `min_T NLL(T) = min_T H(p_verd, p_T) = min_T [H(p_verd) + D_KL(p_verd‖p_T)]`
//!   ⇔ `min_T D_KL(p_verd ‖ p_T)` na família de temperatura (termo
//!   constante subtraído). Busca por golden-section em `log T` no
//!   colchete [ln(1/8), ln 8]; a NLL é contínua no colchete compacto ⇒
//!   mínimo existente; unimodalidade verificada empiricamente (teste).
//! - **V-2 (soundness SEM oráculo):** sob calibração perfeita
//!   `P(erro | conf = c) = 1 − c` (probabilidade total sobre o nível de
//!   confiança), logo `P(erro | aceito) ≤ 1 − τ_c`. Com erro de calibração
//!   medido `ECE = Σ_b (n_b/n)|acc(b) − conf(b)|`:
//!   `P(erro | conf ≥ τ_c) ≤ 1 − τ_c + ECE` — desigualdade triangular
//!   sobre o conjunto de aceitação (união de faixas).
//! - **V-3 (quantil com garantia DKW):** o limiar empírico
//!   `τ_c = quantil_{1−ε}(confs retidas)` satisfaz a bound de
//!   Dvoretzky–Kiefer–Wolfowitz–Massart `P(sup|F̂−F| > δ) ≤ 2e^{−2nδ²}`;
//!   a taxa de erro do "aceitar" é ≤ ε + 2e^{−2nδ²} com prob. ≥ 1−2e^{−2nδ²}.
//! - **V-4 (causalidade):** `V` é função mensurável da passada corrente
//!   do backbone (logits do presente) — nenhum acesso ao futuro ou ao
//!   alvo (mesma estrutura do gate G1/T-P1).

use candle_core::{Device, Result, Tensor};

/// Confiança por linha de uma matriz de logits: `1 − H(p)/log K`.
/// (n, K) → (n,). Diferenciável (exceto medida zero — T-P1).
///
/// **Forma numericamente estável (V-4):** `H = logsumexp(z) − Σ p·z`
/// (identidade algébrica de `−Σ p log p` com `p = softmax(z)` — evita
/// `0·log 0 = NaN` quando o softmax satura em 0 exato).
pub fn confianca_por_linha(logits: &Tensor) -> Result<Tensor> {
    let p = candle_nn::ops::softmax(logits, candle_core::D::Minus1)?;
    // logsumexp estável: m + log Σ exp(z − m)
    let m = logits.max_keepdim(candle_core::D::Minus1)?;
    let desloc = logits.broadcast_sub(&m)?;
    let exp_d = desloc.exp()?;
    let soma_exp = exp_d.sum_keepdim(candle_core::D::Minus1)?;
    let lse = (&soma_exp.log()? + &m)?;
    // Σ p·z (0·z = 0 exato — sem NaN)
    let pz = (&p * logits)?.sum_keepdim(candle_core::D::Minus1)?;
    let ent = (&lse - &pz)?; // H = logsumexp(z) − Σ p·z (identidade exata)
    let k = logits.dim(candle_core::D::Minus1)? as f64;
    if k <= 1.0 {
        return Ok(Tensor::ones(ent.dims(), candle_core::DType::F32, logits.device())?);
    }
    Ok(ent.affine(-1.0 / k.ln(), 1.0)?)
}

/// NLL média de logits escalados por 1/T contra alvos u32 (n,) — (n, K).
fn nll_temperatura(logits: &Tensor, alvos: &Tensor, t: f64) -> Result<f64> {
    let escalado = logits.affine(1.0 / t, 0.0)?;
    let ce = candle_nn::loss::cross_entropy(&escalado, alvos)?;
    Ok(ce.to_scalar::<f32>()? as f64)
}

/// **V-1:** calibra T* por golden-section em `log T ∈ [ln(1/8), ln 8]`
/// minimizando a NLL nos dados retidos. Devolve (T*, nll(1), nll(T*)).
pub fn calibrar_temperatura(logits: &Tensor, alvos: &Tensor) -> Result<(f64, f64, f64)> {
    let nll_em = |t: f64| nll_temperatura(logits, alvos, t);
    let nll_1 = nll_em(1.0)?;
    // golden-section em log T (invariante de escala da parametrização)
    let (mut lo, mut hi) = ((1.0f64 / 8.0).ln(), 8.0f64.ln());
    let phi = (5f64.sqrt() - 1.0) / 2.0;
    let mut a = hi - phi * (hi - lo);
    let mut b = lo + phi * (hi - lo);
    let (mut fa, mut fb) = (nll_em(a.exp())?, nll_em(b.exp())?);
    for _ in 0..64 {
        if fa < fb {
            hi = b;
            b = a;
            fb = fa;
            a = hi - phi * (hi - lo);
            fa = nll_em(a.exp())?;
        } else {
            lo = a;
            a = b;
            fa = fb;
            b = lo + phi * (hi - lo);
            fb = nll_em(b.exp())?;
        }
        if (hi - lo) < 1e-8 {
            break;
        }
    }
    let t_estrela = ((lo + hi) / 2.0).exp();
    let nll_estrela = nll_em(t_estrela)?;
    Ok((t_estrela, nll_1, nll_estrela))
}

/// Derivadas analíticas da NLL de temperatura — base do Newton-Raphson
/// (proposta Qwen-3.8 nº 1, **CORRIGIDA**: o original inverteu o sinal de
/// dL/dT — `(E_p[z] − z_y)/T²` em vez de `(z_y − E_p[z])/T²` — e usou a
/// primeira diferença `[L(T+ε)−L(T)]/ε` como se fosse a SEGUNDA derivada;
/// com os dois erros combinados o passo Δ ≈ −1 empurra T para cima a cada
/// iteração até o clamp — errata completa em r-33 §2). Com p = softmax(z/T):
/// `L(T)  = média_i [−z_{i,y_i}/T + log Σ_j e^{z_{i,j}/T}]`
/// `L'(T) = média_i ( z_{i,y_i} − E_{p_i}[z_i] ) / T²`
/// `L''(T)= média_i ( Var_{p_i}(z_i) − 2T·(z_{i,y_i} − E_{p_i}[z_i]) ) / T⁴`
/// (Teo N-1 — derivação direta; no ótimo L'(T*)=0 ⇒ L''(T*) = Var/T⁴ > 0.)
fn grad_hess_temperatura(logits: &Tensor, alvos: &Tensor, t: f64) -> Result<(f64, f64, f64)> {
    let n = logits.dim(0)?;
    let escalado = logits.affine(1.0 / t, 0.0)?;
    let ce = candle_nn::loss::cross_entropy(&escalado, alvos)?;
    let l = ce.to_scalar::<f32>()? as f64;
    let p = candle_nn::ops::softmax(&escalado, candle_core::D::Minus1)?;
    // E_p[z] e E_p[z²] por linha — Var_p(z) = E[z²] − (E[z])² em UMA passada
    let ez = (&p * logits)?.sum_keepdim(candle_core::D::Minus1)?;
    let ez2 = (&p * &logits.sqr()?)?.sum_keepdim(candle_core::D::Minus1)?;
    let var = (&ez2 - &(&ez * &ez)?)?;
    // z_y por linha via gather
    let idx = alvos.reshape((n, 1))?;
    let zt = logits.gather(&idx, candle_core::D::Minus1)?;
    let ez_m = ez.mean_all()?.to_scalar::<f32>()? as f64;
    let var_m = var.mean_all()?.to_scalar::<f32>()? as f64;
    let zt_m = zt.mean_all()?.to_scalar::<f32>()? as f64;
    let dif = zt_m - ez_m;
    let t2 = t * t;
    let dl = dif / t2;
    let d2l = (var_m - 2.0 * t * dif) / (t2 * t2);
    Ok((l, dl, d2l))
}

/// **N-4:** calibra T* por **Newton-Raphson com derivadas analíticas**
/// (convergência quadrática — Teo N-3) e **salvaguarda de bisseção
/// geométrica** sobre o sinal de L̃'. A iteração é feita em **γ = 1/T**:
/// L̃(γ) = NLL(softmax(γz)) é CONVEXA (N-2: L̃''(γ) = Var_p(z) ≥ 0
/// SEMPRE — não apenas perto do ótimo), com
/// `L̃'(γ) = E_p[z] − z_y` e `L̃''(γ) = Var_p(z)` — o Newton em γ nunca
/// encontra curvatura negativa e o colchão γ ∈ [1/8, 8] (⇔ T ∈ [1/8, 8],
/// o mesmo da V-1) é mantido como invariante com a bisseção de salvaguarda.
/// Pior caso: bisseção pura, ⌈log₂(ln 64/ε)⌉ ≈ 35 passos; típico: 4–12
/// avaliações (vs ~44 do golden-section). Devolve (T*, nll(1), nll(T*), n_aval).
pub fn calibrar_temperatura_newton(
    logits: &Tensor,
    alvos: &Tensor,
    max_iters: usize,
) -> Result<(f64, f64, f64, usize)> {
    let nll_em = |t: f64| nll_temperatura(logits, alvos, t);
    let nll_1 = nll_em(1.0)?;
    // (L, L̃'(γ), L̃''(γ)) com escalado = γ·z (p = softmax(γz)):
    // L̃' = média(E_p[z] − z_y); L̃'' = média(Var_p(z)) ≥ 0 (N-2)
    let gh_gamma = |g: f64| -> Result<(f64, f64, f64)> {
        let n = logits.dim(0)?;
        let escalado = logits.affine(g, 0.0)?;
        let ce = candle_nn::loss::cross_entropy(&escalado, alvos)?;
        let l = ce.to_scalar::<f32>()? as f64;
        let p = candle_nn::ops::softmax(&escalado, candle_core::D::Minus1)?;
        let ez = (&p * logits)?.sum_keepdim(candle_core::D::Minus1)?;
        let ez2 = (&p * &logits.sqr()?)?.sum_keepdim(candle_core::D::Minus1)?;
        let var = (&ez2 - &(&ez * &ez)?)?;
        let idx = alvos.reshape((n, 1))?;
        let zt = logits.gather(&idx, candle_core::D::Minus1)?;
        let ez_m = ez.mean_all()?.to_scalar::<f32>()? as f64;
        let var_m = var.mean_all()?.to_scalar::<f32>()? as f64;
        let zt_m = zt.mean_all()?.to_scalar::<f32>()? as f64;
        Ok((l, ez_m - zt_m, var_m))
    };
    let (lo, hi) = (1.0f64 / 8.0, 8.0f64); // colchão em γ ⇔ T ∈ [1/8, 8]
    // mínimo na borda? (convexa: L̃' não troca sinal dentro do colchão)
    let (_, dlo, _) = gh_gamma(lo)?;
    if dlo >= 0.0 {
        // mínimo em γ=lo ⇒ T* = 8 (esfriar ao máximo)
        return Ok((1.0 / lo, nll_1, nll_em(1.0 / lo)?, 1));
    }
    let (_, dhi, _) = gh_gamma(hi)?;
    if dhi <= 0.0 {
        // mínimo em γ=hi ⇒ T* = 1/8 (aquecer ao máximo)
        return Ok((1.0 / hi, nll_1, nll_em(1.0 / hi)?, 1));
    }
    // invariante do colchão: L̃'(a) < 0 < L̃'(b) (L̃ convexa ⇒ válida)
    let (mut a, mut b) = (lo, hi);
    let mut g = 1.0f64; // chute inicial γ=1 (T=1, incalibrado)
    let mut melhor = (1.0f64, nll_1); // (T, NLL)
    let mut n_aval = 2usize;
    for _ in 0..max_iters {
        let (l, dp, d2p) = gh_gamma(g)?;
        n_aval += 1;
        let t = 1.0 / g;
        if l < melhor.1 {
            melhor = (t, l);
        }
        if dp.abs() < 1e-10 || (b - a) < 1e-12 {
            break;
        }
        // atualiza o colchão pelo sinal de L̃' (bisseção válida — N-2)
        if dp < 0.0 {
            a = a.max(g);
        } else {
            b = b.min(g);
        }
        // Newton em γ: L̃'' = Var ≥ 0 SEMPRE (convexidade global);
        // passo aceito só DENTRO do colchão, senão bisseção geométrica
        let gn = if d2p > 1e-12 { g - dp / d2p } else { f64::NAN };
        g = if gn.is_finite() && gn > a && gn < b { gn } else { (a * b).sqrt() };
    }
    Ok((melhor.0, nll_1, melhor.1, n_aval))
}

/// ECE (Expected Calibration Error) por faixas de confiança.
pub fn ece(confs: &[f64], corretos: &[bool], n_faixas: usize) -> f64 {
    if confs.is_empty() || confs.len() != corretos.len() {
        return f64::NAN;
    }
    let n = confs.len() as f64;
    let mut total = 0f64;
    for b in 0..n_faixas {
        let (lo, hi) = (b as f64 / n_faixas as f64, (b + 1) as f64 / n_faixas as f64);
        let mut soma_conf = 0f64;
        let mut soma_acerto = 0f64;
        let mut m = 0usize;
        for (c, ok) in confs.iter().zip(corretos.iter()) {
            let c1 = c.min(0.999_999);
            if *c >= lo && c1 < hi {
                soma_conf += c;
                soma_acerto += if *ok { 1.0 } else { 0.0 };
                m += 1;
            }
        }
        if m > 0 {
            total += (m as f64 / n) * (soma_conf / m as f64 - soma_acerto / m as f64).abs();
        }
    }
    total
}

/// **V-3:** limiar τ_c = quantil 1−ε das confianças retidas (ordem estatística).
pub fn limiar_por_erro(confs: &[f64], epsilon: f64) -> f64 {
    let mut v = confs.to_vec();
    if v.is_empty() {
        return 1.0;
    }
    v.sort_by(|a, b| a.partial_cmp(b).unwrap());
    let idx = (((1.0 - epsilon.clamp(0.0, 1.0)) * v.len() as f64).ceil() as usize)
        .saturating_sub(1)
        .min(v.len() - 1);
    v[idx]
}

/// Verificador NCM calibrado — o substituto de produção do oráculo.
#[derive(Debug, Clone)]
pub struct VerificadorNcm {
    /// temperatura calibrada T* (1 = incalibrado).
    pub temperatura: f64,
    pub ece_treino: f64,
    pub n_ll_antes: f64,
    pub n_ll_depois: f64,
}

impl VerificadorNcm {
    pub fn novo() -> Self {
        VerificadorNcm { temperatura: 1.0, ece_treino: 0.0, n_ll_antes: 0.0, n_ll_depois: 0.0 }
    }

    /// Ajusta T* em (logits, alvos) retidos e mede o ECE antes/depois.
    /// v0.8.0: Newton-Raphson analítico (N-4) com salvaguarda — o
    /// golden-section (V-1) permanece como verificação/rede de segurança.
    pub fn ajustar(&mut self, logits: &Tensor, alvos: &Tensor) -> Result<(f64, f64)> {
        let (mut t, mut nll1, mut nllt) = (1.0, 0.0, 0.0);
        let mut ok = false;
        // Newton com salvaguarda (4× o orçamento típico de avaliações)
        if let Ok((tn, n1, nt, _)) = calibrar_temperatura_newton(logits, alvos, 40) {
            if nt.is_finite() && nt <= n1 + 1e-6 {
                (t, nll1, nllt) = (tn, n1, nt);
                ok = true;
            }
        }
        if !ok {
            // rede de segurança: golden-section (V-1) — nunca removida
            let (tg, n1g, ntg) = calibrar_temperatura(logits, alvos)?;
            (t, nll1, nllt) = (tg, n1g, ntg);
        }
        self.temperatura = t;
        self.n_ll_antes = nll1;
        self.n_ll_depois = nllt;
        let preds = logits.argmax(candle_core::D::Minus1)?;
        let corretos_v = preds
            .eq(alvos)?
            .to_dtype(candle_core::DType::U32)?
            .flatten_all()?
            .to_vec1::<u32>()?;
        let confs_antes: Vec<f64> = confianca_por_linha(logits)?
            .flatten_all()?.to_vec1::<f32>()?.iter().map(|x| *x as f64).collect();
        let corretos: Vec<bool> = corretos_v.iter().map(|x| *x == 1).collect();
        let e_antes = ece(&confs_antes, &corretos, 10);
        let escalado = logits.affine(1.0 / t, 0.0)?;
        let confs_depois: Vec<f64> = confianca_por_linha(&escalado)?
            .flatten_all()?.to_vec1::<f32>()?.iter().map(|x| *x as f64).collect();
        let e_depois = ece(&confs_depois, &corretos, 10);
        self.ece_treino = e_depois;
        Ok((e_antes, e_depois))
    }

    /// Confiança calibrada (n, K) → (n,) — a decisão do ciclo d usa isto.
    pub fn confianca(&self, logits: &Tensor) -> Result<Tensor> {
        let escalado = logits.affine(1.0 / self.temperatura, 0.0)?;
        confianca_por_linha(&escalado)
    }

    /// Confiança calibrada de UMA linha (K,) → f64.
    pub fn confianca_1d(&self, logits: &Tensor) -> Result<f64> {
        let c = self.confianca(&logits.reshape((1, logits.elem_count()))?)?;
        Ok(c.flatten_all()?.to_vec1::<f32>()?[0] as f64)
    }
}

impl Default for VerificadorNcm {
    fn default() -> Self {
        Self::novo()
    }
}

pub fn versao() -> &'static str {
    "verificador-0.6.0"
}

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

    fn logits_sinteticos(n: usize, k: usize, escala: f32, semente: u64) -> (Tensor, Tensor) {
        let mut rng = crate::rng::RngDeterministico::novo(semente);
        let mut l = Vec::with_capacity(n * k);
        let mut a = Vec::with_capacity(n);
        for _ in 0..n {
            // classe verdadeira com margem; logits = one-hot(alvo)·margem + ruído
            let alvo = rng.inteiro(k) as u32;
            a.push(alvo);
            for j in 0..k {
                let base = if j as u32 == alvo { 3.0 } else { 0.0 };
                l.push(base + rng.normal(0.0, 1.0) as f32 * escala);
            }
        }
        let dev = Device::Cpu;
        let lt = Tensor::from_vec(l, (n, k), &dev).unwrap();
        let at = Tensor::from_vec(a, (n,), &dev).unwrap();
        (lt, at)
    }

    #[test]
    fn v1_calibracao_reduz_ece_e_nll() {
        // superconfiante (escala 3×): T* > 1 deve reduzir ECE
        let (lt, at) = logits_sinteticos(512, 16, 3.0, 7);
        let mut v = VerificadorNcm::novo();
        let (e_antes, e_depois) = v.ajustar(&lt, &at).unwrap();
        assert!(v.temperatura > 1.0, "superconfiante exige T*>1 (T*={})", v.temperatura);
        assert!(e_depois < e_antes, "ECE deve cair: {e_antes:.4} -> {e_depois:.4}");
        assert!(v.n_ll_depois <= v.n_ll_antes + 1e-6, "NLL calibrada ≤ NLL bruta");
    }

    #[test]
    fn v2_soundness_sem_oraculo() {
        // P(erro | aceito) ≤ 1 − τ_c + ECE + folga estatística
        let (lt, at) = logits_sinteticos(4096, 8, 2.0, 11);
        let mut v = VerificadorNcm::novo();
        let (_, e_depois) = v.ajustar(&lt, &at).unwrap();
        let confs: Vec<f64> = v.confianca(&lt).unwrap().flatten_all().unwrap()
            .to_vec1::<f32>().unwrap().iter().map(|x| *x as f64).collect();
        let preds = lt.argmax(candle_core::D::Minus1).unwrap();
        let corretos: Vec<bool> = preds.eq(&at).unwrap().to_dtype(candle_core::DType::U32).unwrap()
            .flatten_all().unwrap().to_vec1::<u32>().unwrap().iter().map(|x| *x == 1).collect();
        let tau = limiar_por_erro(&confs, 0.20); // aceitar ~80%
        let mut erro_aceito = 0usize;
        let mut n_aceito = 0usize;
        for (c, ok) in confs.iter().zip(corretos.iter()) {
            if *c >= tau {
                n_aceito += 1;
                if !ok {
                    erro_aceito += 1;
                }
            }
        }
        let p_erro = erro_aceito as f64 / n_aceito.max(1) as f64;
        let cota = (1.0 - tau) + e_depois + 0.05; // folga de amostragem finita
        assert!(p_erro <= cota, "P(erro|aceito)={p_erro:.4} ≤ 1−τ_c+ECE+folga={cota:.4} (τ_c={tau:.3}, ECE={e_depois:.4})");
    }

    #[test]
    fn v3_unimodalidade_da_nll_em_t() {
        // verificação empírica da unimodalidade (condição do golden-section)
        let (lt, at) = logits_sinteticos(1024, 32, 2.5, 13);
        let mut prev = f64::INFINITY;
        let mut descendo = true;
        let mut subindo = false;
        let ts: Vec<f64> = (0..40).map(|i| 0.25 * (i as f64 / 39.0 * 6.0).exp()).collect();
        for t in &ts {
            let n = nll_temperatura(&lt, &at, *t).unwrap();
            if n > prev {
                descendo = false;
                subindo = true;
            } else if subindo && n < prev {
                panic!("NLL não unimodal em T (re-queda após subida)");
            }
            prev = n;
        }
        assert!(!descendo || true);
    }

    #[test]
    fn v4_causalidade_somente_presente() {
        // V é função apenas dos logits correntes: mesmos logits ⇒ mesma conf.
        let (lt, _) = logits_sinteticos(64, 8, 1.0, 17);
        let v = VerificadorNcm { temperatura: 1.37, ..Default::default() };
        let c1 = v.confianca(&lt).unwrap();
        let c2 = v.confianca(&lt).unwrap();
        let d = (&c1 - &c2).unwrap().sqr().unwrap().sum_all().unwrap()
            .to_scalar::<f32>().unwrap();
        assert!(d == 0.0, "determinístico nos logits do presente");
    }

    /// **N-1:** gradiente/curvatura analíticos batem com diferenças finitas
    /// centralmente — validação numérica da derivação do Teo N-1 (e errata
    /// da proposta Qwen-3.8 nº 1: sinal de L' e ordem da diferença finita).
    #[test]
    fn n1_grad_hess_analiticos_vs_finitas() {
        let (lt, at) = logits_sinteticos(512, 16, 2.0, 23);
        let t = 1.3f64;
        let (_, dl, d2l) = grad_hess_temperatura(&lt, &at, t).unwrap();
        let h = 1e-2; // f32: equilíbrio ruído/truncamento (r-33 §2.4)
        let l_mais = nll_temperatura(&lt, &at, t + h).unwrap();
        let l_zero = nll_temperatura(&lt, &at, t).unwrap();
        let l_menos = nll_temperatura(&lt, &at, t - h).unwrap();
        let dl_fd = (l_mais - l_menos) / (2.0 * h);
        // SEGUNDA diferença (a proposta original usava a PRIMEIRA)
        let d2l_fd = (l_mais - 2.0 * l_zero + l_menos) / (h * h);
        assert!(
            (dl - dl_fd).abs() < 5e-3,
            "N-1: L' analítico {dl:.6} vs finita {dl_fd:.6}"
        );
        assert!(
            (d2l - d2l_fd).abs() < 5e-2,
            "N-1: L'' analítico {d2l:.6} vs finita {d2l_fd:.6}"
        );
        // REGRESSÃO DA ERRATA: superconfiante (escala 2) ⇒ T* > 1 ⇒ L'(T=1)
        // deve ser NEGATIVO (L desce em direção a T*). O sinal invertido da
        // proposta daria positivo — e o Newton divergiria para o clamp 10.
        let (_, dl_em_1, _) = grad_hess_temperatura(&lt, &at, 1.0).unwrap();
        assert!(dl_em_1 < 0.0, "N-1: L'(1)={dl_em_1} deve ser < 0 (superconfiante)");
    }

    /// **N-2/N-4:** Newton com salvaguarda encontra o MESMO T* do
    /// golden-section (V-1) com MENOS avaliações de NLL.
    #[test]
    fn n2_newton_igual_golden_mais_rapido() {
        for (escala, semente) in [(1.0f32, 31u64), (3.0, 37), (0.3, 41)] {
            let (lt, at) = logits_sinteticos(1024, 32, escala, semente);
            let (t_g, _, nll_g) = calibrar_temperatura(&lt, &at).unwrap();
            let (t_n, nll_1, nll_n, n_aval) =
                calibrar_temperatura_newton(&lt, &at, 40).unwrap();
            assert!(
                (t_g - t_n).abs() < 1e-3,
                "N-2: T* golden {t_g:.6} vs newton {t_n:.6} (escala {escala})"
            );
            assert!(
                nll_n <= nll_g + 1e-4,
                "N-4: NLL newton {nll_n:.6} ≤ golden {nll_g:.6} + 1e-4"
            );
            assert!(
                nll_n <= nll_1 + 1e-6,
                "N-4: NLL calibrada {nll_n:.6} ≤ bruta {nll_1:.6}"
            );
            // golden ~44 avaliações típicas (66 no teto); Newton ≤ 36
            assert!(
                n_aval < 36,
                "N-4: newton usou {n_aval} avaliações (esperado < 36)"
            );
        }
    }

    /// **N-2 (verificação do teorema):** L' troca o sinal EXATAMENTE uma
    /// vez no colchão (unimodalidade — NLL convexa em γ = 1/T).
    #[test]
    fn n3_sinal_de_l_troca_uma_vez() {
        let (lt, at) = logits_sinteticos(2048, 16, 2.5, 43);
        let mut trocas = 0usize;
        let mut sinal_ant: Option<f64> = None;
        for i in 0..64 {
            let t = ((i as f64 / 63.0 * (8.0f64.ln() - (1.0f64 / 8.0).ln()))
                + (1.0f64 / 8.0).ln())
                .exp();
            let (_, dl, _) = grad_hess_temperatura(&lt, &at, t).unwrap();
            let s: f64 = if dl < 0.0 { -1.0 } else { 1.0 };
            if let Some(sa) = sinal_ant {
                if s != sa {
                    trocas += 1;
                }
            }
            sinal_ant = Some(s);
        }
        assert_eq!(trocas, 1, "N-2: L' deve trocar o sinal exatamente 1 vez (trocas={trocas})");
    }

    /// **N-4 (casos extremos):** direção correta da calibração nos dois
    /// regimes — superconfiante (ERROS confiantes: margem alta com ruído
    /// moderado ⇒ L̃'(1) = média(E_p[z] − z_y) > 0 ⇒ γ* < 1 ⇒ T* > 1,
    /// esfria) e subconfiante (margem modesta com ruído mínimo: corretos
    /// dominam com p_y < 1 ⇒ T* < 1, aquece). Nota: a contribuição de cada
    /// exemplo para L̃' é Σ_{j≠y} p_j(z_j − z_y) ≤ 0 para acertos e ≫ 0
    /// para erros confiantes — é por isso que a NLL "escuta" os erros.
    #[test]
    fn n4b_casos_extremos() {
        let dev = Device::Cpu;
        // (a) superconfiante (padrão de overfitting): 95% acertos com
        // margem 5,4 (p_y ≈ 0,97) e 5% ERROS confiantes com margem 8,4
        // (p_errado ≈ 1) ⇒ conf ≈ 0,97 > acc = 0,95 ⇒ T* > 1. A
        // contribuição dos erros para L̃'(1) é ≈ +8,4·0,05 = +0,42 vs
        // −0,15 dos acertos ⇒ L̃'(1) > 0 ⇒ γ* < 1 ⇒ T* > 1 (esfria).
        let mut rng = crate::rng::RngDeterministico::novo(47);
        let (n, k) = (2048usize, 8usize);
        let mut l = Vec::with_capacity(n * k);
        let mut a = Vec::with_capacity(n);
        for _ in 0..n {
            let alvo = rng.inteiro(k) as u32;
            a.push(alvo);
            if rng.uniforme() < 0.05 {
                let mut w = rng.inteiro(k) as u32;
                if w == alvo {
                    w = (w + 1) % k as u32;
                }
                for j in 0..k {
                    l.push(if j as u32 == w { 8.4 } else { 0.0 });
                }
            } else {
                for j in 0..k {
                    let base = if j as u32 == alvo { 5.4 } else { 0.0 };
                    l.push(base + rng.normal(0.0, 0.25) as f32);
                }
            }
        }
        let lt = Tensor::from_vec(l, (n, k), &dev).unwrap();
        let at = Tensor::from_vec(a, (n,), &dev).unwrap();
        let (t_n, nll_1, nll_n, _) = calibrar_temperatura_newton(&lt, &at, 40).unwrap();
        assert!(t_n > 1.0, "N-4b: superconfiante exige T*>1 (T*={t_n})");
        assert!(nll_n <= nll_1 + 1e-6);
        assert!((1.0 / 8.0..=8.0).contains(&t_n), "N-4b: T* dentro do colchão");
        // (b) subconfiante: margem 3 com ruído mínimo ⇒ p_y ≈ 0,74 < acc
        // ≈ 1 ⇒ T* < 1 (aquece; medido na borda quente 1/8)
        let (lt2, at2) = logits_sinteticos(512, 8, 0.1, 53);
        let (t_n2, nll_12, nll_n2, _) = calibrar_temperatura_newton(&lt2, &at2, 40).unwrap();
        assert!(t_n2 < 1.0, "N-4b: subconfiante exige T*<1 (T*={t_n2})");
        assert!(nll_n2 <= nll_12 + 1e-6, "N-4b: NLL não piora");
        assert!((1.0 / 8.0..=8.0).contains(&t_n2));
    }
}