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
//! Split-out sibling test module, carved from `tests.rs` to keep that tracked
//! file under the 10k-line build gate (`build.rs` `MAX_TRACKED_FILE_LINES`),
//! mirroring the existing `tests_parallelism_invariance_1557` /
//! `tests_logdet_adjoint_780` splits. These three regressions need only the
//! manifold re-exports plus the shared `gamma_fd_tiny_fixture` helper, which
//! remains defined in `tests.rs`:
//!
//! * `streaming_polar_refresh_reorients_frame` — the closed-form streaming polar
//! frame refresh re-orients the decoder frame toward an accumulated
//! cross-moment span while staying column-orthonormal.
//! * `small_p_zero_decoder_stays_full_b` — a zero decoder at small ambient `p`
//! keeps the full-`B` border (frame activation is a no-op).
//! * `forward_alpha_data_derivative_skips_ungated_atom_1026` — the learnable-α
//! forward data-derivative gives an ungated background-tier atom zero
//! α-sensitivity (FD-checked against the data NLL).
use super::tests::gamma_fd_tiny_fixture;
use super::*;
use approx::assert_abs_diff_eq;
/// Streaming polar refresh from an accumulated cross-moment re-orients the
/// frame toward the cross-moment span and keeps `B_k`'s in-span component
/// while staying column-orthonormal (the closed-form streaming step).
#[test]
pub(crate) fn streaming_polar_refresh_reorients_frame() {
let m = 4usize;
let p = 12usize;
let r = 2usize;
let mut frame0 = Array2::<f64>::zeros((p, r));
frame0[[0, 0]] = 1.0;
frame0[[1, 1]] = 1.0;
let mut c0 = Array2::<f64>::zeros((m, r));
for mu in 0..m {
c0[[mu, 0]] = 1.0 + mu as f64;
c0[[mu, 1]] = 0.5 - mu as f64;
}
let decoder = fast_abt(&c0, &frame0);
let mut phi = Array2::<f64>::zeros((m, m));
let mut jet = Array3::<f64>::zeros((m, m, 1));
for mu in 0..m {
phi[[mu, mu]] = 1.0;
jet[[mu, mu, 0]] = 1.0;
}
let s_raw = gam_terms::basis::create_difference_penalty_matrix(m, 2, None).unwrap();
let mut atom = SaeManifoldAtom::new(
"stream",
SaeAtomBasisKind::EuclideanPatch,
1,
phi,
jet,
decoder,
s_raw,
)
.unwrap();
atom.maybe_activate_decoder_frame().expect("activate");
// New cross-moment pointing at axes {2,3}: refreshed frame must span them.
let mut cross = Array2::<f64>::zeros((p, r));
cross[[2, 0]] = 3.0;
cross[[3, 1]] = 2.0;
atom.refresh_frame_from_cross_moment(cross.view())
.expect("refresh");
let frame = atom.decoder_frame.as_ref().expect("frame");
// Frame stays orthonormal.
let gram = fast_atb(&frame.frame().to_owned(), &frame.frame().to_owned());
for i in 0..r {
for j in 0..r {
let expect = if i == j { 1.0 } else { 0.0 };
assert_abs_diff_eq!(gram[[i, j]], expect, epsilon = 1.0e-9);
}
}
// Refreshed span aligns with the cross-moment axes {2,3} (angle ~0).
let mut target_span = Array2::<f64>::zeros((p, r));
target_span[[2, 0]] = 1.0;
target_span[[3, 1]] = 1.0;
let angle = frame
.max_principal_angle(target_span.view())
.expect("angle");
assert_abs_diff_eq!(angle, 0.0, epsilon = 1.0e-9);
}
#[test]
pub(crate) fn small_p_zero_decoder_stays_full_b() {
let m = 3usize;
let p = 8usize;
let mut phi = Array2::<f64>::zeros((m, m));
let mut jet = Array3::<f64>::zeros((m, m, 1));
for row in 0..m {
phi[[row, row]] = 1.0;
jet[[row, row, 0]] = 1.0;
}
let smooth_penalty = gam_terms::basis::create_difference_penalty_matrix(m, 2, None).unwrap();
let mut atom = SaeManifoldAtom::new(
"small-p-zero",
SaeAtomBasisKind::EuclideanPatch,
1,
phi,
jet,
Array2::<f64>::zeros((m, p)),
smooth_penalty,
)
.unwrap();
assert_eq!(atom.decoder_frame_activation_rank().unwrap(), None);
assert_eq!(atom.maybe_activate_decoder_frame().unwrap(), None);
assert_eq!(atom.border_frame_rank(), p);
}
/// #1026/#1417: the learnable-α forward data-derivative must give an UNGATED
/// (background-tier) atom ZERO α-sensitivity. An ungated atom's gate is forced
/// to 1.0 (`has_ungated` override), so its mass `a_k ≡ 1` is α-independent and
/// `∂a_k/∂logα = 0` — the `π_k(α)` chain applies only to gated atoms. Before the
/// fix the code credited the ungated atom `(1/π_k)·dπ_k/dρ ≠ 0`, biasing the
/// data α-gradient. FD-check the analytic against the data NLL ½Σ‖fitted−target‖²
/// (where the ungated atom's reconstruction is α-constant) on a 2-atom fixture
/// with atom 1 ungated.
#[test]
pub(crate) fn forward_alpha_data_derivative_skips_ungated_atom_1026() {
let (mut term, target, mut rho) = gamma_fd_tiny_fixture();
term.assignment.mode = AssignmentMode::ibp_map(0.7, 0.9, true);
// Atom 1 is the #1026 ungated background tier (gate ≡ 1).
term.assignment = term
.assignment
.clone()
.with_ungated(vec![false, true])
.unwrap();
rho.log_lambda_sparse = 0.3;
let analytic = term
.learnable_ibp_forward_alpha_data_derivative(&rho, target.view())
.unwrap();
// FD of the data NLL ½Σ‖fitted−target‖² wrt ρ₀ (= logα offset, since
// α = α₀·e^{ρ₀} ⇒ ∂logα/∂ρ₀ = 1). The ungated atom's fitted contribution is
// α-constant, so the FD sees only the gated atom's π-derivative.
let data_nll = |t: &SaeManifoldTerm, r: &SaeManifoldRho| -> f64 {
let fitted = t.try_fitted_for_rho(r).unwrap();
let mut s = 0.0_f64;
for row in 0..fitted.nrows() {
for c in 0..fitted.ncols() {
let d = fitted[[row, c]] - target[[row, c]];
s += d * d;
}
}
0.5 * s
};
let h = 1.0e-6;
let mut rp = rho.clone();
let mut rm = rho.clone();
rp.log_lambda_sparse += h;
rm.log_lambda_sparse -= h;
let fd = (data_nll(&term, &rp) - data_nll(&term, &rm)) / (2.0 * h);
assert!(
(analytic - fd).abs() <= 1.0e-5 * (1.0 + fd.abs()),
"forward-α data derivative must match FD with an ungated atom: \
analytic={analytic:.8e}, fd={fd:.8e}"
);
// Non-vacuity: the gated atom must give a materially nonzero derivative
// (otherwise the test would pass even if everything were zeroed).
assert!(
fd.abs() > 1.0e-6,
"fixture must exercise a nonzero gated-atom α-derivative; fd={fd:.3e}"
);
}