gam-sae 0.3.153

Sparse-autoencoder latent-manifold terms for the gam penalized-likelihood engine
Documentation
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
//! #2111 INTEGRATED dense-torus acceptance test — the end-to-end bar the issue
//! specifies, closing the gap between the ISA producer / pair-κ machinery (unit-
//! tested) and the full birth pipeline (`fit_stagewise`).
//!
//! FIXTURE. A dense product-of-circles torus: `k = 6` circles on DISJOINT
//! axis-aligned output frames (circle `c` on dims `(2c, 2c+1)`), every row on
//! EVERY circle (density 6 — the degenerate regime where single-plane ring-ness
//! fails and only 4th-order ISA separates the factors), distinct amplitudes
//! `1.0 … 0.55`, independent angles, small isotropic noise. This is the
//! `probe_2101_birth_locus_disjoint_6circle_ordered_beta_bernoulli` structure at a sample size
//! (`n = 700`) clear of the dense-case small-sample floor (`n ≥ 300`; the
//! gated-edge `ISA_SUBSAMPLE_FLOOR` resolution bound concerns `q → ½` gates,
//! not this density-1 fixture).
//!
//! PIPELINE. Seed a single K=1 circle atom on circle 0's true coordinate, then run
//! the integrated forward-birth + backfit engine [`fit_stagewise`]. On a disjoint
//! residual the shared-factor model is rank-0, so births fall through to the ISA
//! fallback seed (`residual_principal_birth_candidate` → `isa_extract_certified_plane`)
//! — the exact machinery #2111 specifies. The seed contributes circle 0; the ISA
//! births must recover circles 1–5 from the blended dense residual.
//!
//! ACCEPTANCE BAR (#2111). 6 born atoms; every atom's decoder output-plane matches
//! a distinct true circle at overlap ≥ 0.9; every decoder is clean (singular-value
//! participation ratio ≤ 3 — a rank-2 circle decoder has PR ≈ 2); `n_distinct = 6`,
//! `n_clean = 6` (best overlap ≥ 0.9 AND second-best ≤ 0.2); and the forward phase
//! exits NATURALLY (`stopped_reason != MaxBirths`).
//!
//! If the bar is not met, the printed per-atom overlap / PR table + birth ledger
//! localise WHICH stage drops the ball (ISA rotation, birth acceptance, or joint
//! backfit) — the failure mode is the finding.

// `manifold/mod.rs` declares this module as
// `#[cfg(test)] mod tests_2111_dense_torus_acceptance;` — its single declaration. Saying so in-file
// makes the test scope a claim the compiler enforces rather than one the
// filename merely implies, which is what puts the fixture helpers below in
// the same scope as the `#[test]` fns they serve.
#![cfg(test)]

use crate::manifold::{
    AssignmentMode, BirthCandidateDecision, BirthRejection, PeriodicHarmonicEvaluator,
    SaeAssignment, SaeAtomBasisKind, SaeBasisEvaluator, SaeManifoldAtom, SaeManifoldRho,
    SaeManifoldTerm, StagewiseConfig, StagewiseStop, fit_stagewise,
};
use gam_linalg::faer_ndarray::FaerSvd;
use gam_terms::latent::LatentManifold;
use ndarray::{Array1, Array2, ArrayView2};
use std::sync::Arc;

fn lcg(s: &mut u64) -> f64 {
    *s = s
        .wrapping_mul(6364136223846793005)
        .wrapping_add(1442695040888963407);
    ((*s >> 11) as f64) / ((1u64 << 53) as f64)
}
fn lcg_normal(s: &mut u64) -> f64 {
    let u1 = lcg(s).max(1e-12);
    let u2 = lcg(s);
    (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
}

/// Dense product-of-circles torus target (`n × p`) + the true axis-aligned circle
/// planes. Circle `c` lives on dims `(2c, 2c+1)` with amplitude `amps[c]` and an
/// independent per-row angle; every row carries every circle (dense).
fn dense_torus(
    n: usize,
    p: usize,
    k: usize,
    amps: &[f64],
    sigma: f64,
    seed: u64,
) -> (Array2<f64>, Vec<Array2<f64>>, Vec<Array2<f64>>) {
    assert!(p >= 2 * k && amps.len() == k);
    let mut s = seed;
    let mut data = Array2::<f64>::zeros((n, p));
    // Per-circle true per-row angle (turns) — kept to seed circle 0 and to sanity
    // the fixture; the fitter never sees the angles of circles 1..k.
    let mut turns = vec![Array2::<f64>::zeros((n, 1)); k];
    for i in 0..n {
        for c in 0..k {
            let t = lcg(&mut s);
            turns[c][[i, 0]] = t;
            let ang = std::f64::consts::TAU * t;
            data[[i, 2 * c]] += amps[c] * ang.cos();
            data[[i, 2 * c + 1]] += amps[c] * ang.sin();
        }
        for j in 0..p {
            data[[i, j]] += sigma * lcg_normal(&mut s);
        }
    }
    let true_planes: Vec<Array2<f64>> = (0..k)
        .map(|c| {
            Array2::from_shape_fn(
                (p, 2),
                |(row, col)| {
                    if row == 2 * c + col { 1.0 } else { 0.0 }
                },
            )
        })
        .collect();
    (data, true_planes, turns)
}

/// Top-2 right-singular (output-space) plane of a decoder `D` (`m × p`): the two
/// leading rows of `Vᵀ` transposed into a `(p, 2)` orthonormal basis. This is the
/// ambient 2-plane the atom reconstructs into.
fn decoder_output_plane(decoder: &Array2<f64>) -> Array2<f64> {
    let (_, _, vt) = decoder
        .svd(false, true)
        .expect("decoder svd for output plane");
    let vt = vt.expect("vt present");
    let p = decoder.ncols();
    Array2::from_shape_fn((p, 2), |(row, col)| vt[[col, row]])
}

/// Singular-value participation ratio `PR = (Σσ²)² / Σσ⁴` of a decoder. A clean
/// rank-2 circle decoder (two comparable singular values, the rest ~0) gives
/// PR ≈ 2; a blended / higher-rank decoder gives PR > 3.
fn decoder_sv_pr(decoder: &Array2<f64>) -> f64 {
    let (_, sv, _) = decoder.svd(false, false).expect("decoder svd for PR");
    let s2: Vec<f64> = sv.iter().map(|&s| s * s).collect();
    let num: f64 = s2.iter().sum::<f64>().powi(2);
    let den: f64 = s2.iter().map(|v| v * v).sum::<f64>();
    if den > 0.0 { num / den } else { 0.0 }
}

/// Subspace affinity `‖UᵀV‖²_F / 2 ∈ [0, 1]` between two orthonormal `(p, 2)`
/// planes (1 = identical plane).
fn plane_overlap(u: &Array2<f64>, v: &Array2<f64>) -> f64 {
    let m = u.t().dot(v);
    m.iter().map(|x| x * x).sum::<f64>() / 2.0
}

/// A degree-1 periodic circle atom whose decoder plants cos→`dir_a`, sin→`dir_b`.
fn circle_atom(
    name: &str,
    evaluator: &Arc<PeriodicHarmonicEvaluator>,
    coords: &Array2<f64>,
    dir_a: usize,
    dir_b: usize,
    p: usize,
) -> SaeManifoldAtom {
    let (phi, jet) = evaluator.evaluate(coords.view()).unwrap();
    let mut decoder = Array2::<f64>::zeros((3, p));
    decoder[[1, dir_a % p]] = 1.0;
    decoder[[2, dir_b % p]] = 1.0;
    SaeManifoldAtom::new_with_provided_function_gram(
        name.to_string(),
        SaeAtomBasisKind::Periodic,
        1,
        phi,
        jet,
        decoder,
        Array2::<f64>::eye(3),
    )
    .unwrap()
    .with_basis_second_jet(evaluator.clone())
}

/// K=1 seed term: circle 0 on its true coordinate, active on every row.
fn seed_term(coords0: &Array2<f64>, p: usize) -> (SaeManifoldTerm, SaeManifoldRho) {
    let evaluator = Arc::new(PeriodicHarmonicEvaluator::new(3).unwrap());
    let atom = circle_atom("seed_c0", &evaluator, coords0, 0, 1, p);
    let n = coords0.nrows();
    let logits = Array2::<f64>::from_elem((n, 1), 6.0);
    // ordered Beta--Bernoulli (independent per-atom Bernoulli) assignment — NOT softmax. The dense
    // torus has every row co-active on EVERY circle; softmax forces per-row
    // competition (probabilities sum to 1) and its entropy sparsity prior
    // maximally penalises exactly the dense co-activation the fixture requires,
    // so no born circle can clear the frozen-ρ evidence gate. This matches the
    // `probe_2101_birth_locus_disjoint_6circle_ordered_beta_bernoulli` structure this test
    // reproduces (see the module docstring) and every sibling circle-recovery
    // fixture (#2027/#2089/#2101), all of which seed `ordered_beta_bernoulli(0.7, 1.0, false)`.
    let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
        logits,
        vec![coords0.clone()],
        vec![LatentManifold::Circle { period: 1.0 }],
        AssignmentMode::ordered_beta_bernoulli(0.7, 1.0, false),
    )
    .unwrap();
    let mut term = SaeManifoldTerm::new(vec![atom], assignment).unwrap();
    term.set_guards_enabled(false);
    // Charge births on the occupancy-aware BIC rank scale (½·d_eff·ln N_eff),
    // not the raw per-row coordinate-block Laplace log-det ½log|H_tt|. At n=700
    // the raw coordinate log-det grows ≈ O(n) per born atom with NO compensating
    // occam offset (λ_smooth = 1 ⇒ occam = 0), so it dwarfs the deviance gain and
    // the frozen-ρ birth gate rejects every genuinely-good circle (measured: a
    // clean rank-2 circle that lifts EV 0.27→0.43 raised REML by +391 purely
    // through +598 of uncompensated ½log|H_tt|). The rank charge is the honest,
    // rotation-invariant Laplace complexity for a realised-rank decoder and is
    // exactly the decoder-scale-mispricing remedy this path documents.
    let rho = SaeManifoldRho::new(0.0, 0.0, vec![Array1::<f64>::zeros(1)]);
    (term, rho)
}

/// Score every born atom against the true planes; returns
/// `(n_distinct, n_clean, n_real, per_atom rows)` where each row is
/// `(best_overlap, second_overlap, best_circle, pr)`.
fn score_atoms(
    term: &SaeManifoldTerm,
    true_planes: &[Array2<f64>],
) -> (usize, usize, usize, Vec<(f64, f64, usize, f64)>) {
    let mut claimed = std::collections::HashSet::new();
    let (mut n_real, mut n_clean) = (0usize, 0usize);
    let mut rows = Vec::new();
    for k in 0..term.k_atoms() {
        let decoder = term.atoms[k].decoder_coefficients();
        let plane = decoder_output_plane(decoder);
        let pr = decoder_sv_pr(decoder);
        let mut ov: Vec<(f64, usize)> = true_planes
            .iter()
            .enumerate()
            .map(|(idx, tp)| (plane_overlap(&plane, tp), idx))
            .collect();
        ov.sort_by(|a, b| b.0.total_cmp(&a.0));
        let best = ov[0].0;
        let second = ov.get(1).map(|x| x.0).unwrap_or(0.0);
        claimed.insert(ov[0].1);
        if best >= 0.9 {
            n_real += 1;
            if second <= 0.2 {
                n_clean += 1;
            }
        }
        rows.push((best, second, ov[0].1, pr));
    }
    (claimed.len(), n_clean, n_real, rows)
}

/// THE #2111 INTEGRATED ACCEPTANCE TEST.
#[test]
fn dense_torus_integrated_birth_recovery_2111() {
    let k = 6usize;
    let n = 700usize;
    let p = 16usize;
    // Distinct amplitudes 1.0 … 0.55 (the real fixture's identifying signal).
    let amps: Vec<f64> = (0..k)
        .map(|c| 1.0 - 0.45 * (c as f64) / ((k - 1) as f64))
        .collect();
    let (data, true_planes, turns) = dense_torus(n, p, k, &amps, 0.05, 0x2111_D0_7A_5EED);

    let config = StagewiseConfig {
        inner_max_iter: 24,
        learning_rate: 1.0,
        ridge_ext_coord: 1e-6,
        ridge_beta: 1e-6,
        max_births: 10, // safety BOUND (> k so a natural stop is observable)
        max_backfit_sweeps: 3,
        min_effect_ev: 0.0,
        max_factor_rank: 4,
        structured_whitening: false,
    };

    // Seed circle 0 on its true coordinate; fit the K=1 seed before SAC entry.
    let (mut seed, mut rho) = seed_term(&turns[0], p);
    seed.run_joint_fit_arrow_schur(
        data.view(),
        &mut rho,
        None,
        config.inner_max_iter,
        config.learning_rate,
        config.ridge_ext_coord,
        config.ridge_beta,
    )
    .expect("K=1 seed fit must complete");

    let result = fit_stagewise(seed, rho, data.view(), None, None, &config, None, None)
        .expect("fit_stagewise must complete on the dense torus");

    let (n_distinct, n_clean, n_real, rows) = score_atoms(&result.term, &true_planes);
    let natural_exit = result.report.stopped_reason != StagewiseStop::MaxBirths;

    eprintln!(
        "\n[#2111 dense torus] K={} births_accepted={} births_rejected={} stop={:?} natural_exit={}",
        result.term.k_atoms(),
        result.report.births_accepted,
        result.report.births_rejected,
        result.report.stopped_reason,
        natural_exit,
    );
    eprintln!("[#2111] per-atom decoder plane vs true circles:");
    eprintln!("   atom  best_overlap  second_overlap  best_circle   sv_PR");
    for (k, (best, second, circ, pr)) in rows.iter().enumerate() {
        eprintln!("   {k:>4}  {best:>12.4}  {second:>14.4}  {circ:>11}   {pr:>6.3}");
    }
    eprintln!(
        "[#2111] n_distinct={n_distinct} n_real={n_real} n_clean={n_clean}  (bar: 6/6/6, all PR<=3, natural_exit)"
    );
    eprintln!("[#2111] birth ledger:");
    for (r, br) in result.report.birth_records.iter().enumerate() {
        eprintln!(
            "   round {r}: before={:.5} min_effect_ev={:.5} accepted={}",
            br.joint_penalized_quasi_laplace_before,
            br.min_effect_ev,
            br.accepted_count(),
        );
        for (candidate, record) in br.candidates.iter().enumerate() {
            match &record.decision {
                BirthCandidateDecision::FitFailed(_) => {
                    assert_eq!(record.delta_ev, None);
                    assert_eq!(record.joint_penalized_quasi_laplace, None);
                }
                decision => {
                    let delta_ev = record
                        .delta_ev
                        .expect("every measured birth candidate must retain its ΔEV");
                    let criterion = record.joint_penalized_quasi_laplace.expect(
                        "every measured birth candidate must retain its joint criterion",
                    );
                    match decision {
                        BirthCandidateDecision::GateRejected(
                            BirthRejection::NonFiniteCriterion,
                        ) => assert!(!criterion.is_finite()),
                        BirthCandidateDecision::GateRejected(
                            BirthRejection::EvidenceNotImproved {
                                criterion: rejected,
                                must_be_below,
                            },
                        ) => {
                            assert_eq!(criterion, *rejected);
                            assert_eq!(
                                br.joint_penalized_quasi_laplace_before,
                                *must_be_below
                            );
                            assert!(!(criterion < *must_be_below));
                        }
                        BirthCandidateDecision::GateRejected(BirthRejection::NonFiniteEv) => {
                            assert!(!delta_ev.is_finite())
                        }
                        BirthCandidateDecision::GateRejected(
                            BirthRejection::EffectBelowFloor {
                                delta_ev: rejected,
                                floor,
                            },
                        ) => {
                            assert_eq!(delta_ev, *rejected);
                            assert_eq!(br.min_effect_ev, *floor);
                            assert!(!(delta_ev >= *floor));
                        }
                        BirthCandidateDecision::Accepted
                        | BirthCandidateDecision::Outranked
                        | BirthCandidateDecision::DeferredByBatchSelection => assert!(
                            criterion.is_finite()
                                && criterion < br.joint_penalized_quasi_laplace_before
                                && delta_ev >= br.min_effect_ev
                        ),
                        BirthCandidateDecision::FitFailed(_) => unreachable!(),
                    }
                }
            }
            eprintln!(
                "      candidate {candidate}: decision={:?} kind={:?} dEV={:?} \
                 factor_energy={:.5} joint_penalized_quasi_laplace={:?}",
                record.decision,
                record.kind,
                record.delta_ev,
                record.factor_energy,
                record.joint_penalized_quasi_laplace,
            );
        }
    }

    // ── The #2111 acceptance bar ────────────────────────────────────────────────
    let all_real = rows.iter().all(|(b, _, _, _)| *b >= 0.9);
    let all_clean = rows.iter().all(|(b, s, _, _)| *b >= 0.9 && *s <= 0.2);
    let all_pr_ok = rows.iter().all(|(_, _, _, pr)| *pr <= 3.0);
    assert!(
        result.term.k_atoms() == k,
        "expected K={k} born atoms, got {}",
        result.term.k_atoms()
    );
    assert!(
        all_real,
        "every atom must match a true circle at overlap >= 0.9"
    );
    assert!(
        all_clean,
        "every atom must be CLEAN (best>=0.9 AND second<=0.2)"
    );
    assert!(
        all_pr_ok,
        "every decoder must have SV participation ratio <= 3"
    );
    assert!(n_distinct == k, "n_distinct must be {k}; got {n_distinct}");
    assert!(n_real == k, "n_real must be {k}; got {n_real}");
    assert!(n_clean == k, "n_clean must be {k}; got {n_clean}");
    assert!(
        natural_exit,
        "forward phase must exit naturally (not MaxBirths)"
    );
}

/// Fixture sanity (fast, no fit): the planted dense torus really carries `2k`
/// above-noise directions in the expected axis-aligned frames — a guard that the
/// integrated test above is exercising the intended structure, not a degenerate
/// input. Uses the shared column-second-moment eigenstructure.
#[test]
fn dense_torus_fixture_has_2k_signal_dirs_2111() {
    let k = 6usize;
    let (data, _planes, _turns) = dense_torus(700, 16, k, &vec![1.0; k], 0.05, 0x2111_F1F7);
    let signal = column_signal_rank(data.view(), 0.05 * 0.05);
    eprintln!(
        "[#2111 fixture] above-noise signal directions = {signal} (expect {})",
        2 * k
    );
    assert!(
        signal == 2 * k,
        "dense {k}-torus must show exactly {} signal directions; got {signal}",
        2 * k
    );
}

/// Count column-covariance eigenvalues above an isotropic-noise Marchenko–Pastur
/// edge — the number of real signal directions in the centered data.
fn column_signal_rank(data: ArrayView2<'_, f64>, noise_var: f64) -> usize {
    use gam_linalg::faer_ndarray::FaerEigh;
    let (n, p) = data.dim();
    let mut mean = Array1::<f64>::zeros(p);
    for i in 0..n {
        for j in 0..p {
            mean[j] += data[[i, j]];
        }
    }
    mean.mapv_inplace(|v| v / n as f64);
    let mut cov = Array2::<f64>::zeros((p, p));
    for i in 0..n {
        for a in 0..p {
            let ra = data[[i, a]] - mean[a];
            for b in a..p {
                cov[[a, b]] += ra * (data[[i, b]] - mean[b]);
            }
        }
    }
    for a in 0..p {
        for b in a..p {
            let v = cov[[a, b]] / n as f64;
            cov[[a, b]] = v;
            cov[[b, a]] = v;
        }
    }
    let (evals, _) = cov.eigh(crate::manifold::Side::Lower).expect("cov eigh");
    let edge = noise_var * (1.0 + (p as f64 / n as f64).sqrt()).powi(2);
    evals.iter().filter(|&&e| e > edge).count()
}